前言
這里參考了《高質量C++C 編程指南 林銳》、《google C++編程指南》以及《華為C++語言編程規范》編寫了這份C++語言編程規范文檔,以合理使用 C++。
一、文件結構
每個 C++/C 程序通常分為兩個文件。一個文件用於保存程序的聲明(declaration),稱為頭文件。另一個文件用於保存程序的實現,稱為定義(definition)文件。
C++/C 程序的頭文件以 “.h” 為后綴,C 程序的定義文件以 “.c” 為后綴,C++ 程序的定義文件通常以 “.cpp” 為后綴(也有一些系統以 “.cc” 或 “.cxx” 為后綴)。
1.1 版權和版本的聲明
版權和版本的聲明位於頭文件的開頭,主要內容有:
1)版權信息;
2)文件名稱,標識符,摘要;
3)當前版本號,作者/修改者,完成日期;
4)版本歷史信息 。
/*
* Copyright (c) 2019,google
* All rights reserved.
*
* 文件名稱:fileName.h
* 摘 要:簡要描述本文件的功能和用法
*
* 當前版本:1.1
* 作 者:輸入作者(或修改者)名字
* 完成日期:2019 年 7 月 20 日
*
* 取代版本:1.0
* 原作者 :輸入原作者(或修改者)名字
* 完成日期:2019 年 5 月 10 日
*/
1.2 頭文件的結構
頭文件由三部分內容組成:
(1)頭文件開頭處的版權和版本聲明。
(2)預處理塊。
(3)函數和類結構聲明等。
【規則 1-2-1】為了防止頭文件被重復引用,應當用 ifndef/define/endif
結構產生預處理塊。
【規則 1-2-2】用 #include <filename.h>
格式來引用標准庫的頭文件(編譯器將從標准庫目錄開始搜索)。
【規則 1-2-3】用 #include “filename.h”
格式來引用非標准庫的頭文件(編譯器將從用戶的工作目錄開始搜索)。
【規則 1-2-4】頭文件中只存放 “聲明” 而不存放 “定義” 。
在 C++ 語法中,類的成員函數可以在聲明的同時被定義,並且自動成為內聯函數。這雖然會帶來書寫上的方便,但卻造成了風格不一致,弊大於利。建議將成員函數的定義與聲明分開,不論該函數體有多么小。
【規則 1-2-5】不提倡使用全局變量,盡量不要在頭文件中出現像 extern int value
這類聲明。
// 版權和版本聲明見示上例,此處省略。
#ifndef GRAPHICS_H // 防止 graphics.h 被重復引用
#define GRAPHICS_H
#include <math.h> // 引用標准庫的頭文件
…
#include “myheader.h” // 引用非標准庫的頭文件
…
void fun(…); // 全局函數聲明
…
class Box // 類結構聲明
{
…
};
#endif
1.3 頭文件依賴
【規則 1-3-1】使用前置聲明盡量減少 .h 文件中 #include 的數量。
當一個頭文件被包含的同時也引入了一項新的依賴 ,只要該頭文件被修改,代碼就要重新編譯。 如果你的頭文件包含了其他頭文件, 這些頭文件的任何改變也將導致那些包含了你的頭文件的代碼重新編譯。因此,我們寧可盡量少包含頭文件,尤其是那些包含在其他頭文件中的。
使用前置聲明可以顯著減少需要包含的頭文件數量。舉例說明:頭文件中用到類 File,但不需要訪問File的定義,則頭文件中只需前置聲明 class File
;無需 #include "file/base/file.h"
。
在頭文件如何做到使用類 Foo 而無需訪問類的定義?
-
將數據成員類型聲明為 Foo *或Foo &;
-
參數、返回值類型為 Foo的函數只是聲明(但不定義實現);
-
靜態數據成員的類型可以被聲明為 Foo,因為靜態數據成員的定義在類定義之外。
1.4 包含文件的次序
【規則 1-4-1】將包含次序標准化可增強可讀性,次序如下: C庫、 C++庫、其他庫的.h、項目內的.h。
項目內頭文件應按照項目源代碼目錄樹結構排列,並且避免使用 UNIX文件路徑 .
(當前目錄)和 ..
(父目錄)。例如, google-project/src/base/logging.h
應像這樣被包含:#include "base/logging.h
。示例如下:
#include <sys/types.h> // C庫
#include <unistd.h> // C庫
#include <hash_map> // C++庫
#include <vector> // C++庫
#include "base/basictypes.h" // 其他庫的.h
#include "base/commandlineflags.h" // 其他庫的.h
#include "foo/public/bar.h //項目內的.h
1.5 目錄結構
【規則 1-5-1】如果一個軟件的頭文件數目比較多(如超過十個),通常應將頭文件和定義文件分別保存於不同的目錄,以便於維護。
例如可將頭文件保存於 include 目錄,將定義文件保存於 source 目錄(可以是多級目錄)。
如果某些頭文件是私有的,它不會被用戶的程序直接引用,則沒有必要公開其“聲明”。為了加強信息隱藏,這些私有的頭文件可以和定義文件存放於同一個目錄。
二、程序的版式
版式雖然不會影響程序的功能,但會影響可讀性。程序的版式追求清晰、美觀,是程序風格的重要構成因素。
可以把程序的版式比喻為“書法”。好的“書法”可讓人對程序一目了然,看得興致勃勃。差的程序“書法”如螃蟹爬行,讓人看得索然無味,更令維護者煩惱有加。 所以學習程序的“書法”, 很有必要。
2.1 空格還是制表位
只使用空格,每次縮進 4 個空格。有些人更加青睞每次縮進 2 個空格,也是可以的,這個純粹看個人喜好,但如果是團隊協作的話需要統一。
【規則 2-1-1】只使用空格進行縮進,不要在代碼中使用 tabs,設定編輯器將 tab 轉為空格。(有些編輯器已經這樣默認設置了,例如Qt)
2.2 空行
空行起着分隔程序段落的作用。空行得體(不過多也不過少)能將使程序的布局更加清晰。空行不會浪費內存,雖然打印含有空行的程序是會多消耗一些紙張,但是值得。所以不要舍不得用空行。
【規則 2-2-1】在每個類聲明之后、每個函數定義結束之后都要加空行。
【規則 2-2-2】在一個函數體內,邏揖上密切相關的語句之間不加空行,其它地方應加空行分隔。
while (condition)
{
statement1;
// 空行
if (condition)
{
statement2;
}
// 空行
statement3;
}
2.3 代碼行
【規則 2-3-1】 一行代碼只做一件事情,如只定義一個變量,或只寫一條語句。這樣的代碼容易閱讀,並且方便寫注釋。
// 風格良好的代碼行
int width; // 寬度
int height; // 高度
int depth; // 深度
// 風格不良的代碼行
int width, height, depth; // 寬度 高度 深度
【規則 2-3-2】 if、for、while、do 等語句自占一行,執行語句不得緊跟其后。 不論執行語句有多少都要加{}。這樣可以防止書寫失誤。
// 風格良好的代碼行
if (width < height)
{
doSomething();
}
// 風格不良的代碼行
if (width < height) doSomething();
【建議 2-3-3】盡可能在定義變量的同時初始化該變量(就近原則)
如果變量的引用處和其定義處相隔比較遠,變量的初始化很容易被忘記。如果引用了未被初始化的變量,可能會導致程序錯誤。本建議可以減少隱患。例如 :
int width = 10; // 定義並初紿化 width
int height = 10; // 定義並初紿化 height
int depth = 10; // 定義並初紿化 depth
2.4 代碼行內的空格
【規則 2-4-1】關鍵字之后要留空格。像 if、for、while 等關鍵字之后應留一個空格再跟左括號 ‘(’,以突出關鍵字。
【規則 2-4-2】函數名之后不要留空格,緊跟左括號 ‘(’,以與關鍵字區別。
【規則 2-4-3】‘,’ 之后要留空格,如 fun(x, y, z)
。如果 ‘;’ 不是一行的結束符號,其后要留空格,如 for (initialization; condition; update)
。
【規則 2-4-4】賦值操作符、比較操作符、算術操作符、邏輯操作符、位域操作符,如 “=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”、“^” 等二元操作符的前后要加上空格。
【規則 2-4-5】一元操作符如 “!”、“~”、“++”、“--”、“&”(地址運算符)等前后不加空格。
【規則 2-4-6】像 “[]”、“.”、“->” 這類操作符前后不加空格。
【建議 2-4-7】對於表達式比較長的 for 語句和 if 語句,為了緊湊起見可以適當地去掉一些空格,如 for (i=0; i<10; i++)
和 if ((a<=b) && (c<=d))
。
void fun(int x, int y, int z); // 良好的風格
void fun (int x,int y,int z); // 不良的風格
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 ++) // 過多的空格
array[5] = 0; // 不要寫成 array [ 5 ] = 0;
a.Function(); // 不要寫成 a . Function();
b->Function(); // 不要寫成 b -> Function();
2.5 對齊
【規則 2-5-1】程序的分界符 ‘{’ 和 ‘}’ 應獨占一行並且位於同一列,同時與引用它們的語句左對齊。
【規則 2-5-2】{ } 之內的代碼塊在 ‘{’ 右邊縮進后再左對齊。
// 良好的風格
void function(int x)
{
doSomething();
other();
}
// 不良的風格
void function(int x) {
doSomething();
other();
}
2.6 長行拆分
【規則 2-6-1】代碼行最大長度宜控制在 70 至 80 個字符以內。代碼行不要過長,否則眼睛看不過來,也不便於打印。
【規則 2-6-2】長表達式要在低優先級操作符處拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要進行適當的縮進,使排版整齊,語句可讀。
【規則 2-6-3】構造函數初始化列表過長,可以按 4 格縮進並排幾行。
// 良好的風格
if (theOneThing > ONE
&& theThridThing == TWO
&& yetAnother == LAST)
{
doSomething();
}
// 良好的風格
MyClass::MyClass(int var)
: some_var_(var),
some_other_var_(var + 1)
{
doSomething();
}
2.7 修飾符的位置
修飾符 * 和 & 應該靠近數據類型還是該靠近變量名,是個有爭議的話題。
若將修飾符 * 靠近數據類型,例如:int* x;
從語義上講此寫法比較直觀,即 x 是 int 類型的指針。
上述寫法的弊端是容易引起誤解,例如:int* x, y;
此處 y 容易被誤解為指針變量。雖然將 x 和 y 分行定義可以避免誤解,但並不是人人都願意這樣做。
【規則 2-7-1】應當將修飾符 * 和 & 緊靠變量名。
// 良好的風格
char *name;
int *x, y; // 此處 y 不會被誤解為指針
2.8 函數參數順序
【規則 2-8-1】定義函數時,參數順序為:輸入參數在前,輸出參數在后。
C/C++ 函數參數分為輸入參數和輸出參數兩種, 有時輸入參數也會輸出 (值被修改時)。
輸入參數一般傳值或常數引用 ,輸出參數或輸入/輸出參數為非常數指針 。
對參數排序時,將所有輸入參數置於輸出參數之前。不要僅僅因為是新添加的參數,就將其置於最后,而應該依然置於輸出參數之前。
2.9 預處理指令
【規則 2-9-1】預處理指令不要縮進,從行首開始。即使預處理指令位於縮進代碼塊中,指令也應從行首開始。
// 良好的風格
void main()
{
if (condition)
{
#if DISASTER_PENDING // Good
dropEverything(); // 預處理指令中的程序仍然正常縮進
#endif
backToNormal();
}
}
2.10 類格式
【規則 2-10-1】類的聲明屬性依次序是public:
、protected:
、private:
,都位於行首。除第一個關鍵詞(一般是public)外,其他關鍵詞前空一行。
class MyClass : public OtherClass
{
public:
MyClass();
~MyClass() {}
void someFunction();
void setSomeVar(int var) { m_someVar = var; }
int someVar() const { return m_someVar; }
protected:
bool someInternalFunction();
private:
int m_someVar;
int m_someOtherVar;
}
2.11 命名空間格式化
【規則 2-11-1】命名空間內容不縮進。命名空間不添加額外縮進層次。
namespace {
void foo() { // Correct. No extra indentation within namespace.
...
}
注意:命名空間的左大括號可以在 namespace
所在一行,之間留一個空格。
三、命名規則
比較著名的命名規則當推 Microsoft 公司的“匈牙利”法,該命名規則的主要思想是“在變量和函數名中加入前綴以增進人們對程序的理解”。例如所有的字符變量均以 ch 為前綴,若是指針變量則追加前綴 p。
“匈牙利”法最大的缺點是煩瑣,例如:
int i, j, k;
float x, y, z;
倘若采用“匈牙利”命名規則,則應當寫成:
int iI, iJ, ik; // 前綴 i 表示 int 類型
float fX, fY, fZ; // 前綴 f 表示 float 類型
如此煩瑣的程序會讓絕大多數程序員無法忍受。
據考察,沒有一種命名規則可以讓所有的程序員贊同,程序設計教科書一般都不指定命名規則。命名規則對軟件產品而言並不是“成敗悠關”的事,我們不要花太多精力試圖發明世界上最好的命名規則,而應當制定一種令大多數項目成員滿意的命名規則,並在項目中貫徹實施。
3.1 共性規則
本節論述的共性規則是被大多數程序員采納的,我們應當在遵循
這些共性規則的前提下,再擴充特定的規則。
【規則 3-1-1】標識符應當直觀且可以拼讀,可望文知意,不必進行 “解碼”。
標識符最好采用英文單詞或其組合,便於記憶和閱讀。切忌使用漢語拼音來命名。程序中的英文單詞一般不會太復雜,用詞應當准確。例如不要把 CurrentValue 寫成 NowValue。
【規則 3-1-2】命名規則盡量與所采用的操作系統或開發工具的風格保持一致。
例如 Windows 應用程序的標識符通常采用 “大小寫” 混排的方式,如 addChild。而 Unix 應用程序的標識符通常采用 “小寫加下划線” 的方式,如 add_child。別把這兩類風格混在一起用。
【規則 3-1-3】程序中不要出現僅靠大小寫區分的相似的標識符。
int x, X; // 變量 x 與 X 容易混淆
void foo(int x); // 函數 foo 與 FOO 容易混淆
void FOO(float x);
【規則 3-1-4】程序中不要出現標識符完全相同的局部變量和全局變量,盡管兩者的作用域不同而不會發生語法錯誤,但會使人誤解。
【規則 3-1-5】變量的名字應當使用“名詞”或者“形容詞+名詞”。
float value;
float oldValue;
float newValue;
【規則 3-1-6】函數的名字應當使用“動詞”或者“動詞+名詞”(動賓詞組)。
drawBox(); // 普通函數
box->draw(); // 類的成員函數
【規則 3-1-7】用正確的反義詞組命名具有互斥意義的變量或相反動作的函數等。
int minValue;
int maxValue;
int SetValue(…);
int GetValue(…);
【規則 3-1-8】盡量避免名字中出現數字編號,如 Value1,Value2 等,除非邏輯上的確需要編號。這是為了防止程序員偷懶,不肯為命名動腦筋而導致產生無意義的名字(因為用數字編號最省事)。
【規則 3-1-8】除非縮寫放到項目外也非常明確,否則不要使用縮寫。
// 良好的風格
int num_dns_connections; // Most people know what "DNS" stands for
int price_count_reader; // OK, price count. Makes sense
// 不良的風格
int wgc_connections; // Only your group knows what this stands for
int pc_reader; // Lots of things can be abbreviated "pc"
3.2 簡單的 Windows 應用程序命名規則
作者對 “匈牙利” 命名規則做了合理的簡化,下述的命名規則簡單易用,比較適合於Windows 應用軟件的開發。
【規則 3-2-1】文件命名使用 “小駝峰命名法”,除第一個單詞之外,其他單詞首字母大寫。
lockScreenW.h
changePasswdW.cpp
【規則 3-2-2】無論是普通函數還是成員函數的命名,都使用 “小駝峰命名法”,除第一個單詞之外,其他單詞首字母大寫。
// 普通函數
void setAge()
{
...
}
// 成員函數
class MyClass
{
public:
void setAge(int age);
}
【規則 3-2-3】結構體、類型定義( typedef)、枚舉等所有類型,均使用 “大駝峰命名法”,所有單詞首字母大寫。
// classes and structs
class UrlTable { ...
struct UrlTableProperties { ...
// typedefs
typedef hash_map<UrlTable*, string> UrlTableMap;
// enums
enum UrlTableErrors { ...
【規則 3-2-4】變量和參數命名使用 “小駝峰命名法”,除第一個單詞之外,其他單詞首字母大寫。
bool flag;
int drawMode;
class MyClass :
{
private:
string m_tableName;
};
【規則 3-2-5】無論是宏常量還是普通常量的命名,都全用大寫的字母,用下划線分割單詞。
// 宏常量
#define MAX_ARRAY_LEN 100
// 普通常量
const int MAX_ARRAY_LEN = 100;
const float PI = 3.14159;
【規則 3-2-6】如果不得已需要全局變量,則使全局變量加前綴 g_(表示global),即 “匈牙利+小駝峰命名法”。
int g_howManyPeople; // 全局變量
【規則 3-2-7】靜態變量加前綴 s_(表示 static),即 “匈牙利+小駝峰命名法”。
void init()
{
static int s_initValue; // 靜態變量
}
【規則 3-2-8】類的數據成員加前綴 m_(表示 member),即 “匈牙利+小駝峰命名法”,這樣可以避免數據成員與成員函數的參數同名。
void Object::SetValue(int width, int height)
{
m_width = width;
m_height = height;
}
【規則 3-2-9】枚舉值推薦全部大寫,單詞間以下划線相連。
enum UrlTableErrors
{
OK = 0,
ERROR_OUT_OF_MEMORY,
ERROR_MALFORMED_INPUT,
};
四、表達式和基本語句
表達式和語句都屬於 C++/C 的短語結構語法。它們看似簡單,但使用時隱患比較多。本節歸納了正確使用表達式和語句的一些規則與建議。
4.1 運算符的優先級
【規則 4-1-1】如果代碼行中的運算符比較多,用括號確定表達式的操作順序,避免使用默認的優先級。
word = (high << 8) | low
if ((a | b) && (a & c))
4.2 復合表達式
如 a = b = c = 0
這樣的表達式稱為復合表達式。允許復合表達式存在的理由是:(1)書寫簡潔;
(2)可以提高編譯效率。但要防止濫用復合表達式
【規則 4-2-1】不要編寫太復雜的復合表達式。
i = a >= b && c < d && c + f <= g + h ; // 復合表達式過於復雜
【規則 4-2-2】不要有多用途的復合表達式。
d = (a = b + c) + r ; // 該表達式既求 a 值又求 d 值。
4.3 if 語句
if 語句是 C++/C 語言中最簡單、最常用的語句,然而很多程序員用隱含錯誤的方式寫 if 語句。本節以 “與零值比較” 為例,展開討論。
【規則 4-3-1】不可將布爾變量直接與 TRUE、FALSE 或者 1、0 進行比較。
// 良好的風格
if (flag) // 表示 flag 為真
if (!flag) // 表示 flag 為假
// 不良的風格
if (flag == TRUE)
if (flag == 1 )
if (flag == FALSE)
if (flag == 0)
【規則 4-3-2】整型變量與零值比較。
// 良好的風格
if (value == 0)
if (value != 0)
// 不良的風格
if (value) // 會讓人誤解 value 是布爾變量
if (!value)
【規則 4-3-3】不可將浮點變量用 “==” 或 “!=” 與任何數字比較。
// 良好的風格
if ((f>=-EPSINON) && (f<=EPSINON)) // EPSINON 是允許的誤差(即精度)
// 不良的風格
if (f == 0.0) // 隱含錯誤的比較
【規則 4-3-4】應當將指針變量用 “==” 或 “!=” 與 NULL 比較。
// 良好的風格
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 (condition) // if 和左圓括號間有個空格,且不在圓括號中添加空格
{
...
}
else // 關鍵字else另單獨起一行
{
...
}
4.4 循環語句的效率
C++/C 循環語句中,for 語句使用頻率最高,while 語句其次,do 語句很少用。本節重點論述循環體的效率。提高循環體效率的基本辦法是降低循環體的復雜性。
【規則 4-4-1】在多重循環中,如果有可能,應當將最長的循環放在最內層,最短的循環放在最外層,以減少 CPU 跨切循環層的次數。
// 低效率:長循環在最外層
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-2】如果循環體內存在邏輯判斷,並且循環次數很大,宜將邏輯判斷移到循環體的外面。
// 效率低但程序簡潔
for (i=0; i<N; i++)
{
if (condition)
{
doSomething();
}
else
{
doSomething();
}
}
// 效率高但程序不簡潔
if (condition)
{
for (i=0; i<N; i++)
doSomething();
}
else
{
for (i=0; i<N; i++)
doSomething();
}
4.5 for 語句的循環控制變量
【規則 4-5-1】如不可在 for 循環體內修改循環變量,防止 for 循環失去控制。
【規則 4-5-2】建議 for 語句的循環控制變量的取值采用 “半開半閉區間” 寫法。
// (a)循環變量屬於半開半閉區間
for (int x=0; x<N; x++)
{
doSomething();
}
// (b)循環變量屬於閉區間
for (int x=0; x<=N-1; x++)
{
doSomething();
}
示例 (a) 中的 x 值屬於半開半閉區間 “0 =< x < N”,起點到終點的間隔為 N,循環次數為 N。
示例 (b) 中的 x 值屬於閉區間 “0 =< x <= N-1”,起點到終點的間隔為 N-1,循環次數為N。
相比之下,示例 (a) 的寫法更加直觀,盡管兩者的功能是相同的。
4.6 switch 語句
【規則 4-6-1】switch 語句中的 case 塊可以使用大括號也可以不用, 取決於你的喜好。
【規則 4-6-2】如果有不滿足 case 枚舉條件的值,要總是包含一個 default(如果有輸入值沒有 case 去處理,編譯器將報警)。如果 default 永不會執行,可以簡單的使用assert。
switch (var)
{
case 0:
{
...
break;
}
default:
{
assert(false);
}
}
4.7 goto 語句
自從提倡結構化設計以來,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 語句,而不是禁用。
五、函數設計
函數是 C++/C 程序的基本功能單元,其重要性不言而喻。函數設計的細微缺點很容易導致該函數被錯用,所以光使函數的功能正確是不夠的。本節重點論述函數的接口設計和內部實現的一些規則。
5.1 參數的規則
【規則 5-1-1】參數命名要恰當,順序要合理。
例如編寫字符串拷貝函數 stringCopy,它有兩個參數。如果把參數名字起為
str1 和 str2,例如 :
void stringCopy(char *str1, char *str2);
那么我們很難搞清楚究竟是把 str1 拷貝到 str2 中,還是剛好倒過來。可以把參數名字起得更有意義,如叫 strSource 和 strDest。這樣從名字上就可以看出應該把 strSource 拷貝到 strDest。
還有一個問題,這兩個參數那一個該在前那一個該在后?參數的順序要遵循程序員的習慣。一般地,應將目的參數放在前面,源參數放在后面。 如果將函數聲明為:
void stringCopy(char *strSource, char *strDest);
別人在使用時可能會不假思索地寫成如下形式:
char str[20];
StringCopy(str, “Hello World”); // 錯誤,參數順序顛倒了
【規則 5-1-2】如果參數是指針,且僅作輸入用,則應在類型前加 const,以防止該指針在函數體內被意外修改。
void stringCopy(char *strDest,const char *strSource);
【規則 5-1-3】如果輸入參數以值傳遞的方式傳遞對象,則宜改用 “const &” 方式來傳遞,這樣可以省去臨時對象的構造和析構過程,從而提高效率。
【規則 5-1-4】避免函數有太多的參數,參數個數盡量控制在 5 個以內。如果參數太多,在使用時容易將參數類型或順序搞錯。
5.2 返回值的規則
【規則 5-2-1】函數名字與返回值類型在語義上不可沖突。
違反這條規則的典型代表是 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 誤導了使用者。
【規則 5-2-2】不要將正常值和錯誤標志混在一起返回。正常值用輸出參數獲得,而錯誤標志用 return 語句返回。
回顧上例,C 標准庫函數的設計者為什么要將 getchar 聲明為令人迷糊的 int 類型呢?他會那么傻嗎?
在正常情況下,getchar 的確返回單個字符。但如果 getchar 碰到文件結束標志或發生讀錯誤,它必須返回一個標志 EOF。為了區別於正常的字符,只好將 EOF 定義為負數(通常為負 1)。因此函數 getchar 就成了 int 類型。
我們在實際工作中,經常會碰到上述令人為難的問題。為了避免出現誤解,我們應該將正常值和錯誤標志分開。即:正常值用輸出參數獲得,而錯誤標志用return 語句返回。
函數 getchar 可以改寫成:
bool getChar(char *c);
雖然 gechar 比 GetChar 靈活,例如 putchar(getchar());
但是如果 getchar 用錯了,它的靈活性又有什么用呢?
【規則 5-2-3】有時候函數原本不需要返回值,但為了增加靈活性如支持鏈式表達,可以附加返回值。
例如字符串拷貝函數 strcpy 的原型:
char *strcpy(char *strDest,const char *strSrc);
strcpy 函數將 strSrc 拷貝至輸出參數 strDest 中,同時函數的返回值又是 strDest。這樣做並非多此一舉,可以獲得如下靈活性:
char str[20];
int length = strlen( strcpy(str, “Hello World”) );
【規則 5-2-4】函數返回時,return 表達式中不要使用圓括號。
return x; //not return(x);
5.3 函數內部實現的規則
不同功能的函數其內部實現各不相同,看起來似乎無法就“內部實現”達成一致的觀點。但根據經驗,我們可以在函數體的 “入口處” 和 “出口處” 從嚴把關,從而提高函數的質量。
【規則 5-3-1】在函數體的 “入口處”,對參數的有效性進行檢查。
很多程序錯誤是由非法參數引起的,我們應該充分理解並正確使用“斷言”(assert)來防止此類錯誤。 詳見 5.5 節“使用斷言”。
【規則 5-3-2】在函數體的“出口處”,對 return 語句的正確性和效率進行檢查。
如果函數有返回值,那么函數的“出口處”是 return 語句。我們不要輕視 return 語句。如果 return 語句寫得不好,函數要么出錯,要么效率低下。
注意事項如下:
(1)return 語句不可返回指向“棧內存”的“指針”或者“引用”,因為該內存在函數體結束時被自動銷毀。例如:
char * fun(void)
{
char str[] = “hello world”; // str 的內存位於棧上
…
return str; // 將導致錯誤
}
(2)要搞清楚返回的究竟是 “值”、“指針” 還是 “引用”。
5.4 使用斷言
程序一般分為 Debug 版本和 Release 版本,Debug 版本用於內部調試,Release 版本發行給用戶使用。
斷言 assert 是僅在 Debug 版本起作用的宏,它用於檢查“不應該”發生的情況。下例是一個內存復制函數。在運行過程中,如果 assert 的參數為假,那么程序就會中止(一般地還會出現提示對話,說明在什么地方引發了 assert)。
void *memcpy(void *pvTo, const void *pvFrom, size_t size)
{
assert((pvTo != NULL) && (pvFrom != NULL)); // 使用斷言
byte *pbTo = (byte *) pvTo; // 防止改變 pvTo 的地址
byte *pbFrom = (byte *) pvFrom; // 防止改變 pvFrom 的地址
while(size -- > 0 )
*pbTo ++ = *pbFrom ++;
return pvTo;
}
為了不在程序的 Debug 版本和 Release版本引起差別,assert 不應該產生任何副作用。所以 assert 不是函數,而是宏。程序員可以把 assert 看成一個在任何系統狀態下都可以安全使用的無害測試手段。**如果程序在 assert 處終止了,並不是說含有該 assert 的函數有錯誤,而是調用者出了差錯,assert 可以幫助我們找到發生錯誤的原因。 **
5.5 其它建議
【規則 5-5-1】函數的功能要單一,不要設計多用途的函數。
【規則 5-5-2】函數體的規模要小,盡量控制在 50 行代碼之內。
【規則 5-5-3】盡量避免函數帶有“記憶”功能。相同的輸入應當產生相同的輸出。
帶有“記憶”功能的函數,其行為可能是不可預測的,因為它的行為可能取決於某種“記憶狀態”。這樣的函數既不易理解又不利於測試和維護。在 C/C++語言中,函數的 static 局部變量是函數的“記憶”存儲器。建議盡量少用 static局部變量,除非必需。
【規則 5-5-4】不僅要檢查輸入參數的有效性,還要檢查通過其它途徑進入
函數體內的變量的有效性,例如全局變量、文件句柄等。
六、注釋
C++語言中,程序塊的注釋常采用 “/*…*/”,行注釋一般采用 “//…”。注釋通常用於:
(1)版本、版權聲明;
(2)函數接口說明;
(3)重要的代碼行或段落提示。
雖然注釋有助於理解代碼,但注意不可過多地使用注釋。
6.1 類注釋
【規則 6-1-1】每個類的定義要添加描述類的功能和用法的注釋。
如果你覺得已經在文件頂部詳細描述了該類,想直接簡單的來上一句“完整描述見文件頂部”的話,還是多少在類中加點注釋吧。
如果該類的實例可被多線程訪問,使用時務必注意備注說明一下。
6.2 函數注釋
函數聲明處注釋描述函數功能,定義處描述函數實現。
函數聲明
【規則 6-2-1】函數聲明處的注釋,只描述函數功能及用法,而不會描述函數如何實現,因為那是定義部分的事情。
void setAge(int age); // 設置學生年齡
函數定義:
【規則 6-2-2】每個函數定義時要以注釋說明函數功能和實現要點,如使用的漂亮代碼、實現的簡要步驟、如此實現的理由等等。
不要從 .h 文件或其他地方的函數聲明處直接復制注釋,簡要說明函數功能是可以的,但重點要放在如何實現上。
/*
* 函數介紹:設置學生年齡
* 函數實現:將參數age的值賦給成員變量m_age
* 輸入參數:age-傳入的學生年齡值
* 返回值 :NULL
* 注意事項:NULL
*/
void setAge(int age)
{
m_age = age;
}
6.3 注釋風格
關於注釋風格,很多 C++ 的 coders 更喜歡行注釋, C coders 或許對塊注釋依然情有獨
鍾,或者在文件頭大段大段的注釋時使用塊注釋。
【規則 6-3-1】對於行注釋,注釋與 //
留一個空格,若是注釋在程序右側,則 //
與程序之間留一個空格。
// 注釋1
fun1();
fun2(); // 注釋2
七、其他編程經驗
下面介紹一些使 C++ 代碼更加健壯的技巧和使用方式。
7.1 引用參數
【規則 7-1-1】按引用傳遞的參數必須加上 const。
void Foo(const string &in, string *out);
事實上這是一個硬性約定:輸入參數為值或常數引用,輸出參數為指針;輸入參數可以是常數指針,但不能使用非常數引用形參。
7.2 類型轉換
【規則 7-2-1】使用 static_cast<>()
等 C++ 的類型轉換,不要使用 int y = (int)x
。
C 語言的類型轉換問題在於操作比較含糊:有時是在做強制轉換(如 (int)3.5
),有時是在做類型轉換(如 (int)"hello"
)。另外, C++ 的類型轉換查找更容易、更醒目。
7.3 整型
【規則 7-3-1】C++ 內建整型中, 唯一用到的是 int, 如果程序中需要不同大小的變量, 可以使用<stdint.h>中的精確寬度的整型,如 int16_t。
<stdint.h> 定義了 int16_t、 uint32_t、 int64_t 等整型,在需要確定大小的整型時可以
使用它們代替 short、 unsigned long long 等。適當情況下,推薦使用標准類型如 size_t 和 ptrdiff_t。
最常使用的是,對整數來說,通常不會用到太大,如循環計數等,可以使用普通的 int。
7.4 預處理宏
【規則 7-4-1】使用宏時要謹慎,盡量以內聯函數、枚舉和常量代替之。
宏意味着你和編譯器看到的代碼是不同的, 因此可能導致異常行為, 尤其是當宏存在於全局作用域中。
值得慶幸的是, C++ 中,宏不像C中那么必要。宏內聯效率關鍵代碼可以用內聯函數替代; 宏存儲常量可以 const 變量替代; 宏“縮寫”長變量名可以引用替代;
下面給出的用法模式可以避免一些使用宏的問題,供使用宏時參考:
-
可能被多個C++文件用到的宏定義,一般都放在頭文件中(.h),如果只需被一個文件所用,放在 .cpp 或 .h 里面都可以;
-
使用前正確 #define,使用后正確 #undef。
7.5 0和NULL
【規則 7-5-1】整數用0,實數用0.0,指針用NULL,字符(串)用'\0'。
7.6 sizeof( sizeof)
【規則 7-6-1】盡可能用 sizeof(varname) 代替 sizeof(type)。
使用 sizeof(varname) 是因為當變量類型改變時代碼自動同步,有些情況下 sizeof(type) 或許有意義,還是要盡量避免,如果變量類型改變的話不能同步。
Struct data;
memset(&data, 0, sizeof(data)); //Good - 變量類型改變時,代碼自動同步
memset(&data, 0, sizeof(Struct)) //Bad - 變量類型改變時,代碼不會自動同步
7.7 內存管理
【規則 7-7-1】用 malloc 或 new 申請內存之后,應該立即檢查指針值是否為 NULL。防止使用指針值為 NULL 的內存。
【規則 7-7-2】不要忘記為數組和動態內存賦初值。防止將未被初始化的內存作為右值使用。
【規則 7-7-3】動態內存的申請與釋放必須配對,防止內存泄漏。
【規則 7-7-4】用 free 或 delete 釋放了內存之后,立即將指針設置為 NULL,防止產生 “野指針”。
7.8 一些有益的建議
【規則 7-8-1】當心那些視覺上不易分辨的操作符發生書寫錯誤。
我們經常會把 “==” 誤寫成 “=”,象 “||”、“&&”、“<=”、“>=” 這類符號也很容易發生 “丟 1” 失誤。然而編譯器卻不一定能自動指出這類錯誤。
【規則 7-8-2】變量(指針、數組)被創建之后應當及時把它們初始化,以防止把未被初始化的變量當成右值使用。
【規則 7-8-3】當心數據類型轉換發生錯誤。盡量使用顯式的數據類型轉換(讓人們知道發生了什么事),避免讓編譯器輕悄悄地進行隱式的數據類型轉換。
【規則 7-8-4】當心變量發生上溢或下溢,數組的下標越界。
【規則 7-8-5】當心忘記編寫錯誤處理程序,當心錯誤處理程序本身有誤。
【規則 7-8-6】如果原有的代碼質量比較好,盡量復用它。但是不要修補很差勁的代碼,應當重新編寫。
【規則 7-8-7】盡量使用標准庫函數,不要“發明”已經存在的庫函數。
【規則 7-8-8】盡量不要使用與具體硬件或軟件環境關系密切的變量。
【規則 7-8-9】把編譯器的選擇項設置為最嚴格狀態。