高質量C\C++編程

平時的編程中有養成了很多不良的習慣,往往我們都不在意,但總是這些不在意導致我們要浪費很多時間在Debug上,看了本書《高質量C編程指南》,書的地址http://download.csdn.net/detail/zhangyang1990828/5242261

高質量C\C++編程(一)

(第一部分是一些簡單的幫助改正習慣和養成良好習慣的,都是細節,但往往細節決定成敗;第二部分會講述一些更深層的專題)

以下是我看後的整理:

 關於高質量的關鍵詞:正確性、健壯性、可靠性、效率、易用性、可讀性(可理解性)、可擴展性、可復
用性、兼容性、可移植性等

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

 爲了防止頭文件被重複引用,應當用ifndef/define/endif 結構產生預處
理塊。
 用 #include <filename.h> 格式來引用標準庫的頭文件(編譯器將從標
準庫目錄開始搜索)。
 用 #include “filename.h” 格式來引用非標準庫的頭文件(編譯器將從
用戶的工作目錄開始搜索)。
6  頭文件中只存放“聲明”而不存放“定義”
// 版權和版本聲明,此處省略。
#ifndef GRAPHICS_H // 防止graphics.h 被重複引用
#define GRAPHICS_H
#include <math.h> // 引用標準庫的頭文件

#include “myheader.h” // 引用非標準庫的頭文件

void Function1(…); // 全局函數聲明

class Box // 類結構聲明
{

};
#endif

 定義文件:

   (1) 定義文件開頭處的版權和版本聲明。
   (2) 對一些頭文件的引用。
   (3) 程序的實現體(包括數據和代碼)。
假設定義文件的名稱爲 graphics.cpp,定義文件的結構。
// 版權和版本聲明,此處省略。
#include “graphics.h” // 引用頭文件

// 全局函數的實現體
void Function1(…)
{

}
// 類成員函數的實現體
void Box::Draw(…)
{

}

 頭文件作用:
    (1)通過頭文件來調用庫功能。在很多場合,源代碼不便(或不準)向用戶公佈,只要
向用戶提供頭文件和二進制的庫即可。用戶只需要按照頭文件中的接口聲明來調用庫功
能,而不必關心接口怎麼實現的。編譯器會從庫中提取相應的代碼。
    (2)頭文件能加強類型安全檢查。如果某個接口被實現或被使用時,其方式與頭文件中
的聲明不一致,編譯器就會指出錯誤,這一簡單的規則能大大減輕程序員調試、改錯的
負擔。
9  如果一個軟件的頭文件數目比較多(如超過十個),通常應將頭文件和定義文件分別
保存於不同的目錄,以便於維護。
例如可將頭文件保存於include 目錄,將定義文件保存於source 目錄(可以是多級
目錄)。
如果某些頭文件是私有的,它不會被用戶的程序直接引用,則沒有必要公開其“聲
明”。爲了加強信息隱藏,這些私有的頭文件可以和定義文件存放於同一個目錄。

10  在每個類聲明之後、每個函數定義結束之後都要加空行。
11  在一個函數體內,邏揖上密切相關的語句之間不加空行,其它地方應
加空行分隔。

12  一行代碼只做一件事情,如只定義一個變量,或只寫一條語句。這樣
的代碼容易閱讀,並且方便於寫註釋。
13  if、for、while、do 等語句自佔一行,執行語句不得緊跟其後。不論
執行語句有多少都要加{}。這樣可以防止書寫失誤。
14  儘可能在定義變量的同時初始化該變量(就近原則)
如果變量的引用處和其定義處相隔比較遠,變量的初始化很容易被忘記。如果引用
了未被初始化的變量,可能會導致程序錯誤。類中一定要記着構造函數中初始化。

