1 讓自己習慣C++ 條款 01 視 C++ 為一個語言聯邦 C : C++以C為基礎,block、語句、預處理器、內置數據類型、數組、指針都來自於C。當使用C++中的C成分工作時,沒有模板(Template)、沒有異常(Exceptions)、沒有重載(overloading)。 Object-O ...
1 讓自己習慣C++
條款 01 視 C++ 為一個語言聯邦
-
C : C++以C為基礎,block、語句、預處理器、內置數據類型、數組、指針都來自於C。當使用C++中的C成分工作時,沒有模板(Template)、沒有異常(Exceptions)、沒有重載(overloading)。
-
Object-Oriented C++ : 也就是 C with classes,classes(包括構造函數和析構函數)、封裝(encapsulation)、繼承(inheritance)、多態(polymorphism)、virtual函數(動態綁定)......等等。
-
Template C++ : C++的泛型編程部分。
-
STL(Standard Template Library) : 對容器、迭代器、演算法以及函數對象對的規約有極佳的緊密配合與協調。
*請記住 : *
1. 高效編程守則視狀況而變化,取決於你使用C++的哪一部分。
條款 02 儘量以 const,enum,inline 替代 #define
例 : `#define ASPECT_RATIO 1.653`預處理器會將程式中的`ASPET_RATIO`記號全部替換成數值`1.653`,也就是這個記號不會進入記號表內,這樣在程式出錯時獲取的錯誤信息會很難分析。
當我們使用#define 實現巨集
時,更要小心,必須為巨集中的所有實參加上小括弧。例 :
#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))//一定要加小括弧,這裡的例子還體現不出什麼,註意加括弧的位置就行
int a = 5,b = 0;
//當第一個參數(++a)大於第二個參數時,++a會被執行兩次,這顯然很容易出問題
CALL_WITH_MAX(++a,b);
CALL_WITH_MAX(++a,b+10);
解決方法 :
-
以一個常量替換上述巨集。
const double Aspect_ratio = 1.653;
作為一個語言常量,自然會進入記號表,這樣在Debug的時候容易跟蹤,且生成的代碼量比較小,開支較小,因為巨集定義是盲目地替換欄位。以 const 代替 #define 時要註意兩點 :
(1) 定義常量指針時,要註意頂層 const 和底層 const 的區別。頂層 const 是將 const 放在 '*' 左邊,意思是指針指向的常量類型,所以該指針所指向的內容不能被修改;
而底層 const 則是將 const 放在 '*' 右邊,意思是我這個指針是一個常量指針,只能指向在初始化時的值,不能指向其他變數。
(2)定義 class 專屬常量時,也就是類內 static 成員,例如
class GamePlayer{ private: static const int NumTurns = 5; int scores[NumTurns]; };
註意,上例中的的
static const int NumTurns = 5;
是聲明式而不是定義式。C++要求要對使用的任何東西都要提供一個定義式。 但如果它是個 class 專屬常量,又是 static 且為整數類型(integral type,例如int,char,bool),只要不取指針,則只需要提供聲明式,若有些編譯器不支持,則需要在實現文件中定義。(註意非整型必須定義,初值可以放在定義式中)
//.h class CostEstimate{ private: static const double FudgeFactor; }; //.c const double CostEstimate::FudgeFactor = 1.35;
另外,#define 不重視作用域(scope),在定義後編譯過程中都有效。
-
使用
enum hack
這種做法比較像
#define
,同樣不能取地址,同樣會導致非必要的記憶體分配。 -
對於“實現巨集“,用
template inline
函數,同樣是代碼替換,但這種做法屬於函數操作,一切按函數操作就行了,不用操心參數問題,同時遵守作用域和訪問規則。
請記住:
1. 對於單純常量,最好以 const 對象或 enum 替換 #define
2. 對於形似函數的巨集(macros),最好改用 inline template 函數替代 #define
條款 03 儘可能使用const
頂層 const : 表示 const 修飾的類型為常量,特別地,對於指針類型,const
在 ‘*’ 右邊時表示頂層指針,也就是該指針變數為常量, 與該指針變數指向的對象是否為常量無要求。
int i = 0;
int *const p1 = &i;//const 在‘*’右邊,表示p1只能指向i(頂層指針)
底層 const : 與指針和引用類型有關,對於指針類型,當 const
在 ‘*’ 左邊(一般寫在類型左邊)時,表示該指針指向的對象為常量類型, 此時該指針可以指向別的對象,當不能通過指針解引用來更改所指對象的值。
而對於引用類型,都是底層 const,也就是所引用的值為常量,但const引用可以綁定常量與非常量,這是一個特殊的例子。
int i = 0;
const int j = 1;
const int &ref_i = i;//可以綁定非常量
const int &ref_j = j;//可以綁定常量類型
const int &ref = 10;//會創建臨時變數,然後綁定
const可以和函數的返回值、參數、函數自身產生關聯,儘可能地使用它,可以讓編譯器幫你更快的發現錯誤!
const成員函數
將 const
作用在成員函數上,可以區分出常量對象和非常量對象所使用的不同版本的成員函數。const成員函數可以保證不修改類的成員變數。
//.h
class TextBlock{
public:
// ...
const char& operator[](std::size_t position) const//const對象調用此函數
{ return text[position]; }
char& operator[](std::size_t position)//非const對象調用此函數
{ return text[position]; }
private:
std::string text;
};
//.cpp
TextBlock tb("Hello");
std::cout << tb[0];//調用 char& operator[](std::size_t position)
const TextBlock ctb("Hello");
std::cout << ctb[0];//調用 const char& operator[](std::size_t position) const
在C++中,不修改類的成員變數指的是編譯器強制實施的“bitwise constness”,也就是保證每一個bit都不允許修改,但現實中,我們可能會希望一部分成員不被修改,一部分成員被修改。
此時,我們可以將能在const成員函數中修改的成員變數聲明為mutable
//.h
class CTextBlock{
public:
//...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsVaild;
};
//.cpp
std::size_t length() const
{
if(!lengthIsVaild)
{
pText = "Hello";//錯誤,非mutable變數在const成員函數中禁止修改
textLength = std::strlen(pText);//允許修改
lengthIsvaild = true;//允許修改
}
return textLength;
}
在const和non-const成員函數中避免重覆
很多時候,const和non-const函數知識返回值不同,函數定義會出現冗餘。
class TextBlock{
public;
//...
const char& operator[](std::size_t position) const
{
//... //邊界檢查
//... //日誌數據訪問
//... //檢驗數據完整性
return text[position];
}
/*
//冗餘版本
char& operator[](std::size_t position)
{
//... //邊界檢查
//... //日誌數據訪問
//... //檢驗數據完整性
return text[position];
}
*/
char& operator[](std::size_t position)
{
return const_cast<char&>(//去除返回值的const屬性
static_cast<const TextBlock>(*this)[position];//static_cast安全轉型為const對象調用const版本,返回值為const char*
);
}
private:
std::string text;
};
解決冗餘的原則是,在非常量的版本中對常量版本進行轉型,因為我們永遠不對常量版本的東西作任何修改,只單純調用,這完全符合const的設計 : const成員函數保證不修改成員。這避免了不必要的風險。
請記住:
1. 將某些聲明為 const 可幫助編譯器偵測出錯誤用法。 const 可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數 本體。
2. 編譯器強制實施 bitwise constness,但你編寫程式時應該使用“概念上的常量性(conceptul constness)。”
3. 當 const 和 non-const 成員函數有著實質等價的實現時,領 non-const 版本調用 const 版本可避免代碼重覆。
條款 04 確定對象被使用前已先被初始化
有些類型(stl)保證內容能被預設初始化,而有些卻不行。
因此我們強制規定 : 永遠在使用對象之前先將它初始化。
int x = 0;
const char* text = "hello world";
double d;
std::cin >> d;
在C++中,除了內置類型外,初始化往往由構造函數完成。
因此我們強制規定 : 確保每一個構造函數都將對象中的每一個成員初始化。
註意不要混淆初始化和賦值這兩種概念。
//.h
class PhoneNumber {...};
class ABEntry{
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
//.cpp
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
//賦值
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
該構造函數定義中的行為是賦值,因為在進入構造函數定義也就是大括弧部分之前所有的成員變數已經被預設構造函數構造了。此時又 重新執行一遍賦值函數,一共兩次動作,開銷太大。
因此
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{ }
此時對於各個成員,只執行依次構造函數即可完成初始化。
另外,初始化的次序應該對應聲明的次序。可以避免一些隱晦的錯誤。
現在我們關註“不同編譯單元內定義的non-local static對象”的初始化次序 :
- 編譯單元 : 產出單一目標問價的源碼。基本上是 單一源碼文件 + 其所加入到的頭文件。註意!編譯器編譯不同單元的次序無法確定!
- static對象 : 壽命從被構造出來直到程式結束,程式結束時自動調用其析構函數。
- non-local static對象 : 不在函數內的static對象。
- local static對象 : 在函數內的static對象。
我們設想一種這樣的情況 :
在一個編譯單元記憶體在一個對象
class FileSystem{
public:
...
std::size_t numDisks() const;
...
};
extern FileSystem tfs;//聲明外部變數,預備給其他文件(編譯單元)使用
在另一個編譯單元記憶體在另一個對象
class Directroy{
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs.numDisks();
}
Directory temp( params );
因為不同編譯單元編譯的次序是不確定的,我們無法保證Directory對象定義之前,類FileSystem已經被編譯,這樣會出現錯誤。
解決方法 :
Singleton模式
因為C++保證 : 函數內的local static對象會在“該函數被調用期間”“首次遇上該對象的定義式”時被初始化。
//編譯單元。
class FileSystem {...};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
//另一個編譯單元
class Directory {...};
Directory::Directory( params )
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td;
return td;
}
因此可以保證調用tfs()時,只有FileSystem的定義式被編譯器遇到時才給裡面的fs對象初始化並返回。
但內含static對象在多線程系統中線程不安全。
*請記住 : *
1. 為內置型對象進行手工初始化,因為C++不保證初始化它們。
2. 構造函數最好使用成員初始化列表,而不要在構造函數本體內使用賦值操作,但如果有多個構造函數會產生冗餘代碼時,可以將具 有確定的預設初始化值的變數賦值封裝進一個函數,其餘還是使用列表初始化。同時,列表初始化的次序也應該與聲明次序相同。
3. 為免除"跨編譯單元的初始化次序"問題,請以 local static 對象替換 non-local static 對象。