- 條款01:視C++為一個語言聯邦(View C++ as a federation of languages)
- 條款02:儘量以const、enum、inline替換#define(Prefer consts, enums, and inlines to #define)
- 替換原因
- 兩種特殊常量
- 形似函式的宏
- 條款03:儘可能使用const(Use const whenever possible)
- const和指標
- const成員函式
- 在const和non-const成員函式中避免重複
- 條款04:確定物件被使用前已先被初始化(Make sure that objects are initialized before they’re used)
- 成員初值列
- 成員初始化的次序
條款01:視C++為一個語言聯邦(View C++ as a federation of languages)
C++有4個主要的次語言(sublanguage):
- C。包含區塊(blocks)、語句(statements)、前處理器(preprocessor)、內建資料型別(built-in data)、陣列(arrays)、指標(pointers)等;沒有模板(templates)、異常(exceptions)、繼承(inheritance)。
- Object-Oriented C++。這是C with classes部分,包含classes(包括建構函式和解構函式)、封裝(encapsulation)、繼承(inheritance)、多型(polymorphism)、virtual函式等。
- Template C++。這是C++泛型程式設計(generic programming)部分。
- STL。涉及容器(containers)、迭代器(iterators)、演算法(algorithms)、函式物件(function objects)。
Tips:
- C++高效程式設計守則視情況而變化,和使用的次語言種類有關
條款02:儘量以const、enum、inline替換#define(Prefer consts, enums, and inlines to #define)
該條款可表達為:寧可以編譯器替換前處理器
替換原因
#define ASPECT_RATIO 1.653
const double AspectRatio = 1.653; //以常量替換宏
替換原因:
- 記號名稱ASPECT_RATIO可能在編譯器開始處理原始碼之前被前處理器移走,而未被編譯器看到,沒有進入記號表(symbol table),故在編譯錯誤涉及該常量時難以確定1.653的來源。使用語言常量AspectRation則不會有這個問題。
- 對浮點常量(floating point constant,如本例),使用常量可能比使用#define導致更少量的碼,因為前處理器盲目的將宏名稱替換為1.653可能導致目標碼(object code)出現多份1.653,改用常量AspectRatio則不會有此問題
兩種特殊常量
常量指標(constant pointers):常量定義式常位於標頭檔案,故有必要將指標宣告為const
const char* const authorName = "Scott Meyers";
const std::string authorName("Scott Meyers"); //使用string更合適
class專屬常量:為了將常量作用於(scope)限制在class內,需要讓其成為class的一個成員(member);為了確保此常量至多隻有一份實體,需要讓其成為static成員
class GamePlayer {
private:
static const int NumTurns = 5; //常量宣告式
int scores[Numturns]; //使用該常量
...
};
當某個東西是class專屬常量+static+整數型別(integral type,如ints、chars、bools),只要不取地址,則可是有宣告式而無定義式,否則需要提供定義式。
//應放入實現檔案而非標頭檔案
const int Gameplayer::NumTurns; //NumTurns的定義,宣告時設定了初值故此處可不設定值
舊編譯器可能不允許static成員在宣告式上獲得初值,此時可將初值放在定義式。
class CostEstimate {
private:
static const double FudgeFactor; //static class常量宣告,位於標頭檔案
};
const double CostEstimate::FudgeFactor = 1.35; //static class常量定義,位於實現檔案
若譯器不允許static成員在宣告式上獲得初值,且class編譯期間需要一個class常量值(如存在陣列宣告式),則可用“the enum hack”補償,利用列舉型別(enumerated type)的數值可充當ints使用的特點。
enum hack有以下特點:
- 取enum地址不合法,可避免存在指向其的pointer或reference,進而不會導致非必要的記憶體分配
- “enum hack”是template programming(模板超程式設計)的基礎技術
class GamePlayer {
private:
enum { NumTurns = 5 }; //令NumTurns成為5的一個記號名稱
int scores[NumTurns];
...
};
形似函式的宏
類似函式的宏(macros)沒有函式呼叫(function call)帶來的額外開銷,但其缺點顯著,最好替換為inline函式
//帶宏實參的宏,每個實參都需要加上小括號,然而還是可能出現難以預料的問題
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
//使用template inline實現宏的高效以及函式的可預料性和型別安全性(type safety)
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
Tips:
- 對於單純常量,最好用const或enums替換#define
- 對於形似函式的宏,最好用inline函式替換#defines
條款03:儘可能使用const(Use const whenever possible)
const和指標
- 常量指標:const在星號*左邊,則被指物是常量
- 指標常量:const在星號*右邊,則指標自身是常量
void f1(const Widget* pw); //被指物是常量
void f2(Widget const * pw); //同上
STL迭代器的作用類似T*指標,其同樣有指標常量和常量指標的用法
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin(); //指標常量
*iter = 10; //正確,改變iter所指物
++iter; //錯誤!iter本身是const
std::vector<int>::const_iterator cIter = vec.begin(); //常量指標
*cIter = 10; //錯誤!*cIter是const
++cIter; //正確,改變cIter本身
const成員函式
const成員函式有兩個作用:
- 使class介面容易被理解
- 使操作const物件成為可能
//兩個成員函式如果只是常量性(constness)不同,可以被過載
class TextBlock {
public:
...
const char& operator[](std::size_t position) const //對於const物件的運算子[]
{ return text[position]; }
char& operator[](std::size_t position) //對於non-const物件的運算子[]
{ return text[position]; }
private:
std::string text;
};
//operator[]使用方式如下
TextBlock tb("Hello");
std::cout << tb{0]; //呼叫non-const TextBlock::operator[]
tb[0] = 'x'; //正確,寫一個non-const TextBlock,operator[]返回reference to char
const TextBlock ctb("World");
std::cout << ctb[0]; //呼叫const TextBlock::operator[]
ctb[0] = 'x'; //錯誤!寫一個const TextBlock,operator[]呼叫合法,但對其返回的const賦值非法
//更真實的例子
void print(const TextBlock& ctb) //此函式中ctb是const
{
std::cout << ctb[0]; //呼叫const TextBlock::operator[]
...
}
const成員函式有兩個流行概念:
- bitwise const(或physical const):const成員函式不能更改物件的任何成員變數(static除外)
- logical const:const成員函式可以修改其所處理的物件內的某些const
當只有指標(而非其所指物)隸屬於物件,此時更改了指標所指物的成員函式不具備十足的const性質但編譯器認為其滿足bitwise const
class CtextBlock {
public:
...
char& operator[](std::size_t position) const //bitwise const宣告,但不適當
{ return pText[position]; } //operator[]實現程式碼並不更改pText本身
private:
char* pText; //只有指標(而非其所指物)隸屬於物件
}
const CTextBlock cctb("Hello"); //宣告一個常量物件
char* pc = &cctb[0]; //呼叫const operator[]獲得一個指標,指向cctb的資料
*pc = 'J'; //cctb變為"Jello"
當有些量需要修改而違反編譯器的bitwise const,則可利用C++的一個與const相關的擺動場:mutable,釋放掉non-static成員變數的bitwise constness約束
class CTextBlock {
public:
...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength; //mutable使其可在const成員函式內更改
mutable bool lengthIsValid; //否則不能更改,編譯器會堅持bitwise const
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); //正確,宣告時有mutable,否則錯誤
lengthIsValid = true;
}
return textLength;
}
在const和non-const成員函式中避免重複
如果non-const和const operator[]相同,則程式碼會過長。可讓non-const operator[]呼叫const operator[]避免程式碼重複。這需要將常量性轉除(casting away constness)。
class TextBlock {
public:
...
const char& operator[](std::size_t position) const
{
... //邊界檢驗(bounds checking)
... //志記資料訪問(log access data)
... //檢驗資料完整性(verify data integrity)
return text[position];
}
char& operator[](std::size_t position)
{
return
const_cast<char&>( //將op[]返回值的const轉除
static_cast<const TextBlock&>(*this) //為*this加上const
[position] //呼叫const op[]
);
}
private:
std::string text;
}
上述程式碼包含兩個轉型動作:
- 將*this從其原始型別TextBlock&轉型為const TextBlock&,則之後operator[]會呼叫const版本而非non-const版本。直接在non-const operator[]內部呼叫operator[]會遞迴呼叫自己。
- 從const operator[]的返回值中移除const。
在const成員函式中呼叫non-const成員函式會有風險,因為物件有可能因此被改動。
Tips:
- 將某些東西宣告為const可幫助編譯器偵測出錯誤用法。const可被施加於在任何作用域內的物件、函式引數、函式返回型別、成員函式本體
- 編譯器強制實施bitwise constness,但編寫程式時應該使用概念上的常量性(conceptual constness)
- 當const和non-const成員函式有著實質等價實現時,令non-const版本呼叫const版本可避免程式碼重複
條款04:確定物件被使用前已先被初始化(Make sure that objects are initialized before they’re used)
- 使用C part of C++且初始化可能導致執行成本,則C++不保證初始化這些物件
- non-C parts of C++的規則有變化
int x; //x在某些語境中會被初始化(為0),但是其他語境中不保證
class Point {
int x, y;
};
...
Point p; //p的成員變數有時候被初始化(為0),有時候不會
由於是否初始化難以確定,故最好永遠在使用物件之前先將他初始化,對於內建型別以外的任何東西,確保每一個建構函式(constructors)都將物件的每一個成員初始化。
成員初值列
class PhoneNumber {...};
class ABEntry { // Addrress Book Entry
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumer>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNmuber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumer>& phones);
{
theName = name; //這都是賦值(assignments)而非初始化(initializations)
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
上述程式碼中,theName、theAddress、thePhones在進入ABEntry建構函式之前已經被初始化,而numTimesConsulted則不確定是否已被初始化。
ABEntry建構函式最好使用成員初值列(member initialization list)替換賦值動作
ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumer>& phones);
:theName(name), //這些都是初始化
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{} //建構函式本體沒有動作
ABEntry();
:theName(), //也可指定無物(nothing)呼叫default建構函式
theAddress(),
thePhones(),
numTimesConsulted(0)
{} //建構函式本體沒有動作
初值列的效能消耗:
- 一般而言,使用成員初值列只呼叫一次copy建構函式比先呼叫default建構函式再呼叫copy assignment運算子更高效
- 對於內建型物件,初始化和賦值的成本相同,但在初值列中初始化可提升一致性
成員初值列的使用建議:
- 總是在初值列中列出所有成員變數,以免還得記住哪些成員變數無需初值
- 如果成員變數是const或reference,則一定要在初值列中初始化,其不能被賦值
- 對於擁有多個建構函式且存在許多成員變數或base classes的classes,可在初值列中省略賦值和初始化的效能消耗相當的成員變數,將它們的賦值移往某個函式(通常是private),供建構函式呼叫
- 在成員變數的初值有檔案或資料庫讀入時很有效
成員初始化的次序
C++的初始化次序固定為:
- base classes先於derived classes初始化
- class的成員變數按其宣告次序初始化
需要額外關注不同編譯單元內定義的non-local static物件,C++對這類物件的初始化次序無明確定義,故如果這類物件存在依賴關係可能會出問題。決定這類物件的初始化次序非常困難,最常見的形式是經由模板隱式具現化(implicit template instantiations)形成
- static物件:其壽命從被構造出來直到程式結束為止,其解構函式在main()結束時被呼叫,包括global物件、定義餘namespace作用域內的物件、在classes內、在函式內、以及在file作用域內被宣告為static的物件
- local static物件:函式內的static物件
- non-local static物件:其他static物件
- 編譯單元(translation unit):產出單一目標檔案(single object file)的原始碼,基本上是單一原始碼檔案加上其所含入的標頭檔案(#include files)
//假設自己有一個FielSystem Class
class FileSystem {
public:
...
std::size_t numDisks() const;
...
};
extern FileSystem tfs;
//假設客戶在其他位置建立一個class以處理檔案系統內的目錄
class Directory {
public:
Directory( *params* );
...
};
Directory::Directory( *params* )
{
...
std::size_t disks = tfs.numDisks(); //使用tfs物件
...
}
Directory tempDir( *params* ); //放臨時檔案的目錄
上述程式碼無法保證tfs在tempDir之前被初始化
解決方案:將每個non-local static物件搬到自己的專屬函式內並宣告為static,這些函式返回一個reference指向它所含的物件。換句話說,non-local static物件被替換為local static物件,這是單例(Singleton)模式的一個常見實現手法。
class FileSystem {...}; //同前
FileSystem& tfs() //初始化一個local static物件並返回指向其的reference
{
static FileSystem fs;
return fs;
}
class Directory {...}; //同前
Directory::Directory( *params* )
**{
...
std::size_t disks = tfs().numDisks(); //呼叫tfs函式,而非直接用reference to tfs
}
Directory& tempDir()
{
static Directoy td;
return td;
}
reference-returning函式=定義並初始化一個local static物件+返回它
由於在多執行緒環境下任何non-const static物件都會有麻煩,則可在程式的單執行緒啟動階段(singl-threaded startup portion)手工呼叫所偶reference-returning函式,以消除與初始化有關的競速形式(race conditions)
Tips:
- 為內建型物件進行手工初始化,因為C++不保證初始化它們
- 建構函式最好使用成員初值列,而不要在建構函式本體內使用賦值操作。初值列列出的成員變數次序應和class中的宣告次序相同
- 為避免跨編譯單元的初始化次序問題,請以local static物件替換non-local static物件