跳到主要内容 C++ 构造函数初始化列表详解 | 极客日志
C++
C++ 构造函数初始化列表详解 C++ 构造函数初始化列表是在构造函数体执行前对成员变量进行初始化的机制。它区别于构造函数体内的赋值操作,初始化发生在对象内存分配后,而赋值在函数体内。初始化列表对于引用成员、const 成员及没有默认构造函数的类类型成员是必须的。C++11 支持类内成员初始化作为兜底值。成员变量初始化顺序取决于声明顺序而非列表顺序。无论是否显式声明,所有成员都会经历初始化过程,内置类型若未初始化则为垃圾值。
一、构造函数初始化列表
在 C++ 中,构造函数初始化列表是一种在构造函数体执行之前,对类成员变量进行初始化的机制。
二、语法格式
初始化列表位于构造函数的参数列表之后,函数体的大括号之前,以冒号 : 开头,成员之间用逗号 , 分隔。
语法形式:
构造函数 (函数参数 1,函数参数 2) : 成员 1(参数 1), 成员 2(参数 2) { ... }
代码示例:
class MyClass {
public :
MyClass (int x, double y) : a (x), b (y) {
}
private :
int a;
double b;
};
**注意事项:**每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
三、核心区别:初始化与赋值
掌握初始化列表的核心在于明确"初始化"与"赋值"的区别。
在使用初始化列表之前,构造函数中对成员变量的操作实际上是赋值,而初始化列表才是真正的初始化过程。
场景 A:构造函数体内赋值
class MyClass {
public :
MyClass (string s) {
_name = s;
}
private :
string _name;
};
时间轴发生的事情:
初始化阶段(隐式): 编译器发现你没显示在列表里写_name(s),编译器也会生成隐式列表_name(),然后它悄悄调用 string 的默认构造函数。
此时: _name 已经诞生了,它是一个空字符串 ""。
进入函数体 {: 开始执行用户代码。
赋值阶段: 执行 _name = s; 调用 string 的赋值运算符。
此时: 把刚才那个空字符串的内容清掉,换成 s 的内容。
总结: 先生出一个'空壳',然后再往里'填充'。
场景 B:初始化列表
class {
:
(string s) :_name(s) {}
:
string _name;
};
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
MyClass
public
MyClass
private
时间轴发生的事情:
初始化阶段(显式): 编译器看到列表里有 _name(s),直接调用 string 的拷贝构造函数。
此时: _name 在诞生的那一刻,就直接拥有了 s 的值。
进入函数体 {: 执行用户代码(该段代码为空)。
总结: 出生即完美,一步到位。
#include <iostream>
using namespace std;
class MyString {
public :
MyString () {
cout << " [底层] 默认构造 (创建空对象)" << endl;
}
MyString (const MyString& other) {
cout << " [底层] 拷贝构造函数被调用 (拷贝了一个新对象) " << endl;
}
MyString& operator =(const MyString& other) {
cout << " [底层] 赋值运算符被调用 (覆盖旧值)" << endl;
return *this ;
}
};
class InitListTester {
MyString m_str;
public :
InitListTester (const MyString& s) : m_str (s) {
cout << "--- 进入 InitListTester 构造函数体 ---" << endl;
}
};
class AssignmentTester {
MyString m_str;
public :
AssignmentTester (const MyString& s) {
cout << "--- 进入 AssignmentTester 构造函数体 ---" << endl;
m_str = s;
}
};
int main () {
cout << "=== 准备工作:创建一个源对象 source ===" << endl;
MyString source;
cout << "\n=== 验证 1:初始化列表 ===" << endl;
InitListTester t1 (source) ;
cout << "\n=== 验证 2:函数体内赋值 ===" << endl;
AssignmentTester t2 (source) ;
return 0 ;
}
=== 验证 1:初始化列表 (Init List) ===
[底层] 拷贝构造函数被调用 (拷贝了一个新对象)
--- 进入 InitListTester 构造函数体 ---
注意: 它没有打印'默认构造',也没有打印'赋值运算符'。
这证明了: m_str(s) 这一行代码,直接利用 s 为蓝本,通过拷贝构造函数生出了 m_str。
=== 验证 2:函数体内赋值 ===
[底层] 默认构造 (创建空对象)
--- 进入 AssignmentTester 构造函数体 ---
[底层] 赋值运算符被调用 (覆盖旧值)
注意: 先调用了'默认构造'(因为成员必须先存在) -> 然后进入函数体 -> 最后才调用'赋值运算符'。
这证明了: 编译器发现你没在列表里写 m_str(s),编译器也会生成隐式列表 m_str(),它会悄悄调用 MyString 的默认构造函数,然后再执行赋值运算符操作。
**小结:**无论是否显式声明初始化列表,每个构造函数都包含初始化过程。
四、必须用初始化列表的成员 首先建立一个核心概念,C++ 对象的生命周期时间轴。
C.、构造函数体阶段,这是对成员变量进行'修改/赋值'的时刻
在 C++ 中,初始化 和 赋值 是两个完全不同的步骤
成员变量通过初始化列表进行默认初始化,这个过程(初始化列表进行默认初始化)发生在对象内存分配之后、构造函数体执行之前。
当程序执行到构造函数体 { ... } 时,实际上是在进行赋值操作,如果成员变量进入了构造函数体,说明它已经完成了默认初始化过程。
4.1 引用成员 因为引用不能为空,所以对于引用成员而言必须在定义时绑定一个对象,且一旦绑定不可更改(即不能重新指向别的对象),
如果不在初始化列表中绑定,进入函数体时引用就是'未绑定'状态,这是违法的。
class Referencer {
private :
int & m_ref;
public :
Referencer (int & target) {
m_ref = target;
}
};
class Referencer {
public :
Referencer (int & target) : m_ref (target) {
}
private :
int & m_ref;
};
4.2 const 成员变量 const 意味着'只读',它的值必须在创建时确定,之后不能被修改。
如果在构造函数体内赋值,实际上是在试图修改一个已经初始化过的常量,这是违法的。
class ConstHolder {
private :
const int m_val;
public :
ConstHolder (int x) {
m_val = x;
}
};
class ConstHolder {
private :
const int m_val;
public :
ConstHolder (int x) : m_val (x) {
}
};
4.3 没有默认构造函数的类类型变量 这一点最容易让人产生困惑,当类 B 包含类 A 的对象成员时,在创建 B 的实例时,编译器会优先自动创建 A 的成员对象。
如果 B 的初始化列表中没有指定如何初始化 A,编译器会默认调用 A 的无参构造函数,此时若 A 未定义无参构造函数,就会导致编译错误。
错误演示:在初始化列表中没有指定如何初始化 Engine,且 Engine 未定义无参构造函数,编译器无法调用导致编译错误。
class Engine {
public :
Engine (int power) :_power(power) { }
private :
int _power;
};
class Car {
private :
Engine m_engine;
public :
Car (int p) :m_engine (p) {}
};
#include <iostream>
using namespace std;
class Engine {
public :
Engine (int power) :_power(power) { }
private :
int _power;
};
class Car {
private :
Engine m_engine;
public :
Car (int p) :m_engine (p) {}
};
#include <iostream>
using namespace std;
class Engine {
public :
Engine (int power) :_power(power) { }
private :
int _power;
};
class SuperCar {
private :
int & m_refSpeed;
const int m_maxSpeed;
Engine m_engine;
public :
SuperCar (int & speedMetric, int maxS, int power) :
m_refSpeed (speedMetric),
m_maxSpeed (maxS),
m_engine (power)
{}
};
int main () {
int currentSpeed = 0 ;
SuperCar myCar (currentSpeed, 300 , 500 ) ;
return 0 ;
}
五、类内成员初始化 C++11 允许在声明成员变量时直接指定默认值,这些默认值主要用于未被显式列入初始化列表的成员变量。
它的核心作用就是为成员变量提供一个'保底值'(备胎)。
使用条件:如果构造函数在冒号后面显式提到了这个变量,那么类内写的那个缺省值就会被直接忽略,只有当构造函数没提这个变量时,编译器才会去用那个缺省值。
#include <iostream>
using namespace std;
class Settings {
public :
Settings () {
}
Settings (int a) : _v1(a) {
}
Settings (int a, int b) : _v1(a), _v2(b) {
}
void print () {
cout << "_v1: " << _v1 << " " << "_v2: " << _v2 << endl;
}
private :
int _v1 = 50 ;
int _v2 = 80 ;
};
int main () {
Settings s1;
Settings s2 (10 ) ;
Settings s3 (10 , 20 ) ;
s1. print ();
s2. print ();
s3. print ();
return 0 ;
}
**温馨提示:**如果函数参数上带有缺省值,效果与上述一致,如果构造函数在冒号后面显式提到了这个变量,那么类内写的那个缺省值就会被直接忽略,用显示的初始化方式进行初始化,只有当构造函数没提这个变量时,编译器才会去用那个缺省值。
**代码示例:**尽管函数参数带有缺省值,但是初始化列表没有显示初始化方式,编译器只会去用成员变量声明处的缺省值。
#include <iostream>
using namespace std;
class Settings {
public :
Settings (int a=10 ,int b=20 ) {
}
void print () {
cout << "_v1: " << _v1 << " " << "_v2: " << _v2 << endl;
}
private :
int _v1 = 50 ;
int _v2 = 80 ;
};
int main () {
Settings s1;
s1. print ();
return 0 ;
}
引入该特性的优势:在 C++11 之前,若存在多个构造函数且某个成员变量(如 _init)需要在所有构造函数中初始化为 0,我们不得不在每个构造函数中重复编写 : _init(0)。
class OldStyle {
int x;
int y;
public :
OldStyle () : x (0 ), y (0 ) {}
OldStyle (int a) : x (a), y (0 ) {}
OldStyle (int a, int b) : x (a), y (b) {}
};
class NewStyle {
int x = 0 ;
int y = 0 ;
public :
NewStyle () {}
NewStyle (int a) : x (a) {}
NewStyle (int a, int b) : x (a), y (b) {}
};
六、初始化顺序 类成员变量的初始化顺序仅取决于它们在类中的声明顺序,与初始化列表中的排列顺序无关,建议将初始化列表的顺序与成员变量的声明顺序保持一致。
**代码示例:**我们想把 x 存给 m_b,然后把 m_b 的值赋给 m_a。
#include <iostream>
class Trap {
public :
Trap (int x) : m_b (x), m_a (m_b) {
cout << "m_a = " << m_a <<endl;
cout << "m_b = " << m_b <<endl;
}
private :
int m_a;
int m_b;
};
int main () {
Trap t (10 ) ;
return 0 ;
}
编译器实际生成的执行步骤如下:
①第一步:初始化 m_a
编译器看一眼声明,发现 m_a 排第一。
它去看初始化列表,找到了 : m_a(m_b)。
灾难发生: 此时 m_b 还没有被初始化!它里面是内存里的随机垃圾值。
结果: m_a 被初始化为垃圾值。
②第二步:初始化 m_b
编译器发现 m_b 排第二。
它去看初始化列表,找到了 : m_b(x)。
结果: m_b 被正确初始化为 10。
③第三步:进入构造函数体
打印 m_a (垃圾值) 和 m_b (10)。
总结: 这就充分的证明了类成员变量的初始化顺序仅取决于它们在类中的声明顺序,与初始化列表中的排列顺序无关。
class Safe {
public :
Safe (int x) : m_a (x), m_b (x) {}
private :
int m_a;
int m_b;
};
为什么要这样设计?
这是为了保证析构顺序的确定性,在 C++ 中,对象的析构顺序必须严格是构造顺序的逆序。
如果不按声明顺序构造,而是按列表顺序构造:
程序员 A 写了 Class(x) : a(x), b(x) {}
程序员 B 写了 Class(x) : b(x), a(x) {}
同一个类,竟然会有两种不同的构造顺序,那析构函数该按什么顺序销毁成员呢,这会导致混乱。
因此,C++ 规定:声明顺序是唯一的真理,这样析构函数就可以无脑地按照'声明顺序的逆序'来清理资源。
七、初始化列表总结 ①所有构造函数都包含初始化列表,无论是否显式声明;
②每个成员变量都会通过初始化列表进行初始化,不论是否在列表中显式指定。
**核心观念:**初始化列表不是'选修课',而是成员变量出生的'必经之路'。
**简单理解:**无论你写不写冒号 :,无论你写不写初始化列表,每一个成员变量在进入构造函数体 {} 之前,都必须在初始化列表这个阶段完成初始化。
我们可以把这个过程想象成一个'三级过筛'的决策流程图。
假设编译器正在初始化成员变量 m_var,流程如下:
问: 构造函数的冒号后面有没有写 : m_var(x)?
是: 停止检查,直接使用 x 初始化(这是最高优先级)。
否: 进入第二关。
问: 在 class 定义里有没有写 Type m_var = y;?
是: 停止检查,使用缺省值 y 初始化。
否: 进入第三关。
问: m_var 是什么类型?
情况 A:自定义类型(类对象)
调用它的默认构造函数 Type()。
风险提示: 如果该类型没有默认构造函数,编译报错。 *
情况 B:内置类型(int, double, 指针等)
不处理。它里面的值是内存里残留的随机垃圾值。
风险提示: 这是 C++ 中无数莫名其妙 Bug 的根源 *
#include <iostream>
using namespace std;
class Inner {
public :
Inner () {
cout << "Inner: 默认构造" << endl;
}
Inner (int x) {
cout << "Inner: 带参构造 " << x <<endl;
}
};
class MyClass {
public :
MyClass (int val) : m_explicit (val) {
cout << "MyClass 构造函数体开始执行..." <<endl;
cout << "m_explicit: " << m_explicit << " (使用了列表值)" <<endl;
cout << "m_default: " << m_default << " (使用了缺省值)" <<endl;
cout << "m_garbage: " << m_garbage << " (未定义,可能是乱码)" <<endl;
}
private :
int m_explicit = 100 ;
int m_default = 200 ;
Inner m_obj;
int m_garbage;
};
int main () {
MyClass c (999 ) ;
return 0 ;
}