简略来说,编译器会对初始化列表按照成员变量的声明顺序重新一一排序,安插到构造函数中进行初始化操作,而且这些初始化操作在构造函数里面用户自己定义的任何代码之前。
下面是c++源码:
class X { private: int i; int j; int k; int l; public: X() : j(1), i(2), l(3) { k = 4; } }; int main() { X x; }
下面是main函数里面的汇编码:
; 13 : int main() { push ebp mov ebp, esp sub esp, 16 ; 为对象x预留16byte的空间 ; 14 : X x; lea ecx, DWORD PTR _x$[ebp];将对象x的首地址传递给寄存器ecx,作为隐含参数传递给构造函数(即this指针) call ??0X@@QAE@XZ ; 调用构造函数 ; 15 : } xor eax, eax mov esp, ebp pop ebp ret 0 _main ENDP
下面是构造函数的汇编码:
??0X@@QAE@XZ PROC ; X::X, COMDAT ; _this$ = ecx ; 8 : X() : j(1), i(2), l(3) { push ebp mov ebp, esp push ecx;将寄存器ecx压栈的目的是为保留对象首地址预留空间 mov DWORD PTR _this$[ebp], ecx;将对象首地址存到刚才预留的空间里面 mov eax, DWORD PTR _this$[ebp];将对象首地址给寄存器eax mov DWORD PTR [eax], 2;将2写入对象首地址所指内存,即将2赋给成员变量i mov ecx, DWORD PTR _this$[ebp];将对象首地址给寄存器ecx mov DWORD PTR [ecx+4], 1;将1赋给偏移对象首地址4byte处内存,即将1赋给成员变量j mov edx, DWORD PTR _this$[ebp];将对象首地址给edx寄存器 mov DWORD PTR [edx+12], 3;将3写入偏移对象首地址12byte处,即将3赋给成员变量l ; 9 : k = 4; mov eax, DWORD PTR _this$[ebp];将对象首地址给寄存器eax mov DWORD PTR [eax+8], 4;将4写入偏移对象首地址8byte处,即将4赋给成员变量k ; 10 : } mov eax, DWORD PTR _this$[ebp] mov esp, ebp pop ebp ret 0 ??0X@@QAE@XZ ENDP
从汇编吗可以看到,在初始化列表中,虽然j排在i的前面(i的声明在j前),但是,编译器插入到构造函数里面的初始化操作仍然将i的初始化排在了j的前面。并且编译器插入到构造函数里面的初始化操作都在构造函数里面原有初始化操作(初始化k)的前面,尽管l的声明顺序比k的声明顺序晚。
如果不注意声明顺序和初始化列表顺序的这种关系,可能会导致下面的问题:
class X { private: int i; int j; public: X() : j(1), i(j) { } }; int main() { X x; }
通过上面的分析可以知道,i先初始化,但这时候初始化i的时候j还没初值,所以i的值无法预知。
有4中情况必须使用初始化列表:
1 当初始化一个引用成员时
2 当初始化一个const成员时
3 当调用基类的构造函数,而它拥有参数时
4 当调用成员变量的构造函数,而它拥有参数时
下面来看一种情形:
先来看c++源码:
class X { private: int i; public: X(int ii) { i = ii; } X() {} }; class Y { private: int j; X x; public: Y() { j = 1; x = X(2); } }; int main() { Y y; }
接下来是main函数中的汇编码:
_main PROC ; 21 : int main() { push ebp mov ebp, esp sub esp, 8;为对象y预留8byte空间 ; 22 : Y y; lea ecx, DWORD PTR _y$[ebp];将y对象首地址传递给寄存器ecx,作为隐含参数传递给构造函数 call ??0Y@@QAE@XZ ; 调用对象y的构造函数 ; 23 : } xor eax, eax mov esp, ebp pop ebp ret 0 _main ENDP
下面主要来看对象y的构造函数汇编码:
??0Y@@QAE@XZ PROC ; Y::Y, COMDAT ; _this$ = ecx ; 15 : Y() { push ebp mov ebp, esp sub esp, 8;为保留函数中使用到的变量预留空间,这里主要是this指针和临时对象 mov DWORD PTR _this$[ebp], ecx;将对象y的首地址保留到刚才预留空间 mov ecx, DWORD PTR _this$[ebp];将对象首地址给寄存器ecx add ecx, 4;对象首地址加4,此时ecx中存储的是对象y的成员对象x的首地址,它将作为隐含参数传递给成员对象x的构造函数 call ??0X@@QAE@XZ ; 调用成员对象x的默认构造函数 ;在执行对象y构造函数里面的代码之前,调用成员对象的构造函数 ;这里,如果成员对象x没有默认构造函数,将出错 ; 16 : j = 1; mov eax, DWORD PTR _this$[ebp];将对象y首地址给eax寄存器 mov DWORD PTR [eax], 1;将1写入y首地址处内存,寄给成员变量j赋值1 ; 17 : x = X(2); push 2;将2压栈,作为参数传递给成员对象x的构造函数 lea ecx, DWORD PTR $T2568[ebp];将临时对象的首地址给ecx寄存器,作为隐含参数传递给类X的构造函数 call ??0X@@QAE@H@Z ; 调用类X的构造函数,有参数 mov ecx, DWORD PTR [eax];eax寄存器中保留的是临时对象的首地址,这里将临时对象首地址值处内存内容给ecx寄存器, ;即将临时对象成员变量i值给寄存器ecx mov edx, DWORD PTR _this$[ebp];将对象y的首地址给edx寄存器 mov DWORD PTR [edx+4], ecx;将ecx寄存里面的内容给偏移对象y首地址4byte处内存 ;即将临时对象成员变量值拷贝到成员对象x ;这相当于(operate=的拷贝功能) ; 18 : } mov eax, DWORD PTR _this$[ebp] mov esp, ebp pop ebp ret 0 ??0Y@@QAE@XZ ENDP
可以看到,在对象y的构造函数里面,在执行对象y的构造函数里面任何赋值操作之前,首先调用了成员对象x的默认构造函数,接下来产生了一个临时对象,然后将临时对象在拷贝给成员对象x。这里可以看到,初始化成员对象x的操作是调用默认构造函数来完成的,对象y的构造函数里面的赋值操作只是一个拷贝过程。
下面将成员对象的初始化放到初始化列表中,并且去掉其默认构造函数
先看c++源码
class X { private: int i; public: X(int ii) { i = ii; } }; class Y { private: int j; X x; public: Y() : x(2) { j = 1; } }; int main() { Y y; }
下面看main函数汇编码:
_main PROC ; 20 : int main() { push ebp mov ebp, esp sub esp, 8;为对象y预留8byte空间 ; 21 : Y y; lea ecx, DWORD PTR _y$[ebp];将对象y的首地址给ecx寄存器,作为隐含参数传递给构造函数 call ??0Y@@QAE@XZ ; 调用对象y的构造函数 ; 22 : } xor eax, eax mov esp, ebp pop ebp ret 0 _main ENDP
下面主要看对象y的构造函数汇编码:
??0Y@@QAE@XZ PROC ; Y::Y, COMDAT ; _this$ = ecx ; 15 : Y() : x(2) { push ebp mov ebp, esp push ecx;压栈的目的是为了保留对象y的首地址预留空间 mov DWORD PTR _this$[ebp], ecx;将对象y的首地址存到刚才预留的空间 push 2;将2压栈,作为参数传递给成员对象x的构造函数 mov ecx, DWORD PTR _this$[ebp];将对象y的首地址给ecx寄存器 add ecx, 4;将对象y的首地址加4,得到的是成员对象x的首地址,作为隐含参数传递给成员对象x的构造函数 call ??0X@@QAE@H@Z ; 调用x的构造函数,有参数 ; 16 : j = 1; mov eax, DWORD PTR _this$[ebp];将对象y的首地址给寄存器eax mov DWORD PTR [eax], 1;将1写入对象y首地址处内存,即将1赋给对象y的成员变量j ; 17 : } mov eax, DWORD PTR _this$[ebp] mov esp, ebp pop ebp ret 0 ??0Y@@QAE@XZ ENDP
可以看到,和上面的最大不同是,即使类X没有默认构造函数,编译器也没有报错,并且,初始化成员对象的操作由有参数的构造函数完成,当中并没有产生临时对象,效率高。