|
介紹 初 看到這個(gè)題目,你可能會(huì)有些疑惑:C++類對(duì)象的創(chuàng)建還有什么好說的,不就是調(diào)用構(gòu)造函數(shù)么?實(shí)際上情況并不是想象中的那么簡(jiǎn)單,大量的細(xì)節(jié)被隱藏或者被 忽略了,而這些細(xì)節(jié)又是解決一些其他問題的關(guān)鍵,所以我們很有必要深入到這塊"神秘"的區(qū)域,去探索鮮為人知的秘密。
分配空間(Allocation) 創(chuàng)建C++類對(duì)象的第一步就是為其分配內(nèi)存空間。對(duì)于全局對(duì)象,靜態(tài)對(duì)象以及分配在棧區(qū)域內(nèi)的對(duì)象,對(duì)它們的內(nèi)存分配是在編譯階段就完成了,而對(duì)于分配在堆區(qū)域內(nèi)的對(duì)象,它們的分配是在運(yùn)行是動(dòng)態(tài)進(jìn)行的。內(nèi)存空間的分配過程涉及到兩個(gè)關(guān)鍵的問題:
需要分配空間的大小,即類對(duì)象的大小。這么問題對(duì)于編譯器來說并不是什么問題,因?yàn)轭悓?duì)象的大小就是由它決定的,對(duì)于要分配多少內(nèi)存,它最清楚不過了。 是否有足夠的內(nèi)存空間來滿足分配。對(duì)于不同的情況我們需要具體問題具體分析: 全局對(duì)象和靜態(tài)對(duì)象。編譯器會(huì)為他們劃分一個(gè)獨(dú)立的段(全局段)為他們分配足夠的空間,一般不會(huì)涉及到內(nèi)存空間不夠的問題。 分配在棧區(qū)域的對(duì)象。棧區(qū)域的大小由編譯器的設(shè)置決定,不管具體的設(shè)置怎樣,總歸它是有一個(gè)具體的值,所以??臻g是有限的,在棧區(qū)域內(nèi)同時(shí)分配大量的對(duì)象會(huì)導(dǎo)致棧區(qū)域溢出,由于棧區(qū)域的分配是在編譯階段完成的,所以在棧區(qū)域溢出的時(shí)候會(huì)拋出編譯階段的異常。 分配在堆區(qū)域的對(duì)象。堆內(nèi)存空間的分配是在運(yùn)行是進(jìn)行的,由于堆空間也是有限的,在棧區(qū)域內(nèi)試圖同時(shí)分配大量的對(duì)象會(huì)導(dǎo)致導(dǎo)致分配失敗,通常情況會(huì)拋出運(yùn)行時(shí)異?;蛘叻祷匾粋€(gè)沒有意義的值(通常是0)。 初始化(Initialization) 這 一階段是對(duì)象創(chuàng)建過程中最神秘的一個(gè)階段,也是最容易被忽視的一個(gè)階段。要想知道這一階段具體完成那些任務(wù),關(guān)鍵是要區(qū)分兩個(gè)容易混淆的概念:初始化 (Initialization)和賦值(Assignment)。初始化早于賦值,它是隨著對(duì)象的誕生一起進(jìn)行的。而賦值是在對(duì)象誕生以后又給予它一個(gè) 新的值。這里我想到了一個(gè)很好的例子:任何一個(gè)在醫(yī)院誕生的嬰兒,在它誕生的同時(shí)醫(yī)院會(huì)給它一個(gè)標(biāo)識(shí),以防止和其他的嬰兒混淆,這個(gè)標(biāo)識(shí)通常是嬰兒母親所 在床鋪的編號(hào),醫(yī)院給嬰兒一個(gè)標(biāo)識(shí)的過程可以看作是初始化。當(dāng)然當(dāng)嬰兒的父母拿到他們會(huì)為他們起個(gè)名字,起名字的過程就可以看作是賦值。經(jīng)過初始化和賦值 后,其他人就可以通過名字來標(biāo)識(shí)他們的身份了。區(qū)分了這兩個(gè)概念后,我們?cè)俎D(zhuǎn)到對(duì)對(duì)象初始化的分析上。對(duì)類對(duì)象的初始化,實(shí)際上是對(duì)類對(duì)象內(nèi)的所有數(shù)據(jù)成 員進(jìn)行初始化。C++已經(jīng)為我們提供了對(duì)類對(duì)象進(jìn)行初始化的能力,我們可以通過實(shí)現(xiàn)構(gòu)造函數(shù)的初始化列表(member initialization list)來實(shí)現(xiàn)。具體的情況是否是這樣的呢?下面我們就看看具體的情況是什么樣的吧。我寫了兩個(gè)簡(jiǎn)單的類: class CInnerClass { public: CInnerClass(int id):m_iID(id) {} CInnerClass& operator = (const CInnerClass& rb) { m_iID = rb.m_iID; return *this; } private: int m_iID; }; class CJdBase { public: CJdBase::CJdBase(int id):m_innerObj(id),m_iID(id){ m_innerObj = 10; } private: CInnerClass m_innerObj; int m_iID; };我們重點(diǎn)是看看CJdBase類的構(gòu)造函數(shù)。CJdBase類的構(gòu)造函數(shù)提供了初始化列表,用來初始化其成員變量,其相應(yīng)的匯編代碼如下(注:我只保留了關(guān)鍵的代碼): mov DWORD PTR _this$[ebp], ecx mov eax, DWORD PTR _id$[ebp] push eax mov ecx, DWORD PTR _this$[ebp] call ??0CInnerClass@@QAE@H@Z ; CInnerClass::CInnerClass mov eax, DWORD PTR _this$[ebp] mov ecx, DWORD PTR _id$[ebp] mov DWORD PTR [eax+4], ecx
; 5 : m_innerObj = 10;
push 10 ; 0000000aH lea ecx, DWORD PTR $T1359[ebp] call ??0CInnerClass@@QAE@H@Z ; CInnerClass::CInnerClass lea eax, DWORD PTR $T1359[ebp] push eax mov ecx, DWORD PTR _this$[ebp] call ??4CInnerClass@@QAEAAV0@ABV0@@Z ; CInnerClass::operator=從這段匯編代碼中我們可以看到一些有意義的內(nèi)容:
初始化列表先于構(gòu)造函數(shù)體內(nèi)的代碼執(zhí)行; 初始化列表確實(shí)執(zhí)行的是數(shù)據(jù)成員的初始化過程,這個(gè)可以從成員對(duì)象的構(gòu)造函數(shù)被調(diào)用看的出來。 賦值(Assignment) 對(duì) 象經(jīng)過初始化以后,我們?nèi)匀豢梢詫?duì)其進(jìn)行賦值。和類對(duì)象的初始化一樣,類對(duì)象的賦值實(shí)際上是對(duì)類對(duì)象內(nèi)的所有數(shù)據(jù)成員進(jìn)行賦值。C++也已經(jīng)為我們提供了 這樣的能力,我們可以通過構(gòu)造函數(shù)的實(shí)現(xiàn)體(即構(gòu)造函數(shù)中由"{}"包裹的部分)來實(shí)現(xiàn)。這一點(diǎn)也可以從上面的匯編代碼中成員對(duì)象的賦值操作符 (operator =)被調(diào)用得到印證。
結(jié)束 隨著構(gòu)造函數(shù)執(zhí)行完最后一行代碼,可以說類對(duì)象的創(chuàng)建過程也就順利完成了。由以上的分析可以看出,構(gòu)造函數(shù)實(shí)現(xiàn)了對(duì)象的初始化和賦值兩個(gè)過程:對(duì)象的初始化是通過初始化列表來完成,而對(duì)象的賦值則才是通過構(gòu)造函數(shù),或者更準(zhǔn)確的說應(yīng)該是構(gòu)造函數(shù)的實(shí)現(xiàn)體。
虛函數(shù)表指針(VTable Pointer) 我 們?cè)趺纯赡軙?huì)忽視虛函數(shù)表指針呢?如果沒有它的話,C++世界會(huì)清凈很多。我們最關(guān)心的是對(duì)于那些擁有虛函數(shù)的類,它們的類對(duì)象中的虛函數(shù)表指針是什么時(shí) 候賦值的?我們沒有任何代碼,也沒有任何能力(當(dāng)然暴力破解的方法除外)能夠在類對(duì)象創(chuàng)建的時(shí)候給其虛表指針賦值,給虛表指針賦值是編譯器偷偷完成的,具 體的時(shí)機(jī)是在進(jìn)入到虛函數(shù)后,在給對(duì)象的數(shù)據(jù)成員初始化和賦值之前,編譯器偷偷的給虛表指針賦值。下面我們就看看具體的情況是什么樣的吧。在上面的 CJdBase類的基礎(chǔ)上再添加一個(gè)虛函數(shù): class CJdBase { public: CJdBase::CJdBase(int id):m_innerObj(id),m_iID(id){ m_innerObj = 10; } public: virtual void dumpMe() {} private: CInnerClass m_innerObj; int m_iID; };使用VS2002編譯獲得這個(gè)構(gòu)造函數(shù)的匯編代碼,其中最關(guān)鍵的一些代碼如下: mov DWORD PTR _this$[ebp], ecx mov eax, DWORD PTR _this$[ebp] mov DWORD PTR [eax], OFFSET FLAT:??_7CJdBase@@6B@ mov eax, DWORD PTR _id$[ebp] push eax mov ecx, DWORD PTR _this$[ebp] add ecx, 4 call ??0CInnerClass@@QAE@H@Z ; CInnerClass::CInnerClass mov eax, DWORD PTR _this$[ebp] mov ecx, DWORD PTR _id$[ebp] mov DWORD PTR [eax+8], ecx
; 5 : m_innerObj = 10;
push 10 ; 0000000aH lea ecx, DWORD PTR $T1368[ebp] call ??0CInnerClass@@QAE@H@Z ; CInnerClass::CInnerClass lea eax, DWORD PTR $T1368[ebp] push eax mov ecx, DWORD PTR _this$[ebp] add ecx, 4 call ??4CInnerClass@@QAEAAV0@ABV0@@Z ; CInnerClass::operator=從這些代碼中的 mov DWORD PTR [eax], OFFSET FLAT:??_7CJdBase@@6B@我們可以清晰的看到,在構(gòu)造函數(shù)的最開始,在進(jìn)入構(gòu)造函數(shù)體內(nèi)部,甚至是在進(jìn)入初始化列表之前,編譯器會(huì)插入代碼用當(dāng)前正在被構(gòu)造的類的虛表地址給虛表指針賦值。
后記 如果不是親自實(shí)踐和分析,很難想象一個(gè)簡(jiǎn)單的類對(duì)象創(chuàng)建過程竟然蘊(yùn)涵了這么多秘密。了解了這些秘密為我們解決其他的一些問題打開了勝利之門。 試試下面的一些問題,不知道在你看完本文后是否能夠有一種豁然開朗的感覺: 1. 為什么C++需要提供初始化列表?那些情況下必須實(shí)現(xiàn)初始化列表? (提示:有些情況下只能初始化不能賦值) 2. 構(gòu)造函數(shù)可以是虛函數(shù)呢?在構(gòu)造函數(shù)中調(diào)用虛函數(shù)會(huì)有什么樣的結(jié)果? (提示:虛表指針是在構(gòu)造函數(shù)的最開始初始化的) 3. 構(gòu)造函數(shù)和賦值操作符operator=有什么區(qū)別? (提示:區(qū)分初始化和賦值)
歷史記錄 07/29/2007 v1.0 原文的第一版
本文來自CSDN博客,轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/eroswang/archive/2007/08/04/1725640.aspx
|