說明:如果看不懂的童鞋,可以直接跳到最后看總結,再回頭看上文內容,如有不對,請指出~
環境: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 }
初始化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的拷貝或移動構造函數需要定義至少其中一個,否則會報錯
本文鏈接:【原創】c++拷貝初始化和直接初始化的底層區別 http://www.cnblogs.com/cposture/p/4925736.html