條款01 視C++為一個語言聯邦
- C++是多元的,使用時以友善為主。
C++最早隻是簡單的C與對象的結合(C with classes),随着語言的發展,C++已經是一個多重範型語言,一個支援:
- 過程(procedural)
- 面向對象(object-oriented)
- 函數式(functional)
- 泛型(generic)
- 元程式設計(metaprogramming)
需要掌握其中主要的次語言(代表性的語言):
- C
- Object-oriented C++
- Template C++
- STL
根據實際切換已達到合适的程式設計節奏,如在往函數傳遞一個對象,若該對象是一個内置類型,使用C風格傳值即可;若參數是對象,那麼Object-oriented C++傳遞引用的特性就比較合适;有時候我們甚至不知道處理對象的類型,那麼則使用Template模闆抽象化參數類型;STL在使用過程中,疊代器、函數對象也應用到了C傳值風格。總而言之,C++寫程式往往不會是單一的範式,一切以友善為主。
條款02 盡量以const,enum,inline替換#define
- 對于單純的常量最好以const或enums代替#define
- 形似函數的宏,最好改用inline函數
- 為什麼要用const代替?
- 目标碼可能更小
- 更容易發現錯誤(進入記号表)
- 有作用域範圍
-
為什麼要用enums?
因為const具有作用域,自然可能需要一個類專屬的常量,隻保有一個實體,應聲明為 static const:
class A
{
public:
static const int size= 3;//舊式編譯器不支援
A() { std::cout << "A()" << std::endl; }
int a[size];
};
條款提到enum隻是為了給出編譯器不支援static const 類内初始化的解決方案。
- 為什麼用inline函數代替#define定義的函數?
為了防止降低#define容易忘記小括号導緻的錯誤,除此inline是個函數,遵循通路規則和作用域限制。
條款03 盡可能使用const
const這個東西在C++中很多地方都有用到,作用非常廣泛:
- classes外部修飾global或者namespace作用域的常量
- 檔案、函數和塊作用聲明static的對象
- 指針常量、常量指針和引用常量
3.1 const和指向性類型讨論
const的文法與指向類型結合含義較為複雜,簡單來說,const出現在左邊表示所指之物為常量,出現在右邊則表示指向不變,前者是對象具有low-level屬性,後者表示對象具有top-level屬性,當然也可以同時具備兩種。PS,因為引用從綁定之刻就不會發生改變,是以隻具有low-level屬性。
在聲明中,const可以和函數傳回值、參數和成員函數本身起作用,避免一些無意義的情況。const作為傳回值目的是防止類似以下的情況:
class Ratioanl{...}
const Rational operator*(const Rational &lhs,const Rational &rhs);
int main()
{
Rational a,b,c;
(a*b)=c;//a*b傳回值是一個局部變量,進行指派,整個式子沒有意義,用const可以避免,
if(a*b=c){...}//a*b傳回值指派運算始終為真,可以通過const避免
}
3.2 const成員函數讨論
const成員函數是必要的,接口更加清晰,是提升C++效率的重要手段;const成員函數是保證不更改資料成員的聲明;const成員函數分為兩大陣營bitwise和logic,後者通過關鍵字mutable實作bitwise檢查豁免,最後作者介紹了常量性移除用于優化、簡化代碼。
const成員函數很重要,基于兩個理由:
- 接口更加清晰
- 使操作const對象成為可能
void A::fun(int i)const;
盡管
const A a
執行個體是一個常量,但是我們仍能對其進行操作,在保證不修改常量執行個體下進行操作,作者說,改善C++程式效率的根本辦法是pass by reference-to-const,如果不能操作const對象,改善無從談起。作者還提到,const函數和非const函數是一對重載聲明:
void fun(int i)const ;
void fun(int i);
const 成員函數的兩個陣營:bitwise constness(physical constness)和logic constness,這兩個陣營其實争論點在于如何去定義一個const對象,是一個資料成員改變就是nont-const(bitwise constness),還是根據用于具體邏輯,改變一個兩個都可以視作const(logic constness)。
預設情況C++将使用bitwise constness進行常量屬性檢查,但不是所有情況都工作良好,看下面這個例子:
class CTextBlock{
public:
char &operator[](std::size_t position) const;
{
return pText[position];
}
private:
char * pText;
}
bitwise constness失效情況。編譯器根據bitwise constness預設規則檢查後發現下标運算符實作中沒有改變資料成員pText的值,編譯順利通過,但是其傳回了一個左值char &,如果使用者做如下操作:
const CTextBlock cctb("hello");
char *pc=&cctb[0];
*pc="j";
雖然使用者本意是建立一個常量的hello,但是暴露出的引用卻改變了他的常量屬性。
在有些應用,我們希望打破這種bitwise檢查:
class A
{
public:
void doSomething()const
{
m_i=555;//error,bitwise檢查失敗。
m_j=564;//ok,logic檢查,特别通行證
}
private:
int m_i=4396;
mutable int m_j=567;
}
有時候我們既要const成員函數,也要non-const成員函數,但是其中内容相差不多,代碼較為備援。作者提供了一種解決方法:常量性移除(casting away constness)。
class TextBlock
{
public:
const char& operator[](std::size_t position) const{
return text[postion];
}
char &operator[](std::size_t postion)
{
return
const_cast<char &> //強制轉換成const char&
(static_cast<const TextBlock&>(*this) //強制轉換成const,以便複用const char &operator
[position];
)
}
}
這裡利用了兩次強制轉換:
- 第一次,為*this添加const屬性以便調用const成員函數
- 第二次,為const operator[ ]移除const,還原none-const屬性
注意,是在一般函數調用const函數,不是const函數調用一般函數!後者破壞了const的語義。
條款04 确定對象被使用前已先被初始化
4.1 區分初始化和指派
一個對象要麼執行預設初始化,要麼執行值初始化。内置類型有初值則執行值初始化;沒有初值執行預設初始化,預設初始化行為與作用域相關,要麼初始化為0,要麼不執行初始化。
預設初始化結果 | 作用域 |
---|---|
不初始化 | 非static和全局作用域 |
初始化為0 | static和全局作用域 |
推薦使用類内初值或初始化清單進行初始化,而不是先構造再進行指派;有些情況不得不采用前者,這是因為以下情況:
- const成員
- 引用對象
- 資料成員未提供預設構造
清單初始化或者類内初值發生時間早于構造函數。另外,清單初始化在某個情況比類内初值更加靈活,例如一個類中定義了多個構造函數,每個構造函數的初值都有所不同,類内初值都是相同的初值,顯然不合适。
4.2 注意初始化順序
初始化清單隻說明資料成員的值,并不保證初始化順序。看看下面這個例子:
class X
{
int i;
int j;
public:
X(int val):j(val),i(j){}
}
正如一開始說到的,清單初始化表示的是j的值是val,i的值是j,誰先誰後并不保證,是以可能出現j尚未被val初始化就被用于i指派,這樣的i值是沒有任何意義的。C++ primer建議,如果可能盡量避免使用某些資料成員來初始化其他成員。
C++對于定義于不同編譯單元内的non-local-static對象的初始化順序是不确定的。static對象其生存周期是從構造到程式結束,它可以分為local-static和non-local-static兩種,local-static隻有一種情況,那就是位于函數作用域中的;其他的,如global、namespace、class和file作用域都是屬于non-local-static,static對象隻在main()程式結束後調用。當一個non-static對象(以global為例),在一個檔案中被初始化,在另一個檔案中被使用,C++無法為我們保證static在使用之時被初始化,是以可能出現一些意想不到的現象。為了解決non-local-static對象這種不确定性,我們可以通過定義一個函數傳回一個local-static對象的指針或者引用,因為local-static會在第一次調用這個函數進行初始化,而不是在運作前就進行初始化,進而避免了因為調用順序而導緻的問題,這其實是單例模式的一種常見實作,但是在多線程中可能會出現競争問題,如何解決?通過在單線程啟動階段手動調用所有的reference-returning函數。