《深度探索C++物件模型》侯捷譯——筆記(一),讀後感,附帶【插圖】

來源:互聯網
上載者:User
一)、讀後感

    在我參加工作兩年多的時候,工作不算很忙了,《深入理解C++物件模型》開始進入我的視野;或許是因為我要從Symbian.C++ 轉向iOS Objective-C,並開始思考語言本身的一些東西的緣故。

    其實在一年前,出於對C++的迷惑,我已經買了這本書。當時翻了幾頁竟然沒懂,就擱那兒了!可是現在,它讓我隨身攜帶、流連忘返、是個旅途好伴侶;看到它我精神抖擻,它給了我繼續做程式員的信心

    這段時間經常會在晚上11點後,關閉電腦,然後捧著書本兒汲取知識。這種感覺覺很不錯!如果你在北京上班,那麼不要在地鐵上捧著手機看新聞、看微博和QQ,可以看點兒書。

    以上是我感慨,也許你覺得我說的太羅嗦、誇張。我只能再引用李宗盛《鬼迷心竅》中的歌詞:

        “有人問我你究竟是哪裡好, 這麼多年我還忘不了。“

         “春風再美也比不上你的好,沒見過你的人不會明了。”

    我看的是左邊“藍綠色”的老版的,右邊是2012版的。我看過新版的目錄,跟老的基本一樣。買新的吧

                           

    閱讀者要求。需要具備C++的基礎知識。這本說就像譯者評論的那樣,不是嬰幼兒奶粉,它是成人專用的低脂高鈣特殊奶粉。假如把C++比喻成一輛汽車,這本書不是教你怎麼開車,而是將汽車大卸八塊,逐個組件剖析。

    這本說的作者也有一些地方是相互矛盾的,很難理解,難道是C++太複雜了麼。

    專業術語介紹:

derived class 衍生類別
base class 基類
member function 成員函數
nonvirtual function 非虛函數

二)、回答幾個小問題

    這本書的作者就是C++第一個編譯器(cfront)的負責人,所以作者主要從編譯器的角度來剖析C++的物件模型。

    第一個、一般來說在學習C++的時候,如果沒有指明一個建構函式,那麼系統會預設建立建構函式。非也,編譯器會決定是否有必要產生一個建構函式和解構函式。也就預設建構函式可能不存在哦!特別是沒有繼承的情況下,編譯器認為建構函式和解構函式是無用的。(參考p231)

    第二個、假設兩個基類BaseABaseB都有virutal函數,BaseC繼承自BaseABaseB,那麼BaseC會有幾個虛函數表?答案是:根據編譯器不同而不同,有些是兩個虛函數表。有些是一個表,比如sun的編譯器。注意:這種情況屬於多重繼承,BaseC肯定會有兩個虛函數表指標

    第三個、局部變數和全域變數重名了,在局部變數的生命週期的大括弧之內使用這個變數,那個起作用。當然是局部變數,但是C++並不是從一開始就是這麼設計的。

    一定要重複閱讀第三章:Data語意學第四章:function語意學,和五章:構造、析構、拷貝語意學,這時平時開發中最常見的。

    侯捷翻譯的很不錯,很多地方比如“虛函數”,基類,衍生類別,直接用virtual function 、base class derived class代替,很符合程式員的習慣。

下面開始筆記本分三)、類屬性(Data語意學p83-p143)

---》一個空的類,大小不是0而是1,因為編譯器會產生一個隱晦的1bytes,用於區分,當該類多個對象時,各個對象都能在記憶體配置唯一地址。(p84)

---》成員變數的記憶體對齊,例如一個類只有char a一個屬性; 但是它的大小是4.雖然char的大小是1。(p85)

---》為了保持跟C的相容性,C++不要求基類屬性跟衍生類別屬性的排列順序,這個完全有編譯器決定。(p88)

---》局部變數和全域變數重名情況,在局部變數的生命週期的大括弧之內,使用該變數,哪個起作用?在1990年 隨著The Annotated C++ Reference Manual修訂,局部變數開始隱藏全域同名變數。而之前則是不隱藏。(p89)

---》屬性的記憶體順序和聲明順序是一致的。不同層級(public、protected和private)屬性的排列順序是相對一致的,就是說可能不連續,但是必須符合較晚出現的屬性存在較高的地址。(p92)

