大規模c++程序設計摘要

引言


    與主流觀點相反,從根本上說,最普通形式的面向對象程序要比對應的面向過程的程序更難測試和校驗。通過虛函數改變內部行爲的能力可能導致類不變式無效;而對於程序的正確性來說,類不變式是必要的。

 

第一部分 基礎知識


    對符號名稱的使用,而不是聲明本身,導致一個未定義符號被引入到.o目標文件中。

    typedef聲明是的性質屬於"internal linkage",儘管這個說法不嚴謹,但是很容易理解──文件A無法使用文件B中typedef的類型。

    C++中不可能未經定義就聲明一個枚舉類型,即對於class合法的foward declation,並不適用於enum。

    typedef只是提供了”創建新類型“的假象。

    類之間的邏輯關係:每一種都暗示着兩個邏輯實體之間不同程度的物理依賴

        IsA
        Uses-In-The-Interface
        Uses-In-The-Implementation

    繼承和分層是邏輯層次結構的兩種表現形式

    分層:較高抽象層次的類依賴較低抽象層次的類(人依賴於腦、胃,可以理解爲組合)
    繼承:具體的類依賴抽象的類

    避免在頭文件的file scope中使用枚舉、typedef和常量數據;它們都默認爲internal linkage。更好的方法是將他們放入頭文件的class scope中。

    頭文件的"外部冗餘#include guard"

    理論上將,頭文件的"內部 #include guard"就足夠了,但是對於大型項目,事情並不這麼簡單

    對於具有高密度頭文件包含圖的大型項目,在每個頭文件中的每個#include指令周圍,加上一個冗餘的”外部#include guard“,對於降低編譯時間,能起到顯著作用。

    注意,對於.cpp文件,外部冗餘#include guard並不是必需的。

       

第二部分 物理設計概念

 


第三章、組件


3.1 組件和類

    從純粹的邏輯角度看,一個設計可以看作是類和函數的海洋,那裏沒有物理分區存在──每一個類和自由函數都駐留在一個單一的無縫空間內。

    組件 (component)是物理設計的最小單位。

    組件不是類,反之亦然。一個組件一般包括一個或多個相關的類,以及和類相關的非成員函數。

    在結構上,組件是一個不可分隔的物理單位。一個組件嚴格的由一個頭文件和一個實現文件組成。

    組件的邏輯接口:客戶可通過編程訪問或檢測到的東西
    組件的物理接口:頭文件中的所有東西

    從邏輯角度看,組件的實現文件中有或沒有使用什麼都是封裝的細節,並不重要;從物理角度看,這樣的使用可能隱含着對其它組件的物理依賴。在大型系統中這些物理依賴會影響可維護性和可重用性。

3.2 物理設計規則

    1.在一個組件內部聲明的邏輯實體,不應該在該組件之外定義(類聲明除外)。

    2.組成一個組件的.cpp和.h的文件名應該嚴格匹配

    3.每個組件的.cpp文件應該將#include其對應頭文件作爲其代碼的第一行有效語句,這樣每個組件可以確保它自己的頭文件對於編譯來說都是自我滿足的,組件的頭文件中不會遺漏組件物理接口的關鍵信息──如果有的話,在編譯組件時就會發現錯誤。

    4.當需要某個類型的定義時,應該直接包含其頭文件,而不應該依賴另外一個頭文件去包含這個頭文件。

     一個頭文件是否應包含另一個頭文件是個物理問題,不是邏輯問題。

    5.在組件的.cpp文件中,避免出現具有外部連接、卻沒有出現在相應的.h文件中聲明過的定義──確保外界在只看到物理接口的情況下就能完全瞭解組件的邏輯接口是很重要的。

    6.避免通過一個局部聲明來訪問另一組件中帶有外部連接的實體,而是應該通過包含那個組件的頭文件來完成。這麼做的主要目的是讓對其它組件的依賴顯性化,

