亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 學院 > 開發設計 > 正文

C++基本概念在編譯器中的實現

2019-11-17 05:28:11
字體:
來源:轉載
供稿:網友
  對于C++對象模型,相信很多程序員都耳熟能詳。 本文試圖通過一個簡單的例子演示一些C++基本概念在編譯器中的實現,以期達到眼見為實的效果。

  1、對象空間和虛函數

  1.1 對象空間

  在我們為對象分配一塊空間時,例如:

  CChild1 *pChild = new CChild1();

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

  在CChild1沒有虛函數時,CChild1對象空間里依次放著其基類的非靜態成員和其自身的非靜態成員。沒有任何非靜態成員的對象,會有一個字節的占位符。

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

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

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

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

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

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

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

  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中的兩個地址分別指向CChild1的析構函數和CChild1的成員函數test。這兩個函數是CChild1的虛函數。假如打印兩個CChild1對象的內容,可以發現它們Vptr是相同的,即每個有虛函數的類有一個VTable,這個類的所有對象的Vptr都指向這個VTable。

  這里的函數名是不是有點希奇,附錄二簡略介紹了C++的Name Mangling。

  1.3 靜態成員變量

  在C++中,類的靜態變量相當于增加了訪問控制的全局變量,不占用對象空間。它們的地址在編譯鏈接時就確定了。例如:假如我們在項目的Link設置中選擇“Generate mapfile”,build后,就可以在生成的map文件中看到:

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

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

  1.4 調用虛函數

  通過在VC調試環境中設置斷點,并切換到匯編顯示模式,我們可以看到調用虛函數的匯編代碼:

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)將對象地址放到寄存器edx,語句(2)將對象地址處的Vptr裝入寄存器eax,語句(5)跳轉到Vptr指向的VTable第二項的地址,即成員函數test。

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

  1.5 虛函數和非虛函數

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

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

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

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

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

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

  假如我們在跳轉到這段代碼前將對象地址放到寄存器ecx,語句(6)就會將對象地址處的Vptr裝入寄存器eax,語句(7)跳轉到Vptr指向的VTable第二項的地址,即成員函數test?;惡团缮怴Table的虛函數排列順序是相同的,所以可以共用一段thunk代碼。

  這段thunk代碼的用途是通過函數指針調用虛函數。假如我們不取函數地址,編譯器就不會產生這段代碼。請注重不要將本節的thunk代碼與VTable中虛函數地址混淆起來。Thunk代碼根據傳入的對象指針決定調用哪個函數,VTable中的虛函數地址才是真正的函數地址。

  1.6 指向虛函數的指針

  我們試驗一下通過指針調用虛函數。非靜態成員函數指針必須通過對象指針調用:

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

  得到以下輸出:

  ---->通過函數指針調用

  函數指針 0x004018F0
  調用CChild1::test()

  我們從VC調試環境中復制出這段匯編代碼:

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

  語句(9)將對象指針放到寄存器ecx中,語句(10)調用函數指針指向的thunk代碼,就是1.5節的語句(6)。下面會發生什么,前面已經說過了。

  1.7 多態的實現

  經過前面的分析,多態的實現應該是顯而易見的。當用指向派生類對象的基類指針調用虛函數時,因為派生類對象的Vptr指向派生類的VTable,所以調用的當然是派生類的函數。

  通過函數指針調用虛函數同樣要經過VTable確定虛函數地址,所以同樣會發生多態,即調用當前對象VTable中的虛函數。
三層交換技術 交換機與路由器密碼恢復 交換機的選購 路由器設置專題 路由故障處理手冊 數字化校園網解決方案
  2、構造和析構

  2.1 構造函數

  下面的語句:

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

  產生以下輸出:

  ---->構造派生類對象
  構造 CParent1
  構造 CMember1
  構造 CChild1

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

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)調用基類的構造函數,語句(12)構造成員對象,語句(15)以后是用戶代碼。語句(13)和(14)也值得一提:語句(13)將對象地址放到寄存器eax,語句(14)將CChild1類的VTable指針放到對象地址(eax)的起始處。它們建立的正是對象的Vptr。

  假如對象是通過new操作符構造的,編譯器會先調用new函數分配對象空間,然后調用上面這個構造函數。

  2.2 析構函數

  刪除指向派生類對象的指針產生以下輸出:

  ---->刪除指向派生類對象的基類指針
  析構 CChild1
  析構 CMember1
  析構 CParent1

  編譯器會在用戶定義的析構函數中加一些代碼:即先調用用戶代碼,然后析構每個成員對象,最后析構基類的構造函數。下面這段匯編代碼就是編譯器修改過的CChild1類的析構函數:

