引言

在学习类的时候,我在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,如有不妥之处欢迎指正。