15  關鍵字之後要留空格。象const、virtual、inline、case 等關鍵字之後
至少要留一個空格,否則無法辨析關鍵字。象if、for、while 等關鍵字之後應留一個
空格再跟左括號‘(’,以突出關鍵字。
16  函數名之後不要留空格,緊跟左括號‘(’,以與關鍵字區別。 ‘(’向後緊跟,‘)’、‘,’、‘;’向前緊跟,緊跟處不留空格。
17  ‘,’之後要留空格,如Function(x, y, z)。如果‘;’不是一行的結束符號,其後要留空格,如for (initialization;                      condition; update)。
18  賦值操作符、比較操作符、算術操作符、邏輯操作符、位域操作符,
如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元
操作符的前後應當加空格。
19  一元操作符如“!”、“~”、“++”、“--”、“&”(地址運算符)等前後不
加空格。
20  象“[]”、“.”、“->”這類操作符前後不加空格。
21  對於表達式比較長的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();
21  程序的分界符‘{’和‘}’應獨佔一行並且位於同一列,同時與引用
它們的語句左對齊。
22  { }之內的代碼塊在‘{’右邊數格處左對齊。
23  代碼行最大長度宜控制在70 至80 個字符以內。代碼行不要過長,否
則眼睛看不過來,也不便於打印。
24  長表達式要在低優先級操作符處拆分成新行,操作符放在新行之首(以
便突出操作符)。拆分出的新行要進行適當的縮進,使排版整齊,語句可讀。
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();
}

25  應當將修飾符 * 和& 緊靠變量名
26  註釋是對代碼的“提示”,而不是文檔。程序中的註釋不可喧賓奪主,註釋太多了會讓人眼花繚亂。註釋的花樣要少。
27  如果代碼本來就是清楚的,則不必加註釋。否則多此一舉,令人厭煩。
28  邊寫代碼邊註釋,修改代碼同時修改相應的註釋,以保證註釋與代碼的一致性。不再有用的註釋要刪除。
29  註釋應當準確、易懂,防止註釋有二義性。錯誤的註釋不但無益反而有害。
30  儘量避免在註釋中使用縮寫,特別是不常用縮寫。
31  註釋的位置應與被描述的代碼相鄰,可以放在代碼的上方或右方,不可放在下方。
32  當代碼比較長,特別是有多重嵌套時,應當在一些段落的結束處加註釋,便於閱讀。
33  將public 類型的函數寫在前面,而將private 類型的數據寫在後面採用這種版式的程序員主張類的設計“以行爲爲中心”,重點關注的是類應該提供什麼樣的接口(或服務)。

34  標識符應當直觀且可以拼讀,可望文知意,不必進行“解碼”。標識符最好採用英文單詞或其組合,便於記憶和閱讀。切忌使用漢語拼音來命名。程序中的英文單詞一般不會太複雜,用詞應當準確
35  標識符的長度應當符合“min-length && max-information”原則。
36  命名規則儘量與所採用的操作系統或開發工具的風格保持一致。
37  程序中不要出現僅靠大小寫區分的相似的標識符。
38  程序中不要出現僅靠大小寫區分的相似的標識符。
39  變量的名字應當使用“名詞”或者“形容詞+名詞”。
40  全局函數的名字應當使用“動詞”或者“動詞+名詞”(動賓詞組)。
      類的成員函數應當只使用“動詞”,被省略掉的名詞就是對象本身。
41  用正確的反義詞組命名具有互斥意義的變量或相反動作的函數等。
42  儘量避免名字中出現數字編號,如Value1,Value2 等,除非邏輯上的確需要編號。這是爲了防止程序員偷懶,不肯爲命名動腦筋而導致產生無意義的名字(因爲用數字編號最省事)。

43  作者對“匈牙利”命名規則做了合理的簡化,下述的命名規則簡單易用,比較適合
於Windows 應用軟件的開發。
44  類名和函數名用大寫字母開頭的單詞組合而成。
45  變量和參數用小寫字母開頭的單詞組合而成。
46  常量全用大寫的字母,用下劃線分割單詞。
47  靜態變量加前綴s_(表示static)。
48  如果不得已需要全局變量,則使全局變量加前綴g_(表示global)。
49  類的數據成員加前綴m_(表示member),這樣可以避免數據成員與
成員函數的參數同名。
50  爲了防止某一軟件庫中的一些標識符和其它軟件庫中的衝突,可以爲各種標識符加上能反映軟件性質的前綴。例如三維圖形標準OpenGL 的所有庫函數均以gl 開頭,所有常量(或宏定義)均以GL 開頭。

51  如果代碼行中的運算符比較多,用括號確定表達式的操作順序,避免使用默認的優先級。
52  不要編寫太複雜的複合表達式。
53  不要有多用途的複合表達式。
54  不要把程序中的複合表達式與“真正的數學表達式”混淆。
55  不可將布爾變量直接與TRUE、FALSE 或者1、0 進行比較。
56  應當將整型變量用“==”或“!=”直接與0 比較。
       不可模仿布爾變量的風格  
57  不可將浮點變量用“==”或“!=”與任何數字比較。
      假設浮點變量的名字爲 x,應當將if (x == 0.0) // 隱含錯誤的比較轉化爲if ((x>=-EPSINON) && (x<=EPSINON))
      其中EPSINON 是允許的誤差(即精度)。                            
58  應當將指針變量用“==”或“!=”與NULL 比較。

59  在多重循環中,如果有可能,應當將最長的循環放在最內層,最短的循環放在最外層,以減少CPU 跨切循環層的       次數。     
60  如果循環體內存在邏輯判斷,並且循環次數很大,宜將邏輯判斷移到循環體的外面
61  不可在for 循環體內修改循環變量,防止for 循環失去控制。 
62  建議for 語句的循環控制變量的取值採用“半開半閉區間”寫法。

63  每個case 語句的結尾不要忘了加break,否則將導致多個分支重疊(除
非有意使多個分支重疊)。
64  不要忘記最後那個default 分支。即使程序真的不需要default 處理,也應該保留語句 default : break; 這樣做並非多此一舉,而是爲了防止別人誤以爲你忘了default 處理。

65  爲什麼需要常量
    (1) 程序的可讀性(可理解性)變差。程序員自己會忘記那些數字或字符串是什麼意思,用戶則更加不知它們從               何處來、表示什麼。
    (2) 在程序的很多地方輸入同樣的數字或字符串,難保不發生書寫錯誤。
    (3) 如果要修改數字或字符串,則會在很多地方改動,既麻煩又容易出錯。
66  儘量使用含義直觀的常量來表示那些將在程序中多次出現的數字或字符串。
67  const 與 #define 的比較
      C++ 語言可以用const 來定義常量,也可以用 #define 來定義常量。但是前者比後者有更多的優點:
    (1) const 常量有數據類型,而宏常量沒有數據類型。編譯器可以對前者進行類型安全檢查。而對後者只進行字符替換,沒有類型安全檢查,並且在字符替換可能會產生意料不到的錯誤(邊際效應)。
    (2) 有些集成化的調試工具可以對const 常量進行調試,但是不能對宏常量進行調試。
68  在C++ 程序中只使用const 常量而不使用宏常量,即const 常量完全取代宏常量。

69  需要對外公開的常量放在頭文件中,不需要對外公開的常量放在定義文件的頭部。爲便於管理,可以把不同模塊的常量集中存放在一個公共的頭文件中。
70  如果某一常量與其它常量密切相關,應在定義中包含這種關係,而不應給出一些孤立的值
      const float RADIUS = 100;
      const float DIAMETER = RADIUS * 2;
71  有時我們希望某些常量只在類中有效。由於#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)

72  參數的書寫要完整,不要貪圖省事只寫參數的類型而省略參數名字。如果函數沒有參數,則用void 填充。
73  參數命名要恰當,順序要合理。一般地,應將目的參數放在前面,源參數放在後面。
74  如果參數是指針,且僅作輸入用,則應在類型前加const,以防止該指針在函數體內被意外修改。
75  如果輸入參數以值傳遞的方式傳遞對象,則宜改用“const &”方式來傳遞,這樣可以省去臨時對象的構造和析構過程,從而提高效率。   
76  避免函數有太多的參數,參數個數儘量控制在5 個以內。如果參數太多,在使用時容易將參數類型或順序搞錯。
77  儘量不要使用類型和數目不確定的參數。
78  不要省略返回值的類型。
79  不要將正常值和錯誤標誌混在一起返回。正常值用輸出參數獲得,而
錯誤標誌用return 語句返回。
80  有時候函數原本不需要返回值,但爲了增加靈活性如支持鏈式表達,可以附加返回值。
例如字符串拷貝函數 strcpy 的原型:
char *strcpy(char *strDest,const char *strSrc);
strcpy 函數將strSrc 拷貝至輸出參數strDest 中,同時函數的返回值又是strDest。這
樣做並非多此一舉,可以獲得如下靈活性:
char str[20];
int length = strlen( strcpy(str, “Hello World”) );

81 如果函數的返回值是一個對象,有些場合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場合只能用“值傳遞”而不能用“引用傳遞”,否則會出錯。
例如:
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 什麼也得不到,留下了隱患。

82   在函數體的“入口處”,對參數的有效性進行檢查。很多程序錯誤是由非法參數引起的,我們應該充分理解並正確使用“來防止此類錯誤
83  在函數體的“出口處”,對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 的變量不存在構造函數與析構函數,雖然該“臨
時變量的語法”不會提高多少效率,但是程序更加簡潔易讀。
84  函數的功能要單一,不要設計多用途的函數。
85  函數體的規模要小,儘量控制在50 行代碼之內。
86  儘量避免函數帶有“記憶”功能。相同的輸入應當產生相同的輸出。帶有“記憶”功能的函數,其行爲可能是不可預測的,因爲它的行爲可能取決於某種“記憶狀態”。這樣的函數既不易理解又不利於測試和維護。在C/C++語言中,函數的static 局部變量是函數的“記憶”存儲器。建議儘量少用static 局部變量,除非必需。
87  不僅要檢查輸入參數的有效性,還要檢查通過其它途徑進入函數體內的變量的有效性,例如全局變量、文件句柄等。
88  用於出錯處理的返回值一定要清楚,讓使用者不容易忽視或誤解錯誤情況。

89  使用斷言捕捉不應該發生的非法情況。不要混淆非法情況與錯誤情況之間的區別,後者是必然存在的並且是一定要作出處理的。
90  在函數的入口處,使用斷言檢查參數的有效性(合法性)。
91  在編寫函數時,要進行反覆的考查,並且自問:“我打算做哪些假定?”一旦確定了的假定,就要使用斷言對假定進行檢查。
92  一般教科書都鼓勵程序員們進行防錯設計,但要記住這種編程風格可能會隱瞞錯誤。當進行防錯設計時,如果“不可能發生”的事情的確發生了,則要使用斷言進行報警。

93  引用的一些規則如下:
    (1)引用被創建的同時必須被初始化(指針則可以在任何時候被初始化)。
    (2)不能有NULL 引用,引用必須與合法的存儲單元關聯(指針則可以是NULL)。
    (3)一旦引用被初始化,就不能改變引用的關係(指針則可以隨時改變所指的對象)。
以下是“值傳遞”的示例程序。由於Func1 函數體內的x 是外部變量n 的一份拷貝,
改變x 的值不會影響n, 所以n 的值仍然是0。
void Func1(int x)
{
x = x + 10;
}

int n = 0;
Func1(n);
cout << “n = ” << n << endl; // n = 0
以下是“指針傳遞”的示例程序。由於Func2 函數體內的x 是指向外部變量n 的指針,改變該指針的內容將導致n 的值改變,所以n 的值成爲10。
void Func2(int *x)
{
(* x) = (* x) + 10;
}

int n = 0;
Func2(&n);
cout << “n = ” << n << endl; // n = 10
以下是“引用傳遞”的示例程序。由於Func3 函數體內的x 是外部變量n 的引用,x和n 是同一個東西,改變x 等於改變n,所以n 的值成爲10。
void Func3(int &x)
{
x = x + 10;
}

int n = 0;
Func3(n);
cout << “n = ” << n << endl; // n = 10

附一張運算符優先級和結合率的表:


以上這些可能有很多都知道了,但是總有你不知道的!!!


這一章都是一些專題,相比上一章難好多,如果上一章只需要一掃而過,這一章就需要耐心的看和分析了。

高質量編程C\C++編程(二)

 內存分配方式有三種:
  (1) 從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static 變量。
  (2) 在棧上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。(包括指針和對象都會被釋放)
  (3) 從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc 或new 申請任意多少的內存,程序員自己負責在何時用free 或delete 釋放內存。動態內存的生存期由我們決定,使用非常靈活,但問題也最多。(不管是在哪裏進行的new和malloc操作,最後一定要響應的用delete和free來釋放掉,否則會產生野指針)  

2  常見的內存錯誤及其對策如下:
    ①內存分配未成功,卻使用了它。
       編程新手常犯這種錯誤,因爲他們沒有意識到內存分配會不成功。常用解決辦法是,在使用內存之前檢查指針是否爲NULL。如果指針p 是函數的參數,那麼在函數的入口處用assert(p!=NULL)進行檢查。如果是用malloc 或new 來申請內存,應該用if(p==NULL)或if(p!=NULL)進行防錯處理。
    ②內存分配雖然成功,但是尚未初始化就引用它。
       犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以爲內存的缺省初值全爲零,導致引用初值錯(例如數組)。內存的缺省初值究竟是什麼並沒有統一的標準,儘管有些時候爲零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。

        (一般需要給指針賦NULL的初值,給數組用malloc(數組名,0,sizeof(數組名)))
     ③內存分配成功並且已經初始化,但操作越過了內存的邊界。(邊界問題)例如在使用數組時經常發生下標“多    1”或者“少1”的操作。特別是在for 循環語句中,循環次數很容易搞錯,導致數組操作越界。
     ④ 忘記了釋放內存,造成內存泄露。含有這種錯誤的函數每被調用一次就丟失一塊內存。剛開始時系統的內存充足,你看不到錯誤。終有一次程序突然死掉,系統出現提示:內存耗盡。動態內存的申請與釋放必須配對,程序中malloc 與free 的使用次數一定要相同,否則肯定有錯誤(new/delete 同理)。
     ⑤ 釋放了內存卻繼續使用它。
         有三種情況:
       (1)程序中的對象調用關係過於複雜,實在難以搞清楚某個對象究竟是否已經釋放了內存,此時應該重新設計數據結構,從根本上解決對象管理的混亂局面。
       (2)函數的return 語句寫錯了,注意不要返回指向“棧內存”的“指針”或者“引用”,因爲該內存在函數體結束時被自動銷燬。
       (3)使用free 或delete 釋放了內存後,沒有將指針設置爲NULL。導致產生“野指針”。

 C++/C 程序中,指針和數組在不少地方可以相互替換着用,讓人產生一種錯覺,以爲兩者是等價的。

數組要麼在靜態存儲區被創建(如全局數組),要麼在棧上被創建。數組名對應着(而不是指向)一塊內存,其地址與容量在生命期內保持不變,只有數組的內容可以改變。
指針可以隨時指向任意類型的內存塊,它的特徵是“可變”,所以我們常用指針來操作動態內存。指針遠比數組靈活,但也更危險。
char a[] = “hello”;
a[0] = ‘X’;
cout << a << endl;
char *p = “world”; // 注意p 指向常量字符串
p[0] = ‘X’; // 編譯器不能發現該錯誤
cout << p << endl;

以下幾個例子都是指針和數組的特殊性的例子。
// 數組…
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)

char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12 字節
cout<< sizeof(p) << endl; // 4 字節


注意當數組作爲函數的參數進行傳遞時,該數組自動退化爲同類型的指針
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4 字節而不是100 字節
}

 如果函數的參數是一個指針,不要指望用該指針去申請動態內存
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然爲 NULL
strcpy(str, "hello"); // 運行錯誤
}
編譯器總是要爲函數的每個參數製作臨時副本,指針
參數p 的副本是 _p,編譯器使 _p = p。如果函數體內的程序修改了_p 的內容,就導致
參數p 的內容作相應的修改。這就是指針可以用作輸出參數的原因。在本例中,_p 申請
了新的內存,只是把_p 所指的內存地址改變了,但是p 絲毫未變。所以函數GetMemory
並不能輸出任何東西。事實上,每執行一次GetMemory 就會泄露一塊內存,因爲沒有用
free 釋放內存。
如果非得要用指針參數去申請內存,那麼應該改用“指向指針的指針”
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
char *str = NULL;
GetMemory2(&str, 100); // 注意參數是 &str,而不是str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
由於“指向指針的指針”這個概念不容易理解,我們可以用函數返回值來傳遞動態
內存
char *GetMemory3(int num)
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
void Test3(void)
{
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
用函數返回值來傳遞動態內存這種方法雖然好用,但是常常有人把return 語句用錯了。這裏強調不要用return 語句返回指向“棧內存”的指針,因爲該內存在函數結束時自動消亡
char *GetString(void)
{
char p[] = "hello world";
return p; // 編譯器將提出警告
}
void Test4(void)
{
char *str = NULL;
str = GetString(); // str 的內容是垃圾
cout<< str << endl;
}
用調試器逐步跟蹤Test4,發現執行str = GetString 語句後str 不再是NULL 指針,但是str 的內容不是“hello world”而是垃圾。
char *GetString2(void)
{
char *p = "hello world";
return p;
}
void Test5(void)
{
char *str = NULL;
str = GetString2();
cout<< str << endl;
}
函數 Test5 運行雖然不會出錯,但是函數GetString2 的設計概念卻是錯誤的。因爲GetString2 內的“hello world”是常量字符串,位於靜態存儲區,它在程序生命期內恆定不變。無論什麼時候調用GetString2,它返回的始終是同一個“只讀”的內存塊。

5  指針p被free 以後其地址仍然不變(非NULL),只是該地址對應的內存是垃圾,p 成了“野指針”。
指針有一些“似是而非”的特徵:
(1)指針消亡了,並不表示它所指的內存會被自動釋放。
(2)內存被釋放了,並不表示指針會消亡或者成了NULL 指針。
函數體內的局部變量指針在函數結束時不會自動消亡。

 malloc和new的差異:

class Obj
{
public :
Obj(void){ cout << “Initialization” << endl; }
~Obj(void){ cout << “Destroy” << endl; }
void Initialize(void){ cout << “Initialization” << endl; }
void Destroy(void){ cout << “Destroy” << endl; }
};
void UseMallocFree(void)
{
Obj *a = (obj *)malloc(sizeof(obj)); // 申請動態內存
a->Initialize(); // 初始化
//…
a->Destroy(); // 清除工作
free(a); // 釋放內存
}
void UseNewDelete(void)
{
Obj *a = new Obj; // 申請動態內存並且初始化
//…
delete a; // 清除並且釋放內存
}
類 Obj 的函數Initialize 模擬了構造函數的功能,函數Destroy 模擬了析構函數的功能。函數UseMallocFree 中,由於malloc/free 不能執行構造函數與析構函數,必須調用成員函數Initialize 和Destroy 來完成初始化與清除工作。函數UseNewDelete 則簡單得多。既然 new/delete 的功能完全覆蓋了malloc/free,爲什麼C++不把malloc/free 淘汰出
局呢?這是因爲C++程序經常要調用C 函數,而C 程序只能用malloc/free 管理動態內存。


如果在申請動態內存時找不到足夠大的內存塊,malloc 和new 將返回NULL 指針,宣告內存申請失敗。通常有三種方式處理“內存耗盡”問題。
(1)判斷指針是否爲NULL,如果是則馬上用return 語句終止本函數。例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
return;
}

}
(2)判斷指針是否爲NULL,如果是則馬上用exit(1)終止整個程序的運行。例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
cout << “Memory Exhausted” << endl;
exit(1);
}

}
(3)爲new 和malloc 設置異常處理函數。例如Visual C++可以用_set_new_hander 函數
爲new 設置用戶自己定義的異常處理函數,也可以讓malloc 享用與new 相同的異常處
理函數。詳細內容請參考C++使用手冊。
上述(1)(2)方式使用最普遍。如果一個函數內有多處需要申請動態內存,那麼方
式(1)就顯得力不從心(釋放內存很麻煩),應該用方式(2)來處理。
void main(void)
{
float *p = NULL;
while(TRUE)
{
p = new float[1000000];
cout << “eat memory” << endl;
if(p==NULL)
exit(1);
}
}
有一個很重要的現象要告訴大家。對於32 位以上的應用程序而言,無論怎樣使用
malloc 與new,幾乎不可能導致“內存耗盡”。我在Windows 98 下用Visual C++編寫了
測試程序,見示例7-9。這個程序會無休止地運行下去,根本不會終止。因爲32 位操作
系統支持“虛存”,內存用完了,自動用硬盤空間頂替。我只聽到硬盤嘎吱嘎吱地響,
Window 98 已經累得對鍵盤、鼠標毫無反應。

