《c++編程思想》上說一個類如果沒有拷貝函數(shù),那么編譯器就會自動創(chuàng)建一個默認(rèn)的拷貝函數(shù)。下面就讓我們看一下真實的情況。
首先看一個簡單的類X,這個類沒有顯示定義拷貝構(gòu)造函數(shù)。
c++源碼如下:
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
; 7 : int main() {
push ebp
mov ebp, esp
sub esp, 16 ; 為對象x1,x2預(yù)留16byte的棧空間
; 8 : X x1;//先定義對象x1
; 9 : X x2 = x1;//將x1拷貝給x2
mov eax, DWORD PTR _x1$[ebp];將x1的首地址里面的內(nèi)容給寄存器eax,也就將x1中的成員變量i的值給eax
mov DWORD PTR _x2$[ebp], eax;將eax里面的值寫入x2的首地址,也就是將eax里面的值寫給x2的成員變量i
mov ecx, DWORD PTR _x1$[ebp+4];將偏移x1首地址4byte的內(nèi)存里面的值給寄存器eax,也就是將x1中的成員變量j的值給ecx
mov DWORD PTR _x2$[ebp+4], ecx;將ecx里面的值寫入偏移x2首地址4byte的內(nèi)存里面,也就是將ecx里面的值寫給x2的成員變量j
; 10 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
那么,什么時候編譯器才真正的提供默認(rèn)拷貝構(gòu)造函數(shù),并且顯示調(diào)用呢?
下面是一種情況,類X里面含有虛成員函數(shù):
c++源碼:
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
下面是主函數(shù)main里面的匯編代碼:
; 9 : int main() {
push ebp
mov ebp, esp
sub esp, 24 ; 由于引入了虛函數(shù),每個類所占的空間編程12byte 成員變量i,j8byte vptr指針4byte 因此這里為x1 x2預(yù)留24byte
; 10 : X x1;//先定義對象x1
lea ecx, DWORD PTR _x1$[ebp];獲取x1的首地址,放入ecx,為調(diào)用構(gòu)造函數(shù)的秘密參數(shù)傳入,即this
call ??0X@@QAE@XZ;調(diào)用構(gòu)造函數(shù)
; 11 : X x2 = x1;//將x1拷貝給x2
lea eax, DWORD PTR _x1$[ebp];獲取x1的首地址,放入寄存器eax
push eax;將eax壓棧,作為拷貝構(gòu)造函數(shù)的參數(shù)
lea ecx, DWORD PTR _x2$[ebp];獲取x2的首地址,放入寄存器ecx,作為調(diào)用拷貝構(gòu)造函數(shù)的秘密參數(shù)傳入,即this
call ??0X@@QAE@ABV0@@Z;調(diào)用拷貝構(gòu)造函數(shù)
; 12 : }
lea ecx, DWORD PTR _x2$[ebp];獲取x2的首地址,放入ecx寄存器,作為調(diào)用析構(gòu)函數(shù)傳入的秘密參數(shù),即this
call ??1X@@UAE@XZ ; 調(diào)用析構(gòu)函數(shù)
lea ecx, DWORD PTR _x1$[ebp];獲取x1的首地址,放入ecx寄存器,作為調(diào)用析構(gòu)函數(shù)傳入的秘密參數(shù),即this
;析構(gòu)的順序與構(gòu)建的順序相反
call ??1X@@UAE@XZ ; 調(diào)用析構(gòu)函數(shù)
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
由于一個類繼承自虛基類或者繼承自有虛函數(shù)成員的基類,使得它本身也含有虛函數(shù)成員,因此也就屬于上一種情形。所以編譯器在這種情況下,也會提供非無用的默認(rèn)拷貝構(gòu)造函數(shù),并且能夠顯示調(diào)用。
下面是第二種情形,類X繼承自類Y,類Y有顯示定義的拷貝構(gòu)造函數(shù),而類沒有提供拷貝構(gòu)造函數(shù):
下面是c++源碼:
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
push ebp
mov ebp, esp
sub esp, 24 ; 為x1 x2預(yù)留24byte空間
; 17 : X x1;//先定義對象x1
lea ecx, DWORD PTR _x1$[ebp];獲取x1的首地址,作為隱含參數(shù)傳遞給構(gòu)造函數(shù)
call ??0X@@QAE@XZ;//調(diào)用編譯器為類X提供的默認(rèn)構(gòu)造函數(shù)
; 18 : X x2 = x1;//將x1拷貝給x2
lea eax, DWORD PTR _x1$[ebp];獲取x1的首地址,傳給寄存器eax
push eax;將eax壓棧,作為調(diào)用類X的拷貝構(gòu)造函數(shù)的參數(shù)
lea ecx, DWORD PTR _x2$[ebp];獲取x2的首地址,作為調(diào)用類X的拷貝函數(shù)的隱含參數(shù)
call ??0X@@QAE@ABV0@@Z;調(diào)用編譯器提供的默認(rèn)拷貝構(gòu)造函數(shù)
; 19 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
下面是父類Y中的拷貝構(gòu)造函數(shù)匯編碼:
; 5 : Y(const Y& y) {}
push ebp
mov ebp, esp
push ecx;//這里壓棧的目的是為隱含傳給父類拷貝函數(shù)的this(即x2的首地址)
mov DWORD PTR _this$[ebp], ecx;ecx里面含有x2的首地址(即this),放入剛才的預(yù)留空間
mov eax, DWORD PTR _this$[ebp];將x2的首地址寫入eax,作為返回值。構(gòu)造函數(shù)總是返回對象首地址
mov esp, ebp
pop ebp
ret 4
??0Y@@QAE@ABV0@@Z ENDP ; Y::Y
_TEXT ENDS
如果子類X 父類Y都不提供拷貝構(gòu)造函數(shù),情形有時怎樣的呢?
下面是c++源碼:
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
; 12 : int main() {
push ebp
mov ebp, esp
sub esp, 24 ; 為x1 x2預(yù)留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的首地址指向的內(nèi)存,即將eax的值寫給x2中的父類成員變量i
mov ecx, DWORD PTR _x1$[ebp+4];獲取偏移x1首地址4byte處的內(nèi)存里面的值,寫入ecx,即獲取x1子類成員變量i的值寫給ecx
mov DWORD PTR _x2$[ebp+4], ecx;將ecx的值寫入偏移x2首地址4byte處的內(nèi)存里面,即將ecx的值寫給x2中子類成員變量i
mov edx, DWORD PTR _x1$[ebp+8];獲取偏移x1首地址8byte處的內(nèi)存里面的值,寫入edx,即獲取x1子類成員變量j的值寫給edx
mov DWORD PTR _x2$[ebp+8], edx;將edx的值寫入偏移x2首地址8byte處的內(nèi)存里面,即將edx的值寫入x2子類成員變量j
; 15 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
下面看第三種情況,類X含有類Y的成員變量,類Y的成員變量有拷貝構(gòu)造函數(shù)。
c++源碼如下:
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
; 16 : int main() {
push ebp
mov ebp, esp
sub esp, 24 ; 為x1 x2預(yù)留24byte的空間
; 17 : X x1;//先定義對象x1
lea ecx, DWORD PTR _x1$[ebp];獲取x1的首地址,作為隱含參數(shù)傳遞給構(gòu)造函數(shù)
call ??0X@@QAE@XZ;調(diào)用構(gòu)造函數(shù)
; 18 : X x2 = x1;//將x1拷貝給x2
lea eax, DWORD PTR _x1$[ebp];獲取x1的首地址,放入寄存器eax
push eax;將eax壓棧,為作為參數(shù)傳遞給編譯器提供的默認(rèn)拷貝構(gòu)造函數(shù)
lea ecx, DWORD PTR _x2$[ebp];獲取x2的首地址,作為隱含參數(shù)傳遞給編譯器提供的默認(rèn)拷貝構(gòu)造函數(shù)
call ??0X@@QAE@ABV0@@Z;調(diào)用拷貝構(gòu)造函數(shù)
; 19 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
下面是類Y中的拷貝構(gòu)造函數(shù)匯編代碼:
; 6 : Y(const Y& y) {}
push ebp
mov ebp, esp
push ecx;壓棧ecx的目的是為了存放this(x2中成員對象y的首地址)預(yù)留空間
mov DWORD PTR _this$[ebp], ecx;ecx里面有x2中成員對象y的首地址,放入剛才的預(yù)留空間
mov eax, DWORD PTR _this$[ebp];將x2中成員變量首地址放入eax,作為返回值。構(gòu)造函數(shù)總是返回對象首地址
mov esp, ebp
pop ebp
ret 4
??0Y@@QAE@ABV0@@Z ENDP
和繼承一樣,如果成員對象也沒有拷貝構(gòu)造函數(shù)呢?
下面是c++源碼:
};
class X {
private:
int i;
int j;
Y y;
};
int main() {
X x1;//先定義對象x1
X x2 = x1;//將x1拷貝給x2
}
; 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中首地址的內(nèi)容寫入eax,即將x1中的成員變量值i寫入eax
mov DWORD PTR _x2$[ebp], eax;將eax的值寫入x2的首地址處,即將eax的值寫入x2的成員變量i
mov ecx, DWORD PTR _x1$[ebp+4];將偏移x1首地址4byte處的內(nèi)存里面的內(nèi)容寫入ecx,即將x1中成員變量j的值寫入ecx
mov DWORD PTR _x2$[ebp+4], ecx;將ecx的值寫入偏移x2首地址4byte處的內(nèi)存,即將ecx的值寫入x2中成員變量j
mov edx, DWORD PTR _x1$[ebp+8];將偏移x1首地址8byte處(這里是x1成員對象y的首地址)的內(nèi)存值寫入edx,即將x1中成員對象y中的成員變量i值寫入edx
mov DWORD PTR _x2$[ebp+8], edx;將edx的值寫入偏移x2首地址8byte處(這里是x2成員對象y的首地址)的內(nèi)存里面,即將edx的值寫入x2中成員對象y的成員變量i里面
綜合上面的分析,可以看到:
對于一個類,如果它沒有顯示定義拷貝構(gòu)造函數(shù),編譯器并不總是提供非無用的默認(rèn)拷貝構(gòu)造函數(shù),除非:
1 該類包含虛函數(shù)成員函數(shù)(包括繼承自虛基類或者繼承的基類中有虛函數(shù)成員),這時編譯器提供為該類提供非無用的默認(rèn)拷貝構(gòu)造函數(shù)
2 該類繼承自一個基類,而基類含有自定義的拷貝函數(shù),這時編譯器為該類提供非無用的默認(rèn)拷貝構(gòu)造函數(shù)。(如果基類本身沒有定義拷貝構(gòu)造函數(shù),但是編譯器會為基類提供一個非無用的默認(rèn)拷貝構(gòu)造函數(shù),也屬于這種情況。也就是說,基類只要含有一個非無用的拷貝構(gòu)造函數(shù)就行,不管這個非無用的拷貝構(gòu)造函數(shù)是自定義的,還是編譯器提供的)
3 該類包含一個成員對象,而該成員對象有自定的拷貝構(gòu)造函數(shù),這時編譯器為該類提供非無用的默認(rèn)拷貝構(gòu)造函數(shù)。(如果成員對象本身沒有定義拷貝構(gòu)造函數(shù),但是編譯器會為成員對象提供一個非無用的默認(rèn)拷貝構(gòu)造函數(shù),也屬于這種情況。也就是說,成員對象只要包含一個非無用的拷貝構(gòu)造函數(shù)就行,不管這個非無用的拷貝構(gòu)造函數(shù)時自定義的,還是編譯器提供的。這中情況和上一種類似).
并且,如果一個類自定義了一個拷貝構(gòu)造函數(shù),編譯器只是負(fù)責(zé)調(diào)用,不會額外提供任何拷貝過程;而對于編譯器提供的默認(rèn)拷貝函數(shù),不管是無用的,還是非無用的,都僅僅只是位拷貝(即淺拷貝).
新聞熱點
疑難解答