C++類的底層機理

我們首先從一個問題來闡明類的底層機理:
假如有一個類A,裏面有一個成員函數get(),例如:
class A
{
public:
    void get();
}
A a;
那麼a.get()表示什麼呢?首先給出答案是get(&a),因爲在類的底層機制中,成員函數的第一個參數都是一個指向該類數據結構的指針(靜態成員函數除外),所以成員函數get()的存在形式爲void get(A* this);這也能說明爲什麼我們在成員函數的定義中總是可以用this來指代調用對象。
我們知道,要使用一個C++類,必要的條件是在編譯期能得到這個類的頭文件,並在鏈接期可以找到對應的符號的鏈接地址(比如成員函數、靜態數據成員等)。如果這個C++類與你的使用者在同一個工程,那這個條件很好滿足:
首先,C++類的頭文件很好獲得。直接在使用者那裏將類的頭文件include即可;
其次,C++類往往被編譯器作爲一個編譯單元,生成一個obj文件。在最後進行鏈接的過程中,鏈接器會把工程中所有的obj鏈接以生成最終的二進制目標文件。所以鏈接器在遇到一處對類成員函數(或其它形式的符號引用)時,會在這個類生成的obj文件中找到符號的鏈接地址。
那麼,在代碼中使用一個C++類,編譯期和鏈接期需要的到底是些什麼東西呢?換句話說,滿足了什麼樣的條件,編譯器和鏈接器就不會抱怨了呢?
根據C++語言的定義,一個C++類實際上是聲明或定義瞭如下幾類內容:
1.聲明瞭一個數據結構,類中的非靜態數據成員、代碼中看不到但如果有虛函數就會生成的虛表入口地址指針等。
2.聲明並定義了一堆函數,它們第一個參數都是一個指向這個數據結構的指針。這些實際上就是類中那些非靜態成員函數(包括虛函數),它們雖然在類聲明中是寫在類的一對大括號內部,但實際上沒有任何東西被加到前面第1條中所說的內部數據結構中。實際上,這樣的聲明只是爲這些函數增加了兩個屬性:函數名標識符的作用域被限制在類中;函數第一個參數是this,被省略不寫了。
3.聲明並定義了另一堆函數,它們看上去就是一些普通函數,與這個類幾乎沒有關係。這些實際上就是類中那些靜態函數,它們也是一樣,不會在第1條中所說的內部數據結構中增加什麼東西,只是函數名標識符的作用域被限制在類中。
4.聲明並定義了一堆全局變量。這些實際上就是類中那些靜態數據成員。
5.聲明並定義了一個全局變量,此全局變量是一個函數指針數組,用來保存此類中所有的虛函數的入口地址。當然,這個全局變量生成的前提是這個類有虛函數。
看下面的一個例子:
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;
};
對於上面列出的這個類MyClass,C++編譯器多數會以如下的方式進行編譯:
編譯器生成的類的二進制結構
現在我們再來看一下爲什麼編譯器需要頭文件和符號地址就可以編譯鏈接一個使用MyClass的程序了。
首先,由於編譯器需要在編譯期就知道類的內存佈局,以保證可以生成正確的開闢內存的代碼,及那些
sizeof(MyClass)的值。有了頭文件,編譯器就知道,一個MyClass佔用12字節的內存空間(見上圖,兩個整數和一
個指針)。
其次,在調用MyClass的成員函數、靜態函數時,鏈接器需要知道這些函數的入口地址,如果無法提供入口地址,
鏈接器就會報錯。
最後,在引用MyClass的靜態數據成員時,實際上與引用一個外部全局對象一樣,鏈接器需要知道這些變量的地址。
如果無法提供這些變量的地址,鏈接器也會報錯。
可以看出:
1. 編譯期:必須要提供的是類的頭文件,以使編譯器可以得知類實例的尺寸和內存佈局。
2. 鏈接期:必須要提供的是程序中引用過的,類的成員函數、靜態函數、靜態數據成員的地址,以使鏈接器可以正確的生成最終程序。
到這裏,我們可以猜到,實際上,導出一個類,編譯器實際上只需要將這個類中的:成員函數、靜態函數、靜態數
據成員當成普通的函數、全局變量導出即可。也就是說,我們實際上沒有“導出一個類”,而是把這個類中需要被
引用的“有定義的實體”的入口地址像普通函數和變量那樣正常導出即可。由於裏面的純虛函數VBar沒有
定義,所以不會被導出。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章