《c++編程思想》上說一個類如果沒有拷貝函數,那麼編譯器就會自動建立一個預設的拷貝函數。下面就讓我們看一下真實的情況。
首先看一個簡單的類X,這個類沒有顯示定義拷貝建構函式。
c++源碼如下:
複製代碼 代碼如下:class X {
private:
int i;
int j;
};
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
下面是其彙編代碼:
複製代碼 代碼如下:_main PROC
; 7 : int main() {
push ebp
mov ebp, esp
sub esp, 16 ; 為對象x1,x2預留16byte的棧空間
; 8 : X x1;//先定義對象x1
; 9 : X x2 = x1;//將x1拷貝給x2
mov eax, DWORD PTR _x1$[ebp];將x1的首地址裡面的內容給寄存器eax,也就將x1中的成員變數i的值給eax
mov DWORD PTR _x2$[ebp], eax;將eax裡面的值寫入x2的首地址,也就是將eax裡面的值寫給x2的成員變數i
mov ecx, DWORD PTR _x1$[ebp+4];將位移x1首地址4byte的記憶體裡面的值給寄存器eax,也就是將x1中的成員變數j的值給ecx
mov DWORD PTR _x2$[ebp+4], ecx;將ecx裡面的值寫入位移x2首地址4byte的記憶體裡面,也就是將ecx裡面的值寫給x2的成員變數j
; 10 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
從彙編代碼裡面可以看出,根本沒有函數被調用,所有的拷貝賦值都是通過寄存器與記憶體位址相互連信完成。和編譯器提供預設建構函式一樣,可以把這種情況看成是編譯器提供了一個無用的拷貝建構函式。
那麼,什麼時候編譯器才真正的提供預設拷貝建構函式,並且顯示調用呢?
下面是一種情況,類X裡面含有虛成員函數:
c++源碼:
複製代碼 代碼如下:class X {
private:
int i;
int j;
public:
virtual ~X() {}//虛解構函式
};
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
由於這裡只討論拷貝函數,所以只看主函數main和拷貝函數裡面的彙編代碼:
下面是主函數main裡面的彙編代碼:
複製代碼 代碼如下:_main PROC
; 9 : int main() {
push ebp
mov ebp, esp
sub esp, 24 ; 由於引入了虛函數,每個類所佔的空間編程12byte 成員變數i,j8byte vptr指標4byte 因此這裡為x1 x2預留24byte
; 10 : X x1;//先定義對象x1
lea ecx, DWORD PTR _x1$[ebp];擷取x1的首地址,放入ecx,為調用建構函式的秘密參數傳入,即this
call ??0X@@QAE@XZ;調用建構函式
; 11 : X x2 = x1;//將x1拷貝給x2
lea eax, DWORD PTR _x1$[ebp];擷取x1的首地址,放入寄存器eax
push eax;將eax壓棧,作為拷貝建構函式的參數
lea ecx, DWORD PTR _x2$[ebp];擷取x2的首地址,放入寄存器ecx,作為調用拷貝建構函式的秘密參數傳入,即this
call ??0X@@QAE@ABV0@@Z;調用拷貝建構函式
; 12 : }
lea ecx, DWORD PTR _x2$[ebp];擷取x2的首地址,放入ecx寄存器,作為調用解構函式傳入的秘密參數,即this
call ??1X@@UAE@XZ ; 調用解構函式
lea ecx, DWORD PTR _x1$[ebp];擷取x1的首地址,放入ecx寄存器,作為調用解構函式傳入的秘密參數,即this
;析構的順序與構建的順序相反
call ??1X@@UAE@XZ ; 調用解構函式
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
可以看到,編譯器為類X提供了預設的拷貝建構函式(非無用的預設拷貝建構函式),並且顯示調用。
由於一個類繼承自虛基類或者繼承自有虛函數成員的基類,使得它本身也含有虛函數成員,因此也就屬於上一種情形。所以編譯器在這種情況下,也會提供非無用的預設拷貝建構函式,並且能夠顯示調用。
下面是第二種情形,類X繼承自類Y,類Y有顯示定義的拷貝建構函式,而類沒有提供拷貝建構函式:
下面是c++源碼:
複製代碼 代碼如下:class Y {
private:
int j;
public:
Y(const Y& y) {}
Y() {};//必須為Y提供預設的建構函式,否則編譯出錯
};
class X : public Y {
private:
int i;
int j;
};
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
下面是mian函數彙編代碼:複製代碼 代碼如下:; 16 : int main() {
push ebp
mov ebp, esp
sub esp, 24 ; 為x1 x2預留24byte空間
; 17 : X x1;//先定義對象x1
lea ecx, DWORD PTR _x1$[ebp];擷取x1的首地址,作為隱含參數傳遞給建構函式
call ??0X@@QAE@XZ;//調用編譯器為類X提供的預設建構函式
; 18 : X x2 = x1;//將x1拷貝給x2
lea eax, DWORD PTR _x1$[ebp];擷取x1的首地址,傳給寄存器eax
push eax;將eax壓棧,作為調用類X的拷貝建構函式的參數
lea ecx, DWORD PTR _x2$[ebp];擷取x2的首地址,作為調用類X的拷貝函數的隱含參數
call ??0X@@QAE@ABV0@@Z;調用編譯器提供的預設拷貝建構函式
; 19 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
下面是類X的拷貝建構函式的彙編碼:複製代碼 代碼如下:??0X@@QAE@ABV0@@Z PROC ; X::X, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx;ecx裡面有x2的首地址
mov eax, DWORD PTR ___that$[ebp];將x1的首地址給eax
push eax;將eax的首地址壓棧,作為調用父類拷貝建構函式的參數
mov ecx, DWORD PTR _this$[ebp];將x2的首地址給ecx,作為隱含參數傳給父類的拷貝建構函式
call ??0Y@@QAE@ABV0@@Z ; 調用父類拷貝建構函式
mov ecx, DWORD PTR _this$[ebp];擷取x2的首地址給ecx
mov edx, DWORD PTR ___that$[ebp];擷取x1的首地址給edx
mov eax, DWORD PTR [edx+4];將位移x1首地址4byte處的記憶體裡面的值寫給eax,即將x1中子類成員變數i的值寫給eax,因為x1的首地址處存放的是父類成員變數i,其值
;由父類拷貝建構函式負責拷貝給x2
mov DWORD PTR [ecx+4], eax;將eax的值寫入偏離x2首地址4byte處的記憶體裡面,即將eax的值寫入x2中子類成員變數i,因為x2的首地址處存放父類成員變數i,其值
;由父類拷貝建構函式負責拷貝
mov ecx, DWORD PTR _this$[ebp];將x2的首地址給ecx
mov edx, DWORD PTR ___that$[ebp];將x1的首地址給edx
mov eax, DWORD PTR [edx+8];將位移x1首地址8byte處的記憶體裡面的值給eax,即將x1中子類成員變數j的值給eax
mov DWORD PTR [ecx+8], eax;將eax的值寫入位移x2首地址8byte的記憶體裡面,即將eax的值寫入x2子類成員j中
mov eax, DWORD PTR _this$[ebp];將x2的首地址給eax,作為傳回值。建構函式總是返回對象首地址
mov esp, ebp
pop ebp
ret 4
??0X@@QAE@ABV0@@Z ENDP
從彙編碼中可以看到,編譯器確實為類X提供了預設的拷貝建構函式,並且進行了顯示調用。而且在調用類X的拷貝建構函式中,首先調用父類的拷貝建構函式,拷貝父類中的成員變數,然後再拷貝子類中的成員變數。
下面是父類Y中的拷貝建構函式彙編碼:
複製代碼 代碼如下:??0Y@@QAE@ABV0@@Z PROC ; Y::Y, COMDAT
; _this$ = ecx
; 5 : Y(const Y& y) {}
push ebp
mov ebp, esp
push ecx;//這裡壓棧的目的是為隱含傳給父類拷貝函數的this(即x2的首地址)
mov DWORD PTR _this$[ebp], ecx;ecx裡面含有x2的首地址(即this),放入剛才的預留空間
mov eax, DWORD PTR _this$[ebp];將x2的首地址寫入eax,作為傳回值。建構函式總是返回對象首地址
mov esp, ebp
pop ebp
ret 4
??0Y@@QAE@ABV0@@Z ENDP ; Y::Y
_TEXT ENDS
從彙編嗎可以看到,由於父類自己顯示定義了拷貝建構函式,編譯器只是負責調用而已,並不提供像上面子類X裡面預設拷貝建構函式的拷貝功能,即並不拷貝父類成員變數i。因為,在c++源碼裡面,父類拷貝建構函式本身就是空函數,什麼也不做。
如果子類X 父類Y都不提供拷貝建構函式,情形有時怎樣的呢?
下面是c++源碼:
複製代碼 代碼如下:class Y {
private:
int j;
};
class X : public Y {
private:
int i;
int j;
};
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
下面是對應的彙編碼:複製代碼 代碼如下:_main PROC
; 12 : int main() {
push ebp
mov ebp, esp
sub esp, 24 ; 為x1 x2預留24byte空間
; 13 : X x1;//先定義對象x1
; 14 : X x2 = x1;//將x1拷貝給x2
mov eax, DWORD PTR _x1$[ebp];擷取x1的首地址裡面的值,存入eax,即擷取x1父類成員變數i的值寫給eax
mov DWORD PTR _x2$[ebp], eax;將eax的值寫入x2的首地址指向的記憶體,即將eax的值寫給x2中的父類成員變數i
mov ecx, DWORD PTR _x1$[ebp+4];擷取位移x1首地址4byte處的記憶體裡面的值,寫入ecx,即擷取x1子類成員變數i的值寫給ecx
mov DWORD PTR _x2$[ebp+4], ecx;將ecx的值寫入位移x2首地址4byte處的記憶體裡面,即將ecx的值寫給x2中子類成員變數i
mov edx, DWORD PTR _x1$[ebp+8];擷取位移x1首地址8byte處的記憶體裡面的值,寫入edx,即擷取x1子類成員變數j的值寫給edx
mov DWORD PTR _x2$[ebp+8], edx;將edx的值寫入位移x2首地址8byte處的記憶體裡面,即將edx的值寫入x2子類成員變數j
; 15 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
可以看到,編譯器執行了拷貝過程,但是提供的是像剛開始的無用的預設拷貝建構函式,無論是拷貝父類成員變數,還是子類成員變數,都沒有函數的調用。
下面看第三種情況,類X含有類Y的成員變數,類Y的成員變數有拷貝建構函式。
c++源碼如下:
複製代碼 代碼如下:class Y {
private:
int j;
public:
Y(const Y& y) {}
Y() {}//必須為Y提供預設的建構函式,否則編譯報錯
};
class X {
private:
int i;
int j;
Y y;
};
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
下面是main函數中的彙編碼:
複製代碼 代碼如下:_main PROC
; 16 : int main() {
push ebp
mov ebp, esp
sub esp, 24 ; 為x1 x2預留24byte的空間
; 17 : X x1;//先定義對象x1
lea ecx, DWORD PTR _x1$[ebp];擷取x1的首地址,作為隱含參數傳遞給建構函式
call ??0X@@QAE@XZ;調用建構函式
; 18 : X x2 = x1;//將x1拷貝給x2
lea eax, DWORD PTR _x1$[ebp];擷取x1的首地址,放入寄存器eax
push eax;將eax壓棧,為作為參數傳遞給編譯器提供的預設拷貝建構函式
lea ecx, DWORD PTR _x2$[ebp];擷取x2的首地址,作為隱含參數傳遞給編譯器提供的預設拷貝建構函式
call ??0X@@QAE@ABV0@@Z;調用拷貝建構函式
; 19 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
下面是編譯器為類X提供的預設拷貝建構函式的彙編碼:複製代碼 代碼如下:??0X@@QAE@ABV0@@Z PROC ; X::X, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx;壓棧的目的是為this(即x2的首地址)預留空間
mov DWORD PTR _this$[ebp], ecx;ecx裡面含有x2的首地址,放入剛才預留的空間裡面
mov eax, DWORD PTR _this$[ebp];將x2的首地址給eax
mov ecx, DWORD PTR ___that$[ebp];將x1的首地址給ecx
mov edx, DWORD PTR [ecx];將x1的首地址的內容寫入edx,即將x1中的成員變數i寫入edx
mov DWORD PTR [eax], edx;將edx的值寫入x2的首地址,即將edx的值寫入x2的成員變數i
mov eax, DWORD PTR _this$[ebp];將x2的首地址寫入寄存器eax
mov ecx, DWORD PTR ___that$[ebp];將x1的首地址寫入寄存器ecx
mov edx, DWORD PTR [ecx+4];將位移x1首地址4byte處的記憶體裡面的值寫入edx,即將x1的成員變數j的值寫入edx
mov DWORD PTR [eax+4], edx;將edx的值寫入位移x2首地址4byte處的記憶體,即將edx的值寫入x2的成員變數j
mov eax, DWORD PTR ___that$[ebp];將x1的首地址存入寄存器eax
add eax, 8;//將x1的首地址加8,得到x1中成員對象y所處的地址,放入eax中
push eax;將eax的值壓棧,作為調用成員變數y的拷貝函數的參數
mov ecx, DWORD PTR _this$[ebp];將x2的首地址存入寄存器ecx
add ecx, 8;將x2的首地址加8,得到x2中成員對象y所在地址,放入ecx,這個地址作為隱含的參數傳給成員變數函數的拷貝建構函式
call ??0Y@@QAE@ABV0@@Z ; 調用成員對象y的拷貝建構函式
mov eax, DWORD PTR _this$[ebp];將x2的首地址放入eax,作為傳回值。建構函式總是返回對象的首地址
mov esp, ebp
pop ebp
ret 4
??0X@@QAE@ABV0@@Z ENDP
從彙編嗎可以看到,調用類X拷貝建構函式的時候,先將x1中的成員變數i,j拷貝到x2中,然後才是調用成員對象y的拷貝建構函式拷貝y中的成員變數。這和繼承不同,在繼承中,總是先調用父類的拷貝建構函式,再進行子類中的拷貝。這說明,對於這種包含成員對象的情況,成員對象的拷貝函數調用時機與他們定義的位置有關。在這裡,類X的成員對象y在成員變數i,j之後定義,因此,它的拷貝建構函式要等拷貝完i,j之後才會被調用。
下面是類Y中的拷貝建構函式彙編代碼:
複製代碼 代碼如下:??0Y@@QAE@ABV0@@Z PROC ; Y::Y, COMDAT
; _this$ = ecx
; 6 : Y(const Y& y) {}
push ebp
mov ebp, esp
push ecx;壓棧ecx的目的是為了存放this(x2中成員對象y的首地址)預留空間
mov DWORD PTR _this$[ebp], ecx;ecx裡面有x2中成員對象y的首地址,放入剛才的預留空間
mov eax, DWORD PTR _this$[ebp];將x2中成員變數首地址放入eax,作為傳回值。建構函式總是返回對象首地址
mov esp, ebp
pop ebp
ret 4
??0Y@@QAE@ABV0@@Z ENDP
從代碼中可以看到,由於類Y顯示定義了拷貝建構函式,編譯器也只是負責顯示調用,並沒有提供任何的拷貝功能。因為在類Y中,拷貝建構函式就是被定義成了一個空函數
和繼承一樣,如果成員對象也沒有拷貝建構函式呢?
下面是c++源碼:
複製代碼 代碼如下:class Y {
private:
int j;
};
class X {
private:
int i;
int j;
Y y;
};
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
下面是對象的彙編碼:
複製代碼 代碼如下:_main PROC
; 14 : int main() {
push ebp
mov ebp, esp
sub esp, 24 ; 00000018H
; 15 : X x1;//先定義對象x1
; 16 : X x2 = x1;//將x1拷貝給x2
mov eax, DWORD PTR _x1$[ebp];將x1中首地址的內容寫入eax,即將x1中的成員變數值i寫入eax
mov DWORD PTR _x2$[ebp], eax;將eax的值寫入x2的首地址處,即將eax的值寫入x2的成員變數i
mov ecx, DWORD PTR _x1$[ebp+4];將位移x1首地址4byte處的記憶體裡面的內容寫入ecx,即將x1中成員變數j的值寫入ecx
mov DWORD PTR _x2$[ebp+4], ecx;將ecx的值寫入位移x2首地址4byte處的記憶體,即將ecx的值寫入x2中成員變數j
mov edx, DWORD PTR _x1$[ebp+8];將位移x1首地址8byte處(這裡是x1成員對象y的首地址)的記憶體值寫入edx,即將x1中成員對象y中的成員變數i值寫入edx
mov DWORD PTR _x2$[ebp+8], edx;將edx的值寫入位移x2首地址8byte處(這裡是x2成員對象y的首地址)的記憶體裡面,即將edx的值寫入x2中成員對象y的成員變數i裡面
從彙編嗎可以看出,編譯器在這種情況下任然只是提供無用的預設拷貝建構函式,即沒有顯示的函數調用,只是用寄存器和記憶體之間的通訊完成拷貝過程
綜合上面的分析,可以看到:
對於一個類,如果它沒有顯示定義拷貝建構函式,編譯器並不總是提供非無用的預設拷貝建構函式,除非:
1 該類包含虛函數成員函數(包括繼承自虛基類或者繼承的基類中有虛函數成員),這時編譯器提供為該類提供非無用的預設拷貝建構函式
2 該類繼承自一個基類,而基類含有自訂的拷貝函數,這時編譯器為該類提供非無用的預設拷貝建構函式。(如果基類本身沒有定義拷貝建構函式,但是編譯器會為基類提供一個非無用的預設拷貝建構函式,也屬於這種情況。也就是說,基類只要含有一個非無用的拷貝建構函式就行,不管這個非無用的拷貝建構函式是自訂的,還是編譯器提供的)
3 該類包含一個成員對象,而該成員對象有自定的拷貝建構函式,這時編譯器為該類提供非無用的預設拷貝建構函式。(如果成員對象本身沒有定義拷貝建構函式,但是編譯器會為成員對象提供一個非無用的預設拷貝建構函式,也屬於這種情況。也就是說,成員對象只要包含一個非無用的拷貝建構函式就行,不管這個非無用的拷貝建構函式時自訂的,還是編譯器提供的。這中情況和上一種類似).
並且,如果一個類自訂了一個拷貝建構函式,編譯器只是負責調用,不會額外提供任何拷貝過程;而對於編譯器提供的預設拷貝函數,不管是無用的,還是非無用的,都僅僅只是位拷貝(即淺拷貝).