Effective C++ 學習筆記 第一章:讓自己習慣 C++

本來看這本書已經好幾天了,沒準備做筆記,但看了幾個條款,發現這本書總結的太好了,不記一下,回頭忘了不好,如果對其他人有幫助就更好了。

條款 01:讓自己習慣 C++

Accustoming Yourself to C++.

C++是語言聯邦,它綜合了多種編程語言的特點,是多重範型編程語言(注意是範型,不是泛型),支持過程形式(procedural),面向對象形式(object-oriented),函數形式(functionnal),泛型形式(generic),元編程形式(meta programming)。

將 C++認爲是多種次語言的結合,次語言有四種:

  • C 語言基礎。C++最早出現時,從 C 中派生出來的那些特性。
  • Object-Oriented C++。也就是 C with classes,類,繼承,多態,虛函數這些概念。
  • Template C++。C++的泛型編程,類和對象泛型化。
  • STL:最重要的模板庫,提供容器、迭代器、算法和函數對象等。

在次語言之間切換工程,需要認真遵守當前次語言的規範。

原文建議

C++高效編程守則視狀況而變化,取決於你使用 C++的哪一部分。

條款 02:儘量以const, enum, inline 替換 #define

** Prefer consts, enums, and inlines to #defines.**

話題 1:用 const 和 enum 代替 #define 常量

#define 定義之下的標記不會經由編譯器處理,記住這個道理,在編譯器之前被預處理器處理了,所以 #define 引入的問題,編譯器很難查出來。使用 const 代替 #define 來定義常量,編譯器可以幫助檢查如類型錯誤這一類問題。

const 作用於指針,分爲作用到指針指向對象的不變性和指針本身的不變性。

以下代碼定義了一個類內的類靜態常量成員(類專屬常量成員):

class Game {
private:
    static const int NumTurns = 5;    // 注意這是聲明,因爲是常量,必須這裏賦值,有關於爲什麼聲明時可賦值,接下來會介紹
    int scores[NumTurns];             // 可直接使用該常量
}

如果是類靜態整形常量成員,C++ 要求如果你要取 NumTurns 的地址,或者編譯器要查看,那必須爲其提供一個定義式,放到實現文件中:

const int Game::NumTurns;            // 這是定義,因爲聲明時已指定常量值,這裏不能再指定一次

現代編譯器允許對類靜態整形常量成員在聲明時指定常量值。

對於非整數的其他類靜態常量成員,無法在聲明中指定常量值,這時可通過定義指定常量(前者在聲明時指定常量,是爲了能在後續的其他聲明時用,比如 scores 數組長度)。

書中還給出了補償做法。如果編譯器不允許在聲明時指定常量值,可採用 enum 方式:

class Game {
private:
    enum { NumTurns = 5 };          // 這個叫 enum hack
    int scores[NumTurns];

enum 無法取地址,所以其更像 #define 而不是 const,如果你希望能約束對 NumTurns 的取地址操作,可採用 enum hack 的方式。

enum hack 是模板元編程的基礎技術。

話題 2:用 inline 取代 #define 宏

我們已經知道 #define 宏來作爲函數功能會導致一些難以調試的 bug。
爲了避免這些 bug,我們需要做保護性操作,比如對每個參數都加括號,但依然對一些情況無法避免。例子如下:

#define CALL_WITH_MAX(a, b) f((a) > (b)) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b);              // a 被累加 2 次
CALL_WITH_MAX(++a, b+10);           // a 被累加 1 次

如註釋說明。這種問題很難調試,因爲要記住,#define 不會輸入編譯器。

話題 3:#define 不等同於預處理操作

不要認爲因爲 #define 有各種問題,就否定預處理操作。除了 #define 以外,還有很多預處理指令。

  • #include 在文件包含中必不可少;
  • #ifdef 和 #ifndef 在編譯控制中無法被替代;
  • #pragma 控制編譯特性也會被用到;

原文建議

  • 對於單純常量,最好用 const 對象或 enum s 替換 #define s。
  • 對於 #define 宏,最好用 inline 函數替換。

條款 03:儘可能使用 const

** Use const whenever possible. **

話題 1:const 修飾指針

衆所周知,const 修飾指針分爲修飾指針本身(指針只能指向固定的地址,但指向位置的值可變)和修飾指針指向的對象(指針指向的位置的值固定,但指針可指向不同地址),或者兩者。
對於後者,有些人習慣將 const 寫在類型前,有些人習慣寫在 * 前,這都是對的。

void f1(const Widget * pw);
void f2(Widget cosnt * pw);

使用迭代器時,默認迭代器 iterator 在實現上等同於 T *,對其進行 const 修飾,等同於修飾指針本身爲 const,若希望實現 const 修飾迭代器指向位置的值爲固定值,使用 const_iterator

const std::vector<int>::iterator iter = vec.begin();          // 等同於 T * const
*iter = 10;        // 正確
iter ++;           // 錯誤
std::vector<int>::const_iterator cIter = vec.begin();         // 等同於 const T *
*cIter = 10;       // 錯誤
cIter ++;          // 正確

話題 2:const 修飾函數

const 修飾返回值時,可以有效防止錯誤的自定義類型賦值操作:

class Rational { ... };
const Rational operator* (const Rational &lhs, const Ratonal &rhs);
Rational a, b, c;
(a * b) = c;      // 編譯器會報錯,不能給 const 類型賦值
if (a * b = c) { ... }  // 錯將 == 寫作 =,編譯器會報錯,不能給 const 類型賦值

上例中,如果operator* 沒有將返回值修飾爲const,則後續的賦值操作編譯器不會報錯,但其本身也沒意義,還可能隱含有 bug 難以排查。
內置類型不存在這個問題,無法給內置類型運算結果賦值。

話題 3:const 修飾成員函數(重要)

可針對同一種功能接口(成員函數)區分兩種不同返回類型(non-const 和 const),來分別處理 non-const 對象和 const 對象。

另外,重要哲學思辨:const 修飾成員函數本身有兩種解釋:bitwise constness(又稱 physical constness)和 logical constness。
bitwise constness 認爲,成員函數只有在不更改任何成員變量(static 除外)時,纔是 const 的。
logical constness 說,你有漏洞,const 的成員函數中,通過指針更改了指針所指物,如果所指物本身屬於類,而指針本身不會在 const 的成員函數中被修改,那麼編譯器時不會報錯的,比如:

class CTB {
public:
	char& operator[] (std::size_t position) const       // bitwise const 聲明
	{
		return pText[position];
	}
private:
	char* pText;
}

但實際操作中,operator[] 還是會修改類成員變量:

const CTB cctb("Hello");          // 常量對象
char* pc = &cctb[0];              // 使用常量成員函數 operator[] 讀取數據地址
*pc = 'J';                        // 編譯器正常輸出,此時cctb.pText = "Jello"

logical constness 認爲,const 修飾成員函數,可以修改他所處理的對象內的數據,但只有當客戶端感知不到的情況下才可以。
編譯器按 bitwise constness 檢查代碼,但程序員應該做到 logical constness。
另外,引入 mutable 關鍵字,用來釋放掉非靜態成員變量的 bitwise constness 約束:

class CTB {
public:
	std::size_t length() const;
private:
	std::size_t textLength;
	mutable std::size_t mTextLength;   // 該成員變量可以在 const 成員函數內被改變
};
std::size_t CTB::length() const
{
	textLength = 2;        // 錯誤,const 成員函數內無法給成員賦值
	mTextLength = 2;       // 正確
}

話題 4:const_cast 應用

const_cast 用於做與 const 相關的類型轉換,移除 const 或添加 const。
當類中 non-const 和 const 版本的相同功能成員函數的內部功能完全一致的情況下,爲了避免重複 copy-paste,可採用 const_cast 來協助。
請查看如下示例代碼:

class TB {
public:
	const char& operator[] (std::size_t position) const
	{ ... }
	char& operator[] (std::size_t position)
	{
		return
			const_cast<char&>(						// 移除 operator[] const 中的 const
				static_cast<const TB&>(*this)       // 爲 *this 加上 const 修飾,防止自遞歸
					[position]						// 調用 const operator[]
				);
	}
};

注意,反過來,在 const 成員函數中使用這種方法調用 non-const 成員函數,是不合理的。

原文建議