---》虛函數表指標Vptr,可能存在類的開始,也有可能存在類的末尾。通常都是類的末尾。(p92,p111,p112)

首先介紹vptr存在末端模式。示範單一繼承並含有虛函數情況下的資料布局(自然多態)。Point2d 和Point3d是繼承關係,注意:Vptr放在類的末尾。

初學者不要以為衍生類別的虛函數表指標Vptr(類結構中存的是虛函數表指標,並非虛函數表)存在衍生類別的那個部位,它依然是在父類的完整對象結構中

只不過,在衍生類別構造的時候,會將vptr所指向的virtual table修改。

vptr在前端模式,這麼做喪失了與C的相容性。

    如果是前端存放,還存在一個問題:如果基類沒有虛函數,衍生類別有虛函數,那麼單一繼承的自然多態就會被打破。如果要將衍生類別轉換成基類,必須編譯器的介入。(p112)

    編譯器似乎開始發揮它的作用了。多重繼承下又是虛擬繼承,編譯器必須做出必要的位移和調整,才能保證正確的調用虛函數。

---》對一個類對象取地址,那麼並不是第一個屬性的地址,第一個屬性的地址還需要+1,這麼做是為了區分指向第一個屬性和指向所有屬性的指標兩種情況。(p98)

---》一般而言,基類屬性在衍生類別的開始部分,但是C++任何一條規則,只要碰上虛繼承就沒轍兒了。(p99)

---》C++語言保證“出現在衍生類別中的基類對象,有其完整性”,這麼做是為了在位拷貝的時候,能夠拷貝正確。(p106)

假如ClassA 和ClassB都有一個char的屬性,假設ClassB 繼承自ClassA,假設,C++為了節省記憶體,將自己的char類型和基類的char類型綁定一起,那麼經過下面運算式後可能出現問題:

ClassA* a = new ClassB;

ClassB b = *a;

描述的是“緊湊類型”,這樣會導致嚴重後果,衍生類別的屬性可能被“抹掉”,中的char b

不要以為ClassB中的char b和ClassA中的char 會放在一起,由於記憶體對齊的規則,ClassA大小是4B,ClassB大小是8B。這樣即使拷貝就不會出問題。

描述的是父類在子類中有完整的對象結構:

(同樣就像剛才我說的那樣:虛擬繼承將破壞這種父類結構的完整型)

---》單一繼承下,父類通常在衍生類別前端。所以不管繼承有多深,把一個derived class指定給class,該操作不需要編譯器的介入。多重繼承既不像單一繼承,也不容易類比出其模型,多重繼承的複雜度在於derived class和其上一個base class 乃至於上上一個base class......之間的“非自然”關係,(p112)

多重繼承的問題主要發生於derived class和其第二或後繼的base class 之間的轉換。

對於一個多重派生對象,將其地址指定給“最左端(也就是第一個)基類的指標”,情況和單一繼承時相同,因為兩者都指向相同的起始地址。需要付出的成本只是地址的指定操作而已,至於第二個或後繼的base class的地址指定操作,則需要進行地址修改:加上或者減去介於中間base class大小。

展示了多繼承的關係。涉及到4個類 Point2d、Point3d、Vertex和Vertex3d(p115)

下面展示了多重繼承的物件模型。

多繼承的情況下,drived clas可能會有兩個或兩個以上虛函數表指標

請看下面的運算式:

Vertex3d v3d;

Vertex *pv;

Point2d *p2d;

Point3d *p3d;

那麼這個操作 pv = &v3d  需要轉換內部代碼pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));

下面這兩個操作,只需要拷貝地址就行了。

p2d = &v3d;

p3d = &v3d;

---》虛擬多繼承情況(p117)

可以表現Vertex3d 的繼承體系圖。左為多重繼承,右為虛擬多重繼承。

    不論是Vertex還是Point3d都內含一個Point2d。然而在Vertex3d的物件版面配置中,我們只需要單一一份Point2d就好。所以引入虛擬繼承。然而編譯器要實現虛擬繼承,實在是困難度頗高。虛擬繼承的原則就是:讓VertexPoint3d各自維護的Point2d
