前言
今年中下旬就要找工作了,我計划從現在就開始准備一些面試中會問到的基礎知識,包括C++、操作系統、計算機網絡、算法和數據結構等。C++就先從這本《深度探索C++對象模型》開始。不同於《Effective C++》,這本書主要着眼於C++實現的底層機制,因此我在寫這個系列時默認讀者已經熟悉C++的基本語法(包括類、繼承、多態、泛型等等),將更多地介紹C++具體是如何實現這些語法的。這次我就先寫第一、二章,之后每讀兩章都會更新該系列。如果你有什么問題,歡迎在博客的評論版塊和我探討,或者聯系我的郵箱:kevinstigma@gmail.com,很樂意與你互相切磋、提高。
第一章:關於對象
在傳統的C程序中,采用的是過程式的思維:“數據”和“處理數據的操作(函數)”是分開來聲明的,它們二者之間並沒有關聯性。但到了C++里,傾向於采用獨立的“抽象數據類型”(ADT)來實現數據的封裝。從代碼結構上來看,C++似乎比C要消耗更高的時間和空間成本,但事實未必如此。相比於C,C++在布局和時間上主要的額外負擔是由virtual引起的,這分為兩部分:
1.virtual function機制 用以支持一個有效率的“執行期綁定”。
2.virtual base class 用以實現“多次出現在繼承體系中的base class,有一個單一而被共享的實例”。
接下來,書里介紹了三種的C++對象模式:簡單對象模型、表格驅動對象模型、C++對象模型,其中第三種最為編譯器所常用,因此在這里主要介紹第三種。
C++對象模型中,非nonstatic data members被配置於每一個class object之內,static data members、function members則被存放在class object之外。編譯器為每一個class產生一個表格,表格里是一堆指向virtual functions的指針,該表格我們稱之為virtual table(vtbl);每一個class object被安插一個指針,指向相關的virtual table,這個指針被稱為vptr。vptr的設定和重置由constructor、destructor以及copy assignment運算符自動完成。每一個class所關聯的type_info object也存放在virtual table之中。
該模型的優點是,它的空間和時間的效率較高;缺點在於,如果應用程序本身的代碼未改變,而所用到的class object的nonstatic data members有所修改,那么應用程序的代碼同樣需要重新編譯(因為內存分布發生了改變)。
在接下來的篇幅里,書中探討了struct和class的區別,對於這個問題可以參考這篇博文:http://blog.csdn.net/omegayy/article/details/7470316。其實struct和class從語法本質上差別並不大,無非是二者的默認繼承和默認成員訪問級別不同。但從一般來說,我們習慣用struct來代表一些簡單數據的集合,用class來代表更為復雜的封裝、繼承的數據。
與C不同,在C++中,對象的內存分布未必和類中成員的聲明順序一致,比如C++的編譯器可能會將protected data members放在private data members前面存儲,與聲明順序無關;同理,base classes和derived classes的data members的布局也未有誰先誰后的強制規定。但類中處於同一個access section中的數據,其內存分布順序是按照聲明順序排列的,只是不同access section之間未必按聲明順序排列。
之后書中花了一些篇幅介紹多態的機制,我覺得熟悉C++的人應當對此並不難理解,在這里不再贅述。
第二章:構造函數語義學
本章主要介紹了C++中構造函數的生成機制。
首先是default constructor,C++的編譯器真的會為每一個沒有聲明構造函數的類生成default constructor嗎?其實並不是的。書中指出,只有四種情況,會造成“編譯器必須為未聲明constructor的classes合成一個default constructor”:
1.member constructor有defualt constructor;
2.base class具有default constructor;
3.該類具有virtual funciton;
4.該類具有一個virtual base class。
在合成的defualt constructor中,只有base class subobjcets和member class objects會被初始化,而所有其他的nonstatic data member(int、int*、char等類型)都不會被初始化。
之后是copy constructor。與defualt constructor一樣,編譯器並不是為所有無用戶定義copy constructor的類都創建copy constructor,如上的四種情況下,編譯器才會為class合成copy constructor;否則編譯器會執行bitwise copy。
接下來書中介紹了NRV優化以及member initialization list。
對於NRV(Named Return Value)優化,我們可以看一個例子:
假設有一個foo函數:
X foo() { X xx; if(...) return xx; else return xx; }
經過編譯器的優化后,代碼改成如下樣子:
void foo(X &result) { result.X::X(); if(...) { return; } else { return; } }
這樣就省去了臨時對象的默認構造函數、拷貝構造函數、析構函數的成本,從而有助於減少運行時間。另外,有一點要注意的是,NRV優化,有可能帶來程序員並不想要的結果,比如當你的類依賴於構造函數或者拷貝構造函數的調用次數時。
關於member initialization list,我們首先需要知道需要它的時機:
1.當初始化一個reference member時;
2.當初始化一個const member時;
3.當調用一個base class的constructor,而它擁有一組參數時;
4.當調用一個member class的constructor,而它擁有一組參數時。
接下來需要注意的一點就是:list中的初始化順序是由class中member的聲明順序決定的,而不是由initialization list中的排列順序決定的!這是一個非常容易出錯的地方。