国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計(jì) > 正文

C++基本概念在編譯器中的實(shí)現(xiàn)

2019-11-17 05:28:11
字體:
供稿:網(wǎng)友
  對(duì)于C++對(duì)象模型,相信很多程序員都耳熟能詳。 本文試圖通過一個(gè)簡(jiǎn)單的例子演示一些C++基本概念在編譯器中的實(shí)現(xiàn),以期達(dá)到眼見為實(shí)的效果。

  1、對(duì)象空間和虛函數(shù)

  1.1 對(duì)象空間

  在我們?yōu)閷?duì)象分配一塊空間時(shí),例如:

  CChild1 *pChild = new CChild1();

  這塊空間里放著什么東西?

  在CChild1沒有虛函數(shù)時(shí),CChild1對(duì)象空間里依次放著其基類的非靜態(tài)成員和其自身的非靜態(tài)成員。沒有任何非靜態(tài)成員的對(duì)象,會(huì)有一個(gè)字節(jié)的占位符。

  假如CChild1有虛函數(shù),VC6編譯器會(huì)在對(duì)象空間的最前面加一個(gè)指針,這就是虛函數(shù)表指針(Vptr:Virtual function table pointer)。我們來看這么一段代碼:

class CMember1 {
public:
CMember1(){a=0x5678;~CMember1(){printf("析構(gòu) CMember1/n");}
int a;
};

class CParent1 {
public:
CParent1(){parent_data=0x1234;printf("構(gòu)造 CParent1/n");}
virtual ~CParent1(){printf("析構(gòu) CParent1/n");}
virtual void test(){printf("調(diào)用CParent1::test()/n/n");}
void real(){printf("調(diào)用CParent1::test()/n/n");}
int parent_data;
};

class CChild1 : public CParent1 {
public:
CChild1(){printf("構(gòu)造 CChild1/n");}
virtual ~CChild1(){printf("析構(gòu) CChild1/n");}
virtual void test(){printf("調(diào)用CChild1::test()/n/n");}
void real(){printf("調(diào)用CChild1::test()/n/n");}
CMember1 member;
static int b;
};

  CChild1對(duì)象的大小是多少?以下是演示程序的打印輸出:

---->派生類對(duì)象
對(duì)象地址 0x00370FE0
對(duì)象大小 12
對(duì)象內(nèi)容
00370FE0: 00410104 00001234 00005678
vptr內(nèi)容
00410104: 004016a0 00401640 00401f70

  CChild1對(duì)象的大小是12個(gè)字節(jié),包括:Vptr、基類成員變量parent_data、派生類成員變量member。Vptr指向的虛函數(shù)表(VTable)就是虛函數(shù)地址組成的數(shù)組。

  1.2 Vptr和VTable

  假如我們用VC自帶的dumpbin反匯編Debug版的輸出程序:

dumpbin /disasm test_vc6.exe>a.txt

  可以在a.txt中找到:

?test@CChild1@@UAEXXZ:
00401640: 55 push ebp...
??_ECChild1@@UAEPAXI@Z:
004016A0: 55 push ebp

  可見VTable中的兩個(gè)地址分別指向CChild1的析構(gòu)函數(shù)和CChild1的成員函數(shù)test。這兩個(gè)函數(shù)是CChild1的虛函數(shù)。假如打印兩個(gè)CChild1對(duì)象的內(nèi)容,可以發(fā)現(xiàn)它們Vptr是相同的,即每個(gè)有虛函數(shù)的類有一個(gè)VTable,這個(gè)類的所有對(duì)象的Vptr都指向這個(gè)VTable。

  這里的函數(shù)名是不是有點(diǎn)希奇,附錄二簡(jiǎn)略介紹了C++的Name Mangling。

  1.3 靜態(tài)成員變量

  在C++中,類的靜態(tài)變量相當(dāng)于增加了訪問控制的全局變量,不占用對(duì)象空間。它們的地址在編譯鏈接時(shí)就確定了。例如:假如我們?cè)陧?xiàng)目的Link設(shè)置中選擇“Generate mapfile”,build后,就可以在生成的map文件中看到:

0003:00002e18 ?b@CChild1@@2HA 00414e18 test1.obj

  從打印輸出,我們可以看到CChild1::b的地址正是0x00414E18。其實(shí)類定義中的對(duì)變量b的聲明僅是聲明而已,假如我們沒有在類定義外 (全局域) 定義這個(gè)變量,這個(gè)變量根本就不存在。

  1.4 調(diào)用虛函數(shù)

  通過在VC調(diào)試環(huán)境中設(shè)置斷點(diǎn),并切換到匯編顯示模式,我們可以看到調(diào)用虛函數(shù)的匯編代碼:

16: pChild->test();
(1) mov edx,d
Word ptr [pChild]
(2) mov eax,dword ptr [edx]
(3) mov esi,esp
(4) mov ecx,dword ptr [pChild]
(5) call dword ptr [eax+4]

  語句(1)將對(duì)象地址放到寄存器edx,語句(2)將對(duì)象地址處的Vptr裝入寄存器eax,語句(5)跳轉(zhuǎn)到Vptr指向的VTable第二項(xiàng)的地址,即成員函數(shù)test。

  語句(4)將對(duì)象地址放到寄存器ecx,這就是傳入非靜態(tài)成員函數(shù)的隱含this指針。非靜態(tài)成員函數(shù)通過this指針訪問非靜態(tài)成員變量。

  1.5 虛函數(shù)和非虛函數(shù)

  在演示程序中,我們打印了成員函數(shù)地址:

printf("CParent1::test地址 0x%08p/n", &CParent1::test);
printf("CChild1::test地址 0x%08p/n", &CChild1::test);
printf("CParent1::real地址 0x%08p/n", &CParent1::real);
printf("CChild1::real地址 0x%08p/n", &CChild1::real);

  得到以下輸出:

CParent1::test地址 0x004018F0
CChild1::test地址 0x004018F0
CParent1::real地址 0x00401460
CChild1::real地址 0x00401670

  兩個(gè)非虛函數(shù)的地址很輕易理解,在dumpbin的輸出中可以找到它們:

?real@CParent1@@QAEXXZ: 00401460: 55 push ebp...
?real@CChild1@@QAEXXZ: 00401670: 55 push ebp

  為什么兩個(gè)虛函數(shù)的“地址”是一樣的?其實(shí)這里打印的是一段thunk代碼的地址。通過查看dumpbin的輸出,我們可以看到:

_9@$B3AE:
(6) mov eax,dword ptr [ecx]
(7) jmp dword ptr [eax+4]

  假如我們?cè)谔D(zhuǎn)到這段代碼前將對(duì)象地址放到寄存器ecx,語句(6)就會(huì)將對(duì)象地址處的Vptr裝入寄存器eax,語句(7)跳轉(zhuǎn)到Vptr指向的VTable第二項(xiàng)的地址,即成員函數(shù)test。基類和派生類VTable的虛函數(shù)排列順序是相同的,所以可以共用一段thunk代碼。

  這段thunk代碼的用途是通過函數(shù)指針調(diào)用虛函數(shù)。假如我們不取函數(shù)地址,編譯器就不會(huì)產(chǎn)生這段代碼。請(qǐng)注重不要將本節(jié)的thunk代碼與VTable中虛函數(shù)地址混淆起來。Thunk代碼根據(jù)傳入的對(duì)象指針決定調(diào)用哪個(gè)函數(shù),VTable中的虛函數(shù)地址才是真正的函數(shù)地址。

  1.6 指向虛函數(shù)的指針

  我們?cè)囼?yàn)一下通過指針調(diào)用虛函數(shù)。非靜態(tài)成員函數(shù)指針必須通過對(duì)象指針調(diào)用:

typedef void (Parent::*PMem)();
printf("/n---->通過函數(shù)指針調(diào)用/n");
PMem pm = &Parent::test;
printf("函數(shù)指針 0x%08p/n", pm);
(pParent->*pm)();

  得到以下輸出:

  ---->通過函數(shù)指針調(diào)用

  函數(shù)指針 0x004018F0
  調(diào)用CChild1::test()

  我們從VC調(diào)試環(huán)境中復(fù)制出這段匯編代碼:

13: (pParent->*pm)();
(8) mov esi,esp
(9) mov ecx,dword ptr [pParent]
(10) call dword ptr [pm]

  語句(9)將對(duì)象指針放到寄存器ecx中,語句(10)調(diào)用函數(shù)指針指向的thunk代碼,就是1.5節(jié)的語句(6)。下面會(huì)發(fā)生什么,前面已經(jīng)說過了。

  1.7 多態(tài)的實(shí)現(xiàn)

  經(jīng)過前面的分析,多態(tài)的實(shí)現(xiàn)應(yīng)該是顯而易見的。當(dāng)用指向派生類對(duì)象的基類指針調(diào)用虛函數(shù)時(shí),因?yàn)榕缮悓?duì)象的Vptr指向派生類的VTable,所以調(diào)用的當(dāng)然是派生類的函數(shù)。

  通過函數(shù)指針調(diào)用虛函數(shù)同樣要經(jīng)過VTable確定虛函數(shù)地址,所以同樣會(huì)發(fā)生多態(tài),即調(diào)用當(dāng)前對(duì)象VTable中的虛函數(shù)。
三層交換技術(shù) 交換機(jī)與路由器密碼恢復(fù) 交換機(jī)的選購 路由器設(shè)置專題 路由故障處理手冊(cè) 數(shù)字化校園網(wǎng)解決方案
  2、構(gòu)造和析構(gòu)

  2.1 構(gòu)造函數(shù)

  下面的語句:

printf("---->構(gòu)造派生類對(duì)象/n"); CChild1 *pChild = new CChild1();

  產(chǎn)生以下輸出:

  ---->構(gòu)造派生類對(duì)象
  構(gòu)造 CParent1
  構(gòu)造 CMember1
  構(gòu)造 CChild1

  編譯器會(huì)在用戶定義的構(gòu)造函數(shù)中加一些代碼:先調(diào)用基類的構(gòu)造函數(shù),然后構(gòu)造每個(gè)成員對(duì)象,最后才是程序中的構(gòu)造函數(shù)代碼(以下稱用戶代碼)。下面這段匯編代碼就是編譯器修改過的CChild1類的構(gòu)造函數(shù):

