1 文件結(jié)構(gòu)
每個(gè)C++/C程序通常分為兩個(gè)文件。一個(gè)文件用于保存程序的聲明(declaration),稱為頭文件。另一個(gè)文件用于保存程序的實(shí)現(xiàn)(implementation),稱為定義(definition)文件。C++/C程序的頭文件以“.h”為后綴,C程序的定義文件以“.c”為后綴,C++程序的定義文件通常以“.cpp”為后綴。
頭文件的結(jié)構(gòu)與作用
頭文件主要有兩個(gè)作用:
- 通過頭文件來調(diào)用庫(kù)功能。在很多場(chǎng)合,源代碼不便(或不準(zhǔn))向用戶公布,只要向用戶提供頭文件和二進(jìn)制的庫(kù)即可。用戶只需要按照頭文件中的接口聲明來調(diào)用庫(kù)功能,而不必關(guān)心接口怎么實(shí)現(xiàn)的。編譯器會(huì)從庫(kù)中提取相應(yīng)的代碼。
- 頭文件能加強(qiáng)類型安全檢查。如果某個(gè)接口被實(shí)現(xiàn)或被使用時(shí),其方式與頭文件中的聲明不一致,編譯器就會(huì)指出錯(cuò)誤,這一簡(jiǎn)單的規(guī)則能大大減輕程序員調(diào)試、改錯(cuò)的負(fù)擔(dān)。
頭文件由三部分組成:頭文件開頭處的版權(quán)和版本聲明,預(yù)處理塊,函數(shù)和類結(jié)構(gòu)聲明等。
- 為了防止頭文件被重復(fù)引用,應(yīng)當(dāng)用ifndef/define/endif結(jié)構(gòu)產(chǎn)生預(yù)處理塊。
- 用 #include <filename.h> 格式來引用標(biāo)準(zhǔn)庫(kù)的頭文件(編譯器將從標(biāo)準(zhǔn)庫(kù)目錄開始搜索)。
- 用 #include “filename.h” 格式來引用非標(biāo)準(zhǔn)庫(kù)的頭文件(編譯器將從用戶的工作目錄開始搜索)。
- 頭文件中只存放“聲明”而不存放“定義”。在C++ 語法中,類的成員函數(shù)可以在聲明的同時(shí)被定義,并且自動(dòng)成為內(nèi)聯(lián)函數(shù)。這雖然會(huì)帶來書寫上的方便,但卻造成了風(fēng)格不一致,弊大于利。建議將成員函數(shù)的定義與聲明分開,不論該函數(shù)體有多么小。
- 不提倡使用全局變量,盡量不要在頭文件中出現(xiàn)象extern int value 這類聲明。
假設(shè)頭文件名稱為 graphics.h,頭文件的結(jié)構(gòu)參見示例1.1
/*
* Copyright (c) 2001,上海貝爾有限公司網(wǎng)絡(luò)應(yīng)用事業(yè)部
* All rights reserved.
*
* 文件名稱:filename.h
* 文件標(biāo)識(shí):見配置管理計(jì)劃書
* 摘 要:簡(jiǎn)要描述本文件的內(nèi)容
*
* 當(dāng)前版本:1.1
* 作 者:輸入作者(或修改者)名字
* 完成日期:2001年7月20日
*
* 取代版本:1.0
* 原作者 :輸入原作者(或修改者)名字
* 完成日期:2001年5月10日
*/
#ifndef GRAPHICS_H // 防止graphics.h被重復(fù)引用
#define GRAPHICS_H
#include <math.h> // 引用標(biāo)準(zhǔn)庫(kù)的頭文件
…
#include “myheader.h” // 引用非標(biāo)準(zhǔn)庫(kù)的頭文件
…
void Function1(…); // 全局函數(shù)聲明
…
class Box // 類結(jié)構(gòu)聲明
{
…
};
#endif
定義文件的結(jié)構(gòu)
定義文件有三部分內(nèi)容:定義文件開頭處的版權(quán)和版本聲明,對(duì)一些頭文件的引用,程序的實(shí)現(xiàn)體(包括數(shù)據(jù)和代碼)。
假設(shè)定義文件的名稱為 graphics.cpp,定義文件的結(jié)構(gòu)參見示例1.2
// 版權(quán)和版本聲明見示例1-1,此處省略。
#include “graphics.h” // 引用頭文件
…
// 全局函數(shù)的實(shí)現(xiàn)體
void Function1(…)
{
…
}
// 類成員函數(shù)的實(shí)現(xiàn)體
void Box::Draw(…)
{
…
}
目錄結(jié)構(gòu)
如果一個(gè)軟件的頭文件數(shù)目比較多(如超過十個(gè)),通常應(yīng)將頭文件和定義文件分別保存于不同的目錄,以便于維護(hù)。例如可將頭文件保存于include目錄,將定義文件保存于source目錄(可以是多級(jí)目錄)。
如果某些頭文件是私有的,它不會(huì)被用戶的程序直接引用,則沒有必要公開其“聲明”。為了加強(qiáng)信息隱藏,這些私有的頭文件可以和定義文件存放于同一個(gè)目錄。
2 程序的版式
版式雖然不會(huì)影響程序的功能,但會(huì)影響可讀性。程序的版式追求清晰、美觀,是程序風(fēng)格的重要構(gòu)成因素。
- 盡可能在定義變量的同時(shí)初始化該變量。如果變量的引用處和其定義處相隔比較遠(yuǎn),變量的初始化很容易被忘記。如果引用了未被初始化的變量,可能會(huì)導(dǎo)致程序錯(cuò)誤。本建議可以減少隱患。
- 代碼行內(nèi)的空格。關(guān)鍵字之后要留空格,象const、virtual、inline、case 等關(guān)鍵字之后至少要留一個(gè)空格,以突出關(guān)鍵字;函數(shù)名之后不要留空格,緊跟左括號(hào)‘(’,以與關(guān)鍵字區(qū)別;賦值操作符、比較操作符、算術(shù)操作符、邏輯操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后應(yīng)當(dāng)加空格;一元操作符如“!”、“~”、“++”、“--”、“&”(地址運(yùn)算符)等前后不加空格。
- 類的版式。建議采用“以行為為中心”的書寫方式,即將public類型的函數(shù)寫在前面,而將private類型的數(shù)據(jù)寫在后面,重點(diǎn)關(guān)注的是類應(yīng)該提供什么樣的接口(或服務(wù))。
3 命名規(guī)則
- 標(biāo)識(shí)符應(yīng)當(dāng)直觀且可以拼讀,可望文知意。標(biāo)識(shí)符最好采用英文單詞或其組合,便于記憶和閱讀。切忌使用漢語拼音來命名。程序中的英文單詞一般不會(huì)太復(fù)雜,用詞應(yīng)當(dāng)準(zhǔn)確。例如不要把CurrentValue寫成NowValue。
- 標(biāo)識(shí)符的長(zhǎng)度應(yīng)當(dāng)符合“min-length && max-information”原則。單字符的名字也是有用的,常見的如i,j,k,m,n,x,y,z等,它們通??捎米骱瘮?shù)內(nèi)的局部變量。
- 命名規(guī)則盡量與所采用的操作系統(tǒng)或開發(fā)工具的風(fēng)格保持一致。例如Windows應(yīng)用程序的標(biāo)識(shí)符通常采用“大小寫”混排的方式,如AddChild。而Unix應(yīng)用程序的標(biāo)識(shí)符通常采用“小寫加下劃線”的方式,如add_child。別把這兩類風(fēng)格混在一起用。
- 變量的名字應(yīng)當(dāng)使用“名詞”或者“形容詞+名詞”。
float value;``float oldValue; - 全局函數(shù)的名字應(yīng)當(dāng)使用“動(dòng)詞”或者“動(dòng)詞+名詞”(動(dòng)賓詞組)。類的成員函數(shù)應(yīng)當(dāng)只使用“動(dòng)詞”,被省略掉的名詞就是對(duì)象本身。
DrawBox(); // 全局函數(shù)
box->Draw(); // 類的成員函數(shù)
- 盡量避免名字中出現(xiàn)數(shù)字編號(hào),如Value1,Value2等,除非邏輯上的確需要編號(hào)。
簡(jiǎn)單的Windows應(yīng)用程序命名規(guī)則
- 類名和函數(shù)名用大寫字母開頭的單詞組合而成。
class Node; // 類名
class LeafNode; // 類名
void Draw(void); // 函數(shù)名
void SetValue(int value); // 函數(shù)名
- 變量和參數(shù)用小寫字母開頭的單詞組合而成。
- 常量全用大寫的字母,用下劃線分割單詞。
const int MAX_LENGTH = 100; - 靜態(tài)變量加前綴s_(表示static)。
static int s_initValue; - 如果不得已需要全局變量,則使全局變量加前綴g_(表示global)。
- 類的數(shù)據(jù)成員加前綴m_(表示member),這樣可以避免數(shù)據(jù)成員與成員函數(shù)的參數(shù)同名。
void Object::SetValue(int width, int height)
{
m_width = width;
m_height = height;
}
- 為了防止某一軟件庫(kù)中的一些標(biāo)識(shí)符和其它軟件庫(kù)中的沖突,可以為各種標(biāo)識(shí)符加上能反映軟件性質(zhì)的前綴。例如三維圖形標(biāo)準(zhǔn)OpenGL的所有庫(kù)函數(shù)均以gl開頭,所有常量(或宏定義)均以GL開頭。
4 表達(dá)式和基本語句
運(yùn)算符的優(yōu)先級(jí)
C++/C語言的運(yùn)算符有數(shù)十個(gè),運(yùn)算符的優(yōu)先級(jí)與結(jié)合律如圖所示。注意一元運(yùn)算符 + - * 的優(yōu)先級(jí)高于對(duì)應(yīng)的二元運(yùn)算符。如果代碼行中的運(yùn)算符比較多,用括號(hào)確定表達(dá)式的操作順序,避免使用默認(rèn)的優(yōu)先級(jí)。

