C++ 类和对象进阶:初始化列表、static 成员与编译器优化
一、再探构造函数:初始化列表
在构造函数体内给成员变量赋值,严格来说这只能叫函数体内赋初值。对象实例化时,内存分配后成员变量已经存在。进入构造函数 {} 内部是对已存在变量的二次赋值。
遇到以下三种成员变量时,必须在定义瞬间初始化,否则编译报错:
- const 成员变量:常量一旦定义,值锁定。
- 引用成员变量:引用定义时必须绑定实体。
- 无默认构造函数的自定义类型成员:必须显式指定构造方式。
1. 语法格式
以冒号开始,逗号分隔的成员列表,每个成员后跟初始值或表达式。
class A {
public:
// 初始化列表才是成员变量真正定义(出生)的地方!
A(int a, int ref) : _a1(a), _n(10), _ref(ref) {}
private:
int _a1;
const int _n; // 必须走列表
int& _ref; // 必须走列表
};
2. 规则与注意事项
- 唯一性:每个成员变量在初始化列表中只能出现一次。
- 强制初始化:引用、const、无默认构造函数的自定义类型必须在初始化列表中完成。
- 执行顺序:仅由成员变量在类中的声明顺序决定,与初始化列表书写顺序无关。建议保持声明顺序与列表顺序一致。
- C++11 缺省值:支持在成员声明处设置缺省值,供未在初始化列表中显式初始化的成员使用。
3. 初始化顺序陷阱
class A {
public:
A(int a) : _a1(a), _a2(_a1) {} // 踩坑点
void Print() { cout << _a1 << " " << _a2 << endl; }
private:
int _a2; // 先声明
int _a1; // 后声明
};
int main() {
A obj(1);
obj.Print();
return 0;
}
结果:输出 1 和一个随机垃圾值。
原因:_a2 先声明,先被初始化。此时 _a1 尚未初始化,值为随机。随后才初始化 _a1。
二、隐式类型转换与 explicit 关键字
单参数(或除第一个参数外其余都有默认值)的构造函数承担隐式类型转换作用。
class A {
public:
A(int a) : _a(a) {}
private:
int _a;
};
int main() {
A obj1(2); // 正常构造
A obj2 = 3; // 发生隐式类型转换
return 0;
}
为禁止隐式转换,可在构造函数前加 explicit 关键字。
三、static 成员与面试题拆解
静态成员变量为所有类对象共享,不属于具体对象,存放在静态区,不计入 sizeof(对象)。
1. static 成员特性
- 存储:静态区,类外初始化。
- this 指针:静态成员函数没有
this指针,不能访问非静态成员。 - 访问方式:推荐
类名::静态成员。
#include <iostream>
using namespace std;
class A {
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
// 静态成员变量必须类外初始化
int A::_scount = 0;
int main() {
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
return 0;
}
2. 高频题:求 1+2+...+n
题目限制:不能使用乘除法、for、while、if、else、switch、case 等。 思路:利用对象生命周期,实例化 n 个对象触发 n 次构造函数累加。
class Sum {
public:
Sum() { _ret += _i; ++_i; }
static int GetRet() { return _ret; }
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum arr[n];
return Sum::GetRet();
}
};
四、全局、局部、静态对象的构造与析构顺序
假设代码:
C c;
int main() {
A a;
B b;
static D d;
return 0;
}
- 构造顺序:全局对象最先(main 前),局部对象按顺序(main 内),静态局部对象首次执行到语句时。
- 结论:
C -> A -> B -> D
- 结论:
- 析构顺序:后构造的先析构(栈 LIFO)。局部对象先销毁,静态/全局对象最后销毁。
- 结论:
B -> A -> D -> C
- 结论:
五、友元:打破封装的'合法后门'
友元分为友元函数和友元类,允许外部访问私有成员。
1. 友元函数
class A {
friend void func(const A& aa);
int _a1 = 1;
};
void func(const A& aa) {
cout << aa._a1 << endl;
}
2. 友元类
class A {
friend class B;
int _a1 = 1;
};
class B {
public:
void PrintA(const A& aa) {
cout << aa._a1 << endl;
}
};
注意:友元具有单向性和不可传递性。工程中慎用,避免破坏封装。
六、内部类
内部类是定义在另一个类内部的类,完全独立,不影响外部类大小。
- 权限:内部类默认是外部类的友元,可访问外部类所有成员。
- 专属内部类:将内部类置于 private 区域,成为外部类专属。
class Solution {
private:
class Sum {
public:
Sum() { _ret += _i; ++_i; }
};
static int _i;
static int _ret;
public:
int Sum_Solution(int n) {
Sum arr[n];
return _ret;
}
};
int Solution::_i = 1;
int Solution::_ret = 0;
七、匿名对象和有名对象
- 有名对象:
类型 对象名 (实参),生命周期贯穿作用域。 - 匿名对象:
类型 (实参),生命周期仅限当前行。
// 高级写法
Solution().Sum_Solution(10);
八、对象拷贝时的编译器优化
现代编译器会优化传值和返回时的拷贝开销。
场景 A:参数传递优化
void f1(A aa) {}
int main() {
f1(1); // 优化为直接构造
f1(A(2)); // 优化为直接构造
return 0;
}
场景 B:传值返回优化 (RVO/NRVO)
A f2() {
A aa;
return aa;
}
int main() {
A obj = f2();
return 0;
}
- 无优化:1 次构造 + 2 次拷贝。
- NRVO 优化:直接在外部
obj空间构造,0 次拷贝,仅 1 次构造。 建议:若必须传值返回,放心交给编译器优化,无需手动规避。