0CChild1@@QAE@XZ:004014D0 push ebp
...
(11) call CParent1::CParent1 (004013b0)
...
(12) call CMember1::CMember1 (00401550)
(13) mov eax,dword ptr [this]
(14) mov dword ptr [eax],offset CChild1::`vftable' (00410104)
(15) push offset string "/xb9/xb9/xd4/xec CChild1/n" (004122a0)
call printf (004022e0)
...
ret

  語句(11)調(diào)用基類的構(gòu)造函數(shù),語句(12)構(gòu)造成員對(duì)象,語句(15)以后是用戶代碼。語句(13)和(14)也值得一提:語句(13)將對(duì)象地址放到寄存器eax,語句(14)將CChild1類的VTable指針放到對(duì)象地址(eax)的起始處。它們建立的正是對(duì)象的Vptr。

  假如對(duì)象是通過new操作符構(gòu)造的,編譯器會(huì)先調(diào)用new函數(shù)分配對(duì)象空間,然后調(diào)用上面這個(gè)構(gòu)造函數(shù)。

  2.2 析構(gòu)函數(shù)

  刪除指向派生類對(duì)象的指針產(chǎn)生以下輸出:

  ---->刪除指向派生類對(duì)象的基類指針
  析構(gòu) CChild1
  析構(gòu) CMember1
  析構(gòu) CParent1

  編譯器會(huì)在用戶定義的析構(gòu)函數(shù)中加一些代碼:即先調(diào)用用戶代碼,然后析構(gòu)每個(gè)成員對(duì)象,最后析構(gòu)基類的構(gòu)造函數(shù)。下面這段匯編代碼就是編譯器修改過的CChild1類的析構(gòu)函數(shù):

??1CChild1@@UAE@XZ:
00401590 push ebp
...
push offset string "/xce/xf6/xb9/xb9 CChild1/n" (004122c0) call printf (004022e0) ...
(16) call CMember1::~CMember1 (00401610)
...

(17) call CParent1::~CParent1 (004013f0)
...

ret

  前面是用戶代碼,語句(16)調(diào)用成員對(duì)象的析構(gòu)函數(shù),語句(17)調(diào)用基類的析構(gòu)函數(shù)。細(xì)心的朋友會(huì)發(fā)現(xiàn)這里的析構(gòu)函數(shù)的地址與前面VTable中析構(gòu)函數(shù)地址不同。其實(shí),它們的名字也不一樣,它們是兩個(gè)函數(shù):

_ECChild1@@UAEPAXI@Z:004016A0 push ebp
...
(18) call CChild1::~CChild1 (00401590)
...
(19) call Operator delete (004023a0)
...
ret 4

  假如在調(diào)試器中看(或者用dem工具Demangling),第二個(gè)析構(gòu)函數(shù)的名字是CChild1::`scalar deleting destrUCtor',前一個(gè)析構(gòu)函數(shù)的名字是CChild1::~CChild1。函數(shù)CChild1::`scalar deleting destructor'在語句(18)上調(diào)用前面的析構(gòu)函數(shù),在語句(19)上調(diào)用delete函數(shù)釋放對(duì)象空間。

  在通過delete刪除對(duì)象指針時(shí),需要在析構(gòu)后釋放對(duì)象空間,所以編譯器合成了第二個(gè)析構(gòu)函數(shù)。通過VTable調(diào)用析構(gòu)函數(shù),肯定是delete對(duì)象指針引發(fā)的,所以VTable中放的是第二個(gè)析構(gòu)函數(shù)。在析構(gòu)堆棧上的對(duì)象時(shí),只要調(diào)用第一個(gè)析構(gòu)函數(shù)就可以了。

  2.3 虛析構(gòu)函數(shù)

  千萬不要將析構(gòu)函數(shù)和虛函數(shù)混淆起來。不管析構(gòu)函數(shù)是不是虛函數(shù),編譯器都會(huì)按照2.2節(jié)的介紹合成析構(gòu)函數(shù)。將析構(gòu)函數(shù)設(shè)為虛函數(shù)是希望在通過基類指針刪除派生類對(duì)象時(shí)調(diào)用派生類的析構(gòu)函數(shù)。假如析構(gòu)函數(shù)不是虛函數(shù),派生類對(duì)象沒有Vptr,編譯器會(huì)調(diào)用基類的析構(gòu)函數(shù)(在編譯時(shí)就確定了)。

  這樣,用戶在派生類析構(gòu)函數(shù)中填寫的代碼就不會(huì)被調(diào)用,派生類成員對(duì)象的析構(gòu)函數(shù)也不會(huì)被調(diào)用。不過,派生類對(duì)象空間還是會(huì)被正確釋放的,堆治理程序知道對(duì)象分配了多少空間。

  3、不同的實(shí)現(xiàn)

  本文的目的只是通過對(duì)編譯器內(nèi)部實(shí)現(xiàn)的適當(dāng)了解,加深對(duì)C++基本概念的理解,我們的代碼不應(yīng)該依靠可能會(huì)改變的內(nèi)部機(jī)制。其實(shí)各個(gè)編譯器對(duì)相同機(jī)制的實(shí)現(xiàn)也會(huì)有較大差異。例如:Vptr的位置就可能有多種方案:

  VC的編譯器把Vptr放在對(duì)象頭部

  BCB的編譯器將Vptr放在繼續(xù)體系中第一個(gè)有Vptr的對(duì)象頭部

  Dev-C++的編譯器以前將Vptr放在繼續(xù)體系中第一個(gè)有Vptr的對(duì)象尾部

  Dev-C++的最新版本(4.9.9.2)也將Vptr放在對(duì)象頭部。其實(shí)第1個(gè)方案有一個(gè)小問題:假如基類對(duì)象沒有Vptr,而派生類對(duì)象有Vptr,讓基類指針指向派生類對(duì)象時(shí),編譯器不得不調(diào)整基類指針的地址,讓其指向Vptr后的基類非靜態(tài)成員。以后假如通過基類指針delete派生類對(duì)象,由于delete的地址與分配地址不同,就會(huì)發(fā)生錯(cuò)誤。讀者可以在演示程序中找到研究這個(gè)問題的代碼(其實(shí)是CSDN上一個(gè)網(wǎng)友的問題)。將Vptr放在其它兩個(gè)位置,因?yàn)椴挥谜{(diào)整基類指針,就可以避免這個(gè)問題。

  g++編譯器(v3.4.2)產(chǎn)生的程序在打印虛函數(shù)地址時(shí)會(huì)輸出:

CParent1::test地址 0x00000009
CChild1::test地址 0x00000009

  在通過函數(shù)指針調(diào)用函數(shù)時(shí),編譯器會(huì)通過這個(gè)數(shù)字9在對(duì)象的虛函數(shù)表中找到虛函數(shù)test。

  附錄1 增量鏈接和ILT

  為了簡(jiǎn)化表述,演示程序的VC6項(xiàng)目設(shè)置(Debug版)關(guān)閉了“Link Incrementally”選項(xiàng)。假如打開這個(gè)選項(xiàng),編譯器會(huì)通過一個(gè)叫作ILT的數(shù)組間接調(diào)用函數(shù)。數(shù)組ILT的每個(gè)元素是一條5個(gè)字節(jié)的jmp指令,例如:

@ILT+170(?test@CChild2@@QAEXXZ): 004010AF: E9 1C 10 00 00 jmp ?test@CChild2@@QAEXXZ

  編譯器調(diào)用函數(shù)時(shí):

call @ILT+170(?test@CChild2@@QAEXXZ)

  通過ILT跳轉(zhuǎn)到函數(shù)的實(shí)際地址。這樣,在函數(shù)地址變化時(shí),編譯器只需要修改ILT表,而不用修改每個(gè)引用函數(shù)的語句。ILT是編譯器開發(fā)者起的變量名,據(jù)網(wǎng)友Cody2k3猜測(cè),可能是Incremental Linking Table的縮寫。

  附錄2 C++的Name Mangling/Demangling

  C++編譯器會(huì)將程序中的變量名、函數(shù)名轉(zhuǎn)換成內(nèi)部名稱,這個(gè)過程被稱作Name Mangling,反過程被稱作Name Demangling。內(nèi)部名稱包含了變量或函數(shù)的更多信息,例如編譯器看到?g_var@@3HA,就知道這是:

int g_var

  "3H"表示int型的全局變量。編譯器看到?test@CChild2@@QAEXXZ,知道這是:

public: void __thiscall CChild2::test(void)

  編譯器廠商一般不會(huì)公布Mangling的規(guī)則,因?yàn)檫@些規(guī)則可能會(huì)根據(jù)需求變化。不過,微軟提供了一個(gè)Demangling的函數(shù)UnDecorateSymbolName。我用這個(gè)函數(shù)寫了一個(gè)叫作“dem”的小工具,可以從內(nèi)部名稱得到變量或函數(shù)的聲明信息。

  附錄3 關(guān)于thunk

  據(jù)說一個(gè)Algol-60程序員第一次使用“thunk”這個(gè)詞匯,最初的語義源自"thought of (thunked)" 。這個(gè)單詞的主要語義是“地址轉(zhuǎn)換、替換程序”,一般是指通過一小段匯編代碼,轉(zhuǎn)調(diào)另一個(gè)函數(shù)。調(diào)用者在調(diào)用thunk代碼時(shí)以為自己在調(diào)用一個(gè)函數(shù),thunk代碼會(huì)將控制轉(zhuǎn)交給一個(gè)它選擇的函數(shù)。例如:附錄一介紹的ILT數(shù)組的每個(gè)元素都是一小段thunk代碼。

  附錄4 在g++中生成mapfile

  在通過gcc/g++間接調(diào)用鏈接程序ld時(shí),所有的ld選項(xiàng)前必須加上“-Wl,”。所以,要讓g++生成mapfile,需要增加編譯參數(shù)“ -Wl,-Map,mapfile”。



發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 肃北| 茶陵县| 桂林市| 怀集县| 清丰县| 苏尼特右旗| 临西县| 万载县| 广安市| 庆元县| 那坡县| 慈利县| 类乌齐县| 合肥市| 酉阳| 乳源| 方城县| 久治县| 宣恩县| 金乡县| 澜沧| 通榆县| 蕲春县| 麻栗坡县| 威海市| 常宁市| 新宁县| 石嘴山市| 灵寿县| 藁城市| 米林县| 大英县| 大方县| 南华县| 沈丘县| 安新县| 虎林市| 靖安县| 额尔古纳市| 淳化县| 周口市|