3.3 依賴關係(Depends-on)

    Depends-on的含義是物理依賴,而不是邏輯依賴

    組件之間的編譯期依賴:編譯y.cpp時需要x.h,則組件y對組件x存在編譯期依賴
    組件之間的連接期依賴:若對象文件y.o需要x.o來解析自身包含的未定義符號,則組件y對組件x存在連接期依賴

    一個編譯期依賴幾乎總是隱含着一個連接期依賴

    組件的Depends-on關係具有傳遞性

3.4 隱含依賴

    邏輯關係隱含物理依賴。IsA和HasA這樣的邏輯關係,在跨組件實現時總是隱含編譯依賴;HoldA和Uses這樣的關係可能隱含跨組件邊界的連接時依賴

3.5 提取實際的依賴

    可以通過提取源文件(.h和.cpp)中的#include命令,直接構建組件依賴圖

3.6 友元關係

    友元關係和物理設計之間的交互程度強烈的令人驚訝;友元關係表面是個邏輯關係,卻會影響物理設計

    避免將友元關係授予給定義在另一個組件中的邏輯實體

    從嚴格意義上說,友元聲明本身是類接口的一部分,而友元的實現卻不是

    與類處於同一組件中的友元的存在,並不會破壞類的封裝性;

    遠距離友元關係(對另一個組件中的邏輯實體授予友元關係),則會破壞該類的封裝性──客戶代碼完全可以”冒名頂替“,這可能導致嚴重的安全問題

    友元關係並不隱含物理依賴

 


第四章、 物理層次結構


    中心問題是避免循環物理依賴


4.3 測試”好“接口時的困難

    面向對象技術的一種實際有效的應用是把極大的複雜性隱藏在一個小的、定義良好的、易於理解和使用的接口後面,但是,正是這種接口(如果被不成熟的實現)會導致子系統測試起來及其困難

   
4.4 易測試性設計(Design for Testability)

    DFT的重要性在IC工業界是公認的

    對整個設計的層次結構進行分佈式測試,比只在最高層接口進行測試有效的多

    雖然大型軟件的複雜度要比IC複雜很多,但本質上軟件測試要比硬件測試要容易──類的不同實例肯定有一致的行爲,然而不同的晶體管卻未必都是合格的

4.6 非循環物理依賴

    對於一個能夠有效測試的設計來說,一定能夠將其分解爲複雜性可以控制的功能單元

    不含循環的組件依賴圖非常有利於實現易測試性;非循環物理依賴的系統遠比循環物理依賴的系統容易進行有效的測試

    對於非循環物理依賴的系統,至少存在一個隔離測試的合理順序

   
4.7 層次號

    每個有向非循環圖的結點都可被賦予唯一的層次號;一個循環的圖則不能

    一個可被賦予唯一層次號的物理依賴圖被稱爲可層次化的

    子系統中組件的層次號

    層次0:軟件包之外的組件
    層次1:沒有局部物理依賴的組件
    層次N:依賴於層次N-1以下組件的組件

    總是可以對層次爲1的組件進行獨立測試

    大多數情況下,如果大型系統要被有效的測試,它們必須是可層次化的。

    注意,層次化這一術語適用與物理實體而不是邏輯實體

    可層次化分析的最大價值在於,基於組件依賴圖,能夠對物理設計的完整性做出實質性的定性評論;且可層次化的分析是很容易自動化的。

4.8 分層次測試和增量測試

    分層次測試:爲每個組件提供一個獨立的測試驅動程序

    增量測試:只測試一個組件中直接實現的功能,對通過轉發給低層次組件完成的功能不進行測試


4.11 循環物理依賴
   
    設計經常開始於非循環依賴,隨着設計的演化,在系統功能增強的過程中,循環依賴會悄悄的混進來

    緊密相關的類之間相互依賴很平常,但是它們應該完全駐留在同一個組件中

4.12 累計組件依賴(CCD:Cummulative component dependent)

    CCD:對一個系統中的所有組件執行增量式測試時需要的組件數量的總和

    連接大型程序要花費很長的時間

    當依賴關係形成完美二叉樹時,對於CCD是最佳情況,爲N*logN級別的

    對於循環依賴,CCD則是N*N

