深度探索C++對象模型——Function語意學

前言:

最近在讀《深度探索C++對象模型》,收穫不小,整理一些筆記,一來總結加體悟,二來希望以後在某些知識點的遺忘時能快速拾起也希望對讀者有一定的幫助。

C++支持三種類型的成員函數

static
nonstatic
virtual

static成員函數

  • 靜態成員函數不能直接存取非靜態成員(包括靜態成員數據和靜態成員函數):因爲靜態成員函數是屬於類的,不屬於任何一個對象,在類對象創建以前就已經存在了。而非靜態成員屬於類,在類對象創建時才生成,如果在一個靜態成員函數中直接存取非靜態成員相當於在操作一個不存在的東西。
  • 靜態成員函數不能是const成員函數:const成員函數其實是指其隱含的this指針指向的對象是const的,因爲this指針永遠指向當前對象,所以指針本身是const,那const成員函數的隱含參數就是一個指向const對象的const指針。既const成員函數本身是屬於創建它的對象其this指針指向的對象只能是常量對象,而static成員函數屬於類,它並不關心調用它的對象是誰,沒有隱含的this指針,不存在關心指針指向的對象是不是常量的問題。同樣的道理,靜態成員函數也不支持virtual。
  • 網上看到篇分析靜態成員函數的文兒,覺得挺好,貼個鏈接 http://blog.csdn.net/morewindows/article/details/6721430

Nonstatic Member Functions(非靜態成員函數)

注意,以下,如果成員函數或者成員數據未強調靜態,則默認爲非靜態的。
C++的設計準則之一,就是非靜態成員函數至少必須和一般的nonmember function(非成員函數)有相同的效率。因爲編譯器內部會將“成員函數實體”轉換成對等的“非成員函數實體”。
假如現在有一個3d圖形類:Point3d定義如下:

class Point3d
{
public:
    Point3d()
    {
        x_ = 0;
        y_ = 0; 
        z_ = 0;
    }
    Point3d(float x, float y, float z){
        //
    }
    float Sum()
    {
        return (x_*x_ + y_*y_ + z_*z_);
    }
private:
    float x_;
    float y_;
    float z_;
};

以及,一個非成員函數Sum:

float NonSum(const Point3d* s)
{
    return (s->x_*s->x_ + s->y_*s->y_ + s->z_*s->z_);
}

成員函數float Point3d::Sum經過三個步驟被編譯器內化爲非成員函數NonSum函數的形式,如下:

  1. 改寫成員函數的原型,安插一個額外的參數,即this指針到成員函數中,用以提供一個存取管道,使class object得以調用該函數,變成:
/*non-const nonstatic member*/
float Point3d::Sum(Point3d* const this)

/*如果是常量成員函數則*/
float Point3d::NonSum(const Point3d* const this)

2.對每一個nonstatic data member 的存取操作改爲經由this指針來存取

{
    return (this->x_*this->x_ + this->y_*this->y_ + this->z_*this->z_);
}

3.將member function重新寫成一個外部函數,對函數進行”mangling”處理,使它成爲程序中獨一無二的語彙。

如此,這個函數就算是轉換好了,每一個調用操作也必須轉換,於是

obj.Sun();
//變成:
mangling.._Sum(&obj);

ptr->Sum();
//變成:
mangling.._Sum(ptr);

這裏Sum函數的前綴代表轉換後的語彙,具體是什麼視編譯器而定。

名稱的特殊處理(Name Mangling)

編譯器在實現命名機制時,爲了區分兩個類的同名成員給每個成員的命名再加上其類型,爲了區分兩個重載函數,用(函數名+參數類型+參數數目)的方式區分,但是沒有規定根據函數返回類型來區分,這就是爲什麼我們在定義兩個重載函數的時候當兩個函數只有返回值類型不一樣時編譯器會報重複定義的錯誤。注意,這裏的函數參數類型,static int和int的類型是一樣的都是int,切不可認爲它們是不同的類型。

Virtual Member Functions(虛擬成員函數)

注:以下將Virtual Member Functions簡稱爲virtual函數或者虛函數

先簡單說一下我對虛函數(virtual函數)的一點理解,在OO(Object Oriented)設計中,通過虛函數來實現多態。具體實現的方法就是,基類定義一個虛函數,各個子類繼承基類的虛函數並改寫(也可以不改寫),在調用操作的時候通過把子類的指針/引用賦給基類指針/引用,然後通過該被賦值的指針調用虛函數,調用的就是賦值符號右邊的指針/引用類型對象的虛函數。其中,因爲多態性會引入一些額外的執行期信息,所以必須把需要支持多態的類和不需要的類區分開來。而識別一個class是否支持多態的唯一適當的方法是看它是否有任何的virtual函數,只要class擁有一個virtual函數,它就需要這份額外的執行期信息。
我們可以簡單概括一下虛函數的實現原理:

  • 每個含有virtual函數的類都有一個虛函數表(虛表),這個虛表裏面放着索引,每個索引關聯着一個virtual函數地址,編譯器按照virtual函數聲明順序將其索引放置在虛表中,可根據該索引找到虛函數。
  • 每個含有(繼承或者自身定義)虛函數的類對象都會在構造函數執行的時候創建一個指向該類虛函數表的指針,類對象可根據該指針找到其所屬類的虛函數表,虛表裏面對應的虛函數們。
  • 子類繼承父類時,先將父類虛函數放進虛表,然後用新的函數指針代替子類改寫後的虛函數地址,再將新定義的虛函數放在虛表後面。
    可以參考:http://www.cnblogs.com/malecrab/p/5572730.html

好了,緊接我們上面提到的Name Mangling,我們來看一下,編譯器是如何對虛函數實行內部轉化的:

如果normalize()是一個Virtual Member Function,那麼以下的調用
ptr->normalize()
將會被內部轉化爲:

(*ptr->vptr[1])(ptr);
  • vptr表示編譯器產生的指針,即每個對象都有的指向虛表的指針。事實上,vptr名稱也會被mangled,因爲在一個複雜的class派生體系中,可能存在多個vptr。
  • 1是虛函數表的索引值,關聯到normalize()函數
  • 函數參數位置的ptr表示this指針

接着補充一下,如果Point3d::normalize()是一個靜態成員函數的情景:
一下兩個操作:

obj.normalize();
ptr->normalize();

將會被轉換爲一般的的nonmember函數調用,這也就是爲什麼當你定義兩個函數像這樣:

static void print(){}
void print(){}

會在編譯時報重複定義錯誤的原因。

雖然靜態成員函數大部分時候都被class object調用,但是它真的不是必須經過類對象才能調用,它是屬於類的!還是上面說的原因,static member functions缺乏this指針所以它差不多等於nonmember函數,如果取其地址,獲得的是其在內存中的位置,並不是一個指向class member function 的指針而是一個指向nonmember函數指針。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章