寫出更有效的C++代碼

寫在前面

一次偶然的機會同時拿到了遊戲客戶端、TA、和遊戲引擎開發的Offer
因爲對圖形感興趣,所以打算自己造一次輪子
如你所見,開始複習C++
讀了讀《Effective C++》甚至感覺這本書就是專門給我這種弟弟寫的,本文也是此書的閱讀筆記

正文

一、習慣C++

作爲一個寫了兩年C#和Shader被商業引擎慣壞了的程序員,看見老朋友甚至有點陌生

01:把C++當做一個語言聯邦

把C++當做由四個次語言組成的聯邦,從一個次語言到另一個次語言時,守則可能改變。
這四個次語言分別是:C、Object-Oriented C++、Template C++、STL

02:儘量以const、enum、inline 替換 #define

#define ASPECT_RATIO 255

開發者極有可能被它所帶來的編譯錯誤感到困惑,編譯器可能提到255而不是ASPECT_RATIO,也許該語句被其他預處理幹掉,追蹤它會浪費時間。
解決辦法就是用常量替換宏

const int AspectRatio = 255;

着重說明

  1. 由於常量經常定義於頭文件內,因此有必要將指針(而不是指針所指之物)聲明爲const

    const char* const authorName = "TOSHIRON";
    
  2. 對於Class專屬常量,爲了確保他只有一份實體,必須使其成爲static成員

    class GamePlayer
    {
    	private:
    		static const int NumTurns = 5;
    		...
    }
    
  3. 如果不想讓別人獲取到指向某個常量的指針,因爲取const地址是合法的,所以可以用enum取代

    class GamePlayer
    {
    	private:
    		enum{ NumTurns = 5 };
    		...
    }
    
  4. 用內聯函數替代宏,以獲得相同的效率和功能

    #define MAX(a,b) f((a) > (b) ? (a) : (b))
    
    template<typename T>
    inline void callWithMax(const T& a, const T& b)
    {
    	f(a > b ? a : b);
    }
    

03: 儘可能使用 const

const出現在*左邊,被指物是常量
const出現在*右邊,指針自身是常量
const出現在*兩邊,指針和所指事物都是常量

着重說明

  1. 令一個函數返回一個常量值,往往可以降低因客戶錯誤而造成的意外,而又不至於放棄安全性和高效性
    我感覺這幾乎主要是爲了單獨避免一種情況

    class Rational{...};
    const Rational operator* (const Rational& lhs, const Rational& rhs);
    ...
    Rational a,b,c;
    ...
    if(a*b = c){...}
    

    找錯麻煩?因爲常量不允許賦值所以會直接報錯

  2. 利用常量性(constness)不同,重載函數

    class TextBlock
    {
    	public:
    		...
    		const char& operator[] (std::size_t position) const //第二個const是其重載的依據
    		{
    			return text[positon];
    		}
    		char& operator[] (std::size_t position)
    		{
    			return text[position];
    		}
    	private:
    		std::string text;	
    }
    
    TextBlock tb("Hello");
    std::cout << tb[0];		//調用non-const
    
    Const TextBlock ctb("World");
    std::cout << ctb[0];	//調用const
    
  3. const成員函數不可以更改對象內任何non-static成員變量;解決辦法就是用 mutable 關鍵字修飾,使變量總是可更改的,及時在const成員函數內。

  4. 在 const 和 non-const 成員函數中避免重複,常量性重載往往伴隨着大量重複代碼,這時,我們需要讓non-const 利用 const 函數。

    class TextBlock
    {
    	public:
    		...
    		const char& operator[] (std::size_t position) const
    		{
    			...
    			...
    			return text[positon];
    		}
    		char& operator[] (std::size_t position)
    		{
    			return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
    		}
    	private:
    		std::string text;	
    }
    

    首先,將 *this 轉型爲 const,調用const成員函數,再移除const

04: 確定對象在使用前已經初始化

這是當然了,至少我使用過的任何編程語言都有要求這一點

int x;

x有可能被初始化爲0

class Point
{
	int x, y;
};
...
Point p;

p的成員變量有時候會初始化爲0,有時候不會,所以手動初始化很有必要。

着重說明

  1. C++規定,對象的成員變量初始化動作發生在進入構造函數本體之前。

    class PhoneNumber{...};
    class ABEntry
    {
    	public:
    		ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones);
    	private:
    		string theName;
    		string theAddress;
    		list<PhoneNumber> thePhones;
    		int numTimeConsulted;
    		ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
    		{
    			theName = name;
    			theAddress = address;
    			thePhones = phones;
    			numTimesConsulted = 0;
    		}
    

    書上的說法是 構造函數中那四行四賦值,而不是初始化
    構造函數應該改爲

    ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones)
    	:theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0)
    { }
    
  2. 爲解決兩個不同編譯單元內的初始化次序問題,使用local static 替代 non-local

    class FileSystem
    {
    	public: 
    	...
    	size_t numDisks() const;
    	...
    };
    extern FileSystem tfs;
    

    替代爲

    class FileSystem
    {
    	public: 
    	...
    	size_t numDisks() const;
    	...
    };
    FileSystem& tfs()
    {
    	static FileSystem fs;
    	return fs;
    }
    

    這樣,在調用時纔不用在乎初始化順序的問題

