说明:如果看不懂的童鞋,可以直接跳到最后看总结,再回头看上文内容,如有不对,请指出~
环境:visual studio 2013(编译器优化关闭)
源代码
下面的源代码修改自 http://blog.csdn.net/ljianhui/article/details/9245661
1 #include2 #include 3 using namespace std; 4 class ClassTest 5 { 6 public: 7 ClassTest() 8 { 9 c[0] = '\0';10 cout << "ClassTest()" << endl;11 }12 ClassTest& operator=(const ClassTest &ct)13 {14 strcpy(c, ct.c);15 cout << "ClassTest& operator=(const ClassTest &ct)" << endl;16 return *this;17 }18 ClassTest(ClassTest&& ct)19 {20 cout << "ClassTest(ClassTest&& ct)" << endl;21 }22 ClassTest & operator=(ClassTest&& ct)23 {24 strcpy(c, ct.c);25 cout << "ClassTest & operator=(ClassTest&& ct)" << endl;26 return *this;27 }28 ClassTest(const char *pc)29 {30 strcpy(c, pc);31 cout << "ClassTest (const char *pc)" << endl;32 }33 //private: 34 ClassTest(const ClassTest& ct)35 {36 strcpy(c, ct.c);37 cout << "ClassTest(const ClassTest& ct)" << endl;38 }39 virtual int ff()40 {41 return 1;42 }43 private:44 char c[256];45 };46 ClassTest f1()47 {48 ClassTest c;49 return c;50 }51 void f2(ClassTest ct)52 {53 ;54 }55 int main()56 {57 ClassTest ct1("ab");//直接初始化 58 ClassTest ct2 = "ab";//复制初始化 59 ClassTest ct3 = ct1;//复制初始化 60 ClassTest ct4(ct1);//直接初始化 61 ClassTest ct5 = ClassTest("ab");//复制初始化 62 ClassTest ct6 = f1(); 63 f1();64 f2(ct1);65 return 0;66 }
初始化1:ClassTest ct1("ab")
ClassTest ct1("ab");//直接初始化 00B09518 push 0B0DCB8h //"ab"字符串地址00B0951D lea ecx,[ct1] 00B09523 call ClassTest::ClassTest (0DC101Eh)
上面初始化汇编代码中,首先将“ab”字符串的地址压栈,并且取得ct1对象的地址存入寄存器ecx,即通过栈和寄存器传入两个参数, 调用了ClassTest(const char *pc)构造函数。在 ClassTest(const char *pc)函数中利用ct1对象的地址(即this指针)初始化ct1对象。
初始化2:ClassTest ct2 = "ab"
ClassTest ct2 = "ab";//复制初始化 00B09528 push 0B0DCB8h //"ab"字符串地址00B0952D lea ecx,[ct2] 00B09533 call ClassTest::ClassTest (0DC101Eh)
这是一个拷贝初始化式,底层的汇编有点出乎意料。本来赋值表达式右边会利用形参为const char*的构造函数生成一个临时对象,然后再利用这个临时对象拷贝或移动到ct2,但是经过visual studio编译器的处理,使得 赋值表达式右边的字符串作为构造函数的实参直接对ct2进行初始化,和初始化1一样,这样可以省略了一步,加快运行速度,并且达到同样的效果。注意:在上面的汇编中,已经关闭了visual studio编译器优化,说明这种方法已经作为了visual studio的普遍方法,而不是作为一种vs所认为的优化手段了。
初始化3:ClassTest ct3 = ct1
ClassTest ct3 = ct1;//复制初始化 00B09538 lea eax,[ct1] 00B0953E push eax 00B0953F lea ecx,[ct3] 00B09545 call ClassTest::ClassTest (0DC14C4h)
初始化3中 通过栈和寄存器ecx传入了赋值表达式左右两边的对象地址,然后调用了类的拷贝构造函数(注意:函数只有一个形参,但其实也传入了ct3对象的地址,this指针),假如用户没有定义拷贝构造函数,编译器会生成合成的拷贝构造函数。如下:
010B3EE0 push ebp 010B3EE1 mov ebp,esp 010B3EE3 sub esp,0CCh 010B3EE9 push ebx 010B3EEA push esi 010B3EEB push edi 010B3EEC push ecx 010B3EED lea edi,[ebp-0CCh] 010B3EF3 mov ecx,33h 010B3EF8 mov eax,0CCCCCCCCh 010B3EFD rep stos dword ptr es:[edi] 010B3EFF pop ecx 010B3F00 mov dword ptr [this],ecx 010B3F03 mov eax,dword ptr [this] //eax指向ct3对象地址 010B3F06 mov dword ptr [eax],10BDC70h //虚表指针存储在对象偏移量为0的地方 010B3F0C mov esi,dword ptr [__that] //esi存储ct1对象地址 010B3F0F add esi,4 //将esi加4,跳过4个字节的虚表指针,指向ct1后面的成员变量c 010B3F12 mov edi,dword ptr [this] 010B3F15 add edi,4 //edi指向ct2后面成员变量c 010B3F18 mov ecx,40h 010B3F1D rep movs dword ptr es:[edi],dword ptr [esi] //将ct1中字符数组元素拷贝到ct3字符数组 010B3F1F mov eax,dword ptr [this] //通过eax返回ct3对象地址 010B3F22 pop edi 010B3F23 pop esi 010B3F24 pop ebx 010B3F25 mov esp,ebp 010B3F27 pop ebp
初始化4:ClassTest ct4(ct1)
ClassTest ct4(ct1);//直接初始化 010B954A lea eax,[ct1] 010B9550 push eax 010B9551 lea ecx,[ct4] 010B9557 call ClassTest::ClassTest (0DC14C4h)
初始化4和初始化3汇编指令一样,底层都是传入了两个对象的地址,然后再调用拷贝构造函数。
初始化5:ClassTest ct5 = ClassTest()
ClassTest ct5 = ClassTest();//复制初始化 010B955C lea ecx,[ct5] 010B9562 call ClassTest::ClassTest (0DC12ADh)
跟踪下去,发现它跳到了类的默认构造函数那里;
ClassTest()010B4C70 push ebp 010B4C71 mov ebp,esp 010B4C73 sub esp,0CCh 010B4C79 push ebx 010B4C7A push esi 010B4C7B push edi 010B4C7C push ecx 010B4C7D lea edi,[ebp-0CCh] 010B4C83 mov ecx,33h 010B4C88 mov eax,0CCCCCCCCh 010B4C8D rep stos dword ptr es:[edi] 010B4C8F pop ecx 010B4C90 mov dword ptr [this],ecx 010B4C93 mov eax,dword ptr [this] 010B4C96 mov dword ptr [eax],10BDC70h { c[0] = '\0';010B4C9C mov eax,1 010B4CA1 imul ecx,eax,0 010B4CA4 mov edx,dword ptr [this] 010B4CA7 mov byte ptr [edx+ecx+4],0 cout << "ClassTest()" << endl;
说好的生成一个临时对象,再将这个临时对象拷贝或移动到ct5中,其实不然。而是 将ct5对象地址作为实参去调用默认构造函数,进而对ct5进行初始化。
初始化6:ClassTest ct6 = f1()
ClassTest ct6 = f1(); 010B9567 lea eax,[ct6] 010B956D push eax 010B956E call f1 (0DC14BFh) 010B9573 add esp,4
这个初始化的底层实现也是比较出乎意料的一个。首先将已存在在main函数栈中的ct6对象地址压栈,此时根据函数调用规则,可以知道ct6对象地址其实作为了f1的实参。
ClassTest f1(){00DC5830 push ebp //栈帧开始 00DC5831 mov ebp,esp 00DC5833 sub esp,1D0h 00DC5839 push ebx 00DC583A push esi 00DC583B push edi 00DC583C lea edi,[ebp-1D0h] 00DC5842 mov ecx,74h 00DC5847 mov eax,0CCCCCCCCh 00DC584C rep stos dword ptr es:[edi] 00DC584E mov eax,dword ptr ds:[00DD0000h] //初始化栈 00DC5853 xor eax,ebp 00DC5855 mov dword ptr [ebp-4],eax ClassTest c;00DC5858 lea ecx,[c] //c的值ebp+FFFFFEF4h即ebp-12,说明c是一个栈内局部变量 00DC585E call ClassTest::ClassTest (0DC12ADh) //调用默认构造函数初始化c return c;00DC5863 lea eax,[c] 00DC5869 push eax //c对象地址 00DC586A mov ecx,dword ptr [ebp+8] //ct6对象地址 00DC586D call ClassTest::ClassTest (0DC14BAh) //调用移动构造函数,初始化ct6 00DC5872 mov eax,dword ptr [ebp+8] //返回ct6对象地址 }00DC5875 push edx 00DC5876 mov ecx,ebp 00DC5878 push eax 00DC5879 lea edx,ds:[0DC58A4h] 00DC587F call @_RTC_CheckStackVars@8 (0DC1136h) 00DC5884 pop eax //省略余下代码
从上面的汇编代码中可以看出,c是栈内的局部变量,并且调用了默认构造函数对c进行了初始化。但f1代码中return c语句,它就是返回一个和c一样的临时对象了吗?其实不然。 在调用f1的时候,也传进了ct6对象的地址,在f1内部对c进行初始化后,直接通过c对象地址和ct6地址调用移动构造函数,对ct6进行了初始化,最后返回的是ct6对象地址。可以看出vs将ct6的初始化工作放在了函数内部进行!
临时对象:f1()
f1();00DC9576 lea eax,[ebp-814h] 00DC957C push eax 00DC957D call f1 (0DC14BFh) 00DC9582 add esp,4
临时对象可以看成是无名的变量,在内部也是存在于栈中的一个对象。所以和初始化6一样,只不过这个时候 传入的是临时对象的地址而已,最后返回的也是临时对象的地址,返回前也调用了移动构造函数
临时对象:f2(ct1)
f2(ct1);010F9392 sub esp,104h //开辟栈空间,生成一个临时对象,刚好是260个字节(256+4,即虚表指针和私有的char型数组的总大小)010F9398 mov ecx,esp //将esp栈顶指针作为临时对象的起始地址010F939A lea eax,[ct1] //传入ct1对象地址010F93A0 push eax 010F93A1 call ClassTest::ClassTest (010F1078h) 010F93A6 call f2 (010F14BFh) 010F93AB add esp,104h
从上面的汇编代码中可以看出,编译器对于一个形参为类型的函数,不是直接传入ct1对象地址,而是 在栈上生成一个临时对象并且用拷贝构造函数进行初始化,最后再传入临时对象的地址调用f2函数。
总结
这么零散复杂的汇编,大部分人看了都有点头疼,最后再来个总结:
(1)什么是拷贝初始化(也称为复制初始化):将一个已有的对象拷贝到正在创建的对象,如果需要的话还需要进行类型转换。拷贝初始化发生在下列情况:
- 使用赋值运算符定义变量
- 将对象作为实参传递给一个非引用类型的形参
- 将一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
(2)什么是直接初始化:在对象初始化时,通过括号给对象提供一定的参数,并且要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
(3)在底层实现中,可以看出编译器的思想是能不用临时对象就不用临时对象。因此对于下面这些拷贝初始化,都不会生成临时对象再进行拷贝或移动到目标对象,而是直接通过函数匹配调用相应的构造函数。
1 ClassTest ct2 ="ab"; //相当于ClassTest ct2("ab");2 ClassTest ct5 =ClassTest("ab"); //相当于ClassTest ct5("ab")
1 f1(); //临时对象用于存储f1的返回值2 f2(ct1); //临时对象用于拷贝实参,并传入函数
1 ClassTest ct6 = f1();
(4)直接初始化和拷贝初始化效率基本一样,因为在底层的实现基本一样,所以将拷贝初始化改为直接初始化效率提高不大。
(5)拷贝初始化什么时候使用了移动构造函数:当你定义了移动构造函数,下列情况将调用移动构造函数
- 将一个返回类型为非引用类型的函数返回一个对象
(6)拷贝初始化什么时候使用拷贝构造函数:
- 赋值表达式右边是一个对象
- 直接初始化时,括号内的参数是一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 将一个返回类型为引用类型的函数返回一个对象
- 形参为非引用类型的函数,其中是将实参拷贝到临时对象
(7)什么时候使用到拷贝赋值运算符:
- 赋值表达式右边是一个左值对象(如果需要,可以调用构造函数类型转换,生成一个临时对象)
- 当赋值表达式右边是一个右值对象,且没有定义移动赋值运算符函数
(8)什么时候使用移动赋值运算符:
- 当赋值表达式右边是一个右值对象,且定义了移动赋值运算符函数
(9)即使编译器略过了拷贝/移动构造函数,但是在这个程序点上,拷贝/移动构造函数必须存在且是可访问的(例如:不能是private),如下:
ClassTest ct2 = "ab";//复制初始化
编译器会将其等同下面的语句,调用的是ClassTest的ClassTest(const char *pc)构造函数
ClassTest ct2("ab");//直接初始化
但是ClassTest的拷贝或移动构造函数需要定义至少其中一个,否则会报错
本文链接: