class A{
public:
A(int _a, int _b)
:a(_a),
b(_b)
{
// nothing to do
}
private:
int a;
int b;
};
class B{
public:
B(int _a, int _b)
{
a = _a;
b = _b;
}
private:
int a;
int b;
};
这两种成员变量初始化方式都非常常见,一些人(比如我)对左边的成员初值列写法具有天然的抗拒性,总是隐约的认为右边直接在函数中赋值的写法“可读性更高”(实际上只是因为自己更熟悉),而且不会带来任何效率损失。
实际上在上述代码中,这两种写法的编译结果是完全一致的,这也意味着两种写法没有任何效率上的差别。但在一些情况下,两种写法的确有着不容忽视的效率差别。
class A{
public:
A(int _a, int _b)
:a(_a),
b(_b)
{
// do nothing
}
private:
int a = 1;
int b = 2;
};
class B{
public:
B(int _a, int _b)
{
a = _a;
b = _b;
}
private:
int a = 1;
int b = 2;
};
当我们对成员变量定义了默认初始化时,两者在效率上的差别又如何呢?
观察 -O0 参数下的编译结果会发现,成员初值列的效率的确更高一些:
A::A(int, int) [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov DWORD PTR [rbp-12], esi
mov DWORD PTR [rbp-16], edx
mov rax, QWORD PTR [rbp-8]
mov edx, DWORD PTR [rbp-12]
mov DWORD PTR [rax], edx
mov rax, QWORD PTR [rbp-8]
mov edx, DWORD PTR [rbp-16]
mov DWORD PTR [rax+4], edx
nop
pop rbp
ret
B::B(int, int) [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov DWORD PTR [rbp-12], esi
mov DWORD PTR [rbp-16], edx
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 1
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax+4], 2
mov rax, QWORD PTR [rbp-8]
mov edx, DWORD PTR [rbp-12]
mov DWORD PTR [rax], edx
mov rax, QWORD PTR [rbp-8]
mov edx, DWORD PTR [rbp-16]
mov DWORD PTR [rax+4], edx
nop
pop rbp
ret
可以观察到,成员初值列跳过了默认初始化,进而直接用构造函数的参数对成员变量进行初始化;而函数中赋值则仍然使用了成员变量的默认初始化,随后其初始化结果被丢弃,进而使用参数对成员变量进行赋值。
实际上上述情况由于写法不太规范,也比较少见。一种比较常见的情况是某个类声明了一个结构体或是类成员变量,而它们自己定义了默认构造。但为了”确保每一个构造函数都将对象的每一个成员初始化“,我们又需要在构造函数中对其进行初始化,这时使用成员初值列的优势就非常明显了。
class A{
public:
A(T _t)
:t(_t)
{
// do nothing
}
private:
T t; // T is a struct or class type
}
成员初值列的写法没有调用T(),而是直接使用了自动生成的拷贝构造(不推荐使用自动生成的任何构造,以免造成难以预测的不良影响);
而相对的,在函数中赋值的写法在进入函数前先调用了T()对t进行初始化,然后将参数_t拷贝给成员变量t。
两种写法结果相同,但效率上差了一个T的构造,当T的结构非常复杂时,这个差别就会非常明显;如果进一步再去纠结函数跳转和入栈出栈,这种直接在函数中赋值的写法对效率的浪费程度简直让人难以忍受。
成员初值列会用拷贝构造替代成员的默认构造,而函数中赋值的方式会毫无意义的调用默认构造,再对成员变量进行赋值。
为了确保每一个构造函数都将对象的每一个成员初始化,同时为了不浪费效率,应当尽量使用成员初值列的方式来初始化成员变量。