【初始化列表 + 自定义类型转换 + static成员】目录
往期《C++初阶》回顾:
/------------ 入门基础 ------------/
【C++的前世今生】
【命名空间 + 输入&输出 + 缺省参数 + 函数重载】
【普通引用 + 常量引用 + 内联函数 + nullptr】
/------------ 类和对象 ------------/
【类 + 类域 + 访问限定符 + 对象的大小 + this指针】
【类的六大默认成员函数】
前言:
哈喽呀小伙伴们~一周不见想死你们啦!(づ。◕‿‿◕。)づ
上周有没有乖乖学习呀?🤔
至于博主我嘛👉 刚忙完期末复习大作战💻 ,博主可是被期末复习按在地上摩擦了一周呢 (╯°□°)╯︵ ┻━┻
不过现在终于忙完啦!虽然还有一周才考试…但是!咱这闲不住的人哪能真闲着呀✨
所以!今天就马不停蹄给大家带来《C++ 初阶之类和对象》【下】的第一篇宝藏博客啦🎁(๑•̀ㅂ•́)و✧
据说看完这篇的小伙伴都直呼"原来C++还能这样玩?"(≧∇≦)ノ 快搬好小板凳准备上课啦~
---------------初始化列表---------------
什么是初始化列表?
初始化列表(Initializer List)
:是 C++ 中用于在对象构造时直接初始化成员变量的语法。
它位于构造函数的参数列表之后,函数体之前,以冒号
:
开始,后面跟着用逗号分隔的成员变量初始化列表。它核心作用是在对象内存分配完成后立即初始化成员,而非先默认初始化再赋值。
class MyClass { int _a; double _b; std::string _s; public: // 初始化列表语法 MyClass(int a, double b, const std::string& s) : _a(a), _b(b), _s(s) // 初始化列表 { // 构造函数体(此时成员已初始化) } };
为什么要使用初始化列表?
使用初始化列表主要有以下两点原因:
- 为了提高类的成员变量的初始化效率
- 有些成员变量不得不使用初始化列表进行初始化
--------------------
为了提高效率
--------------------对于类类型的成员变量 :
- 如果没有使用初始化列表,在进入构造函数体之前,会先调用成员变量的默认构造函数
进行初始化
,然后在构造函数体中再进行赋值
操作,这会产生额外的开销。- 而使用初始化列表,可以直接调用成员变量的合适构造函数
进行初始化
,避免了先默认构造再赋值的过程,从而提高了初始化效率。
- 例如:当成员变量是
string
类型时,使用初始化列表可以直接调用string
的构造函数来初始化,而不是先默认构造一个空字符串,再进行赋值。class Student { string _name; public: // 低效写法:先默认构造空字符串,再赋值 Student(const std::string& name) { _name = name; } // 高效写法:直接调用string的拷贝构造 Student(const string& name) : _name(name) {} };
--------------------
不得不使用
--------------------
初始化 const 成员和引用成员
:
const
成员变量和引用成员在定义后就不能被修改,因此必须在定义时进行初始化。初始化列表是在构造函数中初始化
const
成员和引用成员的唯一方法。如果不使用初始化列表,试图在构造函数体中对const
成员或引用成员进行赋值,将会导致编译错误。----------------------------初始化 const 成员---------------------------- class ConstDemo { const int _value; public: ConstDemo(int v) : _value(v) {} // 必须用初始化列表 }; ----------------------------初始化 const 成员---------------------------- class RefDemo { int& _ref; public: RefDemo(int& r) : _ref(r) {} // 必须用初始化列表 };
调用无默认构造的成员
:
当成员变量的类没有默认构造函数,只有带参数的构造函数时,必须使用初始化列表来传递参数进行初始化。
否则,编译器无法找到合适的构造函数来初始化成员变量,从而引发编译错误。
class Engine { public: Engine(int power) {} // 只有带参构造 }; class Car { Engine _engine; public: Car() : _engine(100) {} // 必须显式初始化 };
怎么使用初始化列表?
#include<iostream>
using namespace std;
// Time类定义
class Time
{
public:
/*---------------Time类的构造函数(带参数)---------------*/
// 注意:这个类没有提供默认构造函数(无参构造函数)
Time(int hour)
:_hour(hour)
{
cout << "Time类的构造函数Time()" << endl;
}
private:
int _hour;
};
// Date类定义
class Date
{
public:
/*---------------Date类的构造函数---------------*/
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year) // 初始化_year
, _month(month) // 初始化_month
, _day(day) // 初始化_day
, _t(12) // 初始化Time类成员(必须提供参数)
, _ref(x) // 初始化引用成员(必须通过初始化列表)
, _n(1) // 初始化const成员(必须通过初始化列表)
{
// 如果尝试在构造函数体内初始化以下成员,会导致编译错误:
// 1. _t(12) - 错误:Time类没有默认构造函数
// 2. _ref = x - 错误:引用必须在初始化列表中初始化
// 3. _n = 1 - 错误:const成员必须在初始化列表中初始化
}
/*---------------打印日期信息---------------*/
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl; //const成员函数,保证不会修改对象状态
}
private:
int _year;
int _month;
int _day;
Time _t; // Time类成员(没有默认构造函数)
int& _ref; // 引用成员 (必须在初始化列表中初始化)
const int _n; // const成员 (必须在初始化列表中初始化)
};
int main()
{
int i = 0; //定义一个整型变量用于初始化Date的引用成员
// 创建Date对象d1
// 参数:
// i - 用于初始化_ref引用成员
// 其他参数使用默认值
Date d1(i);
// 调用Print方法输出日期信息
d1.Print();
return 0;
}
/*
* 关键点总结:
* 1. 初始化列表的必要性:
* - 必须用于初始化:引用成员(_ref)、const成员(_n)、没有默认构造函数的类成员(_t)
*
* 2. 初始化顺序:
* - 成员变量的初始化顺序由它们在类中的声明顺序决定(_year→_month→_day→_t→_ref→_n)
* - 与初始化列表中的书写顺序无关
*
* 3. 特殊成员初始化:
* - 引用和const成员:只能在初始化列表中初始化
* - 没有默认构造的类成员:必须在初始化列表中显式构造
*
* 4. 良好实践:
* - 即使对普通成员变量(如:_year)也使用初始化列表
* - 保持初始化列表顺序与成员声明顺序一致
*/
初始化列表 vs 构造函数体内赋值谁能获胜?
特性 | 初始化列表 | 构造函数内赋值 |
---|---|---|
const成员 | ✔️ 支持 | ❌ 编译错误 |
引用成员 | ✔️ 支持 | ❌ 编译错误 |
无默认构造的成员 | ✔️ 支持 | ❌ 编译错误 |
性能 | 更高效 (直接构造) |
可能低效 (先默认构造再赋值) |
初始化顺序 | 由成员声明顺序决定 | 无顺序依赖 |
使用初始化列表的注意事项有哪些?
在 C++ 中,成员变量的初始化遵循两大规则:
其一:初始化顺序仅取决于变量在类中声明的先后次序,与初始化列表中的排列顺序无关。
class Order { int a; // 先声明 int b; public: Order() : b(1), a(2) {} // 实际初始化顺序:a→b };
其二:每个成员变量在初始化列表中只能出现一次,作为其完成定义与初始化的关键位置,确保对象创建时各成员获得唯一且正确的初始状态。
勇敢的少年,快去创造奇迹吧?
下面是一道关于:初始化顺序的自测题(看完代码不要看下面的注释分析,因为那是答案)😄
#include <iostream>
using namespace std;
class A
{
public:
/*---------------构造函数---------------*/
/**
* 初始化列表说明:
* 1. _a1初始化为参数a的值
* 2. _a2初始化为_a1的值(此时_a1尚未初始化,存在风险)
*
* 注意:成员初始化顺序由声明顺序决定,与初始化列表顺序无关
*/
A(int a)
:_a1(a) // 初始化_a1为传入的参数a
, _a2(_a1) // 用_a1的值初始化_a2(此时_a1尚未初始化)
{
}
/*---------------打印成员变量值---------------*/
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
// 注意成员变量的声明顺序:_a2在前,_a1在后
// 这将影响初始化顺序,即使初始化列表中的顺序不同
int _a2 = 2; // 成员_a2,有类内初始值2(但会被初始化列表覆盖)
int _a1 = 2; // 成员_a1,有类内初始值2(但会被初始化列表覆盖)
};
int main()
{
// 创建A类对象,传入参数1
A aa(1);
// 打印对象成员值
aa.Print();
return 0;
}
-------------------------------我下面是:分析和答案----------------------------
/*
* 关键点分析:
* 1. 成员初始化顺序:
* - 严格按照类中声明的顺序进行(先_a2,后_a1)
* - 与初始化列表中的书写顺序无关
*
* 2. 初始化过程:
* a) 首先尝试初始化_a2:
* - 使用_a1的值(此时_a1尚未初始化,值不确定)
* - 类内初始值2被初始化列表覆盖
* b) 然后初始化_a1:
* - 使用构造函数参数1
* c) 最后执行构造函数体(本例为空)
*
* 4. 类内初始值的作用:
* - 如果初始化列表不显式初始化成员,则使用类内初始值
* - 本例中初始化列表覆盖了类内初始值
*/
C++11中的新特性缺省成员初始化怎么使用?
类成员变量可以在声明时直接指定初始值(缺省值),缺省值会在构造函数调用前生效,这些值会在对象构造时被使用,除非初始化列表中显式覆盖它们。
#include<iostream>
using namespace std;
// Time类定义
class Time
{
public:
/*---------------Time类构造函数---------------*/
Time(int hour)
:_hour(hour)
{
cout << "Time类构造函数Time(int hour)调用" << endl;
}
private:
int _hour;
};
// Date类定义
class Date
{
public:
/*---------------Date类默认构造函数---------------*/
Date()
:_month(2) // 显式初始化_month为2(会覆盖缺省值1)
{
// 构造函数体内可以执行其他操作
cout << "Date类默认构造函数Date()调用" << endl;
/*
* 注意:以下成员已在类定义中提供了缺省值:
* _year = 1
* _day 未显式初始化(内置类型未初始化时为随机值)
* _t = 1 (调用Time(int)构造函数)
* _n = 1 (const成员)
* _ptr = (int*)malloc(12) (动态分配内存)
*/
}
/*---------------打印日期信息---------------*/
void Print() const
{
// 注意:_day未初始化,打印的值是未定义的
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 成员变量声明与缺省值初始化(C++11特性)
// 内置类型缺省值(如果初始化列表不显式初始化则使用此值)
int _year = 1; // 年份,缺省值为1
int _month = 1; // 月份,缺省值为1(但构造函数初始化列表会覆盖为2)
int _day; // 日期,没有缺省值(未初始化)
// 类类型成员缺省初始化
Time _t = 1; // 调用Time(int)构造函数初始化
// const成员缺省值
const int _n = 1; // const成员必须在声明时或初始化列表中初始化
// 指针成员动态内存分配
int* _ptr = (int*)malloc(12); // 使用malloc分配12字节内存
};
int main()
{
// 创建Date对象d1
Date d1; //调用默认构造函数
// 打印日期信息
d1.Print(); //输出格式:year-month-day
return 0;
}
/*
* 关键点总结:
* 1. 缺省成员初始化(C++11特性):
* - 在类声明中直接为成员变量指定初始值
* - 如果构造函数初始化列表没有显式初始化,则使用缺省值(初始化列表的优先级高于缺省值)
* - 初始化列表的初始化会覆盖缺省值(如:_month被初始化为2而非1)
*
* 2. 成员初始化顺序:
* - 首先按照缺省值初始化
* - 然后执行构造函数初始化列表的初始化
* - 最后执行构造函数体内的代码
*
* 3. 特殊成员处理:
* - _day:没有缺省值,未在初始化列表中初始化,值是未定义的
* - _t:类类型成员,使用Time(int)构造函数初始化
* - _n:const成员,必须在声明时或初始化列表中初始化
* - _ptr:使用malloc动态分配内存
*/
成员变量走初始化列表的时机是什么?
初始化列表的特点:
先于构造函数体
:无论成员变量是否在初始化列表中显式列出,其初始化都在构造函数体执行之前
完成隐式初始化
:未在初始化列表中显式初始化的成员变量,仍会按默认规则初始化(如:默认构造函数、缺省值或未定义值)在 C++ 中,推荐优先使用初始化列表对成员变量进行初始化,
原因如下:即使成员变量未在初始化列表中显式初始化,其初始化过程依然会经过初始化列表阶段。
若成员变量在声明时指定了默认值,初始化列表将直接采用该默认值进行初始化。
若未指定默认值:
对于内置类型成员
,其是否初始化由编译器决定,C++ 标准未作强制规定,这可能导致变量处于未初始化的不确定状态。对于自定义类型成员
:
- 不存在默认构造函数:若该自定义类型不存在默认构造函数,编译器无法自动完成初始化,就会引发编译错误。
- 存在默认构造函数:
- 若自定义类型成员未在初始化列表中显式初始化,编译器将调用该类型的
默认构造函数
进行初始化。- 若自定义类型成员在初始化列表中显式初始化,编译器将直接调用该类型对应的
构造函数
,而非默认构造函数。
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()
{
cout << "默认构造函数被调用" << endl;
}
MyClass(int value)
{
cout << "带参构造函数被调用,参数:" << value << endl;
}
};
// 案例1:未显式初始化(调用默认构造函数)
class Container1
{
private:
MyClass obj1;
public:
Container1() //未在初始化列表中显式初始化
{
cout << "Container1构造函数体执行" << endl;
}
};
// 案例2:显式初始化(调用带参构造函数)
class Container2
{
private:
MyClass obj2;
public:
Container2() : obj2(42) //显式初始化
{
cout << "Container2构造函数体执行" << endl;
}
};
int main()
{
cout << "==== 案例1 ====" << endl;
Container1 c1;
cout << "\n==== 案例2 ====" << endl;
Container2 c2;
return 0;
}
---------------用户自定义类型转换---------------
用户自定义类型转换有哪些?
自定义类型转换的种类:
内置类型 → 类类型
:
当类定义了单参数构造函数(或:除第一个参数外其余参数均有默认值)时,C++ 允许将该参数类型的值隐式转换为类对象。
示例:class MyClass { public: MyClass(int value) { /* ... */ } // 单参数构造函数 }; void func(MyClass obj); func(42); // 隐式转换:int → MyClass
类类型 → 类类型
:
若类 A 定义了以类 B 为参数的构造函数,则类 B 的对象可隐式转换为类 A 的对象。
示例:class B {}; class A { public: A(const B& b) { /* ... */ } // 以B为参数的构造函数 }; void func(A obj); B b; func(b); // 隐式转换:B → A
注意:上面的这两种类型转换都是隐式类型转换
代码小案例:展示使用自定义类型的类型转换
#include<iostream>
using namespace std;
class A
{
public:
/*----------------------单参数构造函数----------------------*/
/**
* 注意:如果加上explicit关键字,将禁止隐式类型转换
* 例如:A aa1 = 1; 这样的语句将无法编译
*/
A(int a1)
:_a1(a1)
{
}
/*----------------------双参数构造函数----------------------*/
/**
* 注意:如果加上explicit关键字,将禁止隐式类型转换
* 例如:A aa3 = {2,2}; 这样的语句将无法编译
*/
A(int a1, int a2)
:_a1(a1), _a2(a2)
{
}
/*----------------------打印成员变量值----------------------*/
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1; // 成员变量_a1,默认值为1
int _a2 = 2; // 成员变量_a2,默认值为2
};
class B
{
public:
/*----------------------构造函数,接收A类型的引用----------------------*/
/**
* 这个构造函数允许从A类型到B类型的隐式转换
*/
B(const A& a)
:_b(a.Get()) // 使用A对象的Get()方法初始化_b
{
}
private:
int _b = 0; // 成员变量_b,默认值为0
};
int main()
{
// 1. 隐式类型转换示例1:int → A
// 编译器实际执行:
// 1) 先用1构造一个临时A对象
// 2) 再用这个临时对象拷贝构造aa1
// 3) 编译器优化为直接构造
A aa1 = 1; // 等价于 A aa1(1);
aa1.Print(); // 输出:1 2
// 2. 隐式类型转换示例2:int → A → const A&
// 用1构造临时A对象,然后绑定到引用aa2
const A& aa2 = 1; // 临时对象生命周期延长至引用作用域结束
// 3. C++11列表初始化(多参数隐式转换)
// 使用初始化列表构造A对象
A aa3 = { 2,2 }; // 等价于 A aa3(2,2);
aa3.Print(); // 输出:2 2
// 4. 隐式类型转换示例3:A → B
// 通过A对象隐式构造B对象
// 实际过程:
// 1) 调用A::Get()获取值
// 2) 用该值构造B对象
B b = aa3; // 等价于 B b(aa3);
// 5. 隐式类型转换示例4:A → B → const B&
// 通过A对象隐式构造临时B对象,然后绑定到引用
const B& rb = aa3;
return 0;
}
/*
* 关键点总结:
* 1. 隐式类型转换规则:
* - 当类有单参数构造函数时,支持从参数类型到类类型的隐式转换
* - C++11开始支持多参数构造函数的隐式转换(使用初始化列表语法)
*
* 2. explicit关键字的作用:
* - 修饰构造函数时,禁止隐式类型转换
* - 需要显式调用构造函数(如 A aa1(1) 而不是 A aa1 = 1)
*
* 3. 引用绑定临时对象:
* - const引用可以绑定到隐式转换产生的临时对象
* - 临时对象的生命周期会延长至引用的作用域结束
*/
什么是explicit关键字?
explicit 关键字
:是 C++ 中用于修饰 构造函数 或 类型转换运算符 的关键字。
- 它的核心作用是 禁止隐式类型转换,强制要求程序员进行显式转换,从而提高代码的安全性和可读性。
示例:
class MyClass { public: explicit MyClass(int value) { /* ... */ } // 禁止隐式转换 }; void func(MyClass obj); func(42); // 错误:无法隐式转换 func(MyClass(42)); // 正确:显式构造
为什么需要explicit关键字?
在没有
explicit
的情况下,C++ 允许 隐式转换,这可能导致意外的行为。class MyString { public: MyString(const char* str) { /* 构造函数 */ } void print() { cout << str; } private: string str; }; void func(MyString s) { s.print(); } int main() { func("Hello"); // 隐式转换:const char* → MyString return 0; }
问题:
func("Hello")
会自动调用MyString(const char*)
构造函数,但可能并非程序员本意。- 如果
MyString
有多个构造函数,隐式转换可能导致歧义。
怎么使用explicit关键字?
修饰构造函数
:禁止编译器进行隐式构造,必须显式调用。class MyString { public: explicit MyString(const char* str) { /* ... */ } // 禁止隐式转换 }; void func(MyString s) { s.print(); } int main() { // func("Hello"); // ❌ 错误:不能隐式转换 func(MyString("Hello")); // ✅ 必须显式构造 return 0; }
修饰类型转换运算符
:禁止隐式类型转换,必须显式转换。class MyInt { public: explicit operator int() const //禁止隐式转换 { return value; } private: int value; }; int main() { MyInt num; // int x = num; // ❌ 错误:不能隐式转换 int x = static_cast<int>(num); // ✅ 必须显式转换 return 0; }
---------------static成员---------------
什么是static成员?
static成员
:是类的特殊成员,它们 不属于任何一个类对象,而是 属于类本身,被所有类对象共享。
static
成员分为两种:
- 静态成员变量(Static Data Members)
- 静态成员函数(Static Member Functions)
什么是static成员变量?
static成员变量
:用static
修饰的成员变量。
定义与特性:
- 它属于整个类,而非类的某个具体对象。
- 无论类创建了多少个对象,
static
成员变量只有一份实例,被所有对象共享。内存分配与初始化:
它在程序启动时分配内存。
它必须在类外进行初始化。(即使有默认值也需在类外显式初始化)
class MyClass { public: static int sharedVar; //声明静态成员变量 }; int MyClass::sharedVar = 10; //在类外初始化
访问方式:
- 可以通过类名加作用域解析符
::
直接访问。
- 例如:
MyClass obj; int value1 = MyClass::sharedVar;
- 也可以能通过对象来访问。
- 例如:
int value2 = obj.sharedVar;
,但推荐用类名访问,更能体现其静态属性。作用:常用于统计类对象的数量,或者存储类的全局相关信息。
static成员变量的特点有哪些?(大总结)
1. 存储特性
全局唯一性
:无论创建多少个类的对象,static
成员变量在内存中仅有一份实例,被所有对象共享。静态存储区
:存储在程序的静态存储区,生命周期从程序启动到结束,不依赖于对象的创建与销毁。
2. 初始化规则
类外显式初始化
:必须在类外进行初始化。不支持类内默认值
:C++ 标准禁止在类内直接为static
成员变量设置缺省值。(除非是constexpr
类型)
3. 访问特性
类名直接访问
:可通过类名::静态成员名
访问,无需创建对象。受访问限定符约束
:与普通成员变量相同,受public
/protected
/private
限制。
4. 功能特性
类级别数据
:用于存储与类相关的全局信息(如:对象计数、配置参数)独立于对象
:不与任何对象绑定,可在无对象实例时使用。可被const成员函数修改
:static
成员变量可在const
成员函数中被修改,因其不属于任何对象实例。
什么是static成员函数?
static 成员函数
:用static
修饰的成员函数。
定义与特性:
同样属于类本身,它不依赖于类的具体对象,没有隐含的
this
指针(不能访问非静态成员变量
和非静态成员函数
)class MyClass { public: static void staticFunc() { // 这里只能访问静态成员变量,不能访问非静态成员 // 如不能访问非静态成员变量:nonStaticVar } static int sharedVar; private: int nonStaticVar; }; int MyClass::sharedVar = 0;
调用方式:
- 可以通过类名直接调用。
- 例如:
MyClass::staticFunc();
- 也可以通过对象调用,但不推荐。
用途:通常用于实现一些与类整体相关,而不涉及具体对象状态的功能。
- 例如:为类提供工具性的方法,或操作类的静态成员变量。
static成员函数的特点有哪些?(大总结)
1. 归属特性
属于类而非对象
:static
成员函数不绑定到任何对象实例,即使没有创建类的对象也可调用。无this指针
:无法使用this
指针,因此不能访问
非静态成员变量或调用
非静态成员函数。
2. 调用方式
类名直接调用
:通过类名::静态函数名()
调用,无需创建对象。对象调用允许但不推荐
:也可通过对象实例调用(如:obj.staticFunc()
),但语义上不直观。
3. 访问权限
仅访问静态成员
:只能访问类的静态成员变量和静态成员函数,无法访问非静态成员(包括普通成员变量和非静态成员函数)。受访问限定符约束
:与普通成员函数相同,受public
/protected
/private
限制。
4. 功能特性
工具性方法
:常用于实现与类相关但不依赖对象状态的工具函数(如:工厂方法、配置管理)全局接口封装
:可作为类的全局接口,隐藏类的实现细节(如:单例模式中的getInstance()
方法)
5. 特殊规则
不能声明为const
:由于没有this
指针,static
成员函数不能被声明为const
不能是虚函数
:static
成员函数不参与多态,无法被声明为virtual
可在类内定义
:可直接在类内实现,也可在类外实现(需指定类名::
作用域)
静态成员怎么使用?
代码案例:使用静态成员变量和静态成员函数
#include<iostream>
using namespace std;
class A
{
public:
/*----------------------默认构造函数----------------------*/
A()
{
++_scount; // 每创建一个对象时,静态计数器_scount加1
}
/*----------------------拷贝构造函数----------------------*/
A(const A& t)
{
++_scount; // 每拷贝构造一个对象时,静态计数器_scount加1
}
/*----------------------析构函数----------------------*/
~A()
{
--_scount; // 对象销毁时,静态计数器_scount减1
}
/*----------------------静态成员函数----------------------*/
/**
* @brief 静态成员函数:获取当前对象数量
* @return 当前存在的对象数量
*
* 注意:
* 1. 静态成员函数可以直接通过类名调用
* 2. 静态成员函数只能访问静态成员变量
*/
static int GetACount()
{
return _scount;
}
private:
// 静态成员变量声明(类内)
static int _scount; //用于记录当前存在的A类对象数量
};
// 静态成员变量定义和初始化(类外)
// 必须单独在类外进行定义和初始化
int A::_scount = 0;
int main()
{
// 测试1:初始对象数量
cout << A::GetACount() << endl; // 输出:0(尚未创建任何对象)
// 测试2:创建对象后的数量
A a1, a2; // 调用两次默认构造函数(_scount = 2)
A a3(a1); // 调用拷贝构造函数(_scount = 3)
cout << A::GetACount() << endl; // 输出:3(三个对象存在)
// 测试3:通过对象调用静态成员函数
// 语法允许但不推荐,容易造成误解
cout << a1.GetACount() << endl; // 输出:3(与类名调用结果相同)
// 测试4:尝试直接访问私有静态成员(编译错误)
// 错误原因:_scount是private成员,只能在类内部访问
//cout << A::_scount << endl;
return 0;
// main函数结束时,a1,a2,a3依次析构,_scount变为0
}
/*
* 关键点总结:
* 1. 静态成员变量_scount:
* - 在类内声明,类外初始化
* - 所有对象共享同一个_scount
* - 用于记录程序中A类对象的实时数量
*
* 2. 静态成员函数GetACount():
* - 可以直接通过类名调用(推荐方式)
* - 也可以通过对象调用(语法允许但不推荐)
* - 只能访问静态成员变量(无法访问普通成员变量)
*
* 3. 访问控制:
* - _scount是private成员,外部无法直接访问
* - 必须通过public的GetACount()方法获取
*/