C++中默认初始化的一些思考
引言
在学习类的时候,我在learncpp上看到了一个说法:对象的初始化方式不同,其成员的初始化结果也可能不同。当时我并没有放在心上,直到自己动手写了一个测试,才发现默认初始化和列表初始化的结果居然真的不一样。
在此篇文章中,我将记录下自己对默认初始化的理解,以供思考,如有不足之处欢迎指正。
正文
用户默认构造与编译器默认构造
要区分这个问题,首先要区分两种"默认构造函数":一种是我们自己写的空函数体,另一种是编译器自动生成的。
class test1 {
private:
int m_x;
int m_y{};//类内初始化,默认为0
public:
test1() {}//用户定义的默认构造,函数体为空
};
class test2 {
private:
int m_x;
int m_y{};
public:
//不带默认构造函数,编译器自动生成
//使用显式默认构造函数的效果一样
//test2() = default;
};
表面上这两种写法"什么都不做",实际上编译器的处理完全不同。
用户定义的test1() {}是一个货真价实的函数体为空的构造函数,编译器不会替我们补任何初始化逻辑。
默认初始化与列表初始化
有了这两类构造函数,就可以做对比实验了。
test1 t1; //默认初始化,m_x未被初始化,m_y{}为0
test1 t2{}; //列表初始化,m_x同样未被初始化,m_y为0
test2 t3; //默认初始化,m_x未被初始化
test2 t4{}; //列表初始化,编译器默认构造下m_x被零初始化
运行后打印m_x/m_y的值,t1和t3的m_x是随机值,而t4的m_x是0。
对于有编译器生成(或= default)的默认构造函数的类型,列表初始化会触发值初始化,进而对成员做零初始化;而用户定义的空构造函数会挡住这条路径,成员保持默认初始化的结果。
这么看来,T t;和T t{};并不是写法上的偏好问题,而是真的可能产生不同的运行结果。
m_y之所以总是0,是因为类内初始化
int m_y{}在构造函数初始化列表未提及它时兜底生效,这与构造函数是哪一种无关。
直接初始化与拷贝初始化
实验里还有一行:
test2 t5 = test2(); //拷贝初始化,test2{}也可以
用=接一个临时对象,外形上是赋值,实际是拷贝初始化,调用的是拷贝构造(现代编译器大多会将其优化掉,但语义上仍是这样)。
我认为可以这么理解:test2()先创建了一个右值临时对象,再用它去初始化t5,只不过编译器把中间过程省略了。
补充要点
既然用户定义的空构造函数会带来"成员不初始化"的风险,实际写代码时我有两个倾向:要么给成员加类内初始化器,要么用= default让编译器生成默认构造。
class test3 {
private:
int m_x{};//类内初始化器,无论哪种构造方式都是0
public:
test3() = default;
};
这样一来,test3 t;和test3 t{};的结果就一致了,算是把不确定性消掉。
小结
默认初始化和列表初始化的区别,核心在于编译器是否有机会进行值初始化,而用户定义的空默认构造函数会把这个机会挡掉。写类的时候多一个{},可能就少一个随机值的坑。
初始化这件事,c++把它藏在了构造函数的细节里,理清楚之后再看类的定义会踏实很多。
本文所用的测试代码收录于Learncpp,如有不妥之处欢迎指正。
$ 正在加载评论…