代碼大全裏有句話:“在一種語言上編程”的程序員將他們的思想限制於“語言直接支持的那些構件”。如果語言工具是初級的,那麼程序員的思想也是初級的。“深入一種語言去編程”的程序員首先決定他要表達的思想是什麼,然後決定如何使用特定語言提供的工具來表達這些思想。
俗一點的說就是,軟件開發,重要的是思想,語言僅是工具。有了好的創意,算法、方案和架構,可以選擇任何語言來實現。
OOP離不開數據抽象,爲了更好的進行數據抽象,需要掌握一些基本的技術,當所有最基本的技術都可信手拈來時就可以發揮強大的創造力。萬變不離其宗,基礎紮實才能遊刃有餘。
1、隱藏抽象的實現細節
一般情況下,我們都會在C++的頭文件中聲明類的所有成員(public,protected,private),對於這種情況,用戶並不能訪問私有和保護成員,但是能看到。有時候我們希望用戶無法訪問的還是不顯示爲好,尤其是庫的提供者。--不能用你還讓我看,調戲還是找罵?
用戶在創建類的對象之前,C++的編譯器需要知道類的所有成員的細節,以便爲對象分配足夠的內存,一般用戶都僅僅只有頭文件,因此在類的頭文件中需要聲明類的私有和保護成員。這樣同時也會增加編譯的成本,因爲類的大小改變時,類的所有用戶必須重新編譯它們的程序。(類加入或刪除數據成員時,類大小改變,而普通成員函數的增減需要重編,虛函數,若加在現存虛函數的末尾(現存函數在VTABLE中的偏移不變),可以不重編)
爲了解決以上問題,可以通過增加一個單獨的實現類來解決。(Qt中大量使用了這種方法,大多是由一個對應XXXPrivate後綴的類提供具體的實現)--其實就是外包出去
在導出的類中僅僅包含實現類的指針,而不是將所有的數據都保留在導出類的頭文件中,這樣XXXImpl的任何更改,都不會影響到XXX類。如下:
class XXXImpl;//此類中包含具體的數據成員,這裏利用了前置聲明,XXXImpl不需要導出給用戶,在XXX的構造函數中創建XXXImpl並讓m_pHandle指向它 calss XXX { public: XXX(); private: XXXImpl* m_pHandle; }
利用指針的好處:
- XXXImpl的改動不影響XXX,用戶不需要重新編譯,只需重新鏈接即可,極大的加快的編譯程序的速度。--外包商換了,但我不告訴你
- XXX的用戶看不到數據成員。--在外包商那裏,你在我這裏看瞎眼啊
利用指針的缺點:
- XXX的任何使用m_pHandle的成員函數不能是內聯的。(因爲內聯會在用戶代碼展開,展開時需要知道細節)--缺點再多該用還得用啊
- XXX每個對象都增加了一個指針的大小(32位機器是4字節)
- 對XXXImpl成員的訪問是通過m_pHandle進行的,增加了間接環節。
- 創建和刪除XXXImpl也需要成本
大型工程重新編譯的成本是非常高的,鏈接的主要任務是地址映射(重定位函數調用的地址等),和編譯相比,鏈接所花費的時間相當少。當修改類的實現,而接口不變時,用戶只需要重新鏈接。類中的任何下列改動都將導致用戶重新編譯代碼:
- 添加數據成員
- 修改數據成員大小或類型
- 修改數據成員順序
- 刪除數據成員
- 添加成員函數
- 修改函數聲明
- 修改函數順序
- 刪除函數
- 修改類內部任何聲明(枚舉、結構體等)
- 修改內聯函數
僅僅修改函數實現(實現重編),用戶只需重新連接(和新函數地址連接)。
2、指針、引用、值
按照按需創建的概念,即對象在需要的時候才創建。
使用指針作爲數據成員的優點:
創建包含指針的對象時,不需要立即給指針綁定對象,可以給NULL,讓對象創建速度更快。 在對象生存期內,指針可以指向不同對象,提供了更大的靈活性。使用指針作爲數據成員的缺點:
- 成員函數使用指針之前需要判空,否則崩潰。
- 若在成員函數中new創建了對象並賦給指針,則析構時需要刪除指針所指對象,否則內存泄漏。
對比引用和值,使用指針作爲數據成員只在需要靈活性的地方纔有用,如在創建類對象時,就需要使用類對象的數據成員,那麼使用引用和值會比指針容易。使用指針和引用時,可以使用多態。
3、控制對象的創建
只允許用new()創建對象
不讓用戶在棧上創建對象(局部),技巧是讓析構函數成爲私有。C++中用戶在棧上創建對象時,編譯器會查找匹配的構造和析構函數,如果其中一個無法訪問,則出現編譯錯誤。如此就只能通過new來創建對象了,但是問題是我們如何刪除對象呢?(析構已經是私有的了),一個簡單的辦法是提供一個Delete成員函數(delete this)。--怎麼看着像一損招?有用沒用,先記着
防止用new()創建對象
實現一個不可訪問new運算符。
private: void* operator new(size_t size); void operator delete(void* p);
4、避免大型數組作爲局部變量或數據成員
由於局部變量是在棧上分配的,棧通常有預設值的大小,且依賴具體平臺,在棧上創建大型數組可能導致程序崩潰,尤其是函數是遞歸的,問題更嚴重。數據成員同理。創建大型數組應該考慮使用堆。
使用數組作爲數據成員時,考慮使用指針代替數組。
使用對象數組和指針數
我們通常避免創建對象數組,使用對象數組有一些缺點:
XXX obArray[10]; //這樣只能調用默認構造函數
希望調用不同的構造函數時,得使用下面的方式:
XXX obArray[10] = { 11, XXX("test")}--對於剩下的8個,還是調用了默認構造函數,這種方法也只適用於小型數組,如果是10000個XXX對象的數組,估計要自殺了。
較好的方案是使用對象指針數組,可以自由使用特定構造函數。
XXX* pObArray[10];
for()
pobArray[i] = 0;//初始化
pObArray[0] = new XXX("test");//保存
更靈活一點,可以創建一個指向指針數組的指針,即指針的指針。XXX ** ppArray; ppArray = new XXX*[size];但使用指針數組,需要判空,切記。-都用STL容器多一些
5、成員函數返回值,數據成員,首選對象,而不是簡單類型的指針
有時候我們會設置類似 const char* GetName() const;這樣的成員函數,返回簡單類型的指針。這樣做並不能防止用戶將指針轉爲char*並修改它。這種時候,最好使用抽象數據類型代替char*來保存字符,如CString。--在可能的地方,使用對象代替原始指針類型。
6、避免臨時對象
如果對象做常量使用,則最好創建1次,然後重複使用。
void f(const XXX& x);
void g()
{
f(XXX("test"));
}
//每次調用g的時候,都將創建臨時對象
可以在全局
XXX Test("test");
g(){ f(Test); }
//或者在內部聲明static對象
void g()
{
static XXX test("Test");
f(test);
}
7、使用複製構造函數初始化對象
很多時候,我們希望用現存對象創建新對象,此時,複製構造函數是最好的選擇。
8、有效的使用代理對象
代理對象的作用:
用於安全共享的代理對象--寫時複製,進程共享內存 爲方便使用的代理對象 爲遠程對象替身的代理對象 提供其他功能的智能代理--智能指針 解決語法/語義問題的代理--操作符重載[] 通用下標代理
9、使用簡單的抽象建立更復雜的抽象
給定一個Fish和Fly,則很容易實現一個FlyFish抽象。
10、抽象必須允許用戶用各種不同的方式使用類
數據抽象和麪向對象編程的威力在於創建簡單的類,並用許多方法擴展它們,以創建更加強大有用的抽象。