摺疊成一個有Vertex3d維護的單一Point2d,並且還可以儲存base class 和derived class的指標之間的多台指定操作。

    如果一個class含有virtual base class 那麼,該對象將被分割為兩部分:一個不變局部和一個共用局部。不變局部中的資料,不管後繼如何演化,總是擁有固定的offset,所以這部分資料可以直接存取。至於共用局部(即virtual base class),這一部分的資料,其位置會因為每次的派生操作而有變化,所以他們只能被間接存取。各家編譯器實現技術之間的差異就是間接存取的方法不同,目前有三種主流策略。

第一個策略:如何存取class的共用局部呢?cfront編譯器會在每一個derived class中安插一個指向virtual base class的指標,這樣就可以間接存取。這樣的實現模型會有下面兩個主要缺點:

1.每一個對象必須針對其每一個virtual base class 背負一個額外的指標。

解決方案有:第一個,Microsoft編譯器引入所謂的virtual base class table。每一個class object如果有一個或多個virtual base class,就會由編譯器安插一個指標,指向virtual base class table。至於真正的virtual base class 指標,當然是被放在該表格中。

請看下面的虛擬繼承物件模型,。


紅框內即所謂的“共用局部”,其位置會因每次派生操作而有所變化。虛擬破壞了base class 的對象完整型,虛擬繼承會在自己類中產生一個虛函數表指標。

第二個、在virtual function table 中放置virtual base class的offset(不是地址)。


這個方法的好處是,巧妙的利用了虛函數表的結構,使得drived class 能夠節省一個指標的大小。中國藍色曲線是offset

2.由於虛擬繼承串鏈的加長,導致間接存取層次的增加。例如:如果我們有三層虛擬衍化,我就需要三次間接存取(經由三個virtual base class指標)。

這個問題的解決方案有:拷貝所有的virtual base class 的指標到drived class中。這樣就解決了存取時間的問題,雖然會有空間的開銷。

一般而言,virtual base class 最有效一種運行形式就是:一個抽象的virtual base class 沒有任何的data members。也許正是java和Objective-c不使用多重繼承,卻使用介面類(OC叫協議)的原因。

---》如果對類的屬性取地址(p130)

比如 &Point3d::z得到的值將是z在所有屬性中位移量。

列印該值的時候必須使用這個方法   :printf("&Point3d::z =%p\n",&Point3d::z);

四)、類方法(function語意學p139-p186)

---》 C++的成員函數有三種:static 、nonstatic和virtual。每一種類型的調用方法都不同。(p140)

---》C++的設計原則之一就是nonstatic member function至少必須和一般的nonmember function有相同的效率。而實際上成員函數也是被轉化為nonmember function調用,下面是轉化步驟:(p142)

1.改寫函數的簽名(signature,函數名稱+參數數目+參數類型)安插一個this指標到函數參數中來。

例如:float Point3d::magnitude3d()const;

經過改寫後的方法為:float Point3d::magnitude3d(const Point3d* const this)const

***這也就是問什麼:const  可以用來區分重載函數的標示的,包括const參數或const函數,但是傳回值不算,因為傳回值不會作為函數的簽名。

2.對nonstatic data member 的存取操作改為經由this指標來完成。

3.將member function重新寫成一個外部函數。對函數進行mangling(重新命名)處理,是它在程式中成為獨一無二的語彙。

---》一般而言,member function(data member也是一樣)的名稱前面會被加上class名稱,形成獨一無二的命名。(p144)

---》目前C++編譯器對name mangling的做法還沒有統一,但是遲早會統一。(p145)

---》虛函數(p147)

如果函數normalize()是一個虛函數,那麼下面的調用 ptr->normalize()將被內部轉化為:

(*ptr->vptr[1])(this); vptr是有編譯器產生的指標,指向virtual table。下標為1說明是是第1個虛函數。

---》靜態成員函數將被轉化為一般的nonmember函數調用。它不能存取nonstatic members,不能聲明為:const、volatile或virtual。

由於靜態成員函數缺乏this指標,因此其差不多等同於nonmember function。它提供了一個意想不到的好處:成為callback函數。

---》虛擬成員函數(p152)

