有關c++空類問題

《 effective c++ 》

空類是沒有數據成員,沒有函數的類,例如 class Empty { };

1. 空類或者空類對象的大小(無繼承情況下):編譯器會在空類中安插一個char,使得這個空類的兩個objects得以在內存中配置獨一無二的地址。只有當一個類爲空的時候,編譯器纔會向該類中加入這樣一個char!

2. 空類或者空類對象的大小(有繼承但無虛繼承情況下):繼承的基類都是空類,大小是1,不管有多少個父類。

3. 空類或者空類對象的大小(單一虛繼承情況下):對每一個虛繼承基類,都有一個虛基類索引(或者偏移量、指針),佔4字節


定義一個空的C++類,例如

class Empty
{
};

一個空的class在C++編譯器處理過後就不再爲空,編譯器會自動地爲我們聲明一些member function,一般編譯過去就相當於

class Empty
{
public:
Empty(); // 缺省構造函數
Empty( const Empty& ); // 拷貝構造函數
~Empty(); // 析構函數
Empty& operator=( const Empty& ); // 賦值運算符
Empty* operator&(); // 取址運算符
const Empty* operator&() const; // 取址運算符 const
};

如果你只是聲明一個空類,不做任何事情的話,編譯器會自動爲你生成一個默認構造函數、一個拷貝默認構造函數、一個默認拷貝賦值操作符和一個默認析構函數這些函數只有在第一次被調用時,纔會別編譯器創建。所有這些函數都是inline和public的。

默認的析構函數是非虛函數(除非基類有自己聲明的虛析構函數)。而拷貝默認構造函數和默認拷貝賦值操作符知識是單純將來源對象的每一個非靜態成員拷貝到對象目標中(bitwise copy)。

其中的默認拷貝賦值操作符只有在生成的代碼合法並且有機會證明它有意義存在時纔會生成。這就說明,如果你打算在一個“內含引用成員”或者“內含const成員”的類內支持賦值操作,就必須定義自己的默認拷貝賦值操作符。因爲C++本身不允許引用改指不同的對象,也不允許更改const成員。

最後一種情況,當基類將自己的默認拷貝賦值操作符聲明爲private時,子類就不會產生自己的的默認拷貝賦值操作符。因爲假如產生了這樣的默認拷貝賦值操作符,它會試着去調用基類的默認拷貝賦值操作符去處理基類的部分,不幸的是,它沒有權利。

你可以將拷貝構造函數或默認拷貝賦值操作符聲明爲private。這樣明確聲明一個成員函數,就阻止了編譯器暗自創建的默認版本,而這些函數爲private,使得可以成功阻止人們調用它。

上面的做法有一個隱患,因爲類自身的member和friend還是可以調用這些private函數。有一個很刁鑽的方法,“將成員函數聲明爲private而且故意不實現它們”,這樣既阻止了默認函數的生成,而且如果你試着調用這些函數,就會得到一個鏈接錯誤。只聲明,不定義,鏈接器報錯。甚至在聲明的時候,你連參數也不用寫。


試着將上述的鏈接器錯誤提前到編譯器也是可以的。我們專門設計一個類Unconpyable。
--------------------------------------------------------------------
class Uncopybale {
protected:
    Uncopyable() {}
    ~Uncopyable() {}
private:
    Ucopyable(const Uncopyable&)
    Uncopyable& operator=(const Uncopyable&)
};

--------------------------------------------------------------------


爲了阻止對象被拷貝,我們唯一需要做的就是繼承Uncopyable。這些函數的默認生成版本會嘗試調用其基類的對應版本,那些調用會被編譯器拒絕,因爲它基類的拷貝函數是private。

Boost提供的noncopyable類也有類似的功能。

忠告:

爲了駁回編譯器自動提供的技能,可將相應的成員函數聲明爲private並且不予實現。使用像Uncopyable這樣的基類也是一種做法。

並不是所有的編譯器都包成對象的內置類型成員會被自動初始化爲0。永遠在使用對象之前先將它初始化。確保每一個構造函數都將對象的每一個成員初始化。

別把賦值錯當成初始化。C++規定,對象的成員變量的初始化動作發生在進入構造函數本體之前(對於內置類型對象可能不確定),這點對於非內置類型對象來說尤其關鍵。如果你沒有在成員初始化列表(member initialization list)爲其初始化,它們將調用自己的默認構造函數,然後才進入構造函數內部(很可能你會在這裏給他們賦值)。在成員初始化列表中的初始化只是調用了拷貝構造函數一次,而在構造函數內部再爲其賦值則在調用默認構造函數後又調用了一次拷貝構造函數。哪個效率高你當然知道。

所以,請用成員初始化列表進行初始化,雖然效率提高只針對於非內置類型成員,但是規定總是在初始化列表中列出所有成員變量,這樣就省的有些未被列出的內置類型成員被忘記初始化。而有些時候,即使成員變量是內置類型,也必須要用成員初始化列表(成員變量爲const或者reference,它們一定要有初值,而且不能被賦值)。

總之,總是使用成員初始化列表,這樣或者必要,或者高效。有個例外,當你重載多個構造函數,每個構造函數有很多成員變量和基類的時候(這意味這成員初始化列表會很多、很長而且重複較多),可以將一些內置類型變量的初始化動作(它們的賦值和初始化不影響效率)移到一個私有函數中,供所有的構造函數調用。

規定:初始化順序是基類早於派生類,類成員變量則以其聲明順序爲準。所以成員初始化列表中列出的各個成員的順序最好與聲明的順序相同。

最後說個不常見的問題:某個對象A的非靜態成員變量初始化動作正好使用了另外一個編譯單元(另外一個cpp)中的某個非靜態對象B,你不能保證A在需要B的時候,B就已經被編譯好而且產生了。解決的辦法是將對象A和對象B都分別放到函數中(貌似是專門爲每個這樣的對象定製的對象),並且聲明爲static。這些函數返回的是靜態對象的引用。這是單例模式的一種實現。在程序中以前需要對象引用的地方直接調用這些函數就好了。這種reference-returning函數對於處理多線程環境下的“競速形勢(race conditions”的方法是:在程序的單線程啓動階段手工調用所有的reference-returning函數。

忠告:

1 爲內置對象進行手工初始化,因爲C++不保證初始化它們。

2 構造函數最好使用成員初始化列表(member initialization list),而不要在構造函數本體內使用賦值操作。初始化列表列出的成員變量,其排列次序應該和它們在class中的聲明次序相同。

3 爲免除“跨編譯單元的初始化次序”問題,請以local static對象替換non-local static對象。

鏈接: http://blog.sina.com.cn/s/blog_5f76aaf20100cwlj.html

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