c++中的源程序:
int main() {
X x;
}
下面為其匯編程序:
push ebp;ebp為一個寄存器,總是指向一個函數調用堆棧的棧底,作為基址,用偏移量來訪問該調用棧上的變量,但這里沒有任何變量要訪問,因此不起作用
mov ebp, esp;這兩句的作用是為了保存調用main之前堆棧的基址ebp的值,并將ebp指向main調用棧的棧底
push ecx;將寄存器ecx的值壓棧, 棧頂指針esp向前移動4byte
;這句的作用,為即將要創建的對象預留了4byte的空間,并向里面寫入ecx的值
; 8 : X x;
; 9 : }
xor eax, eax;eax也是一個寄存器,這里不起作用
mov esp, ebp;將棧頂指針移動到push ecx前的位置,即釋放了4byte的空間
pop ebp;恢復基址到main調用之前的狀態
ret 0;函數返回
下面再看一段c++程序:
int main() {
X x;
}
下面為對應匯編碼:
push ebp
mov ebp, esp
sub esp, 8; 棧頂指針移動8byte,剛好等于類X的大小
; 9 : X x;
; 10 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
所以,綜上所述,在一個類沒有明確定義構造函數的時候,編譯器不會有任何的函數調用來進行初始化操作,僅僅是移動棧頂留出對象所需空間,也就是說,這種情況下,編譯器根本不會提供默認的構造函數。
那么,書上說的由編譯器提供默認的構造函數到底是怎么一回事呢?
下面看第一種情況,類里面有虛成員函數:
c++源碼如下:
int main() {
X x;
}
下面是main函數對應的匯編碼:
push ebp
mov ebp, esp
sub esp, 12 ; 為對象x預留12byte的空間,成員變量int i,int j占8byte,由于有虛函數,因此vptr指針占4byte
; 14 : X x;
lea ecx, DWORD PTR _x$[ebp];獲取x對象的首地址,存入ecx寄存器
call ??0X@@QAE@XZ;這里調用x的構造函數
; 15 : }
lea ecx, DWORD PTR _x$[ebp];獲取對象x的首地址
call ??1X@@UAE@XZ ; 調用析構函數
xor eax, eax
mov esp, ebp
pop ebp
ret 0
下面是構造函數的匯編碼:
從上面可以看出,類里面含有虛函數時,在沒有明確定義構造函數時,編譯器確實會為我們提供一個默認的構造函數。因此當一個類繼承自虛基類時,也滿足上面的情形。
接下來是第二種情形,類Y繼承自類X,X明確定義了一個默認的構造函數(并非編譯器提供),而類Y不定義任何構造函數:
先來看看c++源碼:
class Y : public X{//Y繼承自X
private:
int i;
};
int main() {
Y y;
}
下面是main函數對應的匯編碼:
push ebp
mov ebp, esp
sub esp, 12 ; 為對象y預留12byte空間,y自身成員變量int i占4byte 父類中的成員變量int i int j占8byte
; 20 : Y y;
lea ecx, DWORD PTR _y$[ebp];獲取對象y的首地址,存入寄存器ecx
call ??0Y@@QAE@XZ;調用對象y的構造函數
; 21 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
下面是編譯器提供的y對象默認構造函數的匯編碼:
下面是父類X的構造函數匯編碼:
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx; ecx中存有對象y的首地址
; 8 : i = 0;
mov eax, DWORD PTR _this$[ebp];對象y首地址給寄存器eax
mov DWORD PTR [eax], 0;初始化父類中的變量i
; 9 : j = 1;
mov ecx, DWORD PTR _this$[ebp];對象y首地址給寄存器ecx
mov DWORD PTR [ecx+4], 1;初始化父類中的變量j,在對象y的內存空間中,從首地址開始的8比特用來存儲繼承自父對象的成員變量,后4byte用來存儲自己的成員變量
;由于首地址存儲了父類成員變量i,因此內存地址要從對象y的首地址要移動4byte,才能找到父類成員變量j所處位置
; 10 : }
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0
如果父類X中也沒有定義任何構造函數會怎樣?
下面是c++源碼:
};
class Y : public X{//Y繼承自X
private:
int i;
};
int main() {
Y y;
}
下面是main函數匯編碼:
push ebp
mov ebp, esp
sub esp, 12 ; 和剛才一樣,為對象y預留12byte
; 17 : Y y;
; 18 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
那么,要是父類中帶參數的構造函數,而子類中沒有構造函數呢?這時候編譯器會報錯。
下面看第三種情況,類Y中包含成員對象X,成員對象有顯示定義的默認構造函數,而類Y沒有任何構造函數:
先看c++源碼:
push ebp
mov ebp, esp
sub esp, 12 ; 和剛才一樣,為對象y預留12byte
; 17 : Y y;
; 18 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
push ebp
mov ebp, esp
sub esp, 12 ; 為對象y預留12byte 成員對象的變量占8byte 對象y自身占變量占4byte 成員對象包含在對象y中
; 22 : Y y;
lea ecx, DWORD PTR _y$[ebp];對象y的首地址存入ecx
call ??0Y@@QAE@XZ;調用對象y的構造函數,由編譯器提供的默認構造函數
; 23 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
對象y的構造函數匯編碼:
成員對象x的構造函數匯編碼:
; 7 : X() {
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx;ecx中存有成員對象x的起始地址
; 8 : i = 0;
mov eax, DWORD PTR _this$[ebp];成員對象x的起始地址給eax寄存器
mov DWORD PTR [eax], 0;初始化成員對象x中額成員變量i
; 9 : j = 0;
mov ecx, DWORD PTR _this$[ebp];成員對象x的起始地址給ecx寄存器
mov DWORD PTR [ecx+4], 0;初始化成員對象x中額成員變量j 加4的原因是j的地址偏離了成員對象x起始地址4byte(即成員對象x的成員變量i的字節數)
; 10 : }
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0
下面是c++源碼:
};
class Y {
private:
int i;
X x;//x成員對象
};
int main() {
Y y;
}
push ebp
mov ebp, esp
sub esp, 12 ; 為對象預留12byte空間
; 18 : Y y;
; 19 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
那要是成員對象x有帶參數的構造函數(即非默認構造函數),而對象y沒有任何構造函數呢?此時,編譯器會報錯。
這種情形和前一種情形很相似。
綜合以上的情況,可以總結出,對于一個類不含任何構造函數,而編譯器會提供默認的構造函數,有一下3種情形:
1 類本身函數虛成員函數或者繼承自虛基類
2 類的基類有構造函數,并且基類構造函數還是顯示定義的默認構造函數(非編譯器提供),若基類的構造函數帶有參數(即非默認構造函數),編譯器報錯
3 這種情況和上一種相似,類的成員對象有構造函數,并且成員對象的構造函數還是顯示定義的默認構造函數(非編譯器提供);若成員對象的構造函數帶有參數(即非默認構造函數),編譯器報錯。
以上參考了《VC++深入詳解》里面的知識點,還有自己的分析,歡迎指正
新聞熱點
疑難解答
圖片精選