函數 malloc 的原型如下:
void * malloc(size_t size);
int *p = (int *) malloc(sizeof(int) * length);
malloc 返回值的類型是void *,所以在調用malloc 時要顯式地進行類型轉換,將void* 轉換成所需要的指針類型。
malloc 函數本身並不識別要申請的內存是什麼類型,它只關心內存的總字節數。我們通常記不住int, float 等數據類型的變量的確切字節數,所以需要用sizeof進行確認。

如果對象有多個構造函數,那麼new 的語句也可以有多種形式。例如
class Obj
{
public :
Obj(void); // 無參數的構造函數
Obj(int x); // 帶一個參數的構造函數

}
void Test(void)
{
Obj *a = new Obj;
Obj *b = new Obj(1); // 初值爲1

delete a;
delete b;
}
如果用new 創建對象數組,那麼只能使用對象的無參數構造函數。例如Obj *objects = new Obj[100]; // 創建100 個動態對象不能寫成
Obj *objects = new Obj[100](1);// 創建100 個動態對象的同時賦初值1
在用delete 釋放對象數組時,留意不要丟了符號‘[]’。
例如

delete []objects; // 正確的用法
delete objects; // 錯誤的用法
後者相當於 delete objects[0],漏掉了另外99 個對象。與malloc相比new方便了很多,也不容易出錯,所以儘量要使用new,malloc可以在初始化一些對象上使用。