在C++中多態(polymorphism)表示”以一個public base class的指標(或者reference),定址處一個derived class object“的意思。

在C++中virtual functions可以在編譯時間期獲知,這一組地址是固定不變的,執行期不可能新增或者替換值。

請看下面一個類Point的定義:

class Point {public:          virtual ~Point();          virtual Point& mult(float)=0;          float x()const {return _x;}          float y()const {return 0.0;}          float z()const {return 0.0;}protected:        Point(float x=0.0);        float _x;};

Point2d繼承自Point。Point3d繼承自Point2d。那麼記憶體模型,單一繼承情況

在單一繼承體系中,virtual function機制的行為十分良好,不但有效率而且很容易塑造其模型出來,但是在多重繼承和虛擬繼承中,對virtual function的支援就沒有那麼美好了

---》thunk技術(p162)

所謂的thunk是一段assembly碼,用來以適當的offser值調整this指標,跳到virtual function去。Thunk技術允許virtual table slot 繼續內含一個簡單的指標,因此多重繼承不需要任何空間上的額外負擔。slots中的地址可以直接指向virtual function,也可以指向一個相關的thunk。

---》vptr將在建構函式中被設立初始值。(p164)

---》多重繼承下的虛函數

    多重繼承下,通常衍生類別會有多個virtual table ,最左邊基類的稱之為:“主要表格”,第二或更過多基類的表格稱為:“次要表格”(參考),衍生類別的主要表格和次要表格可以連在一起,比如Sun的編譯器的策略就是這樣的。(p164,p165)

class drived 繼承自 class Base1 class Base2 類結構如下:

class Base1{                                      class Base2{public:                                            public:          Base1();                                        Base2();          virtual ~Base1();                               virtual ~Base2();          virtual void SpeakClearly();                    virtual void mumble();          virtual Base1* clone() const;                   virtual Base2* clone()const;};                                                     };

這兩個類我故意並列在一起,Base1和Base2的區別就是兩個不同的虛函數void SpeakClearly()和void mumble();

class Derived: public Base1,public Base2{public:            Derived();            virtual ~Derived();            virtual Derived* clone()const;protected:            float data_derived;};

那麼這幾個類的virtual table的布局如下:

多重繼承下:derived類會分別重寫“主要表格”和“次要表格”

---》虛擬繼承下的虛函數。

---》當然這本說也不是如此的深入,當一個virtual base class 從另外一個virtual base class派生而來,並且兩者都支援virtual functions和nonstatic data members時,編譯器對於virtual的支援簡直就像進入迷宮一樣。作者只是給了一句話“距離複雜的深淵懸崖不遠了。”(p169)

---》擷取一個nonstatic member function的地址,如果該函數是non virtual,則得到的結果是它在記憶體中的真真實位址。然而這個地址是不全的,他也需要被綁定與某個class object的地址上(this指標),才能過通過它調用函數。(p174)

---》擷取一個virtual member function的地址,只能擷取一個索引值。(p176)

那麼,如果使用一個函數指標float (Point::*pmf)() = &Point::z;這時pmf是一個索引值。

但是,pmf還可以指向一個nonvirtual member function的真真實位址啊?cfront的做法是如果pmf大於127就是真真實位址,如果小於127就是索引值。當然這種設計限定了繼承體系中只能有128個virtual function,這並不是我們希望看到的。在多重繼承的引入後又有了別的方法解決這個問題。然而,剛剛說的這個方法就淘汰了。(p178)

---》多重繼承下,指向member functions的指標。指向member function的指標需要先指向一個結構體,該結構體中存放幾個屬性分別表示virtual table的索引和non virtual member function的地址。詳情見(p179)

---》inline函數提供了一個強有力的工具。然後與non-inline函數比起來,他們需要更小心的處理。

五),構造、析構和拷貝語意學(p191-p236)

看第五章跟打遊戲一樣,看著看著不行了,看不懂了,這關沒過去,還得從頭兒再來。

---》每一個derived class destructor 會被編譯器加以擴充,以靜態調用的方式調用其“每一個virtual base class”已經“上一層base class”的destructor。所以virtual function不要聲明為pure(p193)

point的聲明

type struct{        float x,y,z;}Point;

point的使用:

Point global;

