我们在类的构造函数中使用成员初始化列表可以带来效率上的提升,那么成员初始化列表在编译后会发生什么就是这篇文章要探究的问题
文章目录
- 引入
- 成员初始化列表
- 用成员初始化列表优化上面的代码
- 成员初始化列表展开
- 成员初始化列表的潜在危险
- 参考资料
引入
考虑下面这个例子
#include <iostream>
using namespace std;
class String {
int len;
public:
String() {
len = 0;
}
String(int l) {
len = l;
}
String(const String& str) {
len = str.len;
}
};
class Word {
String _name;
int _cnt;
public:
Word() {
_name = 0;
_cnt = 0;
}
};
int main()
{
Word word;
}
其在VS2022下的汇编码,可以看到在 Word
的默认构造函数中调用了 String
的两个构造函数,分别是默认构造函数和带参数的构造函数
这里在 [rbp+0C4h] 的位置即 _name=0
右边的 0 作为临时 String
类对象,调用带参数的构造函数,然后将 [rbp+0C4h] 里的值逐位拷贝至 [this] 位置(首位置就是成员 _name
的位置),相当于是编译器合成了默认的赋值运算符
其编译器对上述Word
默认构造函数中代码的扩张结果可能如下:
Word::Word()
{
_name.String::String();
String temp = String( 0 );
_name.String::operator=( temp );
temp.String::~String();
_cnt = 0;
}
成员初始化列表
用成员初始化列表优化上面的代码
上面的代码我们可以用成员初始化列表进行优化
Word::Word : _name( 0 )
{
_cnt = 0;
}
它会被扩张成下面的样子
Word::Word()
{
_name.String::String( 0 );
_cnt = 0;
}
汇编代码可以看到只调用了一次带参的构造函数,且可以看到是在 _cnt=0
之前先进行了 _name
的初始化
成员初始化列表展开
当然我们也可以将两个变量全用成员初始化列表实现
Word::Word() : _cnt(0), _name(0)
{}
编译器会逐个操作初始化列表,以适当的次序在构造函数内安插初始化操作,并且位于任何显式的用户代码之前,例如上面的代码会扩充为:
Word::Word()
{
_name.String::String( 0 );
_cnt = 0;
}
可以看到扩充代码是以类中成员变量的声明次序决定,不是由初始化列表的排列次序决定,下面的汇编代码也验证了这一点。
成员初始化列表的潜在危险
如果类中成员变量的声明次序与初始化列表中的项目排列次序是混乱的或是有差异的,可能会导致意想不到的问题。
考虑下面这个例子:
class X {
public:
int i;
int j;
public:
X(int val) : j(val), i(j) {}
};
我们在写这个构造函数的本意可能是想先用 val
初始化 j
再用 j
来初始化 i
,我们用 1 来初始化,然后输出 x.i
和 x.j
可以看到结果不是我们希望的那样,因为正如前面所述,成员初始化列表会以变量声明顺序进行初始化,而不是初始化列表中的排列顺序。
我们可以改为下面的代码,就没有问题,因为合成的代码会插入到用户显式的代码之前。
class X {
public:
int i;
int j;
public:
X(int val) : j(val) {
i = j;
}
};
如果一个派生类成员函数被调用,其返回值当作基类构造函数的一个参数,将会如何:
class FooBar : public X {
int _fval;
public:
int fval() {return _fval;}
FooBar( int val ) : _fval( val ), X ( fval() ) {}
}
它的可能扩张结果如下,也不是一个好主意
FooBar::FooBar ()
{
X::X( this, this->fval() );
_fval = val;
}
以 1 作为初始化参数,然后顺序输出 _fval
、i
、j
结果如下
参考资料
《深度探索C++对象模型》—— Stanley B.Lippman著,侯捷译