7  成員函數被重載的特徵:
(1)相同的範圍(在同一個類中);
(2)函數名字相同;
(3)參數不同;
(4)virtual 關鍵字可有可無。

覆蓋是指派生類函數覆蓋基類函數,特徵是:
(1)不同的範圍(分別位於派生類與基類);
(2)函數名字相同;
(3)參數相同;
(4)基類函數必須有virtual 關鍵字。
隱藏:
(1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual 關
鍵字,基類的函數將被隱藏(注意別與重載混淆)。
(2)如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual
關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)。
#include <iostream.h>
class Base
{
public:
virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
void g(float x){ cout << "Base::g(float) " << x << endl; }
void h(float x){ cout << "Base::h(float) " << x << endl; }
};
class Derived : public Base
{
public:
virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
void main(void)
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
// Good : behavior depends solely on type of the object
pb->f(3.14f); // Derived::f(float) 3.14
pd->f(3.14f); // Derived::f(float) 3.14
// Bad : behavior depends on type of the pointer
pb->g(3.14f); // Base::g(float) 3.14
pd->g(3.14f); // Derived::g(int) 3 (surprise!)
// Bad : behavior depends on type of the pointer
pb->h(3.14f); // Base::h(float) 3.14 (surprise!)
pd->h(3.14f); // Derived::h(float) 3.14
}
// 調用本類型成員函數,隱藏其他