  • 將某些東西聲明爲 const 可以幫助編譯器檢查錯誤。 const 可用於任何作用域內的對象、函數參數、函數返回類型、成員函數本體。
  • 編譯器強制執行 bitwise constness,但編程時應當按照 logical constness 完成。
  • 當 const 和 non-const 成員函數有完全等價的實現時,可使 non-const 版本調用 const 版本來避免代碼重複。

條款 04:確定對象被使用前已先被初始化

** Make sure that objects are initialized before they’re used. **
C++ 在一些情況下,不會給用戶未指定的對象賦初值,這種時候,對象的值是未定義的任意值。
解決方法就是,確保都被初始化,如果是非基本類型對象,確保在構造函數中對對象的所有數據對象初始化。

話題 1:不要混淆賦值和初始化

看下邊的例子:

class ABE {
public:
	ABE(const std::string &name);
private:
	std::string theName;
	int num;
};
ABE::ABE(const std::string& name)
{
	theName = name;			// 這個是賦值,而不是初始化
	num = 0;				// 這個也是賦值
}

如註釋述,這種方式是賦值,雖然也能達到效果,但 C++規定,對象的成員變量的初始化動作要發生在進入構造函數本體之前,所以實際上編譯器會在進入構造函數之前,先給所有數據成員做初始化,然後進去後再做賦值操作。
應當使用初始化列表來完成初始化(而不是在構造函數中賦值)。初始化列表這麼用:

ABE::ABE(const std::string& name)
  : theName(name),
    num(0)
{ }

這麼做效率高,因爲不需要先調 ABE 的 defualt 構造函數再調用 std::string 的 copy 賦值操作,而是直接調用 std::string 的 copy 操作。內置類型沒有影響,但考慮到格式統一,也放到初始化列表中爲宜。
但注意,如果沒有手動對內置類型對象做初始化,可能編譯器也不會幫你做。
建議所有數據成員都寫在 default 構造函數的初始化列表中,包括內置類型。這樣另外一個好處是,const 和 reference 的數據成員可以被初始化了,而賦值不行,既然初始化列表都能做,幹嘛還在構造函數內賦值呢。
初始化列表中的次序與其被初始化的順序無關,初始化順序取決於數據成員的聲明順序,但爲了便於理解,建議順序統一。這種場景比如先初始化 array 的長度,再初始化 array,反過來就會出錯。

話題 2:non-local static 對象

static 對象的生存期從其構造出開始,到程序結束爲止。
位於函數中的 static 對象稱爲 local static 對象,否則稱爲 non-local static 對象。
C++對定義在不同的編譯單元(不同的源文件內)中的 non-local static 對象的初始化次序是未定義的。
所以存在的問題是:如果多個編譯單元內同時有non-local static 對象,而且他們有依賴,將怎麼保證正確性?
答案是沒辦法做到。
建議是,把這些 non-local static 對象放到函數裏邊,變成 local static 對象,然後通過調用函數傳引用的方式來使用這些對象。編譯器可以保證在調用函數且遇到 local static 對象時將其初始化。
但是,在多線程應用時,依然會存在問題,解決辦法是,在多線程應用的單線程運行時(啓動多線程之前),將這些函數都調用一下,讓編譯器創建其內部的所有 local static 對象。

原文建議

  • 爲內置類型對象進行手動初始化,C++不保證爲他們初始化。
  • 構造函數最好用成員初始化列表,而不要在內部使用賦值操作。初始化的排列次序與初始化列表中順序無關,和聲明次序一致。
  • 爲避免跨編譯單元的初始化次序問題,將這些 non-local static 對象變成 local static 對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章