??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)調用成員對象的析構函數,語句(17)調用基類的析構函數。細心的朋友會發現這里的析構函數的地址與前面VTable中析構函數地址不同。其實,它們的名字也不一樣,它們是兩個函數:

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

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

  在通過delete刪除對象指針時,需要在析構后釋放對象空間,所以編譯器合成了第二個析構函數。通過VTable調用析構函數,肯定是delete對象指針引發的,所以VTable中放的是第二個析構函數。在析構堆棧上的對象時,只要調用第一個析構函數就可以了。

  2.3 虛析構函數

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

  這樣,用戶在派生類析構函數中填寫的代碼就不會被調用,派生類成員對象的析構函數也不會被調用。不過,派生類對象空間還是會被正確釋放的,堆治理程序知道對象分配了多少空間。

  3、不同的實現

  本文的目的只是通過對編譯器內部實現的適當了解,加深對C++基本概念的理解,我們的代碼不應該依靠可能會改變的內部機制。其實各個編譯器對相同機制的實現也會有較大差異。例如:Vptr的位置就可能有多種方案:

  VC的編譯器把Vptr放在對象頭部

  BCB的編譯器將Vptr放在繼續體系中第一個有Vptr的對象頭部

  Dev-C++的編譯器以前將Vptr放在繼續體系中第一個有Vptr的對象尾部

  Dev-C++的最新版本(4.9.9.2)也將Vptr放在對象頭部。其實第1個方案有一個小問題:假如基類對象沒有Vptr,而派生類對象有Vptr,讓基類指針指向派生類對象時,編譯器不得不調整基類指針的地址,讓其指向Vptr后的基類非靜態成員。以后假如通過基類指針delete派生類對象,由于delete的地址與分配地址不同,就會發生錯誤。讀者可以在演示程序中找到研究這個問題的代碼(其實是CSDN上一個網友的問題)。將Vptr放在其它兩個位置,因為不用調整基類指針,就可以避免這個問題。

  g++編譯器(v3.4.2)產生的程序在打印虛函數地址時會輸出:

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

  在通過函數指針調用函數時,編譯器會通過這個數字9在對象的虛函數表中找到虛函數test。

  附錄1 增量鏈接和ILT

  為了簡化表述,演示程序的VC6項目設置(Debug版)關閉了“Link Incrementally”選項。假如打開這個選項,編譯器會通過一個叫作ILT的數組間接調用函數。數組ILT的每個元素是一條5個字節的jmp指令,例如:

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

  編譯器調用函數時:

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

  通過ILT跳轉到函數的實際地址。這樣,在函數地址變化時,編譯器只需要修改ILT表,而不用修改每個引用函數的語句。ILT是編譯器開發者起的變量名,據網友Cody2k3猜測,可能是Incremental Linking Table的縮寫。

  附錄2 C++的Name Mangling/Demangling

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

int g_var

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

public: void __thiscall CChild2::test(void)

  編譯器廠商一般不會公布Mangling的規則,因為這些規則可能會根據需求變化。不過,微軟提供了一個Demangling的函數UnDecorateSymbolName。我用這個函數寫了一個叫作“dem”的小工具,可以從內部名稱得到變量或函數的聲明信息。

  附錄3 關于thunk

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

  附錄4 在g++中生成mapfile

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