(1)函數Derived::f(float)覆蓋了Base::f(float)。
(2)函數Derived::g(int)隱藏了Base::g(float),而不是重載。
(3)函數Derived::h(float)隱藏了Base::h(float),而不是覆蓋。

8  參數缺省值只能出現在函數的聲明中,而不能出現在定義體中。
例如:
void Foo(int x=0, int y=0); // 正確,缺省值出現在函數的聲明中
void Foo(int x=0, int y=0) // 錯誤,缺省值出現在函數的定義體中
{

}
如果函數有多個參數,參數只能從後向前挨個兒缺省,否則將導致函
數調用語句怪模怪樣。
正確的示例如下:
void Foo(int x, int y=0, int z=0);
錯誤的示例如下:
void Foo(int x=0, int y, int z=0);

不合理地使用參數的缺省值將導致重載函數output 產生二義性。
#include <iostream.h>
void output( int x);
void output( int x, float y=0.0);
void output( int x)
{
cout << " output int " << x << endl ;
}
void output( int x, float y)
{
cout << " output int " << x << " and float " << y << endl ;
}
void main(void)
{
int x=1;
float y=0.5;
// output(x); // error! ambiguous call
output(x,y); // output int 1 and float 0.5
}

9  在 C++語言中,可以用關鍵字operator 加上運算符來表示函數,叫做運算符重載。
例如兩個複數相加函數:
Complex Add(const Complex &a, const Complex &b);
可以用運算符重載來表示:
Complex operator +(const Complex &a, const Complex &b);
運算符與普通函數在調用時的不同之處是:對於普通函數,參數出現在圓括號內;
而對於運算符,參數出現在其左、右側。例如
Complex a, b, c;

c = Add(a, b); // 用普通函數
c = a + b; // 用運算符 +
如果運算符被重載爲全局函數,那麼只有一個參數的運算符叫做一元運算符,有兩個參數的運算符叫做二元運算符。
如果運算符被重載爲類的成員函數,那麼一元運算符沒有參數,二元運算符只有一個右側參數,因爲對象自己成了左側參數。

                  運算符                                                                             規則

          所有的一元運算符                                                       建議重載爲成員函數
                  = () [] ->                                                              只能重載爲成員函數
+= -= /= *= &= |= ~= %= >>= <<=                                       建議重載爲成員函數
             所有其它運算符                                                        建議重載爲全局函數
(1)不能改變C++內部數據類型(如int,float 等)的運算符。
(2)不能重載‘.’,因爲‘.’在類中對任何成員都有意義,已經成爲標準用法。
(3)不能重載目前C++運算符集合中沒有的符號,如#,@,$等。原因有兩點,一是難以
理解,二是難以確定優先級。
(4)對已經存在的運算符進行重載時,不能改變優先級規則,否則將引起混亂。

10  讓我們看看C++ 的“函數內聯”是如何工作的。

