一、再探构造函数:初始化列表的底层逻辑
之前实现构造函数时,我们习惯在函数体内给成员变量赋值,但这种方式本质是'先默认初始化,再赋值'。而初始化列表是成员变量'定义初始化'的真正场所,直接决定了成员变量的初始状态。
1. 初始化列表的基础语法
初始化列表以冒号开头,用逗号分隔成员变量,每个成员后接括号内的初始值或表达式:
class Date {
public:
// 初始化列表:_year、_month、_day 在定义时直接初始化
Date(int year, int month, int day) : _year(year), _month(month), _day(day) {}
// 函数体可空(无额外赋值逻辑时)
private:
int _year;
int _month;
int _day;
};
2. 必须用初始化列表的 3 种场景
以下成员变量无法通过'函数体内赋值'初始化,必须在初始化列表中指定初始值,否则编译报错:
(1)引用成员变量
引用必须在定义时绑定对象,函数体内赋值会被视为'修改引用指向'(C++ 不允许):
class A {
public:
// 错误:引用_member 未在初始化列表初始化
// A(int& ref) { _ref = ref; }
// 正确:初始化列表绑定引用
A(int& ref) : _ref(ref) {}
private:
int& _ref; // 引用成员
};
(2)const 成员变量
const 变量必须在定义时初始化,函数体内赋值会违反'常量不可修改'规则:
class A {
public:
// 正确:const 成员在初始化列表赋值
A(int n) : _n(n) {}
private:
const int _n; // const 成员
};
(3)无默认构造的自定义类型成员
若自定义类型没有默认构造(如 Time 只有带参构造),编译器无法自动初始化,必须在初始化列表显式传参:
class Time {
public:
// Time 无默认构造(默认构造需无参或全缺省)
Time(int hour) : _hour(hour) {}
private:
int _hour;
};
class Date {
public:
// 正确:初始化列表调用 Time 的带参构造
Date(int hour, int year) : _t(hour), _year(year) {}
private:
Time _t; // 无默认构造的自定义类型成员
int _year;
};
3. 初始化列表的关键规则
(1)初始化顺序由'类内声明顺序'决定
成员变量在初始化列表中的顺序不影响实际初始化顺序,真正顺序是成员在类中声明的顺序。若顺序不匹配,可能导致逻辑错误:
class A {
public:
// 初始化列表顺序:_a2 在前,_a1 在后
A(int a) : _a2(a), _a1(_a2) {}
void Print() { cout << _a1 << " " << _a2 << endl; }
private:
int _a1; // 声明顺序 1:先初始化_a1
int _a2; // 声明顺序 2:后初始化_a2
};
int main() {
A aa(1);
aa.Print(); // 输出:随机值 1(_a1 用未初始化的_a2 赋值)
}
避坑建议:始终让初始化列表顺序与类内声明顺序保持一致。
(2)C++11 成员缺省值与初始化列表的配合
C++11 允许在成员声明时给'缺省值',若初始化列表未显式初始化该成员,会自动使用缺省值:
class Date {
public:
// 初始化列表未显式初始化_year、_month,使用缺省值
Date(int day) : _day(day) {}
private:
int _year = 1; // 缺省值:1
int _month = 1; // 缺省值:1
int _day; // 初始化列表显式赋值
};
int main() {
Date d(20);
d.Print(); // 输出:1-1-20
}
4. 初始化列表的本质总结
- 无论是否显式写初始化列表,每个构造函数都有初始化列表(编译器会补全默认初始化逻辑);
- 无论是否在初始化列表显式初始化,每个成员变量都要走初始化列表(内置类型可能随机,自定义类型调用默认构造);
- 优先用初始化列表:减少'默认初始化→赋值'的冗余步骤,避免上述 3 种场景的编译错误。
二、类型转换:隐式转换与 explicit 关键字
C++ 支持'内置类型→类类型''类类型→类类型'的隐式转换,但过度隐式转换可能导致意外逻辑,explicit 关键字可精准控制转换行为。
1. 内置类型到类类型的隐式转换
若类有'单个内置类型参数的构造函数',编译器会自动将该内置类型隐式转换为类对象:
class A {
public:
// 单个 int 参数的构造函数:支持 int→A 的隐式转换
A(int a1) : _a1(a1) {}
void Print() { cout << _a1 << endl; }
private:
int _a1 = 1;
};
int main() {
// 隐式转换:1→A 临时对象,再拷贝构造 aa1(编译器优化为直接构造)
A aa1 = 1;
aa1.Print(); // 输出:1
// 隐式转换:const 引用绑定临时对象(临时对象具有常性)
const A& aa2 = 2;
aa2.Print(); // 输出:2
}
2. explicit 阻止隐式转换
在构造函数前加 explicit,会禁用上述隐式转换,仅允许'显式构造':
class A {
public:
// explicit 禁用隐式转换
explicit A(int a1) : _a1(a1) {}
private:
int _a1;
};
int main() {
A aa1 = 1; // 错误:无法隐式转换
const A& aa2 = 2; // 错误:无法隐式转换
A aa3(3); // 正确:显式构造
A aa4 = A(4); // 正确:显式构造临时对象再拷贝(允许)
}
3. 类类型到类类型的隐式转换
若类 B 有'以类 A 为参数的构造函数',编译器会自动将 A 对象隐式转换为 B 对象:
class A {
public:
A(int a1) : _a1(a1) {}
int GetA1() const { return _a1; }
private:
int _a1;
};
class B {
public:
// 以 A 为参数的构造函数:支持 A→B 的隐式转换
B(const A& a) : _b(a.GetA1()) {}
void Print() { cout << _b << endl; }
private:
int _b;
};
int main() {
A aa(10);
B bb = aa; // 隐式转换:A 对象→B 对象
bb.Print(); // 输出:10
}
使用建议:仅在转换逻辑明确且必要时保留隐式转换(如 string s = "hello"),否则加 explicit 避免意外转换。
三、static 成员:属于类的共享资源
用 static 修饰的成员变量 / 函数,不属于任何对象,而是属于整个类,被所有对象共享,存储在静态区(而非对象的栈 / 堆内存)。
1. 静态成员变量的特性与用法
(1)必须在类外初始化
静态成员变量在类内仅声明,初始化需在类外(全局作用域),且不加 static:
class A {
public:
static int _scount; // 类内声明
private:
int _a; // 非静态成员(每个对象独有)
};
// 类外初始化:类型 + 类域 + 变量名,不加 static
int A::_scount = 0;
(2)所有对象共享,不占对象内存
静态成员变量不存储在对象中,sizeof 对象时不包含静态成员:
int main() {
A aa1, aa2;
aa1._scount++; // 访问静态成员:对象。静态成员
A::_scount++; // 访问静态成员:类名::静态成员(推荐)
cout << sizeof(A) << endl; // 输出:4(仅包含非静态成员_a)
}
(3)受访问限定符控制
静态成员虽属于类,但仍受 public / private 限制,私有静态成员无法在类外直接访问:
class A {
private:
static int _scount; // 私有静态成员
};
int A::_scount = 0;
int main() {
cout << A::_scount << endl; // 错误:私有成员无法访问
}
2. 静态成员函数的特性与用法
(1)没有 this 指针,仅能访问静态成员
静态成员函数 不依赖对象调用,没有隐式的 this 指针,因此无法访问非静态成员(非静态成员需通过 this 指向对象):
class A {
public:
static int GetCount() {
// 正确:访问静态成员
return _scount;
// 错误:无法访问非静态成员(无 this 指针)
// return _a;
}
private:
static int _scount;
int _a;
};
(2)调用方式:类名::函数 或 对象 . 函数
静态成员函数可直接通过类名调用,无需实例化对象:
int main() {
// 类名直接调用(推荐)
cout << A::GetCount() << endl;
// 对象调用(允许,但无必要)
A aa;
cout << aa.GetCount() << endl;
}
3. 实战案例:用 static 成员统计对象个数
静态成员的核心场景是'共享状态管理',例如统计程序中创建的对象总数:
class A {
public:
// 构造:对象创建时计数 +1
A() { ++_scount; }
// 拷贝构造:拷贝对象也是新对象,计数 +1
A(const A& t) { ++_scount; }
// 析构:对象销毁时计数 -1
~A() { --_scount; }
// 静态函数:获取当前对象个数
static int GetObjectCount() { return _scount; }
private:
static int _scount; // 静态成员:对象计数
};
// 类外初始化计数为 0
int A::_scount = 0;
int main() {
cout << A::GetObjectCount() << endl; // 输出:0(无对象)
A a1, a2;
A a3(a1); // 拷贝构造
cout << A::GetObjectCount() << endl; // 输出:3(3 个对象)
return 0;
}
四、友元:突破封装的特殊通道
友元提供了一种'选择性打破封装'的方式,允许外部函数或类访问当前类的私有 / 保护成员,同时避免全公开带来的安全风险。但友元会增加类间耦合,需谨慎使用。
1. 友元函数:外部函数访问类私有成员
若函数需频繁访问多个类的私有成员(如 operator<< 重载),可声明为这些类的友元函数:
// 前置声明:告诉编译器 B 是类(否则 A 的友元声明无法识别 B)
class B;
class A {
// 声明 func 为友元函数:func 可访问 A 的私有成员
friend void func(const A& aa, const B& bb);
private:
int _a = 1;
};
class B {
// 声明 func 为友元函数:func 可访问 B 的私有成员
friend void func(const A& aa, const B& bb);
private:
int _b = 2;
};
// 友元函数:可直接访问 A 和 B 的私有成员
void func(const A& aa, const B& bb) {
cout << aa._a << endl; // 输出:1
cout << bb._b << endl; // 输出:2
}
友元函数规则:
- 友元声明仅需在类内,函数定义在类外(无需加
friend); - 一个函数可同时是多个类的友元;
- 友元函数不受类访问限定符限制(声明在
public/private均可)。
2. 友元类:整个类的成员函数都可访问私有成员
若类 B 需频繁访问类 A 的私有成员,可将 B 声明为 A 的友元类,此时 B 的所有成员函数都能访问 A 的私有成员:
class A {
// 声明 B 为友元类:B 的所有成员函数可访问 A 的私有成员
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B {
public:
void PrintA(const A& aa) {
// 正确:B 是 A 的友元类,可访问 A 的私有成员
cout << aa._a1 << " " << aa._a2 << endl;
}
private:
int _b = 3;
};
int main() {
A aa;
B bb;
bb.PrintA(aa); // 输出:1 2
}
友元类规则:
- 友元关系是单向的:A 是 B 的友元,不代表 B 是 A 的友元;
- 友元关系不可传递:A 是 B 的友元,B 是 C 的友元,不代表 A 是 C 的友元;
- 友元关系不可继承:子类不会继承父类的友元关系。
五、内部类:紧密关联类的专属封装
若类 A 仅为类 B 服务(如 B 的辅助工具类),可将 A 定义在 B 的内部,称为'内部类'。内部类是独立类,仅受 B 的类域和访问限定符限制。
1. 内部类的基础特性
(1)默认是外部类的友元
内部类可直接访问外部类的私有成员(无需显式声明友元),但外部类无法直接访问内部类的私有成员:
class A {
private:
static int _k; // 外部类私有静态成员
int _h = 1; // 外部类私有非静态成员
public:
// 内部类:默认是 A 的友元
class B {
public:
void PrintA(const A& a) {
// 正确:内部类可访问外部类私有成员
cout << _k << endl; // 访问静态成员(无需对象)
cout << a._h << endl; // 访问非静态成员(需外部类对象)
}
private:
int _b = 2; // 内部类私有成员
};
};
// 外部类静态成员初始化
int A::_k = 10;
int main() {
A::B b; // 访问内部类:外部类名::内部类名
A a;
b.PrintA(a); // 输出:10 1
}
(2)不占外部类对象内存
内部类是独立类,外部类对象中不包含内部类成员,sizeof 外部类时不包含内部类:
int main() {
cout << sizeof(A) << endl; // 输出:4(仅包含 A 的非静态成员_h)
cout << sizeof(A::B) << endl; // 输出:4(包含 B 的非静态成员_b)
}
2. 内部类的实战场景
当两个类耦合度极高(如'解决方案类'与'求和辅助类'),且辅助类仅给外部类使用时,用内部类可避免全局作用域污染:
class Solution {
// 内部类:仅给 Solution 使用,外部无法访问
class Sum {
public:
Sum() { _ret += _i; ++_i; }
static int GetRet() { return _ret; }
private:
static int _i;
static int _ret;
};
public:
// 计算 1+2+...+n(利用变长数组触发 Sum 构造)
int Sum_Solution(int n) {
Sum arr[n]; // 创建 n 个 Sum 对象,触发 n 次构造(累加 1~n)
return Sum::GetRet();
}
};
// 内部类静态成员初始化
int Solution::Sum::_i = 1;
int Solution::Sum::_ret = 0;
六、匿名对象:临时使用的轻量对象
匿名对象是'无对象名'的对象,用 类型 (实参) 定义,生命周期仅当前行,适合临时使用一次的场景(如调用单次成员函数)。
1. 匿名对象的基础用法
class A {
public:
A(int a = 0) : _a(a) { cout << "A(int a)" << endl; }
~A() { cout << "~A()" << endl; }
void Print() { cout << _a << endl; }
private:
int _a;
};
int main() {
// 有名对象:生命周期到 main 函数结束
A aa1(1);
// 匿名对象:生命周期仅当前行(下一行即析构)
A(2);
cout << "----------------" << endl;
// 匿名对象调用成员函数(单次使用场景)
A(3).Print(); // 输出:3(调用后立即析构)
}
输出结果(注意析构顺序):
A(int a) // aa1 构造
A(int a) // 匿名对象 A(2) 构造
~A() // A(2) 析构(生命周期结束)
----------------
A(int a) // 匿名对象 A(3) 构造
3 // Print() 输出
~A() // A(3) 析构
~A() // aa1 析构(main 结束)
2. 匿名对象的实战价值
匿名对象可简化'临时调用函数'的代码,避免创建无用的有名对象:
class Solution {
public:
int Sum_Solution(int n) {
// 业务逻辑...
return n * (n + 1) / 2;
}
};
int main() {
// 传统方式:创建有名对象再调用函数
Solution s;
cout << s.Sum_Solution(10) << endl;
// 匿名对象:直接调用函数,代码更简洁
cout << Solution().Sum_Solution(10) << endl;
}
七、对象拷贝的编译器优化
现代编译器会在不影响正确性的前提下,优化对象拷贝过程,减少'构造 + 拷贝构造'的冗余步骤,提升性能。优化规则因编译器而异,但核心是'合并连续的拷贝操作'。
1. 常见优化场景
(1)隐式类型转换的优化
A aa = 1 本质是'构造临时对象→拷贝构造 aa',编译器会优化为'直接构造 aa':
class A {
public:
A(int a) : _a(a) { cout << "A(int a)" << endl; }
A(const A& aa) : _a(aa._a) { cout << "A(const A& aa)" << endl; }
private:
int _a;
};
int main() {
// 优化前:A(1) 构造 → 拷贝构造 aa
// 优化后:直接调用 A(int a) 构造 aa(无拷贝)
A aa = 1;
}
(2)传值返回的优化
函数 A f() 返回局部对象时,优化前会'构造局部对象→拷贝构造临时对象→拷贝构造接收对象',优化后直接'构造接收对象':
A f() {
A aa(2);
return aa;
}
int main() {
// 优化前:f() 内 aa 构造 → 拷贝临时对象 → 拷贝构造 aa2
// 优化后:直接在 aa2 的内存上构造(无拷贝)
A aa2 = f();
}
2. 关闭优化验证(GCC)
Linux 下用 g++ test.cpp -fno-elide-constructors 关闭拷贝优化,可观察未优化的拷贝过程:
# 关闭优化编译
g++ test.cpp -otest -fno-elide-constructors
# 运行程序,观察多次拷贝构造输出
./test
八、思考与总结
💡 一句话总结:
C++ 类的进阶特性不是'语法炫技',而是为了解决工程化问题 —— 初始化列表保证成员正确初始化,static 管理共享状态,友元平衡封装与访问便利,匿名对象简化临时操作,编译器优化提升性能。理解这些特性的'设计初衷',才能在实战中灵活运用。