二、構造、析構、賦值運算

因爲GC的存在,很長時間沒有用析構函數了

05: 瞭解C++默默編寫並調用哪些函數

編譯器可以暗自爲 class 創建 default 構造函數、copy 構造函數、copy assignment 操作符,以及析構函數。

class Empty{};

相當於

class Empty
{
	public:
		Empty(){...}
		Empty(const Empty& rhs){...}
		~Empty(){...}
		Empty& operator = (const Empty& rhs){...}
};

着重說明

  1. 當對一個“內含reference 成員”或“內含const 成員”進行賦值操作時,編譯器自己生成的賦值重寫無法完成此工作,需要自己專門寫。
  2. 如果把賦值重寫設爲private,那更調用不了。

06: 如果不想用編譯器自動生成的函數,就應該明確拒絕

如果想要駁回編譯器自動提供的函數,可以將成員函數聲明爲 private 並且不與實現。

class Uncopyable
{
	protected:
		Uncopyable(){}
		~Uncopyable(){}
	private:
		Uncopyable(const Uncopyable&);
		Uncopyable& operator = (const Uncopyable&);
};

然後之後的類可以繼承Uncopyable,反正C++可以多繼承,但是多繼承會阻止empty base class optimization,慎重

class Abc:private Uncopyable{...}

07: 爲多態基類生命 virtual 析構函數

class TimeKeeper
{
	public:
		TimeKeeper();
		virtual ~TimeKeeper();
		...
};

class WaterClock:public TimeKeeper{...};
class AtomicClock:public TimeKeeper{...};

這時,纔會正確摧毀整個對象

WaterClock wc;
...
delete &wc;

然而,沒有多態用途的類,儘量避免使用 virtual。

08: 別讓異常逃離析構函數

  1. 在析構函數中用 try-catch 把錯誤蓋住,至少要比草率結束程序要好,比如在構造函數中建立了數據庫鏈接(?誰會幹這種傻事)

    ~DBconn()
    {
    	try{db.close();}
    	catch(...)
    	{
    		記錄錯誤日誌
    	{
    }
    
  2. 對上面的方法進行改良的話,就是用一個bool記錄是否關閉過,如果是,就不必再close了

    void close()
    {
    	db.close();
    	closed = true;
    }
    ~DBconn()
    {
    	if(!closed)
    	{
    		try{db.close();}
    		catch(...)
    		{
    			記錄錯誤日誌
    		}
    	}
    }
    

09: 絕不在構造和析構函數中調用 virtual 函數

據說像我這種C#過來的,更應該重視這一點。
主要是因爲,首先會調用基類的版本。理由就是:base class構造函數會優先於其派生類的構造函數,這時,派生類的變量什麼的還沒初始化。不會下降到派生類的重寫版。

10: 令 operator= 返回一個 reference to *this

這是一個協議?也包括*= += -= /=等。

class Widget
{
	public:
		...
		Widget& operator = (const Widget& rhs)
		{
			...
			return *this;
		}
		...
};

11: 在 operator= 中處理 “自我賦值”

大概是這麼個情況

class Bitmap{...};
class Widget
{
	...
	private:
		Bitmap* pb;
};
...
Widget w;
...
w = w;

主要爲了避免在delete的時候把=左邊的也刪除了,主要解決辦法有:

  1. 證同測試

    Widget& Widget::operator=(const Widget& rhs)
    {
    	if(this == &rhs) return *this;
    
    	delete pb;
    	pb = new Bitmap(*rhs.pb);
    	return *this;
    }
    
  2. 第二個做法是先複製一份pb,再刪除之前的,但我感覺有點浪費,所以覺得好像不是一個好方法。

12: 複製對象時不要遺漏任何一個部分

如果爲class添加了成員變量,那必須同時修改copying函數,以及operator=的重寫。

  1. 在派生類的copying函數調用基類的構造。
  2. 在operator= 中對基類成員變量賦值。

三、資源管理

好吧,我被GC慣壞了

13: 以對象管理資源

看起來沒毛病的操作

void f()
{
	Investment* pInv = createInvestment();
	...
	delete pInv;
}

竟然考慮到在…提前return的情況。使用auto_ptr 智能指針

void f()
{
	auto_ptr<Investment> pInv(createInvestment());
	...
}

auto_ptr在銷燬時會自動銷燬它的所指之物,但是要注意不能讓多個auto_ptr指向同一個對象,如果利用copying來複制,那麼將會得到"剪切"的效果。

void f()
{
	std::auto_ptr<Investment> pInv(createInvestment());
	std::auto_ptr<Investment> pInv2(pInv); //pinv 設爲null,pInv2 指向原對象
	...
}

解決辦法就是用shared_ptr 替代,“引用計數型智慧指針” 持續追蹤共有多少對象指向某筆資源,並在無人指向它時自動刪除該資源。(有點GC的意思)

void f()
{
	std::tr1::shared_ptr<Investment> pInv(createInvestment());
	std::tr1::shared_ptr<Investment> pInv2(pInv); //pInv 和 pInv2 指向同一個對象
	...
}

所以建議用 shared_ptr。

14: 在資源管理類中小心 copying 行爲

首先思考:被複制時會發生什麼?可能會重複鎖定同一個資源,在析構的時候可能重複銷燬同一個資源。
解決方法

  1. 禁止複製,利用條款6,把copying private掉。
  2. 使用引用計數法,讓最後一個使用者銷燬資源。
    class Lock
    {
    	public:
    		explicit Lock(Mutex* pm): mutexPtr(pm,unlock)   ///unlock爲刪除資源的函數
    		{
    			lock(mutexPtr.get());
    		}
    	private:
    		std::tr1::shared_ptr<Mutex> mutexPtr;
    }
    
  3. 複製底部資源,深度拷貝大法
  4. 當只想有一個對象加工資源時,可以利用auto_ptr轉變擁有權。

15: 在資源管理類中提供對原始資源的訪問

就像 auto_ptr.get() 從智能指針中獲取原始指針那樣

int daysHeld(const Investment* pi);
...
std::tr1::shared_ptr<Investment> pInv(createInvestment());
int days = daysHeld(pInv.get());

提供一個函數(顯示轉換),或者提供隱式轉換

class FontHandle{...}
class Font
{
	public:
		...
		operator FontHandle() const 
		{
			return f;
		}
	private:
		FontHandle f;
}

16: 成對使用 new 和 delete 時要採用相同形式

std::string* stringArray = new std::string[100];
...
delete stringArray;

看起來好像沒什麼毛病,但是實際上只刪除了第一個string。
應該使用

delete[] stringArray;

如果 new 表達式中使用 [],對應的 delete 表達式中也使用 []。
如果 new 表達式中不使用 [],對應的 delete 表達式中一定不要使用 []。

17: 以獨立語句將 newed 對象置入智能指針

int A();
void B(std::tr1::shared_ptr<CClass> pw, int a);

務必不要直接

B(std::tr1::shared_ptr<CClass>(new CClass), A());

因爲,編譯器執行次序不定,如果A()導致異常,可能導致難以察覺的錯誤
所以至少要把智能指針的創建分離出來

pc = std::tr1::shared_ptr<CClass>(new CClass);
B(pc, A());

四、設計與聲明

18: 讓接口容易被正確使用,不易被誤用

  1. “促進正確使用”可以 功能性相似接口的一致性,以及內置類型的行爲兼容。以C#的 .Length 和 .Count() 爲反例
  2. “阻止誤用”可以 對類型限制,建立新類型,束縛對象值。
    Date a(1998,12,28);
    //爲了防止無效日期,增加Day,Month,Year類,對Int封裝,做有效性限制
    Date a(Year(1998),Month(12),Day(28));
    
    但我覺得這十分麻煩,或許只是這個例子不好,比如2001.02.29這個日期,感覺還是在函數內檢驗比較好
  3. 利用shared_ptr提供的某個構造函數接受兩個實參:一個是被管理的指針,另一個是引用次數變成0時將被調用的“刪除器”。
    std::tr1::shared_ptr<Investment> createInvestment()
    {
    	std::tr1::shared_ptr<Investment> retVal(static_cast<Investment *>(0), getRidOfInvestment);
    	retVal = ...;
    	return retVal;
    }
    

19: 設計 class 猶如設計 type

  1. 新 type 的對象應該如何被創建和銷燬?構造,析構,內存分配及釋放
  2. 對象的初始化和對象的賦值該有什麼樣的差別?明確構造函數和賦值操作符的行爲
  3. 新 type 被 passed by value 意味着什麼? copy構造函數的實現
  4. 什麼是新 type 的“合法值”?在構造函數,賦值操作上的約束
  5. 新 type 需要配合某個繼承圖系嗎?注意他們的virtual函數
  6. 新 type 需要什麼樣的轉換? 顯示、隱式轉化 參考15
  7. 新 type 允許那些操作符和函數?
  8. 什麼標準函數需要駁回? 參考6
  9. 新 type 成員的作用域
  10. 什麼是新 type 的“未聲明接口”? 參考29 對效率。異常安全性及資源運用提供保證
  11. 如果需要一個types家族,那應該定義一個 class template
  12. 有沒有必要定義一個新的 type

20: 寧以 pass-by-reference-to-const 替換 pass-by-value

因爲值傳遞會調用 copy 構造函數帶來不必要的構造和析構,所以可以

bool validateStudent(const Student& s);

這樣,因爲const不允許更改,函數內編寫時也會自律不去修改
注意
這並不適用於內置類型,STL 迭代器 和函數對象。

21: 必須返回對象時,別妄想返回其 reference

爲了正常的 delete 和析構,在返回reference 和 object之間做出選擇。不要忘記可能同時需要多個引用或指針指向的對象,而在內存釋放上出現問題。

22: 將成員變量聲明爲 private

  1. 將成員變量聲明爲 private,可以更細微的劃分訪問控制。
    class AccessLevels
    {
    	public:
    		...
    		int getReadOnly() const { return readOnly;}
    		void setReadWrite(int value) { readWirte = value;}
    		int getReadWrite() const { return readWrite;}
    		void setWriteOnly(int value) { writeOnly = value;}
    	private:
    		int noAccess;
    		int readOnly;
    		int readWrite;
    		int writeOnly;
    }
    
  2. protected 並不比 public 更具封裝性。

23: 寧以 non-nember、non-friend 替換 member 函數

好吧,C#過來的感到震驚

class WebBrowser
{
	public:
	...
	void clearCache();
	void clearHistory();
	void removeCookies();
	...
};

現在想要做一個同時調用 WebBrowser裏三個清理函數的函數,下面兩種做法那種好

  1. 在類裏提供
    class WebBrowser
    {
    	public:
    	...
    	void clearEverthing();
    	...
    };
    
  2. 在同空間名裏額外做一個函數
    void clearBrowser(WebBrowser& wb);
    

居然是2好,因爲更具有封裝性。它的邏輯大概是這樣
首先,成員變量聲明爲 private 就意味着有更少的函數能訪問它,如果不是 private 那麼就有很多函數可以訪問它,它就不具有封裝性。那麼,越少函數能訪問 private,那封裝性越好。所以2好。

24: 若所有參數皆需類型轉換,請爲此採用 non-member 函數

令 classes 支持隱式類型轉換通常是個糟糕的主意。
請記住:如果需要爲某個函數的所有參數進行類型轉換,那麼這個函數必須是個 non-member.

25: 考慮寫出一個不拋異常的 swap 函數

  1. 交換兩個對象真正要做的是交換它們的指針
  2. 當 std::swap 對你的類型效率不高時,提供一個 swap 成員函數,並確定這個函數不拋出異常
  3. 如果你提出一個 member swap,也應該提供一個 non-member swap 來調用前者
  4. 調用 swap 時應針對 std::swap 使用 using 聲明式,然後調用 swap 並且不帶任何 “命名空間資格修飾”
  5. 爲“用戶自定義類型”進行 std templates 全特化是好的,但千萬不要嘗試在 std 內加入某些對 std 而言全新的東西

五、實現

有些細節我從來沒有考慮過

26: 儘可能拖延變量定義式的出現時間

定義一個帶有構造和析構函數類型的變量,就要承擔其構造和析構帶來的消耗。
好吧,摳得真細,說來慚愧,這是我從來沒有考慮過的事情。
再加上 通過 default 構造函數構造出一個對象然後對它賦值 比 直接在構造時指定初值 效率差。可以看情況選擇在必要時利用其 copy 構造函數初始化。
遇到循環時,我還是覺得應該在循環外聲明,構造代價小於賦值 個人認爲情況很少。

27: 儘量少做轉型動作

兩種形式的 “舊式轉型”,c風格

(T)expression
T(expression)

c++的新式轉型

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
  • const_cast 通常被用來將對象的常量性轉除
  • dynamic_cast 主要用來執行“安全向下轉型”,也就是用來決定某個對象是否歸屬繼承體系中的某個類型,效率低
  • reinterpret_cast 執行低級轉型,動作及結果取決於編譯器,不可移植
  • static_cast 強迫隱式轉換,例如將 non-const 對象轉換爲 const 對象,或將 int 轉換爲double 等。可以執行上述多種轉換或反向轉換,除了const 轉換爲 non-const(這隻有 const_cast 纔行)

注意

  1. c++ 中一個對象可以擁有一個以上的地址(如以派生類指向它 和 以基類指向它)
  2. dynmaic_cast 往往在你手上只有對象的基類但是又想當派生類處理一個對象時,只能用它處理
    class Window{...};
    class SpecialWindow: public Window
    {
    	public:
    		void blink();
    		...
    };
    typedef
    std::vector<std::tr1::shared_ptr<Window>> VPW;
    VPW winPtrs;
    ...
    for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
    {
    	if(SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
    	{
    		spw->blink();
    	}
    }
    
    所以還是儘量在基類裏提供虛函數比較好
  3. 儘可能的使用新式轉換

28: 避免返回 handles 指向對象內部成分

handles 包括 references、指針、迭代器
既然成員變量是private,就不能把它的指針return
可以參考3的做法用const

29: 爲“異常安全”而努力是值得的

異常安全函數提供以下三個保證之一:

  • 基本承諾:如果異常拋出,程序內的任何事物仍然保持在有效狀態下。雖然結果可能不同,但是沒有對象或數據結構被破壞。
  • 強烈保證:如果異常拋出,程序狀態保持不變。如果函數失敗,程序會恢復到“調用函數之前”的狀態。
  • 不拋擲保證:承諾不拋出異常,因爲他們總是能完成他們原先承諾的功能。

強烈保證往往用 copy-and-swap 的方法完成:修改對象數據的副本,然後如果不拋出異常,就將修改後的數據和原件置換。
至少完成基本承諾。

30: 透徹瞭解 inlining 的裏裏外外

  • 熱衷於 inlining 會造成程序體積太大,inline造成的代碼碰撞會導致額外的換頁行爲,降低指令告訴緩存設置的擊中率

  • 編譯器通常不對“通過函數指針而進行的調用”實施inlining

    inline void f(){...}
    void(* pf)() = f;  //指針pf指向f
    ...
    f(); //被inlined
    pf(); //不被inlined
    
  • inline 函數無法隨着程序庫的升級而升級。如果改變inline 函數 f,那所有用到f的客戶端程序都要重新編譯

  • 大多數 inlining 限制在小型、頻繁調用的函數身上

31: 將文件間的編譯依存關係降至最低

目的是降低修改實現導致不必要的編譯

  • 如果使用 object references 或 object pointers 可以完成任務,就不要使用 objects。

  • 如果可以,儘量以 class 聲明式替換 class 定義式。注意:當聲明一個函數而它用到某個 class 時,你並不需要該 class 的定義

    class Date; //class 聲明式
    //下面兩個在不使用的情況下不會用到定義式,關鍵點在於,並不是每個人都會調用它
    Date today; 
    void clearAppointments(Date d);
    
  • 聲明式和定義式提供不同的頭文件。

    #include "classafwd.h" //此頭文件中申明,但沒有定義 ClassA
    ClassA Fun1();
    void Fun2();
    
  • C++ 允許在Interfaces 內實現成員變量或成員函數。來自C#玩家的震驚

  • 程序庫頭文件應該以 “完全且僅有聲明式”的形式存在。

依賴於聲明式,不依賴於定義式的兩個手段

  1. 利用 Interface class,解除接口和實現之間的耦合關係,從而降低編譯依賴性,在interface class 內聲明 static 處理函數,和下面有點類似。
  2. Handle classes,專門用來處理的類,不會有依賴其它類的成員函數,一切輸入來自於傳遞給函數的指針或引用。
    #include "Person.h" //定義式頭文件
    #include "PersonImpl.h" //實現類的頭文件,接口相同
    Person::Person(const string& name. const Date& birthday, const Address& addr)
    	: pImpl(new PersonImpl(name,birthday,addr)){}
    std::string Person::name() const
    {
    	return pImpl->name();
    }
    

六、繼承與面向對象設計

32: 確定你的 public 繼承塑模出 is-a 關係

書上的例子很生動 -_-||
public 繼承 意味着 is-a。適用於 base classes 身上的每一件事一定也適用於 derived classes 身上。因爲每一個 derived class 對象也都是一個 base class 對象。

33: 避免遮掩繼承而來的名字

  • 只要重寫一個虛函數,基類的重載也無效了

    class Base
    {
    	private:
    		int x;
    	public:
    		virtual void mf1() = 0;
    		virtual void mf1(int);
    		virtual void mf2();
    		void mf3();
    		void mf3(double);
    		...
    };
    class Derived: public Base
    {
    	public:
    		virtual void mf1();
    		void mf3();
    		void mf4();
    		...
    };
    
    Derived d;
    int x;
    ...
    d.mf1(); //調用 Derived:mf1
    d.mf1(x); //錯誤,被掩蓋
    d.mf2(); //調用 Base::mf2
    d.mf3(); //調用 Derived::mf3
    d.mf3(x); //錯誤,被掩蓋
    

    如何解決呢?使用 using 聲明式

    class Derived: public Base
    {
    	public:
    		using Base::mf1; //讓Base class內名爲 mf1 和 mf3 的所有東西可見
    		using Base::mf3;
    		virtual void mf1();
    		void mf3();
    		void mf4();
    		...
    };
    
  • 如果我們只想讓上例中 Derived 只繼承 mf1無參版本,應該怎麼辦? 定義轉交函數

    class Derived: public Base
    {
    	public:
    		virtual void mf1({
    			Base::mf1();
    		}
    		...
    };
    

34: 區分接口繼承和實現繼承

  • C#中沒有的操作,調用基類中虛函數的實現
    class Base
    {
    	public:
    		virtual void draw() cosnt = 0;
    		...
    }
    class A:Base{...}
    ...
    Base* a = new A;
    a->draw();
    a->Base::draw(); //調用基類的虛函數
    
  • 接口繼承和實現繼承不同。在public繼承下,derived classes 總是繼承 base class 的接口。
  • pure virtual 函數只具體制定接口繼承
  • impure virtual 函數具體指定接口繼承及缺省實現繼承
  • non-virtual 函數具體指定接口繼承以及強制性實現繼承

35: 考慮 virtual 函數之外的其他選擇

  • 使用 non-virtual interface 手法,以 public non-virtual 成員函數包裹較低訪問性(private 或 protected) 的 virtual 函數。和模板模式很像

  • 將 virtual 函數替換爲“函數指針成員變量” 或 用tr1::function 成員變量替換 virtual函數

  • 將繼承體系內的 virtual 函數替換爲另一個繼承體系內的 virtual 函數

  • tr1::function 對象行爲就像一般函數指針,可以接納“與給定之目標籤名式兼容”的所有可調用物

    typedef std::tr1::function<int (const ClassA&)> TypeA;
    

    類型TypeA接受任何兼容 ClassA& ,返回 int

    可以在構造時賦值,之後使用

    class A; 
    int DealFunction(const A& a);
    class A
    {
    	public:
    		typedef std::tr1::function<int (const ClassA&)> TypeDealA;
    		explicit A(TypeDealA tda = DealFunction) : dealFun(hcf){}
    		int Deal() const
    		{
    			return dealFun(*this);
    		}
    		...
    	private:
    		TypeDealA dealFun;
    };	
    

    在C#裏通過Action或delegate來傳遞函數,C++可以直接這樣做

36: 絕不重新定義繼承而來的 non-virtual 函數

其實這本身就符合條例32,不重新 non-virtual 就對了

37: 絕不重新定義繼承而來的缺省參數值

絕對不要重新定義一個繼承而來的缺省參數值,因爲缺省參數值都是靜態綁定,而 virtual 函數——你唯一應該複寫的東西是動態綁定

class Shape
{
	public:
		enum ShapeColor
		{
			Red,
			Green,
			Blue
		};
		virtual void draw(ShapeColor color = Red) const = 0;
		...
};

class Rectangle:public Shape
{
	public:
		virtual void draw(ShapeColor color = Green) const;
		...
};
class Rectangle:public Shape
{
	public:
		virtual void draw(ShapeColor color) const;
		...
};

這個時候

Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;

pc->draw(); // color == Red 自動調用缺省
pr-draw(); // color == Red 因爲pr 的靜態類型是 Shape 所以調用 基類的缺省參數值

38: 通過複合塑造模出 has-a 或 “根據某物實現出”

  • 複合的意義和public繼承完全不同
  • 應用域,複合意味 has-a。在實現域,複合意味 is-implemented-in-terms-of

實例中彷彿在講如何處理 set 和 list 的關係。

39: 明智而審慎地使用 private 繼承

  • private 繼承 編譯器不會自動將派生類對象轉換爲 基類對象。
  • 由 基類繼承而來的所有成員,在派生類中會變成 private 屬性。
  • 作用類似 C# 中的 sealed,禁止重寫
    class Widget
    {
    	private:
    		class WidgetTimer:public Timer
    		{
    			public:
    				virtual void onTick() const;
    				...
    		}
    		WidgeTimer timer;
    		...
    }
    
  • Private 繼承意味 is-implemented-in-terms of。它通常比複合的級別低。但是當 派生類需要訪問 protected base class 的成員,需要重新定義繼承而來的 virtual 函數時,這麼設計是合理的。
  • 和複合不同,private 繼承可以造成 empty base 最優化。
  • 當面對“並不存在 is-a 關係”的兩個 classes,其中一個需要訪問另一個的 protected 成員,或需要重新定義其中一個或多個 virtual 函數,private 繼承極有可能成爲正統策略。

40: 明智而慎重地使用多繼承

  • 多重繼承比單一繼承複雜。它可能導致新的歧義性,以及對 virtual 繼承的需要。
  • virtual 繼承會增加大小、速度、初始化複雜度等等成本。如果 virtual base classes 不帶任何數據,將是最具有實用價值的情況。
  • 多重繼承的確有正當用途。其中一個情節涉及“public 繼承某個 Interface class” 和 “private繼承某個協助實現的 class” 的兩個組合。

情況說明

  1. 鑽石型多繼承導致歧義性
    class File{...};
    class InputFile: public File{...};
    class OutputFile: public File{...};
    class IOFile: public InputFile, public OutputFile{...};
    
    如果File Class有個成員變量 fileName,在IOFile中調用 fileName,就會出現歧義性。
  2. 使用 virtual base classes 繼承可以解決,但是非必要不要用,而且用也不要在 virtual base class 中放置數據。
    class File{...};
    class InputFile: virtual public File{...};
    class OutputFile: virtual public File{...};
    class IOFile: public InputFile, public OutputFile{...};
    

七、模板與泛型編程

讓我看看有什麼不一樣

41: 瞭解隱式接口和編譯期多態

  • classes 和 templates 都支持接口 (interfaces)和多態(polymorphism)。
  • 對 classes 而言接口是顯式的,以函數簽名爲中心。多態則是通過 virtual 函數發生於運行期。
  • 對 template 參數而言,接口是隱式的,奠基於有效表達式。多態則是通過 template 具體化和函數重載解析發生於編譯期。

42: 瞭解 typename 的雙重意義

在 template 聲明式中,class和typename沒有什麼不同

template<class T> class Widget;
template<typename T> class Widget;

我覺得,書上的例子太極端了。

  • 聲明 template 參數時,前綴關鍵字 class 和 typename 可互換,
  • 請使用外部關鍵字 typename 標識嵌套從屬類型名稱;但是不得在 base class lists 或 member initialization list 內以它作爲 base class修飾符。
template<typename T>
class Derived: public Base<T>::Nested //不允許
{
	public:
		explicit Derived(int x) : Base<T>::Nested(x) //不允許
		{
			typename Base<T>::Nested temp; //必須以 typename 修飾
			...
		}
		...
};

43: 學習處理模塊化基類內的名稱

class CompanyA
{
	public:
		...
		void sendCleartext(const std::string& msg);
		void sendEncrypted(cosnt std::string& msg);
		...
};
class CompanyB
{
	public:
		...
		void sendCleartext(const std::string& msg);
		void sendEncrypted(cosnt std::string& msg);
		...
};
...
class MsgInfo{...};

template<typename Company>
class MsgSender
{
	public:
		...
		void sendClear(const MsgInfo& info)
		{
			std::string msg;
			Company c;
			c.sendClearText(msg);
		}
		void sendSecret(const MsgInfo& info)
		{...}
};

因爲不知道 LoggingMsgSender 繼承什麼樣的Class,他繼承自模板,所以調用sendClear會報錯

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
	public:
		...
		void sendClearMsg(const MsgInfo& info)
		{
			sendClear(info); //報錯
		}
		...
};

解決辦法

  1. 在 base class 函數調用動作之前加上“this->”

    template<typename Company>
    class LoggingMsgSender: public MsgSender<Company>
    {
    	public:
    		...
    		void sendClearMsg(const MsgInfo& info)
    		{
    			this->sendClear(info); 
    		}
    		...
    };
    
  2. 使用 using 聲明式

    template<typename Company>
    class LoggingMsgSender: public MsgSender<Company>
    {
    	public:
    		using MsgSender<Company>::sendClear; //告訴編譯器,假設sendClear 位於 base class
    		...
    		void sendClearMsg(const MsgInfo& info)
    		{
    			sendClear(info); 
    		}
    		...
    };
    
  3. 明白支持被調用的函數位於 base class內

    template<typename Company>
    class LoggingMsgSender: public MsgSender<Company>
    {
    	public:
    		...
    		void sendClearMsg(const MsgInfo& info)
    		{
    			MsgSender<Company>::sendClear(info); 
    		}
    		...
    };
    

44: 將與參數無關的代碼抽離 templates

  • Templates 生成多個 classes 和多個函數,任何 template 代碼都不該與某個造成膨脹的 template 參數產生依賴關係
  • 因非類型模板參數造成的代碼膨脹,往往可以消除,以函數參數或 class成員變量替換 template參數
  • 因類型參數造成的代碼膨脹,往往可以降低,讓帶有完全相同二進制表述的具體類型共享實現碼

45: 運用成員函數模板接受所有兼容類型

  • 使用 member function templates (成員函數模板) 生成 “可接受所有兼容類型” 的函數
  • 如果你聲明 member templates 用於“泛化 copy 構造” 或 “泛化 assignment 操作”,你還是需要聲明正常的 copy 構造函數和 copy assignment 操作符。
template<class T>
class shared_ptr
{
	public:
		//構造
		shared_ptr(shared_ptr const& r);
		template<class Y>
		shared_ptr(shared_ptr<Y> const& r);
		
		//copy
		shared_ptr& operator = (shared_ptr const& r);
		template<class Y>
		shared_ptr& operator = (shared_ptr<Y> const& r);
		...

46: 需要類型轉換時請爲模板定義非成員函數

當我們編寫一個 class template,而它所提供之 “於此 template 相關的”函數支持“所有參數之隱式類型轉換”時,請將那些函數定義爲“class template內部的friend 函數”。

template<typename T>
class Rational
{
	public:
		Rational(const T& numerator = 0, const T& denominator = 1);
		cosnt T numerator() cosnt;
		const T denominator() const;
		...
};
template<typename T>
const Rational<T> operator* (cosnt Rational<T>& lhs, const Rational<T>& rhs){...}

然後

Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf * 2; //報錯,無法通過編譯

因爲編譯器不知道T是什麼,所以找不到正確的 operator*
必須先有相關函數推導出參數類型,聲明 operator* 爲友元函數可以化簡這個過程

template<typename T> class Rational;

template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);

template<typename T>
class Rational
{
	public:
		...
	friend const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs);
	{
		return doMultiply(lhs, rhs);
	}
	...
};

template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
	return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

現在執行不會報錯了,因爲,當對象 oneHalf 被聲明爲一個 Rational<int>,classRational<int>於是被具現化出來,而作爲過程的一部分,friend 函數operator* 也就被自動聲明出來。後者身爲一個函數而非函數模板,因此編譯器可在調用它時使用隱式轉換函數。

47: 請使用 traits classes 表現類型信息

  • STL 主要由 “用以表現容器、迭代器和算法” 的 templates 構成
  • Traits classes 使得“類型相關信息”在編譯期可用。它們以 templates 和 “templates 特化”完成實現
  • 整合重載技術後,traits classes 有可能在編譯期對類型執行 if…else 測試

48: 認識 template 元編程

  • Template metaprogramming(TMP,模板元編程) 可將工作由運行期移往編譯器,因而得以實現早期錯誤偵測和更高的執行效率
  • TMP 可被用來生成“基於政策選擇組合”的客戶定製代碼,也用來避免生成對某些特殊類型並不合適的代碼。

喫鯨,還有這種操作

template<unsigned n>
struct Factorial
{
	enum
	{
		value = n * Factorial<n-1>::value
	};
};

template<>
struct Factorial<0>
{
	enum
	{
		value = 1
	};
};

在main中

std::cout<<Factorial<5>::value; //打印 5! 120

還能這樣?這只是“hello world”而已

八、定製 new 和 delete

STL容器所使用的 heap 內存是由容器所擁有的分配器對象(allocator objects)管理,不是被 new 和 delete 直接管理。所以本章並不討論

49: 瞭解 new-handler 行爲

可以理解爲專門catch operator new 的函數

void outOfMem()
{
	std::cerr << "Unable to satisfy request for memory\n";
	std::abort();
}

int main()
{
	std::set_new_handler(outOfMem);
	int* pBigDataArray = new int[1000000000L];
	...
}

當 無法爲 100000000個整數分配足夠的空間,outOfMem會被調用。
一個設計良好的 new-handler 函數必須做以下事情:

  • 讓更多內存可被使用。這使得 new 操作下一次分配內存動作成功
  • 安裝另一個 new-handler。就像是交換機一樣,尋找合適的。
  • 卸除 new-handler。將null指針傳給 set_new_handler,拋出異常
  • 拋出 bad_alloc的異常。不會被 operator new捕捉
  • 不返回。一般是調用 abort 或 exit

50: 瞭解 new 和 delete 的合理替換機制

替換編譯器提供的operator new 或 operator delete 主要有三個理由:

  • 用來檢測運行上的錯誤
  • 增加效能
    • 增加分配和歸還的速度
    • 降低缺省內存管理器帶來的空間額外開銷
    • 彌補缺省分配器中的非最佳齊位
    • 將相關對象成簇集中
  • 爲了收集使用上的統計數據

51: 編寫 new 和 delete 時需要固守常規

  • operator new 應該內含一個無窮循環,並在其中嘗試分配內存,如果它無法滿足內存需求,就該調用 new-handler。它也應該有能力處理0 bytes申請。class 專屬版本則還應該處理“比正確大小更大的申請”。
  • operator delete 應該在收到 null 指針時不做任何事。Class 專屬版本則還應該處理“比正確大小更大的申請”。

52: 寫了 placement new 也要寫 placement delete

  • 當寫了一個 placement operator new,請確定也寫出了對應的 placement operator delete。如果沒有這樣做,你的程序可能會發生隱微而時斷時續的內存泄漏。
  • 當你聲明 placement new 和 placement delete,請確定不要無意識地遮掩了他們的正常版本。

九、雜項討論

53: 不要輕忽編譯器的警告

54: 讓自己熟悉包括 TR1 在內的標準程序庫

C++標準程序庫包括

  • STL,包括
    • 覆蓋容器,如 vector、string、map
    • 迭代器 iterators
    • 算法 find、sort、transform
    • 函數對象 less、greater
    • 各種容器適配器 stack、priority_queue
  • Iostreams,包括 自定義緩衝區、國際化I/O,以及預定義好的 cin、cout、cerr 和 clog
  • 國際化支持,多區域能力
  • 數值處理,包括 複數模板 complex 和 純數值數組 valarray
  • 異常階層體系,包括 base class expection 、derived classes logic_error 和 runtime_error
  • C89 標準程序庫

TR1 詳細描述了 14 個新組件

  • tr1::shared_ptrtr1::weak_ptr 智能指針
  • tr1::function 可表示任何符合簽名的函數和函數對象
  • tr1::bind 綁定器
  • tr1::unordered_settr1::unordered_multisettr1::unordered_map 以及 tr1::unordered_multimap 哈希表
  • 正則表達式
  • Tuples 變量組
  • tr1::array 支持成員函數的數組
  • tr1::mem_fn 類成員函數指針功能
  • tr1::referene_wrapper 讓引用行爲更新對象?
  • 隨機數工具
  • 數學特殊函數
  • C99 兼容擴充
  • Type traits 用以提供類型(types) 的編譯期信息
  • tr1::result_of template,用來推導函數調用的返回類型

55: 讓自己熟悉 Boost

C++開發者社區

寫在後面

感覺C++具有高自由度,我不得不說一下我的感觸。用C#的時候,就彷彿用SRP寫管線,一些底層的東西你沒有參與其中(比如爲物體維護光源索引),這可能也是SRP迷人的地方,輕輕鬆鬆讓Unity按照你的想法渲染;用C++感覺像是直接拿着大寫開頭的DX API寫管線,你要考慮所有細節。
閱讀此書給我打開了新世界。
書中的一部分內容我還沒有看懂,之後如果在使用過程中產生感觸,會繼續補充。
下一步我要更加熟悉C++標準庫(正如54所說),下一本書不出意外是《Effective STL》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章