【原创】c++拷贝初始化和直接初始化的底层区别

说明:如果看不懂的童鞋,可以直接跳到最后看总结,再回头看上文内容,如有不对,请指出~
环境:visual studio 2013(编译器优化关闭)

源代码

下面的源代码修改自http://blog.csdn.net/ljianhui/article/details/9245661
 1 #include <iostream>  
 2 #include <cstring>  
 3 using namespace std;
 4 class ClassTest
 5 {
 6 public:
 7     ClassTest()
 8     {
 9         c[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 }
View Code

 初始化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] = '';
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)什么是拷贝初始化(也称为复制初始化):将一个已有的对象拷贝到正在创建的对象,如果需要的话还需要进行类型转换。拷贝初始化发生在下列情况:
  1. 使用赋值运算符定义变量
  2. 将对象作为实参传递给一个非引用类型的形参
  3. 将一个返回类型为非引用类型的函数返回一个对象
  4. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
 
(2)什么是直接初始化:在对象初始化时,通过括号给对象提供一定的参数,并且要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
 
(3)在底层实现中,可以看出编译器的思想是能不用临时对象就不用临时对象。因此对于下面这些拷贝初始化,都不会生成临时对象再进行拷贝或移动到目标对象,而是直接通过函数匹配调用相应的构造函数。
1 ClassTest ct2 ="ab"; //相当于ClassTest ct2("ab");
2 ClassTest ct5 =ClassTest("ab"); //相当于ClassTest ct5("ab")
下面的语句,visual studio才会生成一个无名的临时对象(位于main函数的栈中),注意:f1的返回值类型是非引用的,f2的形参类型是非引用的。
1 f1(); //临时对象用于存储f1的返回值
2 f2(ct1); //临时对象用于拷贝实参,并传入函数
而下面则是直接传入赋值表达式左边对象地址,然后再对该对象进行移动拷贝,注意f1返回值类型是非引用的,如果是引用的,则会调用拷贝构造函数。
1 ClassTest ct6 = f1();
 
(4)直接初始化和拷贝初始化效率基本一样,因为在底层的实现基本一样,所以将拷贝初始化改为直接初始化效率提高不大。
 
(5)拷贝初始化什么时候使用了移动构造函数:当你定义了移动构造函数,下列情况将调用移动构造函数
  1. 将一个返回类型为非引用类型的函数返回一个对象
 
(6)拷贝初始化什么时候使用拷贝构造函数:
  1. 赋值表达式右边是一个对象
  2. 直接初始化时,括号内的参数是一个对象
  3. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
  4. 将一个返回类型为引用类型的函数返回一个对象
  5. 形参为非引用类型的函数,其中是将实参拷贝到临时对象
 
(7)什么时候使用到拷贝赋值运算符:
  • 赋值表达式右边是一个左值对象(如果需要,可以调用构造函数类型转换,生成一个临时对象)
  • 当赋值表达式右边是一个右值对象,且没有定义移动赋值运算符函数
 
(8)什么时候使用移动赋值运算符:
  • 当赋值表达式右边是一个右值对象,且定义了移动赋值运算符函数
 
(9)即使编译器略过了拷贝/移动构造函数,但是在这个程序点上,拷贝/移动构造函数必须存在且是可访问的(例如:不能是private),如下:
ClassTest ct2 = "ab";//复制初始化
编译器会将其等同下面的语句,调用的是ClassTest的ClassTest(const char *pc)构造函数
ClassTest ct2("ab");//直接初始化
但是ClassTest的拷贝或移动构造函数需要定义至少其中一个,否则会报错
 

本文链接:【原创】c++拷贝初始化和直接初始化的底层区别 http://www.cnblogs.com/cposture/p/4925736.html

原文地址:https://www.cnblogs.com/cposture/p/4925736.html