【原創】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] = '\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] = '\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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM