寫在前面
一次偶然的機會同時拿到了遊戲客戶端、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;
着重說明
-
由於常量經常定義於頭文件內,因此有必要將指針(而不是指針所指之物)聲明爲const
const char* const authorName = "TOSHIRON";
-
對於Class專屬常量,爲了確保他只有一份實體,必須使其成爲static成員
class GamePlayer { private: static const int NumTurns = 5; ... }
-
如果不想讓別人獲取到指向某個常量的指針,因爲取const地址是合法的,所以可以用enum取代
class GamePlayer { private: enum{ NumTurns = 5 }; ... }
-
用內聯函數替代宏,以獲得相同的效率和功能
#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出現在*兩邊,指針和所指事物都是常量
着重說明
-
令一個函數返回一個常量值,往往可以降低因客戶錯誤而造成的意外,而又不至於放棄安全性和高效性
我感覺這幾乎主要是爲了單獨避免一種情況class Rational{...}; const Rational operator* (const Rational& lhs, const Rational& rhs); ... Rational a,b,c; ... if(a*b = c){...}
找錯麻煩?因爲常量不允許賦值所以會直接報錯
-
利用常量性(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
-
const成員函數不可以更改對象內任何non-static成員變量;解決辦法就是用 mutable 關鍵字修飾,使變量總是可更改的,及時在const成員函數內。
-
在 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,有時候不會,所以手動初始化很有必要。
着重說明
-
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) { }
-
爲解決兩個不同編譯單元內的初始化次序問題,使用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){...}
};
着重說明
- 當對一個“內含reference 成員”或“內含const 成員”進行賦值操作時,編譯器自己生成的賦值重寫無法完成此工作,需要自己專門寫。
- 如果把賦值重寫設爲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: 別讓異常逃離析構函數
-
在析構函數中用 try-catch 把錯誤蓋住,至少要比草率結束程序要好,比如在構造函數中建立了數據庫鏈接(?誰會幹這種傻事)
~DBconn() { try{db.close();} catch(...) { 記錄錯誤日誌 { }
-
對上面的方法進行改良的話,就是用一個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的時候把=左邊的也刪除了,主要解決辦法有:
-
證同測試
Widget& Widget::operator=(const Widget& rhs) { if(this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this; }
-
第二個做法是先複製一份pb,再刪除之前的,但我感覺有點浪費,所以覺得好像不是一個好方法。
12: 複製對象時不要遺漏任何一個部分
如果爲class添加了成員變量,那必須同時修改copying函數,以及operator=的重寫。
- 在派生類的copying函數調用基類的構造。
- 在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 行爲
首先思考:被複制時會發生什麼?可能會重複鎖定同一個資源,在析構的時候可能重複銷燬同一個資源。
解決方法
- 禁止複製,利用條款6,把copying private掉。
- 使用引用計數法,讓最後一個使用者銷燬資源。
class Lock { public: explicit Lock(Mutex* pm): mutexPtr(pm,unlock) ///unlock爲刪除資源的函數 { lock(mutexPtr.get()); } private: std::tr1::shared_ptr<Mutex> mutexPtr; }
- 複製底部資源,深度拷貝大法
- 當只想有一個對象加工資源時,可以利用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: 讓接口容易被正確使用,不易被誤用
- “促進正確使用”可以 功能性相似接口的一致性,以及內置類型的行爲兼容。以C#的 .Length 和 .Count() 爲反例
- “阻止誤用”可以 對類型限制,建立新類型,束縛對象值。
但我覺得這十分麻煩,或許只是這個例子不好,比如2001.02.29這個日期,感覺還是在函數內檢驗比較好Date a(1998,12,28); //爲了防止無效日期,增加Day,Month,Year類,對Int封裝,做有效性限制 Date a(Year(1998),Month(12),Day(28));
- 利用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
- 新 type 的對象應該如何被創建和銷燬?構造,析構,內存分配及釋放
- 對象的初始化和對象的賦值該有什麼樣的差別?明確構造函數和賦值操作符的行爲
- 新 type 被 passed by value 意味着什麼? copy構造函數的實現
- 什麼是新 type 的“合法值”?在構造函數,賦值操作上的約束
- 新 type 需要配合某個繼承圖系嗎?注意他們的virtual函數
- 新 type 需要什麼樣的轉換? 顯示、隱式轉化 參考15
- 新 type 允許那些操作符和函數?
- 什麼標準函數需要駁回? 參考6
- 新 type 成員的作用域
- 什麼是新 type 的“未聲明接口”? 參考29 對效率。異常安全性及資源運用提供保證
- 如果需要一個types家族,那應該定義一個 class template
- 有沒有必要定義一個新的 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
- 將成員變量聲明爲 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; }
- protected 並不比 public 更具封裝性。
23: 寧以 non-nember、non-friend 替換 member 函數
好吧,C#過來的感到震驚
class WebBrowser
{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
現在想要做一個同時調用 WebBrowser裏三個清理函數的函數,下面兩種做法那種好
- 在類裏提供
class WebBrowser { public: ... void clearEverthing(); ... };
- 在同空間名裏額外做一個函數
void clearBrowser(WebBrowser& wb);
居然是2好,因爲更具有封裝性。它的邏輯大概是這樣
首先,成員變量聲明爲 private 就意味着有更少的函數能訪問它,如果不是 private 那麼就有很多函數可以訪問它,它就不具有封裝性。那麼,越少函數能訪問 private,那封裝性越好。所以2好。
24: 若所有參數皆需類型轉換,請爲此採用 non-member 函數
令 classes 支持隱式類型轉換通常是個糟糕的主意。
請記住:如果需要爲某個函數的所有參數進行類型轉換,那麼這個函數必須是個 non-member.
25: 考慮寫出一個不拋異常的 swap 函數
- 交換兩個對象真正要做的是交換它們的指針
- 當 std::swap 對你的類型效率不高時,提供一個 swap 成員函數,並確定這個函數不拋出異常
- 如果你提出一個 member swap,也應該提供一個 non-member swap 來調用前者
- 調用 swap 時應針對 std::swap 使用 using 聲明式,然後調用 swap 並且不帶任何 “命名空間資格修飾”
- 爲“用戶自定義類型”進行 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 纔行)
注意
- c++ 中一個對象可以擁有一個以上的地址(如以派生類指向它 和 以基類指向它)
- 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(); } }
- 儘可能的使用新式轉換
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#玩家的震驚
-
程序庫頭文件應該以 “完全且僅有聲明式”的形式存在。
依賴於聲明式,不依賴於定義式的兩個手段
- 利用 Interface class,解除接口和實現之間的耦合關係,從而降低編譯依賴性,在interface class 內聲明 static 處理函數,和下面有點類似。
- 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” 的兩個組合。
情況說明
- 鑽石型多繼承導致歧義性
如果File Class有個成員變量 fileName,在IOFile中調用 fileName,就會出現歧義性。class File{...}; class InputFile: public File{...}; class OutputFile: public File{...}; class IOFile: public InputFile, public OutputFile{...};
- 使用 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); //報錯
}
...
};
解決辦法
-
在 base class 函數調用動作之前加上“this->”
template<typename Company> class LoggingMsgSender: public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { this->sendClear(info); } ... };
-
使用 using 聲明式
template<typename Company> class LoggingMsgSender: public MsgSender<Company> { public: using MsgSender<Company>::sendClear; //告訴編譯器,假設sendClear 位於 base class ... void sendClearMsg(const MsgInfo& info) { sendClear(info); } ... };
-
明白支持被調用的函數位於 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_ptr
和tr1::weak_ptr
智能指針tr1::function
可表示任何符合簽名的函數和函數對象tr1::bind
綁定器tr1::unordered_set
,tr1::unordered_multiset
,tr1::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》