上一篇文章中,我們介紹了怎么從一個(gè)DLL中導(dǎo)出C++類,及選擇性導(dǎo)出C++類的成員的方法。那么,整個(gè)系統(tǒng)的底層機(jī)制是怎么樣的?是通過什么途徑,使得我們可以在另一個(gè)程序中使用一個(gè)DLL中導(dǎo)出的類的呢?
讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對(duì)這個(gè)行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長期合作伙伴,公司提供的服務(wù)項(xiàng)目有:域名注冊(cè)、網(wǎng)頁空間、營銷軟件、網(wǎng)站建設(shè)、渝水網(wǎng)站維護(hù)、網(wǎng)站推廣。
我們知道,要使用一個(gè)C++類,必要的條件是在編譯期能得到這個(gè)類的頭文件,并在鏈接期可以找到對(duì)應(yīng)的符號(hào)的鏈接地址(比如成員函數(shù)、靜態(tài)數(shù)據(jù)成員等)。如果這個(gè)C++類與你的使用者在同一個(gè)工程,那這個(gè)條件很好滿足:
首先,C++類的頭文件很好獲得。直接在使用者那里將類的頭文件include即可
其次,C++類往往被編譯器作為一個(gè)編譯單元,生成一個(gè)obj文件。在最后進(jìn)行鏈接的過程中,鏈接器會(huì)把工程中所有的obj鏈接以生成最終的二進(jìn)制目標(biāo)文件。所以鏈接器在遇到一處對(duì)類成員函數(shù)(或其它形式的符號(hào)引用)時(shí),會(huì)在這個(gè)類生成的obj文件中找到符號(hào)的鏈接地址。
那么,在代碼中使用一個(gè)C++類,編譯期和鏈接期需要的到底是些什么東西呢?換句話說,滿足了什么樣的條件,編譯器和鏈接器就不會(huì)抱怨了呢?
根據(jù)C++語言的定義,一個(gè)C++類實(shí)際上是聲明或定義了如下幾類內(nèi)容:
1. 聲明了一個(gè)數(shù)據(jù)結(jié)構(gòu),類中的非靜態(tài)數(shù)據(jù)成員、代碼中看不到但如果有虛函數(shù)就會(huì)生成的虛表入口地址指針等。
2. 聲明并定義了一堆函數(shù),它們第一個(gè)參數(shù)都是一個(gè)指向這個(gè)數(shù)據(jù)結(jié)構(gòu)的指針。這些實(shí)際上就是類中那些非靜態(tài)成員函數(shù)(包括虛函數(shù)),它們雖然在類聲明中是寫在類的一對(duì)大括號(hào)內(nèi)部,但實(shí)際上沒有任何東西被加到前面第1條中所說的內(nèi)部數(shù)據(jù)結(jié)構(gòu)中。實(shí)際上,這樣的聲明只是為這些函數(shù)增加了兩個(gè)屬性:函數(shù)名標(biāo)識(shí)符的作用域被限制在類中;函數(shù)第一個(gè)參數(shù)是this,被省略不寫了。
3. 聲明并定義了另一堆函數(shù),它們看上去就是一些普通函數(shù),與這個(gè)類幾乎沒有關(guān)系。這些實(shí)際上就是類中那些靜態(tài)函數(shù),它們也是一樣,不會(huì)在第1條中所說的內(nèi)部數(shù)據(jù)結(jié)構(gòu)中增加什么東西,只是函數(shù)名標(biāo)識(shí)符的作用域被限制在類中。
4. 聲明并定義了一堆全局變量。這些實(shí)際上就是類中那些靜態(tài)數(shù)據(jù)成員。
5. 聲明并定義了一個(gè)全局變量,此全局變量是一個(gè)函數(shù)指針數(shù)組,用來保存此類中所有的虛函數(shù)的入口地址。當(dāng)然,這個(gè)全局變量生成的前提是這個(gè)類有虛函數(shù)。
下面是一個(gè)例子。
class MyClass { public: int x; int y; void Foo(); void Bar(int newX, int newY); virtual void VFoo(); virtual void VBar(int newX, int newY) = 0; static void SFoo(); static void SBar(int newX, int newY); static int sx; static int sy; };
對(duì)于上面列出的這個(gè)類MyClass,C++編譯器多數(shù)會(huì)以如下的方式進(jìn)行編譯:
現(xiàn)在我們?cè)賮砜匆幌聻槭裁淳幾g器需要頭文件和符號(hào)地址就可以編譯鏈接一個(gè)使用MyClass的程序了。
首先,由于編譯器需要在編譯期就知道類的內(nèi)存布局,以保證可以生成正確的開辟內(nèi)存的代碼,及那些sizeof(MyClass)的值。有了頭文件,編譯器就知道,一個(gè)MyClass占用12字節(jié)的內(nèi)存空間(見上圖,兩個(gè)整數(shù)和一個(gè)指針)。
其次,在調(diào)用MyClass的成員函數(shù)、靜態(tài)函數(shù)時(shí),鏈接器需要知道這些函數(shù)的入口地址,如果無法提供入口地址,鏈接器就會(huì)報(bào)錯(cuò)。
最后,在引用MyClass的靜態(tài)數(shù)據(jù)成員時(shí),實(shí)際上與引用一個(gè)外部全局對(duì)象一樣,鏈接器需要知道這些變量的地址。如果無法提供這些變量的地址,鏈接器也會(huì)報(bào)錯(cuò)。
可以看出:
1. 編譯期:必須要提供的是類的頭文件,以使編譯器可以得知類實(shí)例的尺寸和內(nèi)存布局。
2. 鏈接期:必須要提供的是程序中引用過的,類的成員函數(shù)、靜態(tài)函數(shù)、靜態(tài)數(shù)據(jù)成員的地址,以使鏈接器可以正確的生成最終程序。
到這里,我們可以猜到,實(shí)際上,導(dǎo)出一個(gè)類,編譯器實(shí)際上只需要將這個(gè)類中的:成員函數(shù)、靜態(tài)函數(shù)、靜態(tài)數(shù)據(jù)成員當(dāng)成普通的函數(shù)、全局變量導(dǎo)出即可。也就是說,我們實(shí)際上沒有“導(dǎo)出一個(gè)類”,而是把這個(gè)類中需要被引用的“有定義的實(shí)體”的入口地址像普通函數(shù)和變量那樣正常導(dǎo)出即可。
最后我們來看一下,實(shí)際上生成的一個(gè)導(dǎo)出前面列的的那個(gè)MyClass類的DLL。用Dependence來查看,可以看到下面的結(jié)果:
可以看到,除了VBar函數(shù)是一個(gè)純虛函數(shù)外,其它函數(shù)、靜態(tài)數(shù)據(jù)成員的入口地址都被導(dǎo)出。另外可以看到,vtable也被導(dǎo)出,以便操作虛函數(shù)時(shí)引用。
Balon白話MSDN:從普通DLL中導(dǎo)出C++類(1) – dllexport和dllimport的使用方法(中英對(duì)照、附注解)
我寫這些內(nèi)容時(shí)偷了個(gè)懶,避開了虛表的一大堆復(fù)雜內(nèi)容。謝謝houdy的提示,他的文章對(duì)于虛表,以及從DLL導(dǎo)出虛表的底層機(jī)制進(jìn)行了詳細(xì)的剖析,想對(duì)此刨根問底的同學(xué)一定要看下:
虛函數(shù)表放在哪里?