1. 程式人生 > >一杯茶品人生沉浮,平常心造萬千世界

一杯茶品人生沉浮,平常心造萬千世界

第1章 檔案結構 每個C++/C程式通常分為兩個檔案。一個檔案用於儲存程式的宣告(declaration),稱為標頭檔案。另一個檔案用於儲存程式的實現(implementation),稱為定義(definition)檔案。 C++/C程式的標頭檔案以“.h”為字尾,C程式的定義檔案以“.c”為字尾,C++程式的定義檔案通常以“.cpp”為字尾(也有一些系統以“.cc”或“.cxx”為字尾)。

1.1 版權和版本的宣告

版權和版本的宣告位於標頭檔案和定義檔案的開頭(參見示例1-1),主要內容有: (1)版權資訊。 (2)檔名稱,識別符號,摘要。 (3)當前版本號,作者/修改者,完成日期。 (4)版本歷史資訊。
/* * Copyright (c) 2001,上海貝爾有限公司網路應用事業部 * All rights reserved. * * 檔名稱:filename.h
* 檔案標識:見配置管理計劃書 * 摘    要:簡要描述本檔案的內容 * * 當前版本:1.1 * 作    者:輸入作者(或修改者)名字 * 完成日期:2001年7月20日 * * 取代版本:1.0 * 原作者  :輸入原作者(或修改者)名字 * 完成日期:2001年5月10日 */
示例1-1 版權和版本的宣告

1.2 標頭檔案的結構

標頭檔案由三部分內容組成: (1)標頭檔案開頭處的版權和版本宣告(參見示例1-1)。 (2)預處理塊。 (3)函式和類結構宣告等。 假設標頭檔案名稱為 graphics.h,標頭檔案的結構參見示例1-2。 l         【規則1-2-1
為了防止標頭檔案被重複引用,應當用ifndef/define/endif結構產生預處理塊。 l         【規則1-2-2用 #include <filename.h> 格式來引用標準庫的標頭檔案(編譯器將從標準庫目錄開始搜尋)。 l         【規則1-2-3用 #include “filename.h” 格式來引用非標準庫的標頭檔案(編譯器將從使用者的工作目錄開始搜尋)。 ²        【建議1-2-1標頭檔案中只存放“宣告”而不存放“定義” 在C++ 語法中,類的成員函式可以在宣告的同時被定義,並且自動成為行內函數。這雖然會帶來書寫上的方便,但卻造成了風格不一致,弊大於利。建議將成員函式的定義與宣告分開,不論該函式體有多麼小。 ²        【建議1-2-2
不提倡使用全域性變數,儘量不要在標頭檔案中出現象extern int value 這類宣告。
// 版權和版本宣告見示例1-1,此處省略。 #ifndef   GRAPHICS_H  // 防止graphics.h被重複引用 #define   GRAPHICS_H #include <math.h>     // 引用標準庫的標頭檔案 … #include “myheader.h”   // 引用非標準庫的標頭檔案 … void Function1(…);   // 全域性函式宣告 … class Box             // 類結構宣告 { … }; #endif
示例1-2 C++/C標頭檔案的結構

1.3 定義檔案的結構

定義檔案有三部分內容: (1)       定義檔案開頭處的版權和版本宣告(參見示例1-1)。 (2)       對一些標頭檔案的引用。 (3)       程式的實現體(包括資料和程式碼)。 假設定義檔案的名稱為 graphics.cpp,定義檔案的結構參見示例1-3。
// 版權和版本宣告見示例1-1,此處省略。 #include “graphics.h”     // 引用標頭檔案 … // 全域性函式的實現體 void Function1(…) { … } // 類成員函式的實現體 void Box::Draw(…) { … }
示例1-3 C++/C定義檔案的結構

1.4 標頭檔案的作用

早期的程式語言如Basic、Fortran沒有標頭檔案的概念,C++/C語言的初學者雖然會用使用標頭檔案,但常常不明其理。這裡對標頭檔案的作用略作解釋: (1)通過標頭檔案來呼叫庫功能。在很多場合,原始碼不便(或不準)向用戶公佈,只要向用戶提供標頭檔案和二進位制的庫即可。使用者只需要按照標頭檔案中的介面宣告來呼叫庫功能,而不必關心介面怎麼實現的。編譯器會從庫中提取相應的程式碼。 (2)標頭檔案能加強型別安全檢查。如果某個介面被實現或被使用時,其方式與標頭檔案中的宣告不一致,編譯器就會指出錯誤,這一簡單的規則能大大減輕程式設計師除錯、改錯的負擔。

1.5 目錄結構

如果一個軟體的標頭檔案數目比較多(如超過十個),通常應將標頭檔案和定義檔案分別保存於不同的目錄,以便於維護。 例如可將標頭檔案保存於include目錄,將定義檔案保存於source目錄(可以是多級目錄)。 如果某些標頭檔案是私有的,它不會被使用者的程式直接引用,則沒有必要公開其“宣告”。為了加強資訊隱藏,這些私有的標頭檔案可以和定義檔案存放於同一個目錄。

第2章 程式的版式

       版式雖然不會影響程式的功能,但會影響可讀性。程式的版式追求清晰、美觀,是程式風格的重要構成因素。 可以把程式的版式比喻為“書法”。好的“書法”可讓人對程式一目瞭然,看得興致勃勃。差的程式“書法”如螃蟹爬行,讓人看得索然無味,更令維護者煩惱有加。請程式設計師們學習程式的“書法”,彌補大學計算機教育的漏洞,實在很有必要。

2.1 空行

空行起著分隔程式段落的作用。空行得體(不過多也不過少)將使程式的佈局更加清晰。空行不會浪費記憶體,雖然列印含有空行的程式是會多消耗一些紙張,但是值得。所以不要捨不得用空行。 l         【規則2-1-1在每個類宣告之後、每個函式定義結束之後都要加空行。參見示例2-1(a) l         【規則2-1-2在一個函式體內,邏揖上密切相關的語句之間不加空行,其它地方應加空行分隔。參見示例2-1(b )
// 空行 void Function1(…) {   … } // 空行 void Function2(…) {   … } // 空行 void Function3(…) {   … } // 空行 while (condition) {   statement1;   // 空行   if (condition)   {      statement2;   }   else   {      statement3;   } // 空行   statement4; } 
示例2-1(a) 函式之間的空行                   示例2-1(b) 函式內部的空行

2.2 程式碼行

l         【規則2-2-1一行程式碼只做一件事情,如只定義一個變數,或只寫一條語句。這樣的程式碼容易閱讀,並且方便於寫註釋。 l         【規則2-2-2if、for、while、do等語句自佔一行,執行語句不得緊跟其後。不論執行語句有多少都要加{}。這樣可以防止書寫失誤。 示例2-2(a)為風格良好的程式碼行,示例2-2(b)為風格不良的程式碼行。
int width;    // 寬度 int height;   // 高度 int depth;    // 深度 int width, height, depth; // 寬度高度深度
x = a + b; y = c + d; z = e + f; X = a + b;   y = c + d;  z = e + f;
if (width < height) { dosomething(); } if (width < height) dosomething();
for (initialization; condition; update) { dosomething(); } // 空行 other(); for (initialization; condition; update)      dosomething(); other();
示例2-2(a) 風格良好的程式碼行                 示例2-2(b) 風格不良的程式碼行 ²        【建議2-2-1儘可能在定義變數的同時初始化該變數(就近原則) 如果變數的引用處和其定義處相隔比較遠,變數的初始化很容易被忘記。如果引用了未被初始化的變數,可能會導致程式錯誤。本建議可以減少隱患。例如 int width = 10;     // 定義並初紿化width int height = 10; // 定義並初紿化height int depth = 10;     // 定義並初紿化depth

2.3 程式碼行內的空格

l         【規則2-3-1關鍵字之後要留空格。象const、virtual、inline、case 等關鍵字之後至少要留一個空格,否則無法辨析關鍵字。象if、for、while等關鍵字之後應留一個空格再跟左括號‘(’,以突出關鍵字。 l         【規則2-3-2函式名之後不要留空格,緊跟左括號‘(’,以與關鍵字區別。 l         【規則2-3-3‘(’向後緊跟,‘)’、‘,’、‘;’向前緊跟,緊跟處不留空格。 l         【規則2-3-4‘,’之後要留空格,如Function(x, y, z)。如果‘;’不是一行的結束符號,其後要留空格,如for (initialization; condition; update)。 l         【規則2-3-5賦值操作符、比較操作符、算術操作符、邏輯操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前後應當加空格。 l         【規則2-3-6一元操作符如“!”、“~”、“++”、“--”、“&”(地址運算子)等前後不加空格。 l         【規則2-3-7象“[]”、“.”、“->”這類操作符前後不加空格。 ²        【建議2-3-1對於表示式比較長的for語句和if語句,為了緊湊起見可以適當地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d))
void Func1(int x, int y, int z);          // 良好的風格 void Func1 (int x,int y,int z);           // 不良的風格
if (year >= 2000)                         // 良好的風格 if(year>=2000)                            // 不良的風格 if ((a>=b) && (c<=d))                     // 良好的風格 if(a>=b&&c<=d)                            // 不良的風格
for (i=0; i<10; i++)                      // 良好的風格 for(i=0;i<10;i++)                         // 不良的風格 for (i = 0; I < 10; i ++)                 // 過多的空格
x = a < b ? a : b;                        // 良好的風格 x=a<b?a:b;                                // 不好的風格
int *x = &y;                              // 良好的風格  int * x = & y;                            // 不良的風格 
array[5] = 0;                             // 不要寫成 array [ 5 ] = 0; a.Function();                             // 不要寫成 a . Function(); b->Function();                            // 不要寫成 b -> Function();
示例2-3 程式碼行內的空格

2.4 對齊

l         【規則2-4-1程式的分界符‘{’和‘}’應獨佔一行並且位於同一列,同時與引用它們的語句左對齊。 l         【規則2-4-2{ }之內的程式碼塊在‘{’右邊數格處左對齊。 示例2-4(a)為風格良好的對齊,示例2-4(b)為風格不良的對齊。
void Function(int x) { … // program code } void Function(int x){ … // program code }
if (condition) { … // program code } else { … // program code } if (condition){ … // program code } else { … // program code }
for (initialization; condition; update) { … // program code } for (initialization; condition; update){ … // program code }
While (condition) { … // program code } while (condition){ … // program code }
如果出現巢狀的{},則使用縮排對齊,如:      {         …           {             …           }        … }
示例2-4(a) 風格良好的對齊                       示例2-4(b) 風格不良的對齊

2.5 長行拆分

l         【規則2-5-1程式碼行最大長度宜控制在70至80個字元以內。程式碼行不要過長,否則眼睛看不過來,也不便於列印。 l         【規則2-5-2長表示式要在低優先順序操作符處拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要進行適當的縮排,使排版整齊,語句可讀。
if ((very_longer_variable1 >= very_longer_variable12) && (very_longer_variable3 <= very_longer_variable14) && (very_longer_variable5 <= very_longer_variable16)) {     dosomething(); }
virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix,                                  CMatrix rightMatrix);
for (very_longer_initialization;      very_longer_condition;      very_longer_update) {     dosomething(); }
示例2-5 長行的拆分

2.6 修飾符的位置

修飾符 * 和 & 應該靠近資料型別還是該靠近變數名,是個有爭議的活題。 若將修飾符 * 靠近資料型別,例如:int*  x; 從語義上講此寫法比較直觀,即x是int 型別的指標。 上述寫法的弊端是容易引起誤解,例如:int*  x, y; 此處y容易被誤解為指標變數。雖然將x和y分行定義可以避免誤解,但並不是人人都願意這樣做。 l         【規則2-6-1應當將修飾符 * 和 & 緊靠變數名 例如: char  *name;     int   *x, y;  // 此處y不會被誤解為指標

2.7 註釋

C語言的註釋符為“/*…*/”。C++語言中,程式塊的註釋常採用“/*…*/”,行註釋一般採用“//…”。註釋通常用於: (2)函式介面說明; (3)重要的程式碼行或段落提示。 雖然註釋有助於理解程式碼,但注意不可過多地使用註釋。參見示例2-6。 l         【規則2-7-1註釋是對程式碼的“提示”,而不是文件。程式中的註釋不可喧賓奪主,註釋太多了會讓人眼花繚亂。註釋的花樣要少。 l         【規則2-7-2如果程式碼本來就是清楚的,則不必加註釋。否則多此一舉,令人厭煩。例如 i++;     // i 加 1,多餘的註釋 l         【規則2-7-3邊寫程式碼邊註釋,修改程式碼同時修改相應的註釋,以保證註釋與程式碼的一致性。不再有用的註釋要刪除。 l         【規則2-7-4註釋應當準確、易懂,防止註釋有二義性。錯誤的註釋不但無益反而有害。 l         【規則2-7-5儘量避免在註釋中使用縮寫,特別是不常用縮寫。 l         【規則2-7-6註釋的位置應與被描述的程式碼相鄰,可以放在程式碼的上方或右方,不可放在下方。 l         【規則2-7-8當代碼比較長,特別是有多重巢狀時,應當在一些段落的結束處加註釋,便於閱讀。
/* * 函式介紹: * 輸入引數: * 輸出引數: * 返回值  : */ void Function(float x, float y, float z) {   … } if (…) {   …   while (…)   { … } // end of while … } // end of if
示例2-6 程式的註釋

2.8 類的版式

類可以將資料和函式封裝在一起,其中函式表示了類的行為(或稱服務)。類提供關鍵字public、protected和private,分別用於宣告哪些資料和函式是公有的、受保護的或者是私有的。這樣可以達到資訊隱藏的目的,即讓類僅僅公開必須要讓外界知道的內容,而隱藏其它一切內容。我們不可以濫用類的封裝功能,不要把它當成火鍋,什麼東西都往裡扔。 類的版式主要有兩種方式: (1)將private型別的資料寫在前面,而將public型別的函式寫在後面,如示例8-3(a)。採用這種版式的程式設計師主張類的設計“以資料為中心”,重點關注類的內部結構。 (2)將public型別的函式寫在前面,而將private型別的資料寫在後面,如示例8.3(b)採用這種版式的程式設計師主張類的設計“以行為為中心”,重點關注的是類應該提供什麼樣的介面(或服務)。 很多C++教課書受到Biarne Stroustrup第一本著作的影響,不知不覺地採用了“以資料為中心”的書寫方式,並不見得有多少道理。 我建議讀者採用“以行為為中心”的書寫方式,即首先考慮類應該提供什麼樣的函式。這是很多人的經驗——“這樣做不僅讓自己在設計類時思路清晰,而且方便別人閱讀。因為使用者最關心的是介面,誰願意先看到一堆私有資料成員!”
class A {   private: int    i, j; float  x, y;     …   public: void Func1(void); void Func2(void); … } class A {   public: void Func1(void); void Func2(void); …   private: int    i, j; float  x, y;     … }
示例8.3(a) 以資料為中心版式              示例8.3(b) 以行為為中心的版式

第3章 命名規則

比較著名的命名規則當推Microsoft公司的“匈牙利”法,該命名規則的主要思想是“在變數和函式名中加入字首以增進人們對程式的理解”。例如所有的字元變數均以ch為字首,若是指標變數則追加字首p。如果一個變數由ppch開頭,則表明它是指向字元指標的指標。 “匈牙利”法最大的缺點是煩瑣,例如 int    i,  j,  k;  float  x,  y,  z; 倘若採用“匈牙利”命名規則,則應當寫成 int    iI,  iJ,  ik;  // 字首 i表示int型別 float  fX,  fY,  fZ;  // 字首 f表示float型別 如此煩瑣的程式會讓絕大多數程式設計師無法忍受。 據考察,沒有一種命名規則可以讓所有的程式設計師贊同,程式設計教科書一般都不指定命名規則。命名規則對軟體產品而言並不是“成敗悠關”的事,我們不要化太多精力試圖發明世界上最好的命名規則,而應當制定一種令大多數專案成員滿意的命名規則,並在專案中貫徹實施。

3.1 共性規則

       本節論述的共性規則是被大多數程式設計師採納的,我們應當在遵循這些共性規則的前提下,再擴充特定的規則,如3.2節。 l         【規則3-1-1識別符號應當直觀且可以拼讀,可望文知意,不必進行“解碼”。 識別符號最好採用英文單詞或其組合,便於記憶和閱讀。切忌使用漢語拼音來命名。程式中的英文單詞一般不會太複雜,用詞應當準確。例如不要把CurrentValue寫成NowValue。 l         【規則3-1-2識別符號的長度應當符合“min-length && max-information”原則。

幾十年前老ANSI C規定名字不準超過6個字元,現今的C+ +/C不再有此限制。一般來說,長名字能更好地表達含義,所以函式名、變數名、類名長達十幾個字元不足為怪。那麼名字是否越長約好?不見得! 例如變數名maxval就比maxValueUntilOverflow好用。單字元的名字也是有用的,常見的如i,j,k,m,n,x,y,z等,它們通常可用作函式內的區域性變數。

l         【規則3-1-3命名規則儘量與所採用的作業系統或開發工具的風格保持一致。 例如Windows應用程式的識別符號通常採用“大小寫”混排的方式,如AddChild。而Unix應用程式的識別符號通常採用“小寫加下劃線”的方式,如add_child。別把這兩類風格混在一起用。 l         【規則3-1-4程式中不要出現僅靠大小寫區分的相似的識別符號。 例如: int  x,  X;      // 變數x 與 X 容易混淆 void foo(int x);    // 函式foo 與FOO容易混淆 void FOO(float x); l         【規則3-1-5程式中不要出現識別符號完全相同的區域性變數和全域性變數,儘管兩者的作用域不同而不會發生語法錯誤,但會使人誤解。 l         【規則3-1-6變數的名字應當使用“名詞”或者“形容詞+名詞”。 例如: float  value; float  oldValue; float  newValue; l         【規則3-1-7全域性函式的名字應當使用“動詞”或者“動詞+名詞”(動賓片語)。類的成員函式應當只使用“動詞”,被省略掉的名詞就是物件本身。 例如: DrawBox();              // 全域性函式               box->Draw();        // 類的成員函式 l         【規則3-1-8用正確的反義片語命名具有互斥意義的變數或相反動作的函式等。 例如: int      minValue; int      maxValue; int      SetValue(…); int      GetValue(…); ²        【建議3-1-1儘量避免名字中出現數字編號,如Value1,Value2等,除非邏輯上的確需要編號。這是為了防止程式設計師偷懶,不肯為命名動腦筋而導致產生無意義的名字(因為用數字編號最省事)。

3.2 簡單的Windows應用程式命名規則

       作者對“匈牙利”命名規則做了合理的簡化,下述的命名規則簡單易用,比較適合於Windows應用軟體的開發。 l         【規則3-2-1類名和函式名用大寫字母開頭的單詞組合而成。 例如:   class Node;              // 類名   class LeafNode;           // 類名   void  Draw(void);     // 函式名   void  SetValue(int value);  // 函式名 l         【規則3-2-2變數和引數用小寫字母開頭的單詞組合而成。 例如:     BOOL flag;     int  drawMode; l         【規則3-2-3常量全用大寫的字母,用下劃線分割單詞。 例如:     const int MAX = 100;     const int MAX_LENGTH = 100; l         【規則3-2-4靜態變數加字首s_(表示static)。 例如: void Init(…) {        static int s_initValue;       // 靜態變數        … } l         【規則3-2-5如果不得已需要全域性變數,則使全域性變數加字首g_(表示global)。 例如: int g_howManyPeople;       // 全域性變數 int g_howMuchMoney;       // 全域性變數 l         【規則3-2-6類的資料成員加字首m_(表示member),這樣可以避免資料成員與成員函式的引數同名。 例如:     void Object::SetValue(int width, int height)     {         m_width = width; m_height = height; } l         【規則3-2-7為了防止某一軟體庫中的一些識別符號和其它軟體庫中的衝突,可以為各種識別符號加上能反映軟體性質的字首。例如三維圖形標準OpenGL的所有庫函式均以gl開頭,所有常量(或巨集定義)均以GL開頭。

3.3 簡單的Unix應用程式命名規則


第4章 表示式和基本語句

讀者可能懷疑:連if、for、while、goto、switch這樣簡單的東西也要探討程式設計風格,是不是小題大做? 我真的發覺很多程式設計師用隱含錯誤的方式寫表示式和基本語句,我自己也犯過類似的錯誤。 表示式和語句都屬於C++/C的短語結構語法。它們看似簡單,但使用時隱患比較多。本章歸納了正確使用表示式和語句的一些規則與建議。

4.1 運算子的優先順序

       C++/C語言的運算子有數十個,運算子的優先順序與結合律如表4-1所示。注意一元運算子 +  -  * 的優先順序高於對應的二元運算子。
優先順序 運算子 結合律
從 高 到 低 排 列 ( )  [ ]  ->  . 從左至右
!  ~  ++  --  (型別) sizeof +  -  *  & 從右至左
*  /  % 從左至右
+  - 從左至右
<<  >> 從左至右
<   <=   >  >= 從左至右
==  != 從左至右
& 從左至右
^ 從左至右
| 從左至右
&& 從左至右
|| 從右至左
?: 從右至左
=  +=  -=  *=  /=  %=  &=  ^= |=  <<=  >>= 從左至右
表4-1 運算子的優先順序與結合律 l         【規則4-1-1】如果程式碼行中的運算子比較多,用括號確定表示式的操作順序,避免使用預設的優先順序。 由於將表4-1熟記是比較困難的,為了防止產生歧義並提高可讀性,應當用括號確定表示式的操作順序。例如: word = (high << 8) | low if ((a | b) && (a & c))  

4.2 複合表示式

如 a = b = c = 0這樣的表示式稱為複合表示式。允許複合表示式存在的理由是:(1)書寫簡潔;(2)可以提高編譯效率。但要防止濫用複合表示式。 l         【規則4-2-1不要編寫太複雜的複合表示式。 例如:       i = a >= b && c < d && c + f <= g + h ;   // 複合表示式過於複雜 l         【規則4-2-2不要有多用途的複合表示式。 例如: d = (a = b + c) + r ; 該表示式既求a值又求d值。應該拆分為兩個獨立的語句: a = b + c; d = a + r; l         【規則4-2-3不要把程式中的複合表示式與“真正的數學表示式”混淆。 例如:  if (a < b < c)            // a < b < c是數學表示式而不是程式表示式 並不表示       if ((a<b) && (b<c)) 而是成了令人費解的 if ( (a<b)<c )

4.3 if 語句

    if語句是C++/C語言中最簡單、最常用的語句,然而很多程式設計師用隱含錯誤的方式寫if語句。本節以“與零值比較”為例,展開討論。 4.3.1 布林變數與零值比較 l         【規則4-3-1不可將布林變數直接與TRUE、FALSE或者1、0進行比較。 根據布林型別的語義,零值為“假”(記為FALSE),任何非零值都是“真”(記為TRUE)。TRUE的值究竟是什麼並沒有統一的標準。例如Visual C++ 將TRUE定義為1,而Visual Basic則將TRUE定義為-1。 假設布林變數名字為flag,它與零值比較的標準if語句如下: if (flag)    // 表示flag為真 if (!flag)    // 表示flag為假 其它的用法都屬於不良風格,例如:     if (flag == TRUE)       if (flag == 1 )         if (flag == FALSE)       if (flag == 0)      4.3.2 整型變數與零值比較 l         【規則4-3-2應當將整型變數用“==”或“!=”直接與0比較。     假設整型變數的名字為value,它與零值比較的標準if語句如下: if (value == 0)   if (value != 0) 不可模仿布林變數的風格而寫成 if (value)    // 會讓人誤解 value是布林變數 if (!value) 4.3.3 浮點變數與零值比較 l         【規則4-3-3不可將浮點變數用“==”或“!=”與任何數字比較。     千萬要留意,無論是float還是double型別的變數,都有精度限制。所以一定要避免將浮點變數用“==”或“!=”與數字比較,應該設法轉化成“>=”或“<=”形式。     假設浮點變數的名字為x,應當將   if (x == 0.0)     // 隱含錯誤的比較 轉化為 if ((x>=-EPSINON) && (x<=EPSINON)) 其中EPSINON是允許的誤差(即精度)。 4.3.4 指標變數與零值比較 l         【規則4-3-4應當將指標變數用“==”或“!=”與NULL比較。     指標變數的零值是“空”(記為NULL)。儘管NULL的值與0相同,但是兩者意義不同。假設指標變數的名字為p,它與零值比較的標準if語句如下:         if (p == NULL)    // p與NULL顯式比較,強調p是指標變數         if (p != NULL) 不要寫成         if (p == 0)   // 容易讓人誤解p是整型變數         if (p != 0)         或者 if (p)            // 容易讓人誤解p是布林變數     if (!p)            4.3.5 對if語句的補充說明

有時候我們可能會看到 if (NULL == p) 這樣古怪的格式。不是程式寫錯了,是程式設計師為了防止將 if (p == NULL) 誤寫成 if (p = NULL),而有意把p和NULL顛倒。編譯器認為 if (p = NULL) 是合法的,但是會指出 if (NULL = p)是錯誤的,因為NULL不能被賦值。

程式中有時會遇到if/else/return的組合,應該將如下不良風格的程式     if (condition)         return x;     return y; 改寫為     if (condition)     {         return x;     }     else     { return y; } 或者改寫成更加簡練的 return (condition ? x : y);

4.4 迴圈語句的效率

    C++/C迴圈語句中,for語句使用頻率最高,while語句其次,do語句很少用。本節重點論述迴圈體的效率。提高迴圈體效率的基本辦法是降低迴圈體的複雜性。 l         【建議4-4-1在多重迴圈中,如果有可能,應當將最長的迴圈放在最內層,最短的迴圈放在最外層,以減少CPU跨切迴圈層的次數。例如示例4-4(b)的效率比示例4-4(a)的高。
for (row=0; row<100; row++) { for ( col=0; col<5; col++ ) { sum = sum + a[row][col]; } } for (col=0; col<5; col++ ) { for (row=0; row<100; row++) {     sum = sum + a[row][col]; } }
示例4-4(a) 低效率:長迴圈在最外層           示例4-4(b) 高效率:長迴圈在最內層

l         【建議4-4-2如果迴圈體記憶體在邏輯判斷,並且迴圈次數很大,宜將邏輯判斷移到迴圈體的外面。示例4- 4(c)的程式比示例4-4(d)多執行了N-1次邏輯判斷。並且由於前者老要進行邏輯判斷,打斷了迴圈“流水線”作業,使得編譯器不能對迴圈進行優化處理,降低了效率。如果N非常大,最好採用示例4-4(d)的寫法,可以提高效率。如果N非常小,兩者效率差別並不明顯,採用示例4-4(c)的寫法比較好,因為程式更加簡潔。

for (i=0; i<N; i++) { if (condition)     DoSomething(); else     DoOtherthing(); } if (condition) { for (i=0; i<N; i++)     DoSomething(); } else {     for (i=0; i<N; i++)     DoOtherthing(); }
表4-4(c) 效率低但程式簡潔                表4-4(d) 效率高但程式不簡潔

4.5 for 語句的迴圈控制變數

l         【規則4-5-1不可在for 迴圈體內修改迴圈變數,防止for 迴圈失去控制。 l         【建議4-5-1建議for語句的迴圈控制變數的取值採用“半開半閉區間”寫法。 示例4-5(a)中的x值屬於半開半閉區間“0 =< x < N”,起點到終點的間隔為N,迴圈次數為N。 示例4-5(b)中的x值屬於閉區間“0 =< x <= N-1”,起點到終點的間隔為N-1,迴圈次數為N。 相比之下,示例4-5(a)的寫法更加直觀,儘管兩者的功能是相同的。
for (int x=0; x<N; x++) { … } for (int x=0; x<=N-1; x++) { … }
示例4-5(a) 迴圈變數屬於半開半閉區間           示例4-5(b) 迴圈變數屬於閉區間     有了if語句為什麼還要switch語句? switch是多分支選擇語句,而if語句只有兩個分支可供選擇。雖然可以用巢狀的if語句來實現多分支選擇,但那樣的程式冗長難讀。這是switch語句存在的理由。     switch語句的基本格式是: switch (variable) { case value1 :   … break; case value2 :   … break;     … default :    … break; } l         【規則4-6-1每個case語句的結尾不要忘了加break,否則將導致多個分支重疊(除非有意使多個分支重疊)。 l         【規則4-6-2不要忘記最後那個default分支。即使程式真的不需要default處理,也應該保留語句    default : break; 這樣做並非多此一舉,而是為了防止別人誤以為你忘了default處理。     自從提倡結構化設計以來,goto就成了有爭議的語句。首先,由於goto語句可以靈活跳轉,如果不加限制,它的確會破壞結構化設計風格。其次,goto語句經常帶來錯誤或隱患。它可能跳過了某些物件的構造、變數的初始化、重要的計算等語句,例如: goto state; String s1, s2; // 被goto跳過 int sum = 0;  // 被goto跳過 … state: … 如果編譯器不能發覺此類錯誤,每用一次goto語句都可能留下隱患。     很多人建議廢除C++/C的goto語句,以絕後患。但實事求是地說,錯誤是程式設計師自己造成的,不是goto的過錯。goto 語句至少有一處可顯神通,它能從多重迴圈體中咻地一下子跳到外面,用不著寫很多次的break語句; 例如   { …       { …         { …             goto error;         }       }   }   error:   … 就象樓房著火了,來不及從樓梯一級一級往下走,可從視窗跳出火坑。所以我們主張少用、慎用goto語句,而不是禁用。

第5章 常量

    常量是一種識別符號,它的值在執行期間恆定不變。C語言用 #define來定義常量(稱為巨集常量)。C++ 語言除了 #define外還可以用const來定義常量(稱為const常量)。

5.1 為什麼需要常量

如果不使用常量,直接在程式中填寫數字或字串,將會有什麼麻煩? (1)       程式的可讀性(可理解性)變差。程式設計師自己會忘記那些數字或字串是什麼意思,使用者則更加不知它們從何處來、表示什麼。 (2)       在程式的很多地方輸入同樣的數字或字串,難保不發生書寫錯誤。 (3)       如果要修改數字或字串,則會在很多地方改動,既麻煩又容易出錯。 l         【規則5-1-1儘量使用含義直觀的常量來表示那些將在程式中多次出現的數字或字串。 例如:     #define            MAX   100     /*  C語言的巨集常量  */ const int          MAX = 100;        //  C++ 語言的const常量 const float     PI = 3.14159;    //  C++ 語言的const常量     C++ 語言可以用const來定義常量,也可以用 #define來定義常量。但是前者比後者有更多的優點: (1)       const常量有資料型別,而巨集常量沒有資料型別。編譯器可以對前者進行型別安全檢查。而對後者只進行字元替換,沒有型別安全檢查,並且在字元替換可能會產生意料不到的錯誤(邊際效應)。 (2)       有些整合化的除錯工具可以對const常量進行除錯,但是不能對巨集常量進行除錯。 l         【規則5-2-1在C++ 程式中只使用const常量而不使用巨集常量,即const常量完全取代巨集常量。

5.3 常量定義規則

l         【規則5-3-1需要對外公開的常量放在標頭檔案中,不需要對外公開的常量放在定義檔案的頭部。為便於管理,可以把不同模組的常量集中存放在一個公共的標頭檔案中。 l         【規則5-3-2如果某一常量與其它常量密切相關,應在定義中包含這種關係,而不應給出一些孤立的值。 例如: const  float   RADIUS = 100; const  float   DIAMETER = RADIUS * 2;

5.4 類中的常量

有時我們希望某些常量只在類中有效。由於#define 定義的巨集常量是全域性的,不能達到目的,於是想當然地覺得應該用const修飾資料成員來實現。const資料成員的確是存在的,但其含義卻不是我們所期望的。const資料成員只在某個物件生存期內是常量,而對於整個類而言卻是可變的,因為類可以建立多個物件,不同的物件其const資料成員的值可以不同。

    不能在類宣告中初始化const資料成員。以下用法是錯誤的,因為類的物件未被建立時,編譯器不知道SIZE的值是什麼。     class A     {…         const int SIZE = 100;     // 錯誤,企圖在類宣告中初始化const資料成員         int array[SIZE];        // 錯誤,未知的SIZE     }; const資料成員的初始化只能在類建構函式的初始化表中進行,例如     class A     {…         A(int size);      // 建構函式         const int SIZE ;     };     A::A(int size) : SIZE(size)    // 建構函式的初始化表     {       …     }     A  a(100); // 物件 a 的SIZE值為100     A  b(200); // 物件 b 的SIZE值為200     怎樣才能建立在整個類中都恆定的常量呢?別指望const資料成員了,應該用類中的列舉常量來實現。例如     class A     {…         enum { SIZE1 = 100, SIZE2 = 200}; // 列舉常量         int array1[SIZE1];          int array2[SIZE2];     };     列舉常量不會佔用物件的儲存空間,它們在編譯時被全部求值。列舉常量的缺點是:它的隱含資料型別是整數,其最大值有限,且不能表示浮點數(如PI=3.14159)。

第6章 函式設計

函式是C++/C程式的基本功能單元,其重要性不言而喻。函式設計的細微缺點很容易導致該函式被錯用,所以光使函式的功能正確是不夠的。本章重點論述函式的介面設計和內部實現的一些規則。 函式介面的兩個要素是引數和返回值。C語言中,函式的引數和返回值的傳遞方式有兩種:值傳遞(pass by value)和指標傳遞(pass by pointer)。C++ 語言中多了引用傳遞(pass by reference)。由於引用傳遞的性質象指標傳遞,而使用方式卻象值傳遞,初學者常常迷惑不解,容易引起混亂,請先閱讀6.6節“引用與指標的比較”。

6.1 引數的規則

l         【規則6-1-1引數的書寫要完整,不要貪圖省事只寫引數的型別而省略引數名字。如果函式沒有引數,則用void填充。 例如: void SetValue(int width, int height);   // 良好的風格 void SetValue(int, int);            // 不良的風格 float GetValue(void);    // 良好的風格 float GetValue();       // 不良的風格 l         【規則6-1-2引數命名要恰當,順序要合理。 例如編寫字串拷貝函式StringCopy,它有兩個引數。如果把引數名字起為str1和str2,例如 void StringCopy(char *str1, char *str2); 那麼我們很難搞清楚究竟是把str1拷貝到str2中,還是剛好倒過來。 可以把引數名字起得更有意義,如叫strSource和strDestination。這樣從名字上就可以看出應該把strSource拷貝到strDestination。 還有一個問題,這兩個引數那一個該在前那一個該在後?引數的順序要遵循程式設計師的習慣。一般地,應將目的引數放在前面,源引數放在後面。 如果將函式宣告為: void StringCopy(char *strSource, char *strDestination); 別人在使用時可能會不假思索地寫成如下形式: char str[20]; StringCopy(str, “Hello World”);   // 引數順序顛倒 l         【規則6-1-3如果引數是指標,且僅作輸入用,則應在型別前加const,以防止該指標在函式體內被意外修改。 例如: void StringCopy(char *strDestination,const char *strSource); l         【規則6-1-4如果輸入引數以值傳遞的方式傳遞物件,則宜改用“const &”方式來傳遞,這樣可以省去臨時物件的構造和析構過程,從而提高效率。 ²        【建議6-1-1避免函式有太多的引數,引數個數儘量控制在5個以內。如果引數太多,在使用時容易將引數型別或順序搞錯。 ²        【建議6-1-2儘量不要使用型別和數目不確定的引數。 C標準庫函式printf是採用不確定引數的典型代表,其原型為: int printf(const chat *format[, argument]…); 這種風格的函式在編譯時喪失了嚴格的型別安全檢查。

6.2 返回值的規則

l         【規則6-2-1不要省略返回值的型別。 C語言中,凡不加型別說明的函式,一律自動按整型處理。這樣做不會有什麼好處,卻容易被誤解為void型別。 C++語言有很嚴格的型別安全檢查,不允許上述情況發生。由於C++程式可以呼叫C函式,為了避免混亂,規定任何C++/ C函式都必須有型別。如果函式沒有返回值,那麼應宣告為void型別。 l         【規則6-2-2函式名字與返回值型別在語義上不可衝突。 違反這條規則的典型代表是C標準庫函式getchar。 例如: char c; c = getchar(); if (c == EOF) … 按照getchar名字的意思,將變數c宣告為char型別是很自然的事情。但不幸的是getchar的確不是char型別,而是int型別,其原型如下:         int getchar(void); 由於c是char型別,取值範圍是[-128,127],如果巨集EOF的值在char的取值範圍之外,那麼if語句將總是失敗,這種“危險”人們一般哪裡料得到!導致本例錯誤的責任並不在使用者,是函式getchar誤導了使用者。 l         【規則6-2-3不要將正常值和錯誤標誌混在一起返回。正常值用輸出引數獲得,而錯誤標誌用return語句返回。 回顧上例,C標準庫函式的設計者為什麼要將getchar宣告為令人迷糊的int型別呢?他會那麼傻嗎? 在正常情況下,getchar的確返回單個字元。但如果getchar碰到檔案結束標誌或發生讀錯誤,它必須返回一個標誌EOF。為了區別於正常的字元,只好將EOF定義為負數(通常為負1)。因此函式getchar就成了int型別。 我們在實際工作中,經常會碰到上述令人為難的問題。為了避免出現誤解,我們應該將正常值和錯誤標誌分開。即:正常值用輸出引數獲得,而錯誤標誌用return語句返回。 函式getchar可以改寫成 BOOL GetChar(char *c); 雖然gechar比GetChar靈活,例如 putchar(getchar()); 但是如果getchar用錯了,它的靈活性又有什麼用呢? ²        【建議6-2-1有時候函式原本不需要返回值,但為了增加靈活性如支援鏈式表達,可以附加返回值。 例如字串拷貝函式strcpy的原型: char *strcpy(char *strDest,const char *strSrc); strcpy函式將strSrc拷貝至輸出引數strDest中,同時函式的返回值又是strDest。這樣做並非多此一舉,可以獲得如下靈活性:     char str[20];     int  length = strlen( strcpy(str, “Hello World”) ); ²        【建議6-2-2如果函式的返回值是一個物件,有些場合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場合只能用“值傳遞”而不能用“引用傳遞”,否則會出錯。 例如: class String {…     // 賦值函式     String & operate=(const String &other);    // 相加函式,如果沒有friend修飾則只許有一個右側引數 friend    String   operate+( const String &s1, const String &s2); private:     char *m_data; }        String的賦值函式operate = 的實現如下: String & String::operate=(const String &other) {     if (this == &other)         return *this;     delete m_data;     m_data = new char[strlen(other.data)+1];     strcpy(m_data, other.data);     return *this;    // 返回的是 *this的引用,無需拷貝過程 } 對於賦值函式,應當用“引用傳遞”的方式返回String物件。如果用“值傳遞”的方式,雖然功能仍然正確,但由於return語句要把 *this拷貝到儲存返回值的外部儲存單元之中,增加了不必要的開銷,降低了賦值函式的效率。例如:   String a,b,c;   …   a = b;     // 如果用“值傳遞”,將產生一次 *this 拷貝   a = b = c;   // 如果用“值傳遞”,將產生兩次 *this 拷貝        String的相加函式operate + 的實現如下: String  operate+(const String &s1, const String &s2)   {     String temp;     delete temp.data;    // temp.data是僅含‘/0’的字串         temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];         strcpy(temp.data, s1.data);         strcat(temp.data, s2.data);         return temp;     } 對於相加函式,應當用“值傳遞”的方式返回String物件。如果改用“引用傳遞”,那麼函式返回值是一個指向區域性物件temp的“引用”。由於temp在函式結束時被自動銷燬,將導致返回的“引用”無效。例如:     c = a + b; 此時 a + b 並不返回期望值,c什麼也得不到,流下了隱患。

6.3 函式內部實現的規則

不同功能的函式其內部實現各不相同,看起來似乎無法就“內部實現”達成一致的觀點。但根據經驗,我們可以在函式體的“入口處”和“出口處”從嚴把關,從而提高函式的質量。 l         【規則6-3-1在函式體的“入口處”,對引數的有效性進行檢查。 很多程式錯誤是由非法引數引起的,我們應該充分理解並正確使用“斷言”(assert)來防止此類錯誤。詳見6.5節“使用斷言”。 l         【規則6-3-2在函式體的“出口處”,對return語句的正確性和效率進行檢查。      如果函式有返回值,那麼函式的“出口處”是return語句。我們不要輕視return語句。如果return語句寫得不好,函式要麼出錯,要麼效率低下。 注意事項如下: (1)return語句不可返回指向“棧記憶體”的“指標”或者“引用”,因為該記憶體在函式體結束時被自動銷燬。例如     char * Func(void)     {         char str[] = “hello world”;    // str的記憶體位於棧上         …         return str;     // 將導致錯誤     } (2)要搞清楚返回的究竟是“值”、“指標”還是“引用”。 (3)如果函式返回值是一個物件,要考慮return語句的效率。例如                  return String(s1 + s2); 這是臨時物件的語法,表示“建立一個臨時物件並返回它”。不要以為它與“先建立一個區域性物件temp並返回它的結果”是等價的,如 String temp(s1 + s2); return temp; 實質不然,上述程式碼將發生三件事。首先,temp物件被建立,同時完成初始化;然後拷貝建構函式把temp拷貝到儲存返回值的外部儲存單元中;最後,temp在函式結束時被銷燬(呼叫解構函式)。然而“建立一個臨時物件並返回它”的過程是不同的,編譯器直接把臨時物件建立並初始化在外部儲存單元中,省去了拷貝和析構的化費,提高了效率。 類似地,我們不要將  return int(x + y); // 建立一個臨時變數並返回它 寫成 int temp = x + y; return temp; 由於內部資料型別如int,float,double的變數不存在建構函式與解構函式,雖然該“臨時變數的語法”不會提高多少效率,但是程式更加簡潔易讀。

6.4 其它建議

²        【建議6-4-1函式的功能要單一,不要設計多用途的函式。 ²        【建議6-4-2函式體的規模要小,儘量控制在50行程式碼之內。 ²        【建議6-4-3