effective C++筆記之條款34: 將文件間的編譯依賴性降至最低


1.    先看如下函數:
class Person
{
public:
        Person(const sting& name, const Date& birthday,
const Address& addr, const Country& country);
        virtual ~Person();
        string name() const;
        stirng birthDate() const;
        stirng address() const;
        string nationality() const;
    private:
        string name_;   //實現細節
        Date birthday_;  //實現細節
        Address address_;  //實現細節
        Country citizenship_; // 實現細節
    };


    Person的實現用到了一些類,即string,Date,Address和Country;Person要想被編譯,就得讓編譯器能夠訪問到這些類的定義。一般是通過#include指令來提供的。但是,這樣以來Person的文件和這些頭文件之間就建立了編譯依賴關係。所以任意一個輔助類改變了它的實現,或任一個輔助類所依賴的類改變了實現,包含Person類的文件以及任何使用了Person類的文件就必須重新編譯。
2.    我們應該將一個對象的實現隱藏在指針的身後。看下面經過修改後的Person類:
//編譯器還要知道這些類型名,因爲Person的構造函數要用到它們
class string;  //對標準string來說這樣做不對
class Date;
class Country;
class Address;
//類PersonImpl將包含Person對象的實現細節,此處只是類名的提前聲明
class PersonImpl;
class Person
{
    public:
Person(const string& name, const Date& birthday,
const Address& addr, const Country& country);
        virtual ~Person();
        string name() const;
        string birthDate() const;
        string address() const;
        string nationality() const;
    private:
        PersonImpl *impl; //指向具體的實現類
};


現在Person的用戶完全和string,date,address,country和person的實現細節分家了。那些類可以隨意修改,而Person用戶可以不需要重新編譯。另外,因爲看不到Person的實現細節,用戶不可能寫出依賴這些細節的代碼。這是真正的接口和實現的分離。
3.    只要有可能,儘量讓頭文件不要依賴於別的文件;如果不可能,就藉助於類的聲明,不要依靠類的定義。其他一切方法都源於這一簡單的設計思想。
4.    如果可以使用對象的引用和指針,就要避免使用對象本身。定義某個類型的引用和指針只會涉及到這個類型的聲明,定義此類型的對象則需要類型定義的參與。

5.    儘可能使用類的聲明,而不用類的定義。因爲在聲明一個函數時,如果用到某個類,是絕對不需要這個類的定義的,即使函數是通過傳值來傳遞和返回這個類的。如下所示:

class Date;
Date returnADate();  //正確,不需要Date的定義
void takeADate(Date d);



所以,將提供類定義(通過#include指令)的任務從你的函數聲明頭文件轉交給包含函數調用的用戶文件,就可以消除用戶對類型定義的依賴,而這種依賴本來是不必要的、是人爲造成的。
6.    不要在頭文件中再包含其他頭文件,除非缺少了它們就不能編譯。相反,要一個一個地聲明所需要的類,讓使用這個頭文件的用戶自己去包含其他的頭文件,以使用戶代碼最終得以通過編譯。
7.    Person類僅僅用一個指針來指向某個不確定的實現,這樣的類常常被稱爲句柄類(Handle Class)。對於指向的類來說,叫做主體類)。句柄類只是把所有函數的調用都轉移到了對應的主體類中,主體類真正完成工作。如PersonImpl和Person含有一樣的成員函數,他們的接口完全相同。使Person成爲一個句柄類並不改變Person類的行爲,改變的只是行爲執行的地點。
8.    除了句柄類,另一個選擇是使Person成爲一種特殊類型的抽象基類,成爲協議類。協議類沒有實現;它存在的目的是爲派生類確定一個接口。所以,他們一般沒有數據成員,沒有構造函數;有一個虛析構函數,還有一套純虛函數,用於指定接口,如下所示:
class Person
{
public:
        virtual ~Person();
        virtual string name() const = 0;
        virtual string birthDate const = 0;
        virtual string address() const = 0;
        virtual string nationality() const = 0;
};


和句柄類的用戶一樣,協議類的用戶只是在類的接口被修改的情況下才需要重新編譯。

9.    句柄類和協議類分離了接口和實現,從而降低了文件間編譯的依賴性。但這會帶來代價:
    對於句柄類來說,成員函數必須通過(指向實現的)指針來獲得對象數據。這樣,每次訪問的間接性就會多一層。此外,計算每個對象所佔用的內存大小時,還應該算上這個指針。還有,指針本身還要被初始化(在句柄類的構造函數內),以使之指向被動態分配的實現對象,所以,還要承擔動態內存分配(以及後續的內存釋放)所帶類的開銷。
    對於協議類,每個函數都是虛函數,所以每次調用函數時必須承擔間接跳轉的開銷。而且,每個從協議類派生而來的對象必然包含一個虛指針。這個指針可能會增加對象存儲所需要的內存數量。
10.    句柄類和協議類都不大會使用內聯函數。使用任何內聯函數時都要訪問實現細節,而設計句柄類和協議類的初衷正是爲了避免這種情況。


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