一、基礎議題(Basics)
1、仔細區別 pointers 和 references
當一定會指向某個對象,且不會改變指向時,就應該選擇 references,其它任何時候,應該選擇 pointers。 實現某一些操作符的時候,操作符由於語義要求使得指針不可行,這時就使用引用。
2、最好使用 C++ 轉型操作符
為解決 C 舊式轉型的缺點(允許將任何類型轉為任何類型,且難以辨識),C++ 導入 4 個新的轉型操作符(cast operators):
static_cast , const_cast , dynamic_cast , reinterpret_cast:分別是常規類型轉換,去常量轉換,繼承轉換,函數指針轉換
使用方式都是形如: static_cast<type>(expression) , 如: int d = static_cast<int>(3.14);
#include <iostream> using namespace std; struct B { virtual void print(){}//想要使用 dynamic_cast ,基類中必須有虛函數 }; struct D : B { void print(){} }; int fun(){} int main() { int i = static_cast<int>(3.14); //i == 3 const int j = 10; int *pj = const_cast<int*>(&j); //int *pj = (int*)(&j); //等同於上面 *pj = 20; //雖然 *pj的地址和 j 的地址是一樣的,但是值卻不一樣。 cout<<*pj<<endl; //20 cout<<j<<endl; //10 B *b; dynamic_cast<D*>(b); typedef void (*FunPtr)(); reinterpret_cast<FunPtr>(&fun); //盡量避免使用 }
const_cast :用於去除變量的const或者volatile屬性。但目的絕不是為了修改 const 變量的內容,而是因為無奈,比如說有一個const的值,想代入一個參數未設為const的函數
synamic_cast:用來針對一個繼承體系做向下的安全轉換,目標類型必須為指針或者引用。基類中要有虛函數,否則會編譯出錯;static_cast則沒有這個限制。原因是:存在虛函數,說明它有想要讓基類指針或引用指向派生類對象的情況,此時轉換才有意義。由於運行時類型檢查需要運行時類型信息,而這個信息存儲在類的虛函數表中,只有定義了虛函數的類才有虛函數表。必須保證源類型跟目標類型本來就是一致的,否則返回 null 指針。這個函數使用的是RTTI機制,所以編譯器必須打開這個選項才能編譯。
reinterpret_cast: 不具有移植性,最常用的用途是轉換函數指針類型,但是不建議使用它,除非迫不得已。
3、絕對不要以多態方式處理數組
#include <iostream> using namespace std; struct B { virtual void print() const{cout<<"base print()"<<endl;} }; struct D : B { void print() const{cout<<"derived print()"<<endl;} int id; //如果沒有此句,執行將正確,因為基類對象和子類對象長度相同 }; int fun(const B array[],int size) { for(int i = 0;i<size;++i) { array[i].print(); } } int main() { B barray[5]; fun(barray,5); D darray[5]; fun(darray,5); }
array[i] 其實是一個指針算術表達式的簡寫,它代表的其實是 *(array+i),array是一個指向數組起始處的指針。在 for 里遍歷 array 時,必須要知道每個元素之間相差多少內存,而編譯器則根據傳入參數來計算得知為 sizeof(B),而如果傳入的是派生類數組對象,它依然認為是 sizeof(B),除非正好派生類大小正好與基類相同,否則運行時會出現錯誤。但是如果我們設計軟件的時候,不要讓具體類繼承具體類的話,就不太可能犯這種錯誤。(理由是,一個類的父類一般都會是一個抽象類,抽象類不存在數組)
4、避免無用的 default constructors
沒有缺省構造函數造成的問題:通常不可能建立對象數組,對於使用非堆數組,可以在定義時提供必要的參數。另一種方法是使用指針數組,但是必須刪除數組里的每個指針指向的對象,而且還增加了內存分配量。
提供無意義的缺省構造函數會影響類的工作效率,成員函數必須測試所有的部分是否都被正確的初始化。
二、操作符(Operators)
5、對定制的“類型轉換函數”保持警覺
定義類似功能的函數,而拋棄隱式類型轉換,使得類型轉換必須顯示調用。例如 String類沒有定義對Char*的隱式轉換,而是用c_str函數來實施這個轉換。擁有單個參數(或除第一個參數外都有默認值的多參數)構造函數的類,很容易被隱式類型轉換,最好加上 explicit 防止隱式類型轉換。
6、區別 increment/decrement 操作符的前置和后置形式
#include <iostream> using namespace std; class A { public: A(int i):id(i){} A& operator++() { this->id += 1; return *this; } //返回值為 const ,以避免 a++++這種形式 //因為第二個 operator++ 所改變的對象是第一個 operator++ 返回的對象 //最終結果其實也只是累加了一次,a++++ 也還是相當於 a++,這是違反直覺的 const A operator++(int) { A a = *this; this->id += 1; return a; } int id; }; int main() { A a(3); cout<<++a.id<<endl; //++++a; 也是允許的,但 a++++ 不允許。 cout<<a.id<<endl; cout<<a++.id<<endl; cout<<a.id<<endl; }
后置operator++(int) 的疊加是不允許的,原因有兩個:一是與內建類型行為不一致(內建類型支持前置疊加);二是其效果跟調用一次 operator++(int) 效果一樣,這是違反直覺的。另外,后置式操作符使用 operator++(int),參數的唯一目的只是為了區別前置式和后置式而已,當函數被調用時,編譯器傳遞一個0作為int參數的值傳遞給該函數。
處置用戶定制類型時,盡可能使用前置式,因為后置式會產生一個臨時對象。
7、千萬不要重載 &&, || 和 , 操作符
int *pi = NULL;
if(pi != 0 && cout<<*pi<<endl) { }
上面的代碼不會報錯,雖然 pi 是空指針,但 && 符號采用"驟死式"評估方式,如果 pi == 0 的話,不會執行后面的語句。
不要重載這些操作符,是因為我們無法控制表達式的求解優先級,不能真正模仿這些運算符。操作符重載的目的是使程序更容易閱讀,書寫和理解,而不是來迷惑其他人。如果沒有一個好理由重載操作符,就不要重載。而對於&&,||和“,”,很難找到一個好理由。
8、了解各種不同意義的 new 和 delete
new 操作符的執行過程:
(1). 調用operator new分配內存 ; //這一步可以使用 operator new 或 placement new 重載。
(2). 調用構造函數生成類對象;
(3). 返回相應指針。
函數 operator new 通常聲明如下:
void * operator new(size_t size); //第一個參數必須為 size_t,表示需要分配多少內存。
返回值為void型指針,表示這個指針指向的內存中的數據的類型要由用戶來指定。比如內存分配函數malloc函數返回的指針就是void *型,用戶在使用這個指針的時候,要進行強制類型轉換,如(int *)malloc(1024)。任何類型的指針都可以直接賦給 void * 變量,而不必強制轉換。如果函數的參數可以為任意類型的指針,則可以聲明為 void * 了。
void 有兩個地方可以使用,第一是函數返回值,第二是作為無參函數的參數。(因為在C語言中,可以給無參函數傳任意類型的參數,而且C語言中,沒有指定函數返回值時,默認返回為 int 值)
#include <iostream> using namespace std; class User { public: void * operator new(size_t size) { std::cout<<"size: "<<size<<std::endl; } void * operator new(size_t size,std::string str) { std::cout<<"size: "<<size <<"\nname: " << str<< std::endl; } int id; }; int main() { User* user1 = new User; User* user2 = new ("JIM")User; void *pi = operator new(sizeof(int)); int i = 3; int *p = &i; pi = p; cout<<*(int*)pi<<endl; }
三、異常(Exceptions)
9、利用 destructors 避免泄漏資源
#include <iostream> #include <stdexcept> void exception_fun() { throw std::runtime_error("runtime_error"); } void fun() { int *pi = new int[10000]; std::cout<<pi<<std::endl; try { exception_fun(); //如果此處拋出異常而未處理,則無法執行 delete 語句,造成內存泄漏。 } catch(std::runtime_error& error) { delete pi; throw; } delete pi; } main() { for(;;) { try { fun(); } catch(std::runtime_error& error) { } } }
一個函數在堆里申請內存到釋放內存的過程中,如果發生異常,如果自己不處理而只交給調用程序處理,則可能由於未調用 delete 導致內存泄漏。上面的方法可以解決這一問題,不過這樣的代碼使人看起來心煩且難於維護,而且必須寫雙份的 delete 語句。函數返回時局部對象總是釋放(調用其析構函數),無論函數是如何退出的。(僅有的一種例外是當調用 longjmp 時,而 longjmp 這個缺點也是C++最初支持異常處理的原因)
所以這里使用智能指針或類似於智能指針的對象是比較好的辦法:
#include <iostream> #include <stdexcept> void exception_fun() { throw std::runtime_error("runtime_error"); } void fun() { int *pi = new int[10000]; std::auto_ptr<int> ap(pi); //用 auto_ptr 包裝一下 std::cout<<pi<<std::endl; exception_fun(); } main() { for(;;) { try { fun(); } catch(std::runtime_error& error) { } } }
上面的代碼看起來簡潔多了,因為 auto_ptr 會在離開作用域時調用其析構函數,析構函數中會做 delete 動作。
10、在 constructors 內阻止資源泄漏
這一條講得其實是捕獲構造函數里的異常的重要性。
堆棧輾轉開解(stack-unwinding):如果一個函數中出現異常,在函數內即通過 try..catch 捕捉的話,可以繼續往下執行;如果不捕捉就會拋出(或通過 throw 顯式拋出)到外層函數,則當前函數會終止運行,釋放當前函數內的局部對象(局部對象的析構函數就自然被調用了),外層函數如果也沒有捕捉到的話,會再次拋出到更外層的函數,該外層函數也會退出,釋放其局部對象……如此一直循環下去,直到找到匹配的 catch 子句,如果找到 main 函數中仍找不到,則退出程序。
#include <iostream> #include <string> #include <stdexcept> class B { public: B(const int userid_,const std::string& username_ = "",const std::string address_ = ""): userid(userid_), username(0), address(0) { username = new std::string(username_); throw std::runtime_error("runtime_error"); //構造函數里拋出異常的話,由於對象沒有構造完成,不會執行析構函數 address = new std::string(address_); } ~B() //此例中不會執行,會導致內存泄漏 { delete username; delete address; std::cout<<"~B()"<<std::endl; } private: int userid; std::string* username; std::string* address; }; main() { try { B b(1); } catch(std::runtime_error& error) { } }
C++拒絕為沒有完成構造函數的對象調用析構函數,原因是避免開銷,因為只有在每個對象里加一些字節來記錄構造函數執行了多少步,它會使對象變大,且減慢析構函數的運行速度。
一般建議不要在構造函數里做過多的資源分配,而應該把這些操作放在一個類似於 init 的成員函數中去完成。這樣當 init 成員函數拋出異常時,如果對象是在棧上,析構函數仍會被調用(異常會自動銷毀局部對象,調用局部對象的析構函數,見下面),如果是在堆上,需要在捕獲異常之后 delete 對象來調用析構函數。
11、禁止異常流出 destructors 之外
這一條講得其實是捕獲析構函數里的異常的重要性。第一是防止程序調用 terminate 終止(這里有個名詞叫:堆棧輾轉開解 stack-unwinding);第二是析構函數內如果發生異常,則異常后面的代碼將不執行,無法確保我們完成我們想做的清理工作。
之前我們知道,析構函數被調用,會發生在對象被刪除時,如棧對象超出作用域或堆對象被顯式 delete (還有繼承體系中,virtual 基類析構函數會在子類對象析構時調用)。除此之外,在異常傳遞的堆棧輾轉開解(stack-unwinding)過程中,異常處理系統也會刪除局部對象,從而調用局部對象的析構函數,而此時如果該析構函數也拋出異常,C++程序是無法同時處理兩個異常的,就會調用 terminate()終止程序(會立即終止,連局部對象也不釋放)。另外,如果異常被拋出,析構函數可能未執行完畢,導致一些清理工作不能完成。
所以不建議在析構函數中拋出異常,如果異常不可避免,則應在析構函數內捕獲,而不應當拋出。 場景再現如下:
#include <iostream> struct T { T() { pi = new int; std::cout<<"T()"<<std::endl; } void init(){throw("init() throw");} ~T() { std::cout<<"~T() begin"<<std::endl; throw("~T() throw"); delete pi; std::cout<<"~T() end"<<std::endl; } int *pi; }; void fun() { try{ T t; t.init(); }catch(...){} //下面也會引發 terminate /* try { int *p2 = new int[1000000000000L]; }catch(std::bad_alloc&) { std::cout<<"bad_alloc"<<std::endl; } */ } void terminate_handler() { std::cout<<"my terminate_handler()"<<std::endl; } int main() { std::set_terminate(terminate_handler); fun(); }
12、了解 "拋出一個 exception ” 與 “傳遞一個參數” 或 “調用一個虛函數”之間的差異
拋出異常對象,到 catch 中,有點類似函數調用,但是它有幾點特殊性:
1 #include <iostream> 2 3 void fun1(void) 4 { 5 int i = 3; 6 throw i; 7 } 8 void fun2(void) 9 { 10 static int i = 10; 11 int *pi = &i; 12 throw pi; //pi指向的對象是靜態的,所以才能拋出指針 13 } 14 15 main() 16 { 17 try{ 18 fun1(); 19 }catch(int d) 20 { 21 std::cout<<d<<std::endl; 22 } 23 try{ 24 fun2(); 25 } catch(const void* v) 26 { 27 std::cout<<*(int*)v<<std::endl; 28 } 29 }
如果拋出的是 int 對象的異常,是不能用 double 類型接收的,這一點跟普通函數傳參不一樣。異常處理中,支持的類型轉換只有兩種,一種是上面例子中演示的從"有型指針"轉為"無型指針",所以用 const void* 可以捕捉任何指針類型的 exception。另一種是繼承體系中的類轉換,可見下一條款的例子。
另外,它跟虛擬函數有什么不同呢?異常處理可以出現多個 catch 子句,而匹配方式是按先后順序來匹配的(所以如 exception 異常一定要寫在 runtime_error異常的后面,如果反過來的話,runtime_error異常語句永遠不會執行),而虛函數則是根據虛函數表來的。
13、以 by reference 方式捕捉 exceptions
1 #include <iostream> 2 #include <stdexcept> 3 4 class B 5 { 6 public: 7 B(int id_):id(id_){} 8 B(const B& b){id = b.id;std::cout<<"copy"<<std::endl;} 9 int id; 10 }; 11 12 void fun(void) 13 { 14 static B b(3); //這里是靜態對象 15 throw &b; //只有該對象是靜態對象或全局對象時,才能以指針形式拋出 16 } 17 main() 18 { 19 try{ 20 fun(); 21 }catch(B* b) //這里以指針形式接收 22 { 23 std::cout<<b->id<<std::endl; //輸出3 24 } 25 }
用指針方式來捕捉異常,上面的例子效率很高,沒有產生臨時對象。但是這種方式只能運用於全局或靜態的對象(如果是 new 出來的堆中的對象也可以,但是該何時釋放呢?)身上,否則的話由於對象離開作用域被銷毀,catch中的指針指向不復存在的對象。接下來看看對象方式和指針方式:
#include <iostream> #include <stdexcept> class B { public: B(){} B(const B& b){std::cout<<"B copy"<<std::endl;} virtual void print(void){std::cout<<"print():B"<<std::endl;} }; class D : public B { public: D():B(){} D(const D& d){std::cout<<"D copy"<<std::endl;} virtual void print(void){std::cout<<"print():D"<<std::endl;} }; void fun(void) { D d; throw d; } main() { try{ fun(); }catch(B b) //注意這里 { b.print(); } }
上面的例子會輸出:
可是如果把 catch(B b) 改成 catch(B& b) 的話,則會輸出:
該條款的目的就是告訴我們,請盡量使用引用方式來捕捉異常,它可以避免 new 對象的刪除問題,也可以正確處理繼承關系的多態問題,還可以減少異常對象的復制次數。
14、明智運用 exception specifications
C++提供了一種異常規范,即在函數后面指定要拋出的異常類型,可以指定多個:
#include <iostream> void fun(void) throw(int,double); //必須這樣聲明,而不能是 void fun(void); void fun(void) throw(int,double) //說明可能拋出 int 和 double 異常 { int i = 3; throw i; } main() { try{ fun(); }catch(int d) { std::cout<<d<<std::endl; } }
15、了解異常處理的成本
大致的意思是,異常的開銷還是比較大的,只有在確實需要用它的地方才去用。
四、效率(Efficiency)
16、謹記 80-20 法則
大致的意思是說,程序中80%的性能壓力可能會集中在20%左右的代碼處。那怎么找出這20%的代碼來進行優化呢?可以通過Profiler分析程序等工具來測試,而不要憑感覺或經驗來判斷。
17、考慮使用 lazy evaluation(緩式評估)
除非確實需要,否則不要為任何東西生成副本。當某些計算其實可以避免時,應該使用緩式評估。
18、分期攤還預期的計算成本
跟上一條款相對的,如果某些計算無可避免,且會多次出現時,可以使用急式評估。
19、了解臨時對象的來源
C++真正所謂的臨時對象是不可見的——只要產生一個 non-heap object 而沒有為它命名,就產生了一個臨時對象。它一般產生於兩個地方:一是函數參數的隱式類型轉換,二是函數返回對象時。 任何時候,只要你看到一個 reference-to-const 參數,就極可能會有一個臨時對象被產生出來綁定至該參數上;任何時候,只要你看到函數返回一個對象,就會產生臨時對象(並於稍后銷毀)。
20、協助完成“返回值優化(RVO)”
不要在一個函數里返回一個局部對象的地址,因為它離開函數體后就析構了。不過在GCC下可以正常運行,無論是否打開優化;而在VS2010中如果關閉優化,就會看到效果。
這個條款想說的是:const Test fun(){ return Test(); } 比 const Test fun(){Test test; return test; } 好,更能使編譯器進行優化。
不過現在看來,在經過編譯器優化之后,這兩個好像已經沒有什么區別了。
21、利用重載技術避免隱式類型轉換
#include <iostream> using namespace std; struct B { B(int id_):id(id_){} int id; }; const B operator+(const B& b1,const B& b2) { return B(b1.id + b2.id); } //const B operator+(const B& b1,int i) //如果重載此方法,就不會產生臨時對象了 //{ // return B(b1.id + i); //} int main() { B b1(3),b2(7); B b3 = b1+ b2; B b4 = b1 + 6; //會把 6 先轉換成B對象,產生臨時對象 }
22、考慮以操作符復合形式(op=)取代其獨身形式(op)
使用 operator+= 的實現來實現 operator= ,其它如果 operator*=、operator-= 等類似。
#include <iostream> class B { public: B(int id_):id(id_){} B& operator+=(const B& b) { id += b.id; return *this; } int print_id(){std::cout<<id<<std::endl;} private: int id; }; B operator+(const B& b1,const B& b2) //不用聲明為 B 的 friend 函數,而且只需要維護 operator+= 即可。 { return const_cast<B&>(b1) += b2; //這里要去掉b1的const屬性,才能帶入operator+= 中的 this 中 } int main() { B b1(3),b2(7),b3(100); (b1+b2).print_id(); //10 這里進行 operator+ 操作,會改變 b1 的值,這個不應該吧 b1.print_id(); //10 b3+=b1; b3.print_id(); //110 }
23、考慮使用其它程序庫
提供類似功能的程序庫,可能在效率、擴充性、移植性和類型安全方面有着不同的表現。比如說 iostream 和 stdio 庫,所以選用不同的庫可能會大幅改善程序性能。
24、了解 virtual functions、multiple inheritance、virtual base classes、runtime type identification 的成本
在使用虛函數時,大部分編譯器會使用所謂的 virtual tables 和 virtual table pointers ,通常簡寫為 vtbls 和 vptrs 。vtbl 通常是由 "函數指針" 架構而成的數組,每一個聲明(或繼承)虛函數的類都有一個 vtbl ,而其中的條目就是該 class 的各個虛函數實現體的指針。
虛函數的第一個成本:必須為每個擁有虛函數的類耗費一個 vtbl 空間,其大小視虛函數的個數(包括繼承而來的)而定。不過,一個類只會有一個 vtbl 空間,所以一般占用空間不是很大。
不要將虛函數聲明為 inline ,因為虛函數是運行時綁定的,而 inline 是編譯時展開的,即使你對虛函數使用 inline ,編譯器也通常會忽略。
虛函數的第二個成本:必須為每個擁有虛函數的類的對象,付出一個指針的代價,即 vptr ,它是一個隱藏的 data member,用來指向所屬類的 vtbl。
調用一個虛函數的成本,基本上和通過一個函數指針調用函數相同,虛函數本身並不構成性能上的瓶頸。
虛函數的第三個成本:事實上等於放棄了 inline。(如果虛函數是通過對象被調用,倒是可以 inline,不過一般都是通過對象的指針或引用調用的)
#include <iostream> struct B1 { virtual void fun1(){} int id;}; struct B2 { virtual void fun2(){} }; struct B3 { virtual void fun3(){} }; struct D : virtual B1, virtual B2, virtual B3 {virtual void fun(){} void fun1(){} void fun2(){} void fun3(){}}; int main() { std::cout<<sizeof(B1)<<std::endl; //8 std::cout<<sizeof(B2)<<std::endl; //4 std::cout<<sizeof(B3)<<std::endl; //4 std::cout<<sizeof(D)<<std::endl; //16 } //D 中只包含了三個 vptr ,D和B1共享了一個。
五、技術(Techniques,Idioms,Patterns)
25、將 constructor 和 non-member functions 虛化
這里所謂的虛擬構造函數,並不是真的指在構造函數前面加上 virtual 修飾符,而是指能夠根據傳入不同的參數建立不同繼承關系類型的對象。
被派生類重定義的虛函數可以與基類的虛函數具有不同的返回類型。所以所謂的虛擬復制構造函數,可以在基類里聲明一個 virtual B* clone() const = 0 的純虛函數,在子類中實現 virtual D* clone() const {return new D(*this);}
同樣的,非成員函數虛化,這里也並不是指使用 virtual 來修飾非成員函數。比如下面這個輸出 list 中多態對象的屬性:
#include <iostream> #include <list> #include <string> using namespace std; class B { public: B(string str):value(str){} virtual ostream& print(ostream& s) const = 0; protected: string value; }; class D1 : public B { public: D1(int id_):B("protect value"),id(id_){} //子類構造函數中,要先調用基類構造函數初始化基類 ostream& print(ostream& s) const{cout<<value<<"\t"<<id;;return s;} //如果基類虛函數是 const 方法,則這里也必須使用 const 修飾 private: int id; }; class D2 : public B { public: D2(int id_):B("protect value"),id(id_){} //子類構造函數中,要先調用基類構造函數初始化基類 ostream& print(ostream& s) const{cout<<value<<"\t"<<id;return s;} private: int id; }; ostream& operator<<(ostream& s,const B& b) { return b.print(s); } int main() { list<B*> lt; D1 d1(1); D2 d2(2); lt.push_back(&d1); lt.push_back(&d2); list<B*>::iterator it = lt.begin(); while(it != lt.end()) { cout<<*(*it)<<endl; //D1 D2 it++; } }
在這里,即使給每一個繼承類單獨實現友元的 operator<< 方法,也不能實現動態綁定,只會調用基類的方法。那么,在基類里寫 operator<< 用 virtual 修飾不就行了嗎?遺憾的,虛函數不能是友元。
26、限制某個 class 所能產生的對象數量
類中的靜態成員總是被構造,即使不使用,而且你無法確定它什么時候初始化;而函數中的靜態成員,只有在第一次使用時才會建立,但你也得為此付出代價,每次調用函數時都得檢查一下是否需要建立對象。(另外該函數不能聲明為內聯,非成員內聯函數在鏈接的時候在目標文件中會產生多個副本,可能造成程序的靜態對象拷貝超過一個。)這個已經由標准委員會在1996年把 inline 的默認連接由內部改為外部,所以問題已經不存在了,了解一下即可。 限制對象個數:建立一個基類,構造函數和復制構造函數中計數加一,若超過最大值則拋出異常;析構函數中計數減一。
27、要求(或禁止)對象產生於 heap 中
析構函數私有,有一個致命問題:妨礙了繼承和組合(內含)。
#include <iostream> #include <string> using namespace std; class B1 //禁止對象產生於 heap 中 { public: B1(){cout<<"B1"<<endl;}; private: void* operator new(size_t size); void* operator new[](size_t size); void operator delete(void* ptr); void operator delete[](void* ptr); }; class B2 //要求對象產生於 heap 中 { public: B2(){cout<<"B2"<<endl;}; void destroy(){delete this;} //模擬的析構函數 private: ~B2(){} }; int main() { //B1* b1 = new B1; //Error! B1 b1; //B2 b2; //Error B2* b2 = new B2; b2->destroy(); }
28、Smart Pointer(智能指針)
可以參考 auto_ptr 和 share_ptr(源於boost,已被收錄進c++11標准)源碼。
29、Reference counting(引用計數)
同上。
30、Proxy classes(替身類、代理類)
參考《可復用面向對象軟件基礎》結構型模式之代理模式。
31、讓函數根據一個以上的對象類型來決定如何虛化
六、雜項討論(Miscellany)
32、在未來時態下發展程序
要用語言提供的特性來強迫程序符合設計,而不要指望使用者去遵守約定。比如禁止繼承,禁止復制,要求類的實例只能創建在堆中等等。處理每個類的賦值和拷貝構造函數,如果這些函數是難以實現的,則聲明它們為私有。
所提供的類的操作和函數有自然的語法和直觀的語義,和內建類型(如 int)的行為保持一致。
盡可能寫可移植性的代碼,只有在性能極其重要時不可移植的結構才是可取的。
多為未來的需求考慮,盡可能完善類的設計。
33、將非尾端類設計為抽象類
只要不是最根本的實體類(不需要進一步被繼承的類),都設計成抽象類。
34、如何在同一個程序中結合 C++ 和 C
等有時間看看 C語言的經典書籍后再說。
35、讓自己習慣於標准 C++ 語言
可以參考《C++標准程序庫》,另外可以使用最新編譯器,嘗試c++11新特性。