這個原則著重講述的是編譯的依存關系,篇幅很長,理解起來也比較費勁。
這個原則是針對什么問題而提出來的呢?有的時候你在一個大工程里,你就修改了某個類中的一小部分實現,甚至就是一條語句,結果整個工程各種莫名其妙的錯誤出現了,然后你崩潰了。
作者說這是由于沒有把接口從實現中分離造成的,那這又是什么意思呢?從作者所舉的例子Person類來看,它的私有成員里面有很多其他類的對象,而這些對象又是Person類中某些函數的參數,如下圖所示:
從這圖可以看出作者所說的實現也就是私有成員中所列的這些東西,而它們又是其他類的對象。那既然用到了其他類那必須要引用其他類的頭文件啊,就是使用#include命令。這樣的話就形成了一定的編譯依賴關系,這名詞還是很有學術氣息的。那形成編譯依賴關系又能怎么樣?那可以用一句話來概括——牽一發而動全身。
作者接著引用了一種慣常的思維,既然你說不要把實現和接口摻和在一起,那你就不實現唄,讓那些被引用的類也只不過是類的聲明而已不就得了,讓后把它們一塊放在命名空間里面。這種類的聲明叫做前置聲明。這樣做的好處就是實現不會動,會動的只能是接口,那么用戶只需要在接口被改動之后重新編譯即可。
不過上面這種辦法純屬扯淡!因為編譯器必須知道編譯期間某個對象的大小,編譯器只能通過類的定義才能知道這個對象需要多大空間,現在你就給了一個聲明,編譯器哪知道那個對象到底需要多大地方?!在這一點上C++和Smalltalk,Java還是有區別的,因為后兩者只提供指向類的指針,而指針大小是固定的。
于是乎得出了一種常用的設計方式,那就是接口和實現分離,再具體點就是一個類提供接口,另一個類提供實現。而接口類和實現類中連接的紐帶就是一個作為指向實現類的私有智能指針。這種設計一般被稱為pimpl(pointer to implementation)。
它體現的思想是使用生命的依存性去替換實現的依存性,這是編譯依存性最小化的本質體現。
很奇怪,你僅僅是聲明一個類,你就能用這個類定義一個形參并且放在函數形參列表中。其實還是那種情況,你只需要在用到定義的時候才真正去暴露類的定義,函數也是同理,所以你可以看到某個頭文件中有很多聲明式包括函數的和類的。那么你也會看到我經常把類和函數的聲明的放到頭文件中去,而把實現放到CPP中去。這樣做的目的是降低與實現文件之間的編譯依存關系。
作者還提到有些泛型類也是采用實現和聲明分離的設計方式的,不過要使用關鍵字export,可是這一關鍵字已經很少出現在當代編譯器當中了,所以我所見到的泛型都是實現和生命合而為一的。
從下面這個代碼段可以看出在構造函數的形參列表中類的對象是可以new的,如下圖所示:
另一種pimpl實現手段是寫一個C++的interface,里面是virtual函數和pure virtual虛擬函數,當然interface里面可以含有成員變量,但是人們通常不那么做,因為就好像它的名字那樣interface只是接口,而接口是供別人調用用的,所以它里面只需要函數足矣。Interface肯定是要被繼承的,否則它啥用都沒有,尤其是內涵pure virtual的interface。這個實現的任務就交給了它的子類,在這個interface中有那么一個函數,它的作用是返回一個已經實例化的對象的指針。這個函數被稱為factory函數或者virtual構造函數,當然后者并不是說構造函數可以virtual。這些做法都是基于內聯實現的。
上述這些做法多多少少會帶來效率的低下。
總結一下作者的思想就是:
1、依賴于聲明式,使用pimpl模式進行設計都會減小編譯依存性。
2、頭文件應該只包含聲明。