最近有時間,看了《高品質c++程式設計》把不清晰的概念,列出來了。
1: 當數組作為函數的參數進行傳遞時,該數組自動退化為同類型的指針
void Func( char a[ 100 ])
... {
cout<< sizeof(a) << endl; // 4 位元組而不是100 位元組
}
2: char *p = “world”; // 注意p 指向常量字元串
p[0] = ‘X’; // 編譯器不能發現該錯誤
指針p 指向常量字元串“world”(位于靜态存儲區,内容為world/0),常量字元串的内容是不可以被修改的。從文法上看,編譯器并不覺得語句p[0]= ‘X’有什麼不妥,但是該語句企圖修改常量字元串的内容而導緻運作錯誤
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;
4. 函數的傳回值
一個函數, 用于出錯處理的傳回值一定要清楚,讓使用者不容易忽視或誤解錯誤
5. 常量定義規則
需要對外公開的常量放在頭檔案中,不需要對外公開的常量放在定義檔案的頭部。為便于管理,可以把不同子產品的常量集中存放在一個公共的頭檔案中
如果某一常量與其它常量密切相關,應在定義中包含這種關系,而不
應給出一些孤立的值。
例如:
const float RADIUS = 100;
const float DIAMETER = RADIUS * 2;
6 類中的常量
有時我們希望某些常量隻在類中有效。由于#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)
7: 靜态變量加字首s_(表示static)
8 布爾變量與零值比較
不可将布爾變量直接與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)
9: 整型變量與零值比較
應當将整型變量用“==”或“!=”直接與0 比較。
假設整型變量的名字為value,它與零值比較的标準if 語句如下:
if (value == 0)
if (value != 0)
不可模仿布爾變量的風格而寫成
if (value) // 會讓人誤解 value 是布爾變量
if (!value)
10:浮點變量與零值比較
不可将浮點變量用“==”或“!=”與任何數字比較。
千萬要留意,無論是float 還是double 類型的變量,都有精度限制。是以一定要
避免将浮點變量用“==”或“!=”與數字比較,應該設法轉化成“>=”或“<=”形式。
假設浮點變量的名字為x,應當将
if (x == 0.0) // 隐含錯誤的比較
轉化為
if ((x>=-EPSINON) && (x<=EPSINON))
其中EPSINON 是允許的誤差(即精度)。
11: 指針變量與零值比較
應當将指針變量用“==”或“!=”與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)
12: 循環語句的效率
在多重循環中,如果有可能,應當将最長的循環放在最内層,最短的循環放在最外層,以減少CPU 跨切循環層的次數
13: 如果函數的傳回值是一個對象,有些場合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場合隻能用“值傳遞”而不能用“引用傳遞”,否則會出錯。例如:
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 在函數結束時被自動銷毀,将導緻傳回的“引用”無效。
14:
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;
}
15: 引用與指針的比較
引用的一些規則如下:
(1)引用被建立的同時必須被初始化(指針則可以在任何時候被初始化)。
(2)不能有NULL 引用,引用必須與合法的存儲單元關聯(指針則可以是NULL)。
(3)一旦引用被初始化,就不能改變引用的關系(指針則可以随時改變所指的對象)
引用的主要功能是傳遞函數的參數和傳回值。C++語言中,函數的參數和傳回值的傳遞方式有三種:值傳遞、
指針傳遞和引用傳遞
16:
記憶體配置設定方式
記憶體配置設定方式有三種:
(1) 從靜态存儲區域配置設定。記憶體在程式編譯的時候就已經配置設定好,這塊記憶體在程式的整個運作期間都存在。例如全局變量,static 變量。
(2) 在棧上建立。在執行函數時,函數内局部變量的存儲單元都可以在棧上建立,函數執行結束時這些存儲單元自動被釋放。棧記憶體配置設定運算内置于處理器的指令集中,效率很高,但是配置設定的記憶體容量有限。
(3) 從堆上配置設定,亦稱動态記憶體配置設定。程式在運作的時候用malloc 或new 申請任意多少的記憶體,程式員自己負責在何時用free 或delete 釋放記憶體。動态記憶體的生存期由我們決定,使用非常靈活,但問題也最多
17: return 語句傳回常量字元串
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,它傳回的始終是同一個“隻讀”的記憶體塊。
18: new/delete, malloc/free
對于非内部資料類型的對象而言,光用maloc/free 無法滿足動态對象的要求。對象
在建立的同時要自動執行構造函數, 對象在消亡之前要自動執行析構函數。由于
malloc/free 是庫函數而不是運算符,不在編譯器控制權限之内,不能夠把執行構造函數
和析構函數的任務強加于malloc/free
19: 記憶體耗盡怎麼辦
如果在申請動态記憶體時找不到足夠大的記憶體塊,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)來處理。很多人不忍心用exit(1),問:“不編寫出錯處理程式,讓作業系統自己解決行不行?”不行。如果發生“記憶體耗盡”這樣的事情,一般說來應用程式已經無藥可救。如果
不用exit(1) 把壞程式殺死,它可能會害死作業系統
20: extern "c"
extern “C”
{
void foo(int x, int y);
⋯ // 其它函數
}
或者寫成
extern “C”
{
#include “myheader.h”
⋯ // 其它C 頭檔案
}
這就告訴C++編譯譯器,函數foo 是個C 連接配接,應該到庫中找名字_foo 而不是找_foo_int_int。C++編譯器開發商已經對C 标準庫的頭檔案作了extern“C”處理,是以我們可以用#include 直接引用這些頭檔案
21: 令人迷惑的隐藏規則
本來僅僅差別重載與覆寫并不算困難,但是C++的隐藏規則使問題複雜性陡然增加。
這裡“隐藏”是指派生類的函數屏蔽了與其同名的基類函數,規則如下:
(1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual
關鍵字,基類的函數将被隐藏(注意别與重載混淆)。
(2)如果派生類的函數與基類的函數同名,并且參數也相同,但是基類函數沒有virtual
關鍵字。此時,基類的函數被隐藏(注意别與覆寫混淆)。
22: 運算符重載
運算符 規則
所有的一進制運算符 建議重載為成員函數
= () [] -> 隻能重載為成員函數
+= -= /= *= &= |= ~= %= >>= <<= 建議重載為成員函數
所有其它運算符 建議重載為全局函數
23: 函數内聯
inline 是一種“用于實作的關鍵字”,而不是一種“用于聲明的關鍵字”。
定義在類聲明之中的成員函數将自動地成為内聯函數,例如
class A
{
public:
void Foo(int x, int y) { ⋯ } // 自動地成為内聯函數
}
将成員函數的定義體放在類聲明之中雖然能帶來書寫上的友善,但不是一種良好的程式設計
風格,上例應該改成:
// 頭檔案
class A
{
public:
void Foo(int x, int y);
}
// 定義檔案
inline void A::Foo(int x, int y)
{
⋯
}
24:構造函數的初始化清單
B::B(const A &a) //初始化表裡調用了類A 的拷貝構造函數,進而将成員對象m_a 初始化。
: m_a(a)
{
…
}
B::B(const A &a) //構造函數幹了兩件事:先暗地裡建立m_a對象(調用了A 的無參數構造函數),再調用類A 的指派函數,将參數a 賦給m_a
{
m_a = a;
…
}
25: 構造和析構的次序
構造從類層次的最根處開始,在每一層中,首先調用基類的構造函數,然後調用成員對象的構造函數。析構則嚴格按照與構造相反的次序執行,該次序是唯一的,否則編譯器将無法自動執行析構過程。
一個有趣的現象是,成員對象初始化的次序完全不受它們在初始化表中次序的影響,隻由成員對象在類中聲明的次序決定。這是因為類的聲明是唯一的,而類的構造函數可以有多個,是以會有多個不同次序的初始化表。如果成員對象按照初始化表的次序進行構造,這将導緻析構函數無法得到唯一的逆序
26: 拷貝構造函數與指派函數
編譯器将以“位拷貝”的方式自動生成預設的函數。倘若類中含有指針變量,那麼這兩個預設的函數就隐含了錯誤。
拷貝構造函數和指派函數非常容易混淆,常導緻錯寫、錯用。拷貝構造函數是在對象被建立時調用的,而指派函數隻能被已經存在了的對象調用。以下程式中,第三個語句和第四個語句很相似,你分得清楚哪個調用了拷貝構造函數,哪個調用了指派函數嗎?
String a(“hello”);
String b(“world”);
String c = a; // 調用了拷貝構造函數,最好寫成 c(a);
c = b; // 調用了指派函數
本例中第三個語句的風格較差,宜改寫成String c(a) 以差別于第四個語句
27:
// String 的普通構造函數
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::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 的析構函數
String::~String(void)
{
delete [] m_data;
// 由于m_data 是内部資料類型,也可以寫成 delete m_data;
}
28: 在編寫派生類的指派函數時,注意不要忘記對基類的資料成員重新指派
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;
}
29;
函數傳回值采用“引用傳遞”的場合并不多,這種方式一般隻出現在類的指派函數中,目的是為了實作鍊式表達。
例如
class A
{⋯
A & operate = (const A &other); // 指派函數
};
A a, b, c; // a, b, c 為A 的對象
⋯
a = b = c; // 正常的鍊式指派
(a = b) = c; // 不正常的鍊式指派,但合法
如果将指派函數的傳回值加const 修飾,那麼該傳回值的内容不允許被改動。上例中,語句 a = b = c 仍然正确,但是語句 (a = b) = c 則是非法的。
30: 繼承群組合的規則
(1)若在邏輯上B 是A 的“一種”,并且A 的所有功能和屬性對B 而言都有意義,則允許B 繼承A 的功能和屬性。
(2)若在邏輯上A 是B 的“一部分”(a part of),則不允許B 從A 派生,而是要用A 和其它東西組合出B。
31: 引用本身不占存儲單元,系統也不給引用配置設定存儲單元。故:對引用求位址,就是對目标變量求位址。&ra與&a相等