c++中的來源程式:
複製代碼 代碼如下:class X {
private:
int i;
};
int main() {
X x;
}
上面的類X沒有定義建構函式,僅僅有一個int i。
下面為其組譯工具:
複製代碼 代碼如下:; 7 : int main() {
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;函數返回
通過彙編發現,通過push ecx,編譯器將堆棧棧頂移動4byte,並將寄存器的ecx的值寫入,類X只含有一個int,大小剛好為4byte,因此這一句可以看成是為對象x分配空間。而接下來並沒有任何函數的調用,來對這一塊地區進行適當的初始化。所以,在沒有明確定義一個建構函式的時候,不會有任何的初始化操作。
下面再看一段c++程式:
複製代碼 代碼如下:class X {
private:
int i;
int j;//增加一個成員變數int j
};
int main() {
X x;
}
與上面相比,在類X裡面增加了一個成員變數int j,類的大小變為8位元組。
下面為對應彙編碼:
複製代碼 代碼如下:; 8 : int main() {
push ebp
mov ebp, esp
sub esp, 8; 棧頂指標移動8byte,剛好等於類X的大小
; 9 : X x;
; 10 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
從彙編碼看出,通過sub esp,8指令,堆棧確實留出了8byte的空間,剛好等於類X的大小,同樣沒有調用任何函數,來進行初始化操作。
所以,綜上所述,在一個類沒有明確定義建構函式的時候,編譯器不會有任何的函數調用來進行初始化操作,僅僅是移動棧頂留出對象所需空間,也就是說,這種情況下,編譯器根本不會提供預設的建構函式。
那麼,書上說的由編譯器提供預設的建構函式到底是怎麼一回事呢?
下面看第一種情況,類裡面有虛成員函數:
c++源碼如下:
複製代碼 代碼如下:class X {
private:
int i;
int j;//增加一個成員變數int j
public:
virtual ~X() {
}
};
int main() {
X x;
}
解構函式為虛函數
下面是main函數對應的彙編碼:
複製代碼 代碼如下:; 13 : int 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
可以看到,對象x的建構函式被調用了,編譯器確實合成了預設的建構函式。
下面是建構函式的彙編碼:
複製代碼 代碼如下:??0X@@QAE@XZ PROC ; X::X, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx;ecx寄存器存有對象x的首地址
mov eax, DWORD PTR _this$[ebp];將對象x的首地址給寄存器eax
mov DWORD PTR [eax], OFFSET ??_7X@@6B@;這裡設定vptr指標的值,指向vtable (OFFSET ??_7X@@6B@是獲得vtable的地址)
;並且通過這句,也可以證明vptr指標位於對象其真實位址處
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0
可以看到,由於有虛函數,涉及到多態,因此構函數初始化了vptr指標,但是沒有為另外兩個變數int i,int j賦值。
從上面可以看出,類裡面含有虛函數時,在沒有明確定義建構函式時,編譯器確實會為我們提供一個預設的建構函式。因此當一個類繼承自虛基類時,也滿足上面的情形。
接下來是第二種情形,類Y繼承自類X,X明確定義了一個預設的建構函式(並非編譯器提供),而類Y不定義任何建構函式:
先來看看c++源碼:
複製代碼 代碼如下:class X {
private:
int i;
int j;
public:
X() {//X顯示定義的預設建構函式
i = 0;
j = 1;
}
};
class Y : public X{//Y繼承自X
private:
int i;
};
int main() {
Y y;
}
類Y裡面沒有顯示定義任何建構函式
下面是main函數對應的彙編碼:
複製代碼 代碼如下:; 19 : int 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
main函數中調用了由編譯器提供的預設y對象的預設建構函式。
下面是編譯器提供的y對象預設建構函式的彙編碼:
複製代碼 代碼如下:??0Y@@QAE@XZ PROC ; Y::Y, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx;ecx中存有對象y的首地址
mov ecx, DWORD PTR _this$[ebp]
call ??0X@@QAE@XZ ; 調用父類X的建構函式
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0
??0Y@@QAE@XZ ENDP
可以看到y對象的建構函式又調用了父類的建構函式來初始化繼承自父類的成員變數,但自身成員變數依然沒有初始化。
下面是父類X的建構函式彙編碼:
複製代碼 代碼如下:; 7 : 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
可以看到,y對象繼承自父類的成員變數由父類建構函式初始化。父物件包含在子物件中,並且this指標,即寄存器ecx儲存的首地址始終是子物件y的首地址。
如果父類X中也沒有定義任何建構函式會怎樣?
下面是c++源碼:
複製代碼 代碼如下:class X {
private:
int i;
int j;
};
class Y : public X{//Y繼承自X
private:
int i;
};
int main() {
Y y;
}
父類和子類都沒有任何建構函式。
下面是main函數彙編碼:
複製代碼 代碼如下:; 16 : int 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
可以看到main中根本沒有任何函數的調用,也就是說,編譯器沒有為子物件y提供預設建構函式。
那麼,要是父類中帶參數的建構函式,而子類中沒有建構函式呢?這時候編譯器會報錯。
下面看第三種情況,類Y中包含成員對象X,成員對象有顯示定義的預設建構函式,而類Y沒有任何建構函式:
先看c++源碼:
複製代碼 代碼如下:; 16 : int 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
類X為類Y的成員對象
下面是main函數的彙編碼:
複製代碼 代碼如下:; 21 : int main() {
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的建構函式被調用,即編譯器提供了預設的建構函式
對象y的建構函式彙編碼:
複製代碼 代碼如下:??0Y@@QAE@XZ PROC ; Y::Y, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx;ecx中存有對象y的首地址
mov ecx, DWORD PTR _this$[ebp]
add ecx, 4;加4是因為對象y首地址起始處儲存的是自身成員變數i
call ??0X@@QAE@XZ ; 調用成員對象x的建構函式
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0
對象y的建構函式調用了成員對象x的建構函式,用來初始化成員對象中的成員變數,對象y自身的成員變數沒有初始化。
成員對象x的建構函式彙編碼:
複製代碼 代碼如下:??0X@@QAE@XZ PROC ; X::X, COMDAT
; _this$ = ecx
; 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
但是,如果成員對象x也沒有任何建構函式,情形會怎樣呢?
下面是c++源碼:
複製代碼 代碼如下:class X {
private:
int i;
int j;
};
class Y {
private:
int i;
X x;//x成員對象
};
int main() {
Y y;
}
下面是main函數彙編碼:
複製代碼 代碼如下:; 17 : int main() {
push ebp
mov ebp, esp
sub esp, 12 ; 為對象預留12byte空間
; 18 : Y y;
; 19 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
可以看到,main函數裡面沒有任何函數調用,也就是說編譯器沒有提供預設建構函式。
那要是成員對象x有帶參數的建構函式(即非預設建構函式),而對象y沒有任何建構函式呢?此時,編譯器會報錯。
這種情形和前一種情形很相似。
綜合以上的情況,可以總結出,對於一個類不含任何建構函式,而編譯器會提供預設的建構函式,有一下3種情形:
1 類本身函數虛成員函數或者繼承自虛基類
2 類的基類有建構函式,並且基類建構函式還是顯示定義的預設建構函式(非編譯器提供),若基類的建構函式帶有參數(即非預設建構函式),編譯器報錯
3 這種情況和上一種相似,類的成員對象有建構函式,並且成員對象的建構函式還是顯示定義的預設建構函式(非編譯器提供);若成員對象的建構函式帶有參數(即非預設建構函式),編譯器報錯。
以上參考了《VC++深入詳解》裡面的知識點,還有自己的分析,歡迎指正