Point foobar(){        Point local;        Point *heap = new Point;        *heap = local;        delete heap;        return local;}

觀念上Point的建構函式和解構函式會被編譯器建立,事實上並非如此:Point被編譯器看做是Plain Ol' Data。

---》無繼承情況下的物件建構(p196)

---》不論是private、public存取層,或是member function的聲明,都不會佔用對象的空間。(p199)

---》constructor可能內帶大量隱藏代碼,因為編譯器會擴充每一個constructor,大致有下面幾種情況:(p206)

        1.初始化“初始化列表中的資料”

        2.如果data member沒有出現在初始化列表中,將調用data member的constructor。

        3.如果有vptr進行初始化。

        4.上一層的base class constructor必須唄調用,以base class的聲明順序為準。

        5.所有virtual base class constructor必須被調用。

---》虛擬繼承下的建構函式。(p210)

如的繼承關係。

如果Vertex3d構造的時候,必然調用Point3d的建構函式,同時調用Vertex的建構函式,然而這兩個類都要必須調用Point2d的建構函式,這是不合理的。取而代之的是應該在Vertex3d的建構函式中直接對Point2d初始化。這樣就需要Vertex3d再條用Point3d或者Vertex的建構函式的時候傳遞一個bool參數__most_derived,即“是否是最後一層繼承關係”,然後Point3d或者Vertex的建構函式根據這個bool變數決定是否構造Point2d。

總結為一句話:virtual base class constructor,只有當一個完整的class object被定義出來時,它才會被調用。如果object只是某個完整的object的suboject

,他就不會被調用。

---》vptr的初始化(p213)

在base class constructor叫用作業之後,但是在程式員提供的代碼或是“member initialization list中所列的members初始化操作”之前編譯器對vptr進行初始化。這個過程就像想象的那樣:一個PVertex對象會先成為一個point2d對象。一個point3d對象、一個vertex對象和一個vertex3d對象,最後才成為一個PVertex對象。

---》一個建構函式的真實步驟可能如下:(216)

        1.在derived class constructor 中,“所有virtual base classes”及“上一層base class”的constructor會被調用。

        2.上述完成後,對象vptr(可能多個vptrs)被初始化,指向相關的virtual table(可能多個表)

        3如果有member initialization list 的話,將在constructor體內擴充開來。這必須在vptr被設定之後才進行,以免有一個virtual member function被調用。

        4.最後,執行程式員所提供的代碼。

---》如果不準將一個class object指定給另外一個class object,那麼只要將copy assignment operator聲明為private即可。(p219)

---》解構函式(p231)

如果class 沒有定義destructor,那麼只有在class內帶的member object(或是class自己的base class)擁有destructor的情況下,編譯器才會自動合成出一個來。否者destructor被視為不需要,也就不需要合成(當然更不需要調用)

---》解構函式的實際操作可能如下:

        1.destructor的函數本身首先被執行

        2.如果class擁有member class objects,而後者擁有destructor,那麼它們會以其聲明順尋的反序被調用。

        3.如果object內帶一個vptr,則現在被重新設定,指向適當的base class的virtual table

        4.如果有任何直接的nonvirtual base lasses 擁有destructor,它們會以其聲明的反序被調用。

        5.如果有任何 virtual base class擁有destructor,而當前討論的這個class是最末端(most-derived)的class,那麼它們會以其原來的構造順尋的相反順尋被調用。

以上是第三章、第四章和第五章的主要內容。

 - - - - - - - - -未完待續---------- 剩餘章節會新寫一個blog- - - - - - - - - - - 

六、C++大記事:

    1993年引入RTTI。

    1991年引入templates 模板(在cfront 3.0中引入的)

    1990 隨著The Annotated C++ Reference Manual修訂,局部變數開始隱藏全域同名變數。

    1989年,發布了Release 2.0。引入了多重繼承、抽象類別、常數成員函數,以及成員保護。

    1987年 引入靜態成員函數。

    20世紀80年代中期引入虛函數。

從某種角度上來說,C++的強大要歸功與C++的編譯器的強大。這時我才知道為什麼用很厚一本書來介紹visual studio,可能也是Symbian不用標準C++的原因。

如有問題,歡迎大家斧正!

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.