對於任何內聯函數,編譯器在符號表裏放入函數的聲明(包括名字、參數類型、返回值類型)。如果編譯器沒有發現內聯函數存在錯誤,那麼該函數的代碼也被放入符號表裏。在調用一個內聯函數時,編譯器首先檢查調用是否正(進行類型安全檢查,或者進行自動類型轉換,當然對所有的函數都一樣)。如果正確,內聯函數的代碼就會直接替換函數調用,於是省去了函數調用的開銷。這個過程與預處理有顯著的不同,因爲預處理器不能進行類型安全檢查,或者進行自動類型轉換。假如內聯函數是成員函數,對象的地址(this)會被放在合適的地方,這也是預處理器辦不的。
C++ 語言的函數內聯機制既具備宏代碼的效率,又增加了安全性,而且可以自由操作類的數據成員。所以在C++ 程序中,應該用內聯函數取代所有宏代碼,“斷言assert”恐怕是唯一的例外。
關鍵字 inline 必須與函數定義體放在一起才能使函數成爲內聯,僅將inline 放在函數聲明前面不起任何作用。如下風格的函數Foo 不能成爲內聯函數:
inline void Foo(int x, int y); // inline 僅與函數聲明放在一起
void Foo(int x, int y)
{

}
而如下風格的函數Foo 則成爲內聯函數:
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 與函數定義體放在一起
{

}
inline 不應該出現在函數的聲明中
內聯是以代碼膨脹(複製)爲代價,僅僅省去了函數調用的開銷,從而提高函數的執行效率。如果執行函數體內代碼的時間,相比於函數調用的開銷較大,那麼效率的收
獲會很少。另一方面,每一處內聯函數的調用都要複製代碼,將使程序的總代碼量增大,消耗更多的內存空間。以下情況不宜使用內聯:
(1)如果函數體內的代碼比較長,使用內聯將導致內存消耗代價較高。
(2)如果函數體內出現循環,那麼執行函數體內代碼的時間要比函數調用的開銷大。

11  String的結構如下:
class String
{
public:
String(const char *str = NULL); // 普通構造函數
String(const String &other); // 拷貝構造函數
~ String(void); // 析構函數
String & operate =(const String &other); // 賦值函數
private:
char *m_data; // 用於保存字符串
};
把對象的初始化工作放在構造函數中,把清除工作放在析構函數中。當對象被創建時,構造函數被自動執行。當對象消亡時,析構函數被自動執行。這下就不用擔心忘了對象的初始化和清除工作。

12 構造函數的初始化表
構造函數有個特殊的初始化方式叫“初始化表達式表”(簡稱初始化表)。初始化表位於函數參數表之後,卻在函數體 {} 之前。這說明該表裏的初始化工作發生在函數體內的任何代碼被執行之前。
構造函數初始化表的使用規則:
如果類存在繼承關係,派生類必須在其初始化表裏調用基類的構造函數。
例如
class A
{…
A(int x); // A 的構造函數
};
class B : public A
{…
B(int x, int y);// B 的構造函數
};
B::B(int x, int y)
: A(x) // 在初始化表裏調用A 的構造函數
{

}

在函數的構造函數的過程中一共是分成是兩個階段:變量初始化階段和計算階段,初始化階段先於計算階段。初始化表主要就是用來初始化類中的成員的。在系統默認類型的初始化上,初始化表和在構造函數中進行初始化的性能差不多,但是在類成員的初始化上,初始化表就展現出了非常大的優勢。

以下例子說明:

class Test1
{
Test1() // 無參構造函數
{cout << "Construct Test1" << endl ;}
Test1(const Test1& t1) // 拷貝構造函數
{cout << "Copy constructor for Test1" << endl ;this->a = t1.a ;}
Test1& operator = (const Test1& t1) //賦值運算符
{cout << "assignment for Test1" << endl ;this->a = t1.a ;return *this;}
int a ;
};
struct Test2
{
Test1 test1 ;
Test2(Test1 &t1)
{test1 = t1 ;}
};
調用代碼:
Test1 t1 ;
Test2 t2(t1) ;
輸出:
Construct Test1
Construct Test1
assignment for Test1
解釋一下:
第一行輸出對應調用代碼中第一行,構造一個Test1對象
第二行輸出對應Test2構造函數中的代碼,用默認的構造函數初始化對象test1 // 這就是所謂的初始化階段
第三行輸出對應Test2的賦值運算符,對test1執行賦值操作 // 這就是所謂的計算階段

struct Test2
{
Test1 test1 ;
Test2(Test1 &t1):test1(t1){}
}
使用同樣的調用代碼,輸出結果如下:
Construct Test1
Copy constructor for Test1
第一行輸出對應 調用代碼的第一行
第二行輸出對應Test2的初始化列表,直接調用拷貝構造函數初始化test1,省去了調用默認構造函數的過程
所以一個好的原則是,能使用初始化列表的時候儘量使用初始化列表

除了性能問題之外,有些時場合初始化列表是不可或缺的,以下幾種情況時必須使用初始化列表
1. 常量成員,因爲常量只能初始化不能賦值,所以必須放在初始化列表裏面
2. 引用類型,引用必須在定義的時候初始化,並且不能重新賦值,所以也要寫在初始化列表裏面
3. 沒有默認構造函數的類類型,因爲使用初始化列表可以不必調用默認構造函數來初始化,而是直接調用拷貝構造函數初始化
struct Test1
{
Test1(int a):i(a){}
int i;
};
struct Test2
{
Test1 test1 ;
Test2(Test1 &t1)
{test1 = t1;}
};
以上代碼無法通過編譯,因爲Test2的構造函數中test1 = t1這一行實際上分成兩步執行:
1. 調用Test1的默認構造函數來初始化test1
2. 調用Test1的賦值運算符給test1賦值
但是由於Test1沒有默認的構造函數,所謂第一步無法執行,故而編譯錯誤。正確的代碼如下,使用初始化列表代替賦值操作
struct Test2
{
Test1 test1 ;
Test2(Test1 &t1):test1(t1){}
}
成員是按照他們在類中出現的順序進行初始化的,而不是按照他們在初始化列表出現的順序初始化的,看代碼:
struct foo
{
int i ;int j ;
foo(int x):i(x), j(i){}; // ok, 先初始化i,後初始化j
};
再看下面的代碼:
struct foo
{
int i ;int j ;
foo(int x):j(x), i(j){} // i值未定義
};
這裏i的值是未定義的因爲雖然j在初始化列表裏面出現在i前面,但是i先於j定義,所以先初始化i,而i由j初始化,此時j尚未初始化,所以導致i的值未定義。一個好的習慣是,按照成員定義的順序進行初始化。
由於並非所有的對象都會使用拷貝構造函數和賦值函數,程序員可能對這兩個函數有些輕視。請先記住以下的警告,在閱讀正文時就會多心:
開頭講過,如果不主動編寫拷貝構造函數和賦值函數,編譯器將以“位拷貝”(又稱淺拷貝)的方式自動生成缺省的函數。倘若類中含有指針變量,那麼這兩個缺省的函數就隱含了錯誤。以類String 的兩個對象a,b 爲例,假設a.m_data 的內容爲“hello”,b.m_data的內容爲“world”。現將 a 賦給b,缺省賦值函數的“位拷貝”意味着執行b.m_data = a.m_data。這將造成三個錯誤:一是b.m_data 原有的內存沒被釋放,造成內存泄露;二是b.m_data和a.m_data 指向同一塊內存,a 或b 任何一方變動都會影響另一方;三是在對象被析構時,m_data 被釋放了兩次。
拷貝構造函數和賦值函數非常容易混淆,常導致錯寫、錯用。拷貝構造函數是在對象被創建時調用的,而賦值函數只能被已經存在了的對象調用。以下程序中,第三個語句和第四個語句很相似,你分得清楚哪個調用了拷貝構造函數,哪個調用了賦值函數嗎?
String a(“hello”);
String b(“world”);
String c = a; // 調用了拷貝構造函數,最好寫成 c(a);
c = b; // 調用了賦值函數
本例中第三個語句的風格較差,宜改寫成 String c(a) 以區別於第四個語句。
淺拷貝和深拷貝 
在某些狀況下,類內成員變量需要動態開闢堆內存,如果實行位拷貝,也就是把對象裏的值完全複製給另一個對象,如A=B。這時,如果B中有一個成員變量指針已經申請了內存,那A中的那個成員變量也指向同一塊內存。這就出現了問題:當B把內存釋放了(如:析構),這時A內的指針就是野指針了,出現運行錯誤。 
    深拷貝和淺拷貝的定義可以簡單理解成:如果一個類擁有資源(堆,或者是其它系統資源),當這個類的對象發生複製過程的時候,這個過程就可以叫做深拷貝,反之對象存在資源,但複製過程並未複製資源的情況視爲淺拷貝。 
    淺拷貝資源後在釋放資源的時候會產生資源歸屬不清的情況導致程序運行出錯。
13示例:類String 的拷貝構造函數與賦值函數
// 拷貝構造函數
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);
}
// 賦值函數
String & String::operate =(const String &other)
{
// (1) 檢查自賦值
if(this == &other)
return *this;
// (2) 釋放原有的內存資源
delete [] m_data;
// (3)分配新的內存資源,並複製內容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
// (4)返回本對象的引用
return *this;
}
類 String 拷貝構造函數與普通構造函數(參見9.4 節)的區別是:在函數入口處無需與NULL 進行比較,這是因爲“引用”不可能是NULL,而“指針”可以爲NULL。
類 String 的賦值函數比構造函數複雜得多,分四步實現:
(1)第一步,檢查自賦值。你可能會認爲多此一舉,難道有人會愚蠢到寫出 a = a 這樣的自賦值語句!的確不會。但是間接的自賦值仍有可能出現,例如
// 內容自賦值
b = a;

// 地址自賦值
b = &a;

c = b;

a = c;
a = *b;
也許有人會說:“即使出現自賦值,我也可以不理睬,大不了化點時間讓對象複製自己而已,反正不會出錯!”
他真的說錯了。看看第二步的delete,自殺後還能複製自己嗎?所以,如果發現自賦值,應該馬上終止函數。注意不要將檢查自賦值的if 語句
if(this == &other)
錯寫成爲
if( *this == other)
(2)第二步,用delete 釋放原有的內存資源。如果現在不釋放,以後就沒機會了,將造成內存泄露。
(3)第三步,分配新的內存資源,並複製字符串。注意函數strlen 返回的是有效字符串長度,不包含結束符‘\0’。函數strcpy 則連‘\0’一起復制。
(4)第四步,返回本對象的引用,目的是爲了實現象 a = b = c 這樣的鏈式表達。注意不要將 return *this 錯寫成 return this 。那麼能否寫成return other 呢?效果不是一樣嗎?
不可以!因爲我們不知道參數other 的生命期。有可能other 是個臨時對象,在賦值結束後它馬上消失,那麼return other 返回的將是垃圾。

偷懶的辦法是:只需將拷貝構造函數和賦值函數聲明爲私有函數,不用編寫代碼。
例如:
class A
{ …
private:
A(const A &a); // 私有的拷貝構造函數
A & operate =(const A &a); // 私有的賦值函數
};
如果有人試圖編寫如下程序:
A b(a); // 調用了私有的拷貝構造函數
b = a; // 調用了私有的賦值函數
編譯器將指出錯誤,因爲外界不可以操作 A 的私有函數。
14如何在派生類中實現類的基本函數
基類的構造函數、析構函數、賦值函數都不能被派生類繼承。如果類之間存在繼承關係,在編寫上述基本函數時應注意以下事項:
派生類的構造函數應在其初始化表裏調用基類的構造函數。
基類與派生類的析構函數應該爲虛(即加virtual 關鍵字)。例如
#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;
}
輸出結果爲:
~Derived
~Base
如果析構函數不爲虛,那麼輸出結果爲
~Base
在編寫派生類的賦值函數時,注意不要忘記對基類的數據成員重新賦值。例如:
class Base
{
public:

Base & operate =(const Base &other); // 類Base 的賦值函數
private:
int m_i, m_j, m_k;
};
class Derived : public Base
{
public:

Derived & operate =(const Derived &other); // 類Derived 的賦值函數
private:
int m_x, m_y, m_z;
};
Derived & Derived::operate =(const Derived &other)
{
//(1)檢查自賦值
if(this == &other)
return *this;
//(2)對基類的數據成員重新賦值
Base::operate =(other); // 因爲不能直接操作私有數據成員
//(3)對派生類的數據成員賦值
m_x = other.m_x;
m_y = other.m_y;
m_z = other.m_z;
//(4)返回本對象的引用
return *this;

如果類A 和類B 毫不相關,不可以爲了使B 的功能更多些而讓B繼承A 的功能和屬性。不要覺得“白吃白不吃”,讓一個好端端的健壯青年無緣無故地吃人蔘補身體。
若在邏輯上B 是A 的“一種”(a kind of ),則允許B 繼承A 的功能和屬性。例如男人(Man)是人(Human)的一種,男孩(Boy)是男人的一種。那麼類Man 可以從類Human 派生,類Boy 可以從類Man 派生。
注意事項
看起來很簡單,但是實際應用時可能會有意外,繼承的概念在程序世界與現實世界並不完全相同。
所以更加嚴格的繼承規則應當是:若在邏輯上B 是A 的“一種”,並且A 的所有功能和屬性對B 而言都有意義,則允許B 繼承A 的功能和屬性。
若在邏輯上A 是B 的“一部分”(a part of),則不允許B 從A 派生,而是要用A 和其它東西組合出B。

15  看到 const 關鍵字,C++程序員首先想到的可能是const 常量。這可不是良好的條件反射。如果只知道用const 定義常量,那麼相當於把火藥僅用於製作鞭炮。const 更大的魅力是它可以修飾函數的參數、返回值,甚至函數的定義體。
const 是constant 的縮寫,“恆定不變”的意思。被const 修飾的東西都受到強制保護,可以預防意外的變動,能提高程序的健壯性。所以很多C++程序設計書籍建議:“Use constwhenever you need”。
用const 修飾函數的參數
如果參數作輸出用,不論它是什麼數據類型,也不論它採用“指針傳遞”還是“引用傳遞”,都不能加const 修飾,否則該參數將失去輸出功能。
const 只能修飾輸入參數:
如果輸入參數採用“指針傳遞”,那麼加const 修飾可以防止意外地改動該指針,起到保護作用。
例如 StringCopy 函數:
void StringCopy(char *strDestination, const char *strSource);
其中strSource 是輸入參數,strDestination 是輸出參數。給strSource 加上const 修飾後,如果函數體內的語句試圖改動strSource 的內容,編譯器將指出錯誤。
如果輸入參數採用“值傳遞”,由於函數將自動產生臨時變量用於複製該參數,該輸入參數本來就無需保護,所以不要加const 修飾。
例如不要將函數void Func1(int x) 寫成void Func1(const int x)。同理不要將函數voidFunc2(A a) 寫成void Func2(const A a)。其中A 爲用戶自定義的數據類型。
對於非內部數據類型的參數而言,象void Func(A a) 這樣聲明的函數註定效率比較低。因爲函數體內將產生A 類型的臨時對象用於複製參數a,而臨時對象的構造、複製、析構過程都將消耗時間。
爲了提高效率,可以將函數聲明改爲void Func(A &a),因爲“引用傳遞”僅借用一下參數的別名而已,不需要產生臨時對象。但是函數void Func(A &a) 存在一個缺點:“引用傳遞”有可能改變參數a,這是我們不期望的。解決這個問題很容易,加const 修飾即可,因此函數最終成爲void Func(const A &a)。
const 成員函數
任何不會修改數據成員的函數都應該聲明爲const 類型。如果在編寫const 成員函數時,不慎修改了數據成員,或者調用了其它非const 成員函數,編譯器將指出錯誤,這無疑會提高程序的健壯性。
以下程序中,類stack 的成員函數GetCount 僅用於計數,從邏輯上講GetCount 應當爲const 函數。編譯器將GetCount 函數中的錯誤。
class Stack
{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const; // const 成員函數
private:
int m_num;
int m_data[100];
};
int Stack::GetCount(void) const
{
++ m_num; // 編譯錯誤,企圖修改數據成員m_num
Pop(); // 編譯錯誤,企圖調用非const 函數
return m_num;
}

16  最後的一些建議:

◆不要一味地追求程序的效率,應當在滿足正確性、可靠性、健壯性、可讀性等質量因素的前提下,設法提高程序的效率。
以提高程序的全局效率爲主,提高局部效率爲輔。
在優化程序的效率時,應當先找出限制效率的“瓶頸”,不要在無關緊要之處優化。
先優化數據結構和算法,再優化執行代碼。
有時候時間效率和空間效率可能對立,此時應當分析那個更重要,
作出適當的折衷。例如多花費一些內存來提高性能。
不要追求緊湊的代碼,因爲緊湊的代碼並不能產生高效的機器碼。當心那些視覺上不易分辨的操作符發生書寫錯誤。
我們經常會把“==”誤寫成“=”,象“||”、“&&”、“<=”、“>=”這類符號也很容易發生“丟1”失誤。然而編譯器卻不一定能自動指出這類錯誤。
變量(指針、數組)被創建之後應當及時把它們初始化,以防止把未被初始化的變量當成右值使用。
當心變量的初值、缺省值錯誤,或者精度不夠。
當心數據類型轉換髮生錯誤。儘量使用顯式的數據類型轉換(讓人們知道發生了什麼事),避免讓編譯器輕悄悄地進行隱式的數據類型轉換。
當心變量發生上溢或下溢,數組的下標越界。
當心忘記編寫錯誤處理程序,當心錯誤處理程序本身有誤。
當心文件I/O 有錯誤。
避免編寫技巧性很高代碼。
不要設計面面俱到、非常靈活的數據結構。
如果原有的代碼質量比較好,儘量複用它。但是不要修補很差勁的代碼,應當重新編寫。
儘量使用標準庫函數,不要“發明”已經存在的庫函數。
儘量不要使用與具體硬件或軟件環境關係密切的變量。
把編譯器的選擇項設置爲最嚴格狀態。
如果可能的話,使用PC-Lint、LogiScope 等工具進行代碼審查。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章