發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
欧美日韩国产一区二区三区| 国产精品自拍偷拍| 成人中心免费视频| 日韩电影视频免费| 伊人久久综合97精品| 国产精品自产拍在线观看中文| 在线视频国产日韩| 国产精品亚洲欧美导航| 国产成人aa精品一区在线播放| 尤物九九久久国产精品的特点| 欧美丝袜美女中出在线| 国精产品一区一区三区有限在线| 欧美性感美女h网站在线观看免费| 456国产精品| 日韩欧美国产激情| 精品视频—区二区三区免费| 午夜精品久久久久久久99黑人| 亚洲已满18点击进入在线看片| 中文字幕亚洲欧美日韩在线不卡| 国产精品劲爆视频| 久久国产精品久久久久| 欧美精品中文字幕一区| 亚洲午夜未满十八勿入免费观看全集| 中文欧美日本在线资源| 国产精品狠色婷| 26uuu久久噜噜噜噜| 国产视频久久久久久久| 91亚洲精华国产精华| 成人国产精品久久久| 亚洲乱亚洲乱妇无码| 国产91在线高潮白浆在线观看| 国产精品丝袜一区二区三区| 亚洲人线精品午夜| 久久久999国产| 欧美极品在线播放| 久久久久久91香蕉国产| 97人洗澡人人免费公开视频碰碰碰| 国产日本欧美视频| 91亚洲精品在线| 欧美精品九九久久| 狠狠久久亚洲欧美专区| 日韩欧美亚洲综合| 亚洲国产美女精品久久久久∴| 亚洲精品视频播放| 91精品免费视频| 久久精品美女视频网站| 国产精品白嫩美女在线观看| 91在线高清免费观看| 7777kkkk成人观看| 欧美理论在线观看| 中文在线不卡视频| 日韩一区二区欧美| 欧美自拍大量在线观看| 国产精品美女久久久久av超清| 亚洲人成电影在线观看天堂色| 成人精品一区二区三区| 亚洲国产精品视频在线观看| 欧美亚洲一级片| 国产精品扒开腿做爽爽爽视频| 中文字幕日韩欧美在线视频| 亚洲天堂第一页| 国产欧美va欧美va香蕉在| 黄色成人在线免费| 日韩小视频在线观看| 午夜剧场成人观在线视频免费观看| 成人在线视频网站| 色婷婷成人综合| 国产精品久久综合av爱欲tv| 欧美富婆性猛交| 欧美最顶级丰满的aⅴ艳星| 国产精品亚洲аv天堂网| 亚洲人高潮女人毛茸茸| 亚洲社区在线观看| 国产精品欧美一区二区三区奶水| 欧美二区乱c黑人| 91久久精品美女高潮| 日韩成人激情在线| 97在线观看免费| 成人午夜在线视频一区| 亚洲片在线观看| 国产成人免费91av在线| 国产精品久久一区| 亚洲精品国产拍免费91在线| 欧美有码在线观看视频| 91在线观看免费观看| 午夜精品在线视频| 国产精品尤物福利片在线观看| 久久精品亚洲94久久精品| 午夜精品一区二区三区在线视频| 久久久在线免费观看| 国产精品中文久久久久久久| 国产日韩在线亚洲字幕中文| 久久久精品电影| 日韩在线观看高清| 综合av色偷偷网| 国产z一区二区三区| 久久国产精品久久久久久久久久| 91高清免费在线观看| 久久天天躁狠狠躁夜夜躁2014| 久久综合久久美利坚合众国| 国产精品99导航| 欧美激情在线有限公司| 久久久国产一区二区| 国产成人精品久久二区二区| 国产一区二区av| 2019中文字幕在线观看| 精品成人国产在线观看男人呻吟| 综合久久五月天| 欧美激情在线有限公司| 亚洲一区二区久久| 国产成人小视频在线观看| 精品高清一区二区三区| 综合国产在线观看| 久久人人爽人人爽爽久久| 亚洲性猛交xxxxwww| 亚洲精品国产精品国自产观看浪潮| 97国产suv精品一区二区62| 中文字幕亚洲综合久久| 中文字幕亚洲无线码在线一区| 亚洲欧美精品中文字幕在线| 久久中国妇女中文字幕| 国产精品午夜视频| 国产精品入口免费视| 欧美激情在线观看| 8050国产精品久久久久久| 欧美有码在线观看| 久久精品美女视频网站| 97在线视频观看| 欧美视频不卡中文| 日韩激情在线视频| 九九九久久久久久| 2021久久精品国产99国产精品| 在线观看免费高清视频97| 国产啪精品视频网站| 96sao精品视频在线观看| 亚洲国产精品福利| 精品视频久久久久久久| 日韩在线欧美在线国产在线| 欧美日韩午夜视频在线观看| 欧美日韩成人在线视频| 97在线看福利| 午夜精品理论片| 国产午夜精品免费一区二区三区| 久久久久久国产精品三级玉女聊斋| 国产精品永久免费视频| 日本国产一区二区三区| 亚洲欧美中文字幕在线一区| 主播福利视频一区| 日韩在线观看免费网站| 国产精品久久久久久久久男| 日韩在线免费av| 日韩av在线播放资源| 成人免费福利在线| 一本色道久久88综合亚洲精品ⅰ| 久久视频在线播放| 成人国产精品久久久久久亚洲| 91成品人片a无限观看| 精品久久久久久久久久久久| 91国产精品视频在线| 久久精品视频在线观看| 亚洲国产精品va在线看黑人| 国产精品一区二区久久久久| 最近2019中文字幕在线高清| 亚洲小视频在线观看|