4.13 物理設計的質量

    CCD可用於衡量物理設計的質量

    儘管物理依賴的可層次性是很好的設計特性,但是不同的可層次性結構之間也存在優劣

    例如”垂直“的鏈狀可層次結構,,具有高度的耦合性,CCD的值較大;與之相比,二叉樹形式的可層次化結構,CCD的值較小

    通過使得依賴圖更平而不是更高,我們增強了靈活性。設計越扁平,獨立重用的潛力就越大

    平均組件依賴(ACD)=子系統的CCD與組件數N的比值

    將一給定組件集合的CCD最小化是一個設計目標

    標準累計組件依賴(NCCD)=包含N個組件的子系統的CCD值與相同規模平衡二叉樹對應CCD的比值

    通過NCCD,能夠把子系統的物理依賴區分爲水平、樹狀、垂直或循環的

 


第五章、 層次化


    在衡量一個系統的物理設計質量時,以CCD量化的連接時依賴扮演主要角色。系統質量的其它方面如可理解性、可維護性和可重用性,都緊密的依賴於物理設計的質量。

    好的邏輯設計和好的物理設計之間是存在協同作用的
   
5.2 升級

    如果組件y的層次高於組件x,且y在物理上依賴於x,則稱組件y支配組件x

    支配的意義在於能提供簡單的層次好之外的附加信息:當且僅當組件u不支配v時,添加v對u的依賴纔不會引入循環依賴。

    可以通過將原有相互依賴關係轉換並上推到更高層次的組件中,將循環依賴變成受歡迎的向下依賴。


5.3 降級

    將循環依賴組件中的共用功能下移到物理層次更低的組件中,被稱爲降級──原來的組件依賴於這個新組件

    將共有代碼降級可促成獨立重用

5.4 不透明指針

    實質使用 VS 名稱上使用
   
    如果一個指針所指向的類型的定義不在當前編譯單元內,這個指針被稱爲不透明的(opaque)

    不透明指針的意義在於讓一個對象只在名稱上使用另一個對象

5.5 啞數據

    啞數據是對不透明指針概念的一種範化

    啞數據可以用來打破in-name-only依賴,但是不透明指針可以同時保持類型安全和封裝,而啞數據通常是不能的。

    啞數據的使用是典型的低層次實現細節,通常不會暴露在較高層次子系統的接口中

5.6 冗餘

    任何種類的複用都隱含着某種形式的耦合;冗餘指的是爲了避免由重用導致的不必要的物理依賴而故意重複代碼或數據的技術
  
    如果重用所提供的功能只有很少部分是我們所需要的,然而卻導致不合比例的大量耦合的話,冗餘就是必要的、也是更合理的機制。

    簡而言之,重用很少沒有開銷(編譯器和連接期),在它帶來的益處抵得過開銷時,纔是一種良好的選擇。

5.7 回調

    回調的意義在於允許較低層次的組件,使用客戶提供的函數,在更全局的上下文中執行特定任務
 
    回調是消除物理耦合的強有力工具,但是應該只在必要的時候使用它們

    在面向對象中,通常利用虛函數來實現“回調”;這對應面向過程中,利用函數指針來實現回調。基於虛函數機制的優點在於保證類型安全

    回調,如同遞歸一樣,比傳統的函數調用更難以理解

5.8 管理類

    管理類的意義建立一個擁有和協調較低層次對象的類

   在協同操作對象之間建立清晰的所有權關係,是良好設計的基礎。如果兩個或更多對象之間存在所有權的相互依賴,則應將所有權的管理功能提升到一個管理類中。

    經典範例:圖中Node和Edge的相互關係


5.9 分解

    分解(factoring)的意思是提取小塊的內聚功能,並把它們移到一個較低的層次,以便可以被獨立的測試和重用

    分解是減輕循環依賴的類所強加的負擔的一種非常普通而高效的技術

    分解和降級類似,只是分解不必然消除任何循環,取而代之的是它只減少參與到循環中的功能的數量。通過分解可以將循環依賴升級到一個更高層次,在那裏它們的不利影響較不顯著。

5.10 升級封裝

    將大量有用的低層次類藏在一個單個組件的接口後面,這樣的組件被稱爲包裝器(wrapper),或者理解成應用Facade模式

 


第六章、絕緣


    好的物理設計的另一個方面就是避免不必要的編譯依賴

    一般來說,對駐留在一個組件的物理接口中的、客戶無權訪問的實現細節的修改,將強迫所有的客戶程序重新編譯。

    絕緣是指避免或消除不必要的編譯時耦合的過程。


6.1 從封裝到絕緣

    絕緣是一個物理設計問題,它是邏輯設計中封裝的對應物。

    封裝:圍繞着類的實現的極薄的透明膜,只能防止通過編程來訪問類的實現,但是客戶仍能看到部分實現

    絕緣:無限厚不透明的障礙,它排除了客戶與組件的實現進行直接交互作用的任何可能性

    在大型系統中,修改絕緣組件要比非絕緣組件容易的多

6.2 C++結構和編譯時耦合

    有時組件的邏輯和物理分解是自然一致的,例如類的非內聯成員函數,邏輯接口(聲明)存在於物理接口中,邏輯實現(函數體)存在於物理實現中。這種情況下,聲明並沒有暴露除接口外的更多信息。

    C++並不要求所有邏輯實現的細節都存放於.cpp文件中。

    以下列出的各種情況,都會使得邏輯實現成爲組件物理接口的一部分

6.2.1 繼承與編譯耦合

    一個類只要繼承自另一個類,即使是私有繼承,也可能沒有辦法把客戶程序與這個事實絕緣

6.2.2 分層(HasA/HoldsA)和編譯耦合

    後者比前者的導致的耦合程度要弱

6.2.3 內聯函數與編譯耦合
   
    無論何時將一個組件的實現的一部分放到頭文件中,都無法再將客戶與組件的這部分絕緣

6.2.4 私有成員和編譯耦合

    類個每個私有數據成員──儘管封裝好了──也沒有與該類的客戶程序絕緣

    私有成員函數是類的封裝細節,然而它們並不是絕緣的實現細節──對私有函數原型的修改會迫使客戶代碼重新編譯

6.2.5 保護成員和編譯耦合

    同私有成員一樣,它們也不是絕緣的實現細節

6.2.6 編譯期生成的函數與編譯耦合

    同樣不是絕緣的

6.2.7 #include和編譯耦合

    在頭文件中應謹慎使用 #include指令,避免引入不必要的編譯依賴

6.2.8 默認參數與編譯耦合

    不具備絕緣的性質

6.2.9 枚舉類型和編譯時耦合

    枚舉類型不具備外部連接,如果要在多個組件中被使用,只能將定義放在頭文件中


6.3 部分絕緣技術

    絕緣不是一個“要麼全有,要麼全無”的命題

6.3.1 消除私有繼承

    私有繼承意味着實現細節

6.3.2 消除嵌入數據成員

    將HasA關係轉爲HoldsA,可以實現內部嵌入數據和客戶類之間的絕緣

6.3.3 消除私有成員函數

    私有成員函數儘管封裝了類的邏輯實現細節,但仍然是組件物理接口的一部分

6.3.4 消除保護成員

6.3.5 消除私有成員數據

6.3.6 消除編譯時產生的函數

6.3.7 消除#inlucde指令

    組件頭文件中出現#include指令,通常有三種合理的原因:

    1) IsA關係,需要包含基類的頭文件
    2) HasA關係,需要包含嵌入類的頭文件
    3)inline函數,該組件中的某個inline函數實質使用到了另一個組件中的類

    除此之外,在頭文件中包含#include指令,很少是有道理的

    解決方法很簡單:將頭文件中不必要的#include指令移動到對應的.cpp文件中,並替換爲適當的前置聲明(注意,這裏的觀點存在爭議,C++編程規範第23條認爲“頭文件應該自己自足,爲此需要包含其內容所依賴的所有頭文件")


6.3.8 消除默認參數

6.3.9 消除枚舉類型


6.4 整體的絕緣技術

    分清楚哪些是公共接口哪些不是公共接口,這種知識可以幫助我們決定哪些接口應該絕緣,哪些接口不應該絕緣

6.4.1 協議類

    理想情況下,一個絕緣良好的接口絕對不會定義任何實現細節:

    1) 不含任何數據
    2)非內聯的虛析構函數(函數體爲空)
    3)所有成員函數都是純虛的

    一個協議類幾乎是一個完美的絕緣器

6.4.2 完全絕緣的具體類

    滿足如下條件的具體類,是完全絕緣的

    1) 只包含一個數據成員,爲不透明指針,即所謂的pImpl慣用法
    2)不繼承任何其它類
    3)不聲明任何內聯成員函數

    所有完全絕緣的(具體)類的物理結構在外表上都是一樣的──都可以在不影響任何頭文件的情況下進行修改

6.4.3  絕緣的包裝器

    包裝器不僅可以進行封裝,還可以進行絕緣

6.5 過程接口

    當主要目標是把客戶程序與在一個絕緣層下(爲一個非常複雜的系統所建立的)之下的東西都絕緣的話,必需採取某種妥協:放棄更強的的邏輯封裝,以換取更好的絕緣。

    組織方式:所有公共可訪問的接口函數都是彼此獨立的,它們之間不存在層次化問題,不應該彼此依賴

    過程接口的目的不是封裝組件的使用,而是把客戶程序和它們使用的對象定義絕緣。

6.6 絕緣或不絕緣

    絕緣是會導致性能開銷的

    如果某個組件不會被廣泛使用的話,絕緣就不是關鍵特性

    對於輕量級、廣泛使用的對象進行絕緣,很可能導致嚴重的性能下嫁

    對於大型的、廣發使用的對象,要儘早進行絕緣──後期如果有必要,可以選擇性的去除

    在缺少壓倒性理由的情況下,無論如何請記住:在開發過程的後期,絕緣的去除比絕緣的安裝更經濟

 


第三部分 邏輯設計


    邏輯設計是比物理設計更爲成熟和更容易理解的規則

    設計模式關注的焦點是邏輯設計

    本部分的內容只討論單個組件的設計與實現

 

第八章、構建一個組件


8.2 組件設計規則

   1.    私有接口應該是充分(sufficient)的

    對於被設計爲特定子系統一部分的組件,我們只要求接口對於可預期的客戶來說是充分的就可以了。”充分性“指的是,該接口對於解決某個特定領域的問題來說是合適的

  2.    公共接口應該是完整(complete)的

    對於在整個大系統中會被用於各種目的的組件來說,我們希望組件的藉口是完備的。”完備性“指的是,該藉口適用於解決那個領域中的任意一個問題。

   3.    類接口應該是基本的(primitive)──如果一個操作的有效實現需要訪問類的私有細節,那麼這個操作就是基本的。有用但不是基本的操作硬儘可能的通過類外的自由函數來實現。

   4.    組件接口應該是最小化和易於使用的


    在任何可行的地方,延緩不必要功能的實現可以降低開發和維護成本,而且可以避免過早的進行精確的接口和行爲設計。

    在充分性和完整性之間,存在了廣闊的中間地帶

    讓功能保持在一個可行的最小範圍內可以增強可用性和可重用性

    在一個組件中儘可能少的使用在外部組件中定義的類型,可以促進在更多情況下的重用

    耦合這個術語對邏輯設計和物理設計都適用。物理耦合來源於將邏輯實體放在同一組件中。邏輯耦合來源於在一個組件接口中使用由其它組件定義的類型。

    邏輯耦合常常導致不受歡迎的物理耦合

8.3 封裝程度

    和絕緣一樣,封裝的實現往往會帶來性能開銷

    有時,選擇不完全的封裝是正確的選擇

 


第九章、設計一個函數


9.2 接口中的基本類型

    避免使用short,用int代替──理由,整形提升
    避免使用無符號數,使用有符號數──理由,signed到unsigned的自動轉換
    避免使用long,用int代替──理由,可移植性

    對於整數類型,只使用int
    對於浮點類型,只使用double


本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/lovekatherine/archive/2008/04/13/2287980.aspx

發佈了14 篇原創文章 · 獲贊 2 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章