if語句
- 布爾變量與零值比較:不可將布爾變量直接與TRUE、FALSE或者1、0進(jìn)行比較。根據(jù)布爾類型的語義,零值為“假”(記為FALSE),任何非零值都是“真”(記為TRUE)。TRUE的值究竟是什么并沒有統(tǒng)一的標(biāo)準(zhǔn)。例如Visual C++ 將TRUE定義為1,而Visual Basic則將TRUE定義為-1。假設(shè)布爾變量名字為flag,它與零值比較的標(biāo)準(zhǔn)if語句如下:
if (flag) // 表示flag為真
if (!flag) // 表示flag為假
- 整型變量與零值比較:應(yīng)當(dāng)將整型變量用“==”或“!=”直接與0比較。假設(shè)整型變量的名字為value,它與零值比較的標(biāo)準(zhǔn)if語句如下:
if (value == 0)
if (value != 0)
- 浮點(diǎn)變量與零值比較:不可將浮點(diǎn)變量用“==”或“!=”與任何數(shù)字比較。千萬要留意,無論是float還是double類型的變量,都有精度限制。所以一定要避免將浮點(diǎn)變量用“==”或“!=”與數(shù)字比較,應(yīng)該設(shè)法轉(zhuǎn)化成“>=”或“<=”形式。
if ((x>=-EPSINON) && (x<=EPSINON)) //EPSINON是允許的誤差(即精度)。
- 指針變量與零值比較:應(yīng)當(dāng)將指針變量用“==”或“!=”與NULL比較。指針變量的零值是“空”(記為NULL)。盡管NULL的值與0相同,但是兩者意義不同。假設(shè)指針變量的名字為p,它與零值比較的標(biāo)準(zhǔn)if語句如下:
if (p == NULL) // p與NULL顯式比較,強(qiáng)調(diào)p是指針變量
if (p != NULL)
- 有時(shí)候我們可能會(huì)看到 if (NULL == p) 這樣古怪的格式。不是程序?qū)戝e(cuò)了,是程序員為了防止將 if (p == NULL) 誤寫成 if (p = NULL),而有意把p和NULL顛倒。編譯器認(rèn)為 if (p = NULL) 是合法的,但是會(huì)指出 if (NULL = p)是錯(cuò)誤的,因?yàn)镹ULL不能被賦值。
循環(huán)語句
C++/C循環(huán)語句中,for語句使用頻率最高,while語句其次,do語句很少用。本節(jié)重點(diǎn)論述循環(huán)體的效率。提高循環(huán)體效率的基本辦法是降低循環(huán)體的復(fù)雜性。
- 在多重循環(huán)中,如果有可能,應(yīng)當(dāng)將最長(zhǎng)的循環(huán)放在最內(nèi)層,最短的循環(huán)放在最外層,以減少CPU跨切循環(huán)層的次數(shù)。
- 不可在for 循環(huán)體內(nèi)修改循環(huán)變量,防止for 循環(huán)失去控制。
switch語句
switch是多分支選擇語句,而if語句只有兩個(gè)分支可供選擇。雖然可以用嵌套的if語句來實(shí)現(xiàn)多分支選擇,但那樣的程序冗長(zhǎng)難讀。這是switch語句存在的理由。switch語句的基本格式是:
switch (variable)
{
case value1 : …
break;
case value2 : …
break;
…
default : …
break;
}
- 每個(gè)case語句的結(jié)尾不要忘了加break,否則將導(dǎo)致多個(gè)分支重疊(除非有意使多個(gè)分支重疊)。
- 不要忘記最后那個(gè)default分支。即使程序真的不需要default處理,也應(yīng)該保留語句 default : break; 這樣做并非多此一舉,而是為了防止別人誤以為你忘了default處理。
5 常量
常量是一種標(biāo)識(shí)符,它的值在運(yùn)行期間恒定不變。C++ 語言可以用const來定義常量,也可以用 #define來定義常量。但是前者比后者有更多的優(yōu)點(diǎn),建議用const常量完全取代宏常量。
- const常量有數(shù)據(jù)類型,而宏常量沒有數(shù)據(jù)類型。編譯器可以對(duì)前者進(jìn)行類型安全檢查。而對(duì)后者只進(jìn)行字符替換,沒有類型安全檢查,并且在字符替換可能會(huì)產(chǎn)生意料不到的錯(cuò)誤(邊際效應(yīng))。
- 有些集成化的調(diào)試工具可以對(duì)const常量進(jìn)行調(diào)試,但是不能對(duì)宏常量進(jìn)行調(diào)試。
常量定義規(guī)則:需要對(duì)外公開的常量放在頭文件中,不需要對(duì)外公開的常量放在定義文件的頭部。為便于管理,可以把不同模塊的常量集中存放在一個(gè)公共的頭文件中。如果某一常量與其它常量密切相關(guān),應(yīng)在定義中包含這種關(guān)系,而不應(yīng)給出一些孤立的值。
類中的常量
有時(shí)我們希望某些常量只在類中有效。由于#define定義的宏常量是全局的,不能達(dá)到目的,于是想當(dāng)然地覺得應(yīng)該用const修飾數(shù)據(jù)成員來實(shí)現(xiàn)。const數(shù)據(jù)成員的確是存在的,但其含義卻不是我們所期望的。const數(shù)據(jù)成員只在某個(gè)對(duì)象生存期內(nèi)是常量,而對(duì)于整個(gè)類而言卻是可變的,因?yàn)轭惪梢詣?chuàng)建多個(gè)對(duì)象,不同的對(duì)象其const數(shù)據(jù)成員的值可以不同。
const數(shù)據(jù)成員的初始化只能在類構(gòu)造函數(shù)的初始化表中進(jìn)行,不能在類聲明中初始化。(這里是否可以考慮將其定義為靜態(tài)類型?)
怎樣才能建立在整個(gè)類中都恒定的常量呢?別指望const數(shù)據(jù)成員了,應(yīng)該用類中的枚舉常量來實(shí)現(xiàn)。枚舉常量不會(huì)占用對(duì)象的存儲(chǔ)空間,它們?cè)诰幾g時(shí)被全部求值。枚舉常量的缺點(diǎn)是:它的隱含數(shù)據(jù)類型是整數(shù),其最大值有限,且不能表示浮點(diǎn)數(shù)(如PI=3.14159)。
class A
{…
enum { SIZE1 = 100, SIZE2 = 200}; // 枚舉常量
int array1[SIZE1];
int array2[SIZE2];
};
6 函數(shù)設(shè)計(jì)
函數(shù)接口的兩個(gè)要素是參數(shù)和返回值。C語言中,函數(shù)的參數(shù)和返回值的傳遞方式有兩種:值傳遞(pass by value)和指針傳遞(pass by pointer)。C++ 語言中多了引用傳遞(pass by reference)。
- 如果參數(shù)是指針,且僅作輸入用,則應(yīng)在類型前加const,以防止該指針在函數(shù)體內(nèi)被意外修改。
- 如果輸入?yún)?shù)以值傳遞的方式傳遞對(duì)象,則宜改用“const &”方式來傳遞,這樣可以省去臨時(shí)對(duì)象的構(gòu)造和析構(gòu)過程,從而提高效率。
- 避免函數(shù)有太多的參數(shù),參數(shù)個(gè)數(shù)盡量控制在5個(gè)以內(nèi)。如果參數(shù)太多,在使用時(shí)容易將參數(shù)類型或順序搞錯(cuò)。
- 不要將正常值和錯(cuò)誤標(biāo)志混在一起返回。正常值用輸出參數(shù)獲得,而錯(cuò)誤標(biāo)志用return語句返回。
- 有時(shí)候函數(shù)原本不需要返回值,但為了增加靈活性如支持鏈?zhǔn)奖磉_(dá),可以附加返回值。例如字符串拷貝函數(shù)strcpy的原型:
char *strcpy(char *strDest,const char *strSrc);
strcpy函數(shù)將strSrc拷貝至輸出參數(shù)strDest中,同時(shí)函數(shù)的返回值又是strDest。這樣做并非多此一舉,可以獲得如下靈活性:
char str[20];
int length = strlen( strcpy(str, “Hello World”) );
- 如果函數(shù)的返回值是一個(gè)對(duì)象,有些場(chǎng)合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場(chǎng)合只能用“值傳遞”而不能用“引用傳遞”,否則會(huì)出錯(cuò)。
函數(shù)內(nèi)部實(shí)現(xiàn)的規(guī)則
- 在函數(shù)體的“入口處”,對(duì)參數(shù)的有效性進(jìn)行檢查。很多程序錯(cuò)誤是由非法參數(shù)引起的,我們應(yīng)該充分理解并正確使用“斷言”(assert)來防止此類錯(cuò)誤。
- 在函數(shù)體的“出口處”,對(duì)return語句的正確性和效率進(jìn)行檢查。如果函數(shù)有返回值,那么函數(shù)的“出口處”是return語句。我們不要輕視r(shí)eturn語句。如果return語句寫得不好,函數(shù)要么出錯(cuò),要么效率低下。
- return語句不可返回指向“棧內(nèi)存”的“指針”或者“引用”,因?yàn)樵搩?nèi)存在函數(shù)體結(jié)束時(shí)被自動(dòng)銷毀。
- 要搞清楚返回的究竟是“值”、“指針”還是“引用”。
使用斷言
程序一般分為Debug版本和Release版本,Debug版本用于內(nèi)部調(diào)試,Release版本發(fā)行給用戶使用。斷言assert是僅在Debug版本起作用的宏,它用于檢查“不應(yīng)該”發(fā)生的情況。在運(yùn)行過程中,如果assert的參數(shù)為假,那么程序就會(huì)中止(一般地還會(huì)出現(xiàn)提示對(duì)話,說明在什么地方引發(fā)了assert)。
//復(fù)制不重疊的內(nèi)存塊
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;
}
assert不是一個(gè)倉(cāng)促拼湊起來的宏。為了不在程序的Debug版本和Release版本引起差別,assert不應(yīng)該產(chǎn)生任何副作用。所以assert不是函數(shù),而是宏。程序員可以把a(bǔ)ssert看成一個(gè)在任何系統(tǒng)狀態(tài)下都可以安全使用的無害測(cè)試手段。如果程序在assert處終止了,并不是說含有該assert的函數(shù)有錯(cuò)誤,而是調(diào)用者出了差錯(cuò),assert可以幫助我們找到發(fā)生錯(cuò)誤的原因。
- 使用斷言捕捉不應(yīng)該發(fā)生的非法情況。不要混淆非法情況與錯(cuò)誤情況之間的區(qū)別,后者是必然存在的并且是一定要作出處理的。
- 在函數(shù)的入口處,使用斷言檢查參數(shù)的有效性(合法性)。
- 在編寫函數(shù)時(shí),要進(jìn)行反復(fù)的考查,并且自問:“我打算做哪些假定?”一旦確定了的假定,就要使用斷言對(duì)假定進(jìn)行檢查。
引用與指針的比較
n相當(dāng)于m的別名,對(duì)n的任何操作就是對(duì)m的操作。引用被創(chuàng)建的同時(shí)必須被初始化(指針則可以在任何時(shí)候被初始化)。不能有NULL引用,引用必須與合法的存儲(chǔ)單元關(guān)聯(lián)(指針則可以是NULL)。一旦引用被初始化,就不能改變引用的關(guān)系(指針則可以隨時(shí)改變所指的對(duì)象)。
引用的主要功能是傳遞函數(shù)的參數(shù)和返回值。C++語言中,函數(shù)的參數(shù)和返回值的傳遞方式有三種:值傳遞、指針傳遞和引用傳遞。指針能夠毫無約束地操作內(nèi)存中的如何東西,盡管指針功能強(qiáng)大,但是非常危險(xiǎn)。
7 內(nèi)存管理
內(nèi)存分配方式
內(nèi)存分配方式有三種:
- 從靜態(tài)存儲(chǔ)區(qū)域分配。內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)存在程序的整個(gè)運(yùn)行期間都存在。例如全局變量,static變量。
- 從棧上創(chuàng)建。在執(zhí)行函數(shù)時(shí),函數(shù)內(nèi)局部變量的存儲(chǔ)單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲(chǔ)單元自動(dòng)被釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。
- 從堆上創(chuàng)建。亦稱動(dòng)態(tài)內(nèi)存分配。程序在運(yùn)行的時(shí)候用malloc或new申請(qǐng)任意多少的內(nèi)存,程序員自己負(fù)責(zé)在何時(shí)用free或delete釋放內(nèi)存。動(dòng)態(tài)內(nèi)存的生存期由我們決定,使用非常靈活,但問題也最多。
常見的內(nèi)存錯(cuò)誤及其策略
發(fā)生內(nèi)存錯(cuò)誤是件非常麻煩的事情。編譯器不能自動(dòng)發(fā)現(xiàn)這些錯(cuò)誤,通常是在程序運(yùn)行時(shí)才能捕捉到。而這些錯(cuò)誤大多沒有明顯的癥狀,時(shí)隱時(shí)現(xiàn),增加了改錯(cuò)的難度。常見的內(nèi)存錯(cuò)誤及其對(duì)策如下:
- 內(nèi)存分配未成功,卻使用了它。常用解決辦法是,在使用內(nèi)存之前檢查指針是否為NULL。如果指針p是函數(shù)的參數(shù),那么在函數(shù)的入口處用assert(p!=NULL)進(jìn)行檢查。如果是用malloc或new來申請(qǐng)內(nèi)存,應(yīng)該用if(p==NULL) 或if(p!=NULL)進(jìn)行防錯(cuò)處理。
- 內(nèi)存分配雖然成功,但是尚未初始化就引用它。犯這種錯(cuò)誤主要有兩個(gè)起因:一是沒有初始化的觀念;二是誤以為內(nèi)存的缺省初值全為零,導(dǎo)致引用初值錯(cuò)誤(例如數(shù)組)。內(nèi)存的缺省初值究竟是什么并沒有統(tǒng)一的標(biāo)準(zhǔn),盡管有些時(shí)候?yàn)榱阒?,我們寧可信其無不可信其有。所以無論用何種方式創(chuàng)建數(shù)組,都別忘了賦初值,即便是賦零值也不可省略。
- 內(nèi)存分配成功并且已經(jīng)初始化,但操作越過了內(nèi)存的邊界。例如在使用數(shù)組時(shí)經(jīng)常發(fā)生下標(biāo)“多1”或者“少1”的操作。特別是在for循環(huán)語句中,循環(huán)次數(shù)很容易搞錯(cuò),導(dǎo)致數(shù)組操作越界。
- 忘記了釋放內(nèi)存,造成內(nèi)存泄露。含有這種錯(cuò)誤的函數(shù)每被調(diào)用一次就丟失一塊內(nèi)存。剛開始時(shí)系統(tǒng)的內(nèi)存充足,你看不到錯(cuò)誤。終有一次程序突然死掉,系統(tǒng)出現(xiàn)提示:內(nèi)存耗盡。動(dòng)態(tài)內(nèi)存的申請(qǐng)與釋放必須配對(duì),程序中malloc與free的使用次數(shù)一定要相同,否則肯定有錯(cuò)誤(new/delete同理)。
- 釋放了內(nèi)存卻繼續(xù)使用它。函數(shù)的return語句寫錯(cuò)了,注意不要返回指向“棧內(nèi)存”的“指針”或者“引用”,因?yàn)樵搩?nèi)存在函數(shù)體結(jié)束時(shí)被自動(dòng)銷毀;使用free或delete釋放了內(nèi)存后,沒有將指針設(shè)置為NULL。導(dǎo)致產(chǎn)生“野指針”。
遵守以下規(guī)則:
- 用malloc或new申請(qǐng)內(nèi)存之后,應(yīng)該立即檢查指針值是否為NULL。防止使用指針值為NULL的內(nèi)存。
- 不要忘記為數(shù)組和動(dòng)態(tài)內(nèi)存賦初值。防止將未被初始化的內(nèi)存作為右值使用。
- 避免數(shù)組或指針的下標(biāo)越界,特別要當(dāng)心發(fā)生“多1”或者“少1”操作。
- 動(dòng)態(tài)內(nèi)存的申請(qǐng)與釋放必須配對(duì),防止內(nèi)存泄漏。
- 用free或delete釋放了內(nèi)存之后,立即將指針設(shè)置為NULL,防止產(chǎn)生“野指針”。
指針與數(shù)組的對(duì)比
C/C++程序中,指針和數(shù)組在不少地方可以相互替換著用,讓人產(chǎn)生一種錯(cuò)覺,以為兩者是等價(jià)的。數(shù)組要么在靜態(tài)存儲(chǔ)區(qū)被創(chuàng)建(如全局?jǐn)?shù)組),要么在棧上被創(chuàng)建。數(shù)組名對(duì)應(yīng)著(而不是指向)一塊內(nèi)存,其地址與容量在生命期內(nèi)保持不變,只有數(shù)組的內(nèi)容可以改變。指針可以隨時(shí)指向任意類型的內(nèi)存塊,它的特征是“可變”,所以我們常用指針來操作動(dòng)態(tài)內(nèi)存。指針遠(yuǎn)比數(shù)組靈活,但也更危險(xiǎn)。
不能對(duì)數(shù)組名進(jìn)行直接復(fù)制與比較。若想把數(shù)組a的內(nèi)容復(fù)制給數(shù)組b,不能用語句 b = a ,否則將產(chǎn)生編譯錯(cuò)誤。應(yīng)該用標(biāo)準(zhǔn)庫(kù)函數(shù)strcpy進(jìn)行復(fù)制。同理,比較b和a的內(nèi)容是否相同,不能用if(b==a) 來判斷,應(yīng)該用標(biāo)準(zhǔn)庫(kù)函數(shù)strcmp進(jìn)行比較。
// 數(shù)組…
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)
…
// 指針…
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len+1));
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
…
用運(yùn)算符sizeof可以計(jì)算出數(shù)組的容量(字節(jié)數(shù)),sizeof(a)的值是12(注意別忘了’\0’)。指針p指向a,但是sizeof(p)的值卻是4。這是因?yàn)閟izeof(p)得到的是一個(gè)指針變量的字節(jié)數(shù),相當(dāng)于sizeof(char*),而不是p所指的內(nèi)存容量。C++/C語言沒有辦法知道指針?biāo)傅膬?nèi)存容量,除非在申請(qǐng)內(nèi)存時(shí)記住它。
注意當(dāng)數(shù)組作為函數(shù)的參數(shù)進(jìn)行傳遞時(shí),該數(shù)組自動(dòng)退化為同類型的指針。
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12字節(jié)
cout<< sizeof(p) << endl; // 4字節(jié)
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4字節(jié)而不是100字節(jié)
}
指針參數(shù)是如何傳遞內(nèi)存的?
如果函數(shù)的參數(shù)是一個(gè)指針,不要指望用該指針去申請(qǐng)動(dòng)態(tài)內(nèi)存。下例中main函數(shù)中的語句GetMemory(str, 200)并沒有使str獲得期望的內(nèi)存,str依舊是NULL。這個(gè)問題出在函數(shù)GetMemory中。編譯器總是要為函數(shù)的每個(gè)參數(shù)制作臨時(shí)副本,指針參數(shù)p的副本是 _p,編譯器使 _p = p。如果函數(shù)體內(nèi)的程序修改了_p的內(nèi)容,就導(dǎo)致參數(shù)p的內(nèi)容作相應(yīng)的修改。這就是指針可以用作輸出參數(shù)的原因。在本例中,_p申請(qǐng)了新的內(nèi)存,只是把_p所指的內(nèi)存地址改變了,但是p絲毫未變。所以函數(shù)GetMemory并不能輸出任何東西。事實(shí)上,每執(zhí)行一次GetMemory就會(huì)泄露一塊內(nèi)存,因?yàn)闆]有用free釋放內(nèi)存。
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
int main(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然為 NULL
strcpy(str, "hello"); // 運(yùn)行錯(cuò)誤
}
如果非得要用指針參數(shù)去申請(qǐng)內(nèi)存,那么應(yīng)該改用“指向指針的指針”。
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(sizeof(char) * num);
}
int main(void)
{
char *str = NULL;
GetMemory2(&str, 100); // 注意參數(shù)是 &str,而不是str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
由于“指向指針的指針”這個(gè)概念不容易理解,我們可以用函數(shù)返回值來傳遞動(dòng)態(tài)內(nèi)存。這種方法更加簡(jiǎn)單。
char *GetMemory3(int num)
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
int main(void)
{
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
用函數(shù)返回值來傳遞動(dòng)態(tài)內(nèi)存這種方法雖然好用,但是常常有人把return語句用錯(cuò)了。這里強(qiáng)調(diào)不要用return語句返回指向“棧內(nèi)存”的指針,因?yàn)樵搩?nèi)存在函數(shù)結(jié)束時(shí)自動(dòng)消亡。
杜絕“野指針”
“野指針”不是NULL指針,是指向“垃圾”內(nèi)存的指針。人們一般不會(huì)錯(cuò)用NULL指針,因?yàn)橛胕f語句很容易判斷。但是“野指針”是很危險(xiǎn)的,if語句對(duì)它不起作用。“野指針”的成因主要有兩種:
- 指針變量沒有被初始化。任何指針變量剛被創(chuàng)建時(shí)不會(huì)自動(dòng)成為NULL指針,它的缺省值是隨機(jī)的,它會(huì)亂指一氣。所以,指針變量在創(chuàng)建的同時(shí)應(yīng)當(dāng)被初始化,要么將指針設(shè)置為NULL,要么讓它指向合法的內(nèi)存。例如
char *p = NULL;
char *str = (char *) malloc(100);
- 指針p被free或者delete之后,沒有置為NULL,讓人誤以為p是個(gè)合法的指針。
- 指針操作超越了變量的作用范圍。這種情況讓人防不勝防,示例程序如下:
class A
{
public:
void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // 注意 a 的生命期
}
p->Func(); // p是“野指針”
}
函數(shù)Test在執(zhí)行語句p->Func()時(shí),對(duì)象a已經(jīng)消失,而p是指向a的,所以p就成了“野指針”。但奇怪的是我運(yùn)行這個(gè)程序時(shí)居然沒有出錯(cuò),這可能與編譯器有關(guān)。
malloc/free和new/delete
malloc與free是C++/C語言的標(biāo)準(zhǔn)庫(kù)函數(shù),new/delete是C++的運(yùn)算符。它們都可用于申請(qǐng)動(dòng)態(tài)內(nèi)存和釋放內(nèi)存。
對(duì)于非內(nèi)部數(shù)據(jù)類型的對(duì)象而言,光用maloc/free無法滿足動(dòng)態(tài)對(duì)象的要求。對(duì)象在創(chuàng)建的同時(shí)要自動(dòng)執(zhí)行構(gòu)造函數(shù),對(duì)象在消亡之前要自動(dòng)執(zhí)行析構(gòu)函數(shù)。由于malloc/free是庫(kù)函數(shù)而不是運(yùn)算符,不在編譯器控制權(quán)限之內(nèi),不能夠把執(zhí)行構(gòu)造函數(shù)和析構(gòu)函數(shù)的任務(wù)強(qiáng)加于malloc/free。因此C++語言需要一個(gè)能完成動(dòng)態(tài)內(nèi)存分配和初始化工作的運(yùn)算符new,以及一個(gè)能完成清理與釋放內(nèi)存工作的運(yùn)算符delete。
如果用free釋放“new創(chuàng)建的動(dòng)態(tài)對(duì)象”,那么該對(duì)象因無法執(zhí)行析構(gòu)函數(shù)而可能導(dǎo)致程序出錯(cuò)。如果用delete釋放“malloc申請(qǐng)的動(dòng)態(tài)內(nèi)存”,理論上講程序不會(huì)出錯(cuò),但是該程序的可讀性很差。所以new/delete必須配對(duì)使用,malloc/free也一樣。
函數(shù)malloc的原型為void * malloc(size_t size),用malloc申請(qǐng)一塊長(zhǎng)度為length的整數(shù)類型的內(nèi)存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
malloc返回值的類型是void *,所以在調(diào)用malloc時(shí)要顯式地進(jìn)行類型轉(zhuǎn)換,將void * 轉(zhuǎn)換成所需要的指針類型。malloc函數(shù)本身并不識(shí)別要申請(qǐng)的內(nèi)存是什么類型,它只關(guān)心內(nèi)存的總字節(jié)數(shù),可以使用sizeof運(yùn)算符求變量所占字節(jié)數(shù)。
函數(shù)free的原型為void free( void * memblock )。如果p是NULL指針,那么free對(duì)p無論操作多少次都不會(huì)出問題。如果p不是NULL指針,那么free對(duì)p連續(xù)操作兩次就會(huì)導(dǎo)致程序運(yùn)行錯(cuò)誤。
new內(nèi)置了sizeof、類型轉(zhuǎn)換和類型安全檢查功能。對(duì)于非內(nèi)部數(shù)據(jù)類型的對(duì)象而言,new在創(chuàng)建動(dòng)態(tài)對(duì)象的同時(shí)完成了初始化工作。如果對(duì)象有多個(gè)構(gòu)造函數(shù),那么new的語句也可以有多種形式。
class Obj
{
public :
Obj(void); // 無參數(shù)的構(gòu)造函數(shù)
Obj(int x); // 帶一個(gè)參數(shù)的構(gòu)造函數(shù)
…
}
void Test(void)
{
Obj *a = new Obj;
Obj *b = new Obj(1); // 初值為1
…
delete a;
delete b;
}
如果用new創(chuàng)建對(duì)象數(shù)組,那么只能使用對(duì)象的無參數(shù)構(gòu)造函數(shù)。例如
Obj *objects = new Obj[100]; // 創(chuàng)建100個(gè)動(dòng)態(tài)對(duì)象
在用delete釋放對(duì)象數(shù)組時(shí),留意不要丟了符號(hào)‘[]’。
delete []objects; // 正確的用法
delete objects; // 錯(cuò)誤的用法
后者相當(dāng)于delete objects[0],漏掉了另外99個(gè)對(duì)象。
8 C++函數(shù)的高級(jí)特性
對(duì)比于C語言的函數(shù),C++增加了重載(overloaded)、內(nèi)聯(lián)(inline)、const和virtual四種新機(jī)制。其中重載和內(nèi)聯(lián)機(jī)制既可用于全局函數(shù)也可用于類的成員函數(shù),const與virtual機(jī)制僅用于類的成員函數(shù)。
函數(shù)重載的概念
在C++程序中,可以將語義、功能相似的幾個(gè)函數(shù)用同一個(gè)名字表示,即函數(shù)重載。這樣便于記憶,提高了函數(shù)的易用性。
只能靠參數(shù)而不能靠返回值類型的不同來區(qū)分重載函數(shù)。編譯器根據(jù)參數(shù)為每個(gè)重載函數(shù)產(chǎn)生不同的內(nèi)部標(biāo)識(shí)符。如果C++程序要調(diào)用已經(jīng)被編譯后的C函數(shù),該怎么辦?假設(shè)某個(gè)C函數(shù)的聲明如下:
void foo(int x, int y);
該函數(shù)被C編譯器編譯后在庫(kù)中的名字為_foo,而C++編譯器則會(huì)產(chǎn)生像_foo_int_int之類的名字用來支持函數(shù)重載和類型安全連接。由于編譯后的名字不同,C++程序不能直接調(diào)用C函數(shù)。C++提供了一個(gè)C連接交換指定符號(hào)extern“C”來解決這個(gè)問題。例如:
extern “C”
{
void foo(int x, int y);
… // 其它函數(shù)
}
//或者寫成
extern “C”
{
#include “myheader.h”
… // 其它C頭文件
}
這就告訴C++編譯譯器,函數(shù)foo是個(gè)C連接,應(yīng)該到庫(kù)中找名字_foo而不是找_foo_int_int。
注意并不是兩個(gè)函數(shù)的名字相同就能構(gòu)成重載。全局函數(shù)和類的成員函數(shù)同名不算重載,因?yàn)楹瘮?shù)的作用域不同。
9 類的構(gòu)造函數(shù)、析構(gòu)函數(shù)與賦值函數(shù)
每個(gè)類只有一個(gè)析構(gòu)函數(shù)和一個(gè)賦值函數(shù),但可以有多個(gè)構(gòu)造函數(shù)(包含一個(gè)拷貝構(gòu)造函數(shù),其它的稱為普通構(gòu)造函數(shù))。對(duì)于任意一個(gè)類,如果不想編寫上述函數(shù),C++編譯器將自動(dòng)為產(chǎn)生四個(gè)缺省的函數(shù)。
注意:“缺省的拷貝構(gòu)造函數(shù)”和“缺省的賦值函數(shù)”均采用“位拷貝”而非“值拷貝”的方式來實(shí)現(xiàn),倘若類中含有指針變量,這兩個(gè)函數(shù)注定將出錯(cuò)。
本章以類 String 的設(shè)計(jì)與實(shí)現(xiàn)為例,深入闡述被很多教科書忽視了的道理。String 的結(jié)構(gòu)如下:
class String
{
public:
String(const char *str = NULL); // 普通構(gòu)造函數(shù)
String(const String &other); // 拷貝構(gòu)造函數(shù)
~ String(void); // 析構(gòu)函數(shù)
String & operate =(const String &other); // 賦值函數(shù)
private:
char *m_data; // 用于保存字符串
};
構(gòu)造函數(shù)與析構(gòu)函數(shù)
不少難以察覺的程序錯(cuò)誤是由于變量沒有被正確初始化或清除造成的,而初始化和清除工作很容易被人遺忘。Stroustrup 在設(shè)計(jì) C++語言時(shí)充分考慮了這個(gè)問題并很好地予以解決:把對(duì)象的初始化工作放在構(gòu)造函數(shù)中,把清除工作放在析構(gòu)函數(shù)中。當(dāng)對(duì)象被創(chuàng)建時(shí),構(gòu)造函數(shù)被自動(dòng)執(zhí)行。當(dāng)對(duì)象消亡時(shí),析構(gòu)函數(shù)被自動(dòng)執(zhí)行。這下就不用擔(dān)心忘了對(duì)象的初始化和清除工作。讓構(gòu)造函數(shù)、析構(gòu)函數(shù)與類同名,由于析構(gòu)函數(shù)的目
的與構(gòu)造函數(shù)的相反,就加前綴‘~’以示區(qū)別。
構(gòu)造函數(shù)有個(gè)特殊的初始化方式叫“初始化表達(dá)式表”(簡(jiǎn)稱初始化表)。初始化表位于函數(shù)參數(shù)表之后,卻在函數(shù)體 {} 之前。這說明該表里的初始化工作發(fā)生在函數(shù)體內(nèi)的任何代碼被執(zhí)行之前。構(gòu)造函數(shù)初始化表的使用規(guī)則:
- 如果類存在繼承關(guān)系,派生類必須在其初始化表里調(diào)用基類的構(gòu)造函數(shù)。
- 類的 const 常量只能在初始化表里被初始化,因?yàn)樗荒茉诤瘮?shù)體內(nèi)用賦值的方式來初始化。
- 類的數(shù)據(jù)成員的初始化可以采用初始化表或函數(shù)體內(nèi)賦值兩種方式,這兩種方式的效
率不完全相同。函數(shù)體內(nèi)賦值其實(shí)是先進(jìn)行默認(rèn)初始化再進(jìn)行賦值。非內(nèi)部數(shù)據(jù)類型的成員對(duì)象應(yīng)當(dāng)采用第一種方式初始化,以獲取更高的效率。
// String 的普通構(gòu)造函數(shù)
String::String(const char *str)
{
if(str==NULL)
{
m_data = new char[1];
*m_data = ‘\0’;
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
// String 的析構(gòu)函數(shù)
String::~String(void)
{
// 由于 m_data 是內(nèi)部數(shù)據(jù)類型,也可以寫成 delete m_data;
delete [] m_data;
}
構(gòu)造與析構(gòu)的次序
構(gòu)造從類層次的最根處開始,在每一層中,首先調(diào)用基類的構(gòu)造函數(shù),然后調(diào)用成員對(duì)象的構(gòu)造函數(shù)。析構(gòu)則嚴(yán)格按照與構(gòu)造相反的次序執(zhí)行,該次序是唯一的,否則編譯器將無法自動(dòng)執(zhí)行析構(gòu)過程。
一個(gè)有趣的現(xiàn)象是,成員對(duì)象初始化的次序完全不受它們?cè)诔跏蓟碇写涡虻挠绊?,只由成員對(duì)象在類中聲明的次序決定。這是因?yàn)轭惖穆暶魇俏ㄒ坏?,而類的?gòu)造函數(shù)可以有多個(gè),因此會(huì)有多個(gè)不同次序的初始化表。如果成員對(duì)象按照初始化表的次序進(jìn)行構(gòu)造,這將導(dǎo)致析構(gòu)函數(shù)無法得到唯一的逆序。
拷貝構(gòu)造函數(shù)與賦值函數(shù)
本章開頭講過,如果不主動(dòng)編寫拷貝構(gòu)造函數(shù)和賦值函數(shù),編譯器將以“位拷貝”的方式自動(dòng)生成缺省的函數(shù)。倘若類中含有指針變量,那么這兩個(gè)缺省的函數(shù)就隱含了錯(cuò)誤。以類 String 的兩個(gè)對(duì)象 a,b 為例,假設(shè) a.m_data 的內(nèi)容為“hello”,b.m_data的內(nèi)容為“world”。 現(xiàn)將 a 賦給 b,缺省賦值函數(shù)的“位拷貝”意味著執(zhí)行 b.m_data = a.m_data。這將造成三個(gè)錯(cuò)誤:一是 b.m_data 原有的內(nèi)存沒被釋放,造成內(nèi)存泄露;二是 b.m_data和 a.m_data 指向同一塊內(nèi)存,a 或 b 任何一方變動(dòng)都會(huì)影響另一方;三是在對(duì)象被析構(gòu)時(shí),m_data 被釋放了兩次。
拷貝構(gòu)造函數(shù)和賦值函數(shù)非常容易混淆,常導(dǎo)致錯(cuò)寫、錯(cuò)用??截悩?gòu)造函數(shù)是在對(duì)象被創(chuàng)建時(shí)調(diào)用的,而賦值函數(shù)只能被已經(jīng)存在了的對(duì)象調(diào)用。以下程序中,第三個(gè)語句和第四個(gè)語句很相似,你分得清楚哪個(gè)調(diào)用了拷貝構(gòu)造函數(shù),哪個(gè)調(diào)用了賦值函數(shù)嗎?
String a(“hello”);
String b(“world”);
String c = a; // 調(diào)用了拷貝構(gòu)造函數(shù),最好寫成 c(a);
c = b; // 調(diào)用了賦值函數(shù)
// 拷貝構(gòu)造函數(shù)
String::String(const String &other)
{
// 允許操作 other 的私有成員 m_data
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}
// 賦值函數(shù)
String & String::operate =(const String &other)
{
// (1) 檢查自賦值
if(this == &other)
return *this;
// (2) 釋放原有的內(nèi)存資源
delete [] m_data;
// (3)分配新的內(nèi)存資源,并復(fù)制內(nèi)容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
// (4)返回本對(duì)象的引用
return *this;
}
如果我們實(shí)在不想編寫拷貝構(gòu)造函數(shù)和賦值函數(shù),又不允許別人使用編譯器生成的缺省函數(shù),只需將拷貝構(gòu)造函數(shù)和賦值函數(shù)聲明為私有函數(shù),不用編寫代碼。
在派生類中實(shí)現(xiàn)類的基本函數(shù)
基類的構(gòu)造函數(shù)、析構(gòu)函數(shù)、賦值函數(shù)都不能被派生類繼承。如果類之間存在繼承關(guān)系,在編寫上述基本函數(shù)時(shí)應(yīng)注意以下事項(xiàng):
- 派生類的構(gòu)造函數(shù)應(yīng)在其初始化表里調(diào)用基類的構(gòu)造函數(shù)。
- 基類與派生類的析構(gòu)函數(shù)應(yīng)該為虛(即加 virtual 關(guān)鍵字)。
#include <iostream.h>
class Base
{
public:
virtual ~Base() { cout<< "~Base" << endl ; }
};
class Derived : public Base
{
public:
virtual ~Derived() { cout<< "~Derived" << endl ; }
};
void main(void)
{
Base * pB = new Derived; // upcast
delete pB;
}
輸出結(jié)果為:
~Derived
~Base
如果析構(gòu)函數(shù)不為虛,那么輸出結(jié)果為
~Base
- 在編寫派生類的賦值函數(shù)時(shí),注意不要忘記對(duì)基類的數(shù)據(jù)成員重新賦值。
class Base
{
public:
…
Base & operate =(const Base &other); // 類 Base 的賦值函數(shù)
private: int m_i, m_j, m_k;
};
class Derived : public Base
{
public:
…
Derived & operate =(const Derived &other); // 類 Derived 的賦值函數(shù)
private:
int m_x, m_y, m_z;
};
Derived & Derived::operate =(const Derived &other)
{
//(1)檢查自賦值
if(this == &other)
return *this;
//(2)對(duì)基類的數(shù)據(jù)成員重新賦值
Base::operate =(other); // 因?yàn)椴荒苤苯硬僮魉接袛?shù)據(jù)成員
//(3)對(duì)派生類的數(shù)據(jù)成員賦值
m_x = other.m_x;
m_y = other.m_y;
m_z = other.m_z;
//(4)返回本對(duì)象的引用
return *this;
}