More Effective C++35個改善程式設計與設計的有效方法筆記

Scott Meyers大師Effective三部曲:Effective C++、More Effective C++、Effective STL,這三本書出版已很多年,後來又出版了Effective Modern C++。

這裡是More Effective C++的筆記:

1。 指標與引用的區別

void printDouble(const double& rd){ std::cout< v(10); v[5] = 10; // 這個被賦值的目標物件就是運算子[]返回的值,如果運算子[] // 返回一個指標,那麼後一個語句就得這樣寫: *v[5] = 10; return 0;}

指標與引用看上去完全不同(指標用運算子”*”和”->”,引用使用運算子”。”),但是它們似乎有相同的功能。指標和引用都是讓你間接引用其它物件。

在任何情況下都不能使用指向空值的引用。一個引用必須總是指向某些物件。在C++裡,引用應被初始化。

不存在指向空值的引用這個事實意味著使用引用的程式碼效率比使用指標的要高。因為在使用引用之前不需要測試它的合法性。

指標與引用的另一個重要的不同是指標可以被重新賦值以指向另一個不同的物件。但是引用則總是指向在初始化時被指定的物件,以後不能改變。

總的來說,在以下情況下你應該使用指標,一是你考慮到存在不指向任何物件的可能(在這種情況下,你能夠設定指標為空),二是你需要能夠在不同的時刻指向不同的物件(在這種情況下,你能改變指標的指向)。如果總是指向一個物件並且一旦指向一個物件後就不會改變指向,那麼你應該使用引用。

當你知道你必須指向一個物件並且不想改變其指向時,或者在過載運算子併為防止不必要的語義誤解時(最普通的例子是運算子[]),你不應該使用指標。而在除此之外的其它情況下,則應使用指標。

2。 儘量使用C++風格的型別轉換

class Widget {public: virtual void func() {}}; class SpecialWidget : public Widget {public: virtual void func() {}}; void update(SpecialWidget* psw) {}void updateViaRef(SpecialWidget& rsw) {} typedef void (*FuncPtr)(); // FuncPtr是一個指向函式的指標int doSomething() { return 1; }; int test_item_2(){ int firstNumber = 1, secondNumber = 1; double result1 = ((double)firstNumber) / secondNumber; // C風格 double result2 = static_cast(firstNumber) / secondNumber; // C++風格型別轉換 SpecialWidget sw; // sw是一個非const物件 const SpecialWidget& csw = sw; // csw是sw的一個引用,它是一個const物件 //update(&csw); // 錯誤,不能傳遞一個const SpecialWidget*變數給一個處理SpecialWidget*型別變數的函式 update(const_cast(&csw)); // 正確,csw的const顯示地轉換掉(csw和sw兩個變數值在update函式中能被更新) update((SpecialWidget*)&csw); // 同上,但用了一個更難識別的C風格的型別轉換 Widget* pw = new SpecialWidget; //update(pw); // 錯誤,pw的型別是Widget*,但是update函式處理的是SpecialWidget*型別 //update(const_cast(pw)); // 錯誤,const_cast僅能被用在影響constness or volatileness的地方,不能用在向繼承子類進行型別轉換 Widget* pw2 = nullptr; update(dynamic_cast(pw2)); // 正確,傳遞給update函式一個指標是指向變數型別為SpecialWidget的pw2的指標, 如果pw2確實指向一個物件,否則傳遞過去的將是空指標 Widget* pw3 = new SpecialWidget; updateViaRef(dynamic_cast(*pw3)); // 正確,傳遞給updateViaRef函式SpecailWidget pw3指標,如果pw3確實指向了某個物件,否則將丟擲異常 //double result3 = dynamic_cast(firstNumber) / secondNumber; // 錯誤,沒有繼承關係 const SpecialWidget sw4; //update(dynamic_cast(&sw4)); // 錯誤,dynamic_cast不能轉換掉const FuncPtr funcPtrArray[10]; // funcPtrArray是一個能容納10個FuncPtr指標的陣列 //funcPtrArray[0] = &doSomething; // 錯誤,型別不匹配 funcPtrArray[0] = reinterpret_cast(&doSomething); // 轉換函式指標的程式碼是不可移植的(C++不保證所有的函式指標都被用一樣的方法表示),在一些情況下這樣的轉換會產生不正確的結果,所以應該避免轉換函式指標型別 return 0;}

C++透過引進四個新的型別轉換(cast)運算子克服了C風格型別轉換的缺點(過於粗魯,能允許你在任何型別之間進行轉換;C風格的型別轉換在程式語句中難以識別),這四個運算子是:static_cast、const_cast、dynamic_cast、reinterpret_cast。

static_cast在功能上基本上與C風格的型別轉換一樣強大,含義也一樣。它也有功能上限制。例如,不能用static_cast像用C 風格的型別轉換一樣把struct轉換成int型別或者把double型別轉換成指標型別,另外,static_cast不能從表示式中去除const屬性,因為另一個新的型別轉換運算子const_cast有這樣的功能。

const_cast用於型別轉換掉表示式的const或volatileness屬性。如果你試圖使用const_cast來完成修改constness或者volatileness屬性之外的事情,你的型別轉換將被拒絕。

dynamic_cast被用於安全地沿著類的繼承關係向下進行型別轉換。這就是說,你能用dynamic_cast把指向基類的指標或引用轉換成指向其派生類或其兄弟類的指標或引用,而且你能知道轉換是否成功。失敗的轉換將返回空指標(當對指標進行型別轉換時)或者丟擲異常(當對引用進行型別轉換時)。dynamic_cast在幫助你瀏覽繼承層次上是有限制的,它不能被用來缺乏虛擬函式的型別上,也不能用它來轉換掉constness。如你想在沒有繼承關係的型別中進行轉換,你可能想到static_cast。如果是為了去除const,你總得用const_cast。

reinterpret_cast使用這個運算子的型別轉換,其轉換結果幾乎都是執行期定義(implementation-defined)。因此,使用reinterpret_cast的程式碼很難移植。此運算子最普通的用途就是在函式指標之間進行轉換。

關於型別轉換更多介紹參考:https://blog。csdn。net/fengbingchun/article/details/51235498

3。 不要對陣列使用多型

class BST {public: virtual ~BST() { fprintf(stdout, “BST::~BST\n”); }private: int score;}; class BalancedBST : public BST {public: virtual ~BalancedBST() { fprintf(stdout, “BalancedBST::~BalancedBST\n”); }private: int length; int size; // 如果增加此一個int成員,執行test_item_3會segmentation fault,註釋掉此變數,執行正常}; int test_item_3(){ fprintf(stdout, “BST size: %d\n”, sizeof(BST)); // 16 fprintf(stdout, “BalancedBST size: %d\n”, sizeof(BalancedBST)); // 24 BST* p = new BalancedBST[10]; delete [] p; // 如果sizeof(BST) != sizeof(BalancedBST),則會segmentation fault return 0;}

C++允許你透過基類指標和引用來操作派生類陣列。不過這根本就不是一個特性,因為這樣的程式碼幾乎從不如你所願地那樣執行。陣列與多型不能用在一起。值得注意的是如果你不從一個具體類(concrete classes)(例如BST)派生出另一個具體類(例如BalancedBST),那麼你就不太可能犯這種使用多型性陣列的錯誤。

4。 避免無用的預設建構函式

class EquipmentPiece {public: EquipmentPiece(int IDNumber) {}}; int test_item_4(){ //EquipmentPiece bestPieces[10]; // 錯誤,沒有正確呼叫EquipmentPiece建構函式 //EquipmentPiece* bestPieces2 = new EquipmentPiece[10]; // 錯誤,與上面的問題一樣 int ID1 = 1, ID2 = 2; EquipmentPiece bestPieces3[] = { EquipmentPiece(ID1), EquipmentPiece(ID2) }; // 正確,提供了建構函式的引數 // 利用指標陣列來代替一個物件陣列 typedef EquipmentPiece* PEP; // PEP指標指向一個EquipmentPiece物件 PEP bestPieces4[10]; // 正確,沒有呼叫建構函式 PEP* bestPieces5 = new PEP[10]; // 也正確 // 在指標數組裡的每一個指標被重新賦值,以指向一個不同的EquipmentPiece物件 for (int i = 0; i < 10; ++i) bestPieces5[i] = new EquipmentPiece(ID1); // 為陣列分配raw memory,可以避免浪費記憶體,使用placement new方法在記憶體中構造EquipmentPiece物件 void* rawMemory = operator new[](10*sizeof(EquipmentPiece)); // make bestPieces6 point to it so it can be treated as an EquipmentPiece array EquipmentPiece* bestPieces6 = static_cast(rawMemory); // construct the EquipmentPiece objects in the memory使用“placement new” for (int i = 0; i < 10; ++i) new(&bestPieces6[i]) EquipmentPiece(ID1); // 。。。 // 以與構造bestPieces6物件相反的順序解構它 for (int i = 9; i >= 0; ——i) bestPieces6[i]。~EquipmentPiece(); // 如果使用普通的陣列刪除方法,程式的執行將是不可預測的 // deallocate the raw memory delete [] rawMemory; return 0;}

建構函式能初始化物件,而預設建構函式則可以不利用任何在建立物件時的外部資料就能初始化物件。有時這樣的方法是不錯的。例如一些行為特性與數字相仿的物件被初始化為空值或不確定的值也是合理的,還有比如連結串列、雜湊表、圖等等資料結構也可以被初始化為空容器。但不是所有的物件都屬於上述型別,對於很多物件來說,不利用外部資料進行完全的初始化是不合理的。比如一個沒有輸入姓名的地址薄物件,就沒有任何意義。

利用指標陣列代替一個物件陣列這種方法有兩個缺點:第一你必須刪除數組裡每個指標所指向的物件。如果忘了,就會發生記憶體洩漏。第二增加了記憶體分配量,因為正如你需要空間來容納EquipmentPiece物件一樣,你也需要空間來容納指標。

對於類裡沒有定義預設建構函式還會造成它們無法在許多基於模板(template-based)的容器類裡使用。因為例項化一個模板時,模板的型別引數應該提供一個預設建構函式。在多數情況下,透過仔細設計模板可以杜絕對預設建構函式的需求。

5。 謹慎定義型別轉換函式

class Name {public: Name(const std::string& s); // 轉換string到Name}; class Rational {public: Rational(int numerator = 0, int denominator = 1) // 轉換int到有理數類 { n = numerator; d = denominator; } operator double() const // 轉換Rational類成double型別 { return static_cast(n) / d; } double asDouble() const { return static_cast(n) / d; } private: int n, d;}; templateclass Array {public: Array(int lowBound, int highBound) {} explicit Array(int size) {} T& operator[](int index) { return data[index]; } private: T* data;}; bool operator== (const Array& lhs, const Array& rhs){ return false; } int test_item_5(){ Rational r(1, 2); // r的值是1/2 double d = 0。5 * r; // 轉換r到double,然後做乘法 fprintf(stdout, “value: %f\n”, d); std::cout< a(10); Array b(10); for (int i = 0; i < 10; ++i) { //if (a == b[i]) {} // 如果建構函式Array(int size)沒有explicit關鍵字,編譯器將能透過呼叫Array建構函式能轉換int型別到Array型別,這個建構函式只有一個int型別的引數,加上explicit關鍵字則可避免隱式轉換 if (a == Array(b[i])) {} // 正確,顯示從int到Array轉換(但是程式碼的邏輯不合理) if (a == static_cast>(b[i])) {} // 同樣正確,同樣不合理 if (a == (Array)b[i]) {} // C風格的轉換也正確,但是邏輯依舊不合理 } return 0;}

C++編譯器能夠在兩種資料型別之間進行隱式轉換(implicit conversions),它繼承了C語言的轉換方法,例如允許把char隱式轉換為int和從short隱式轉換為double。你對這些型別轉換是無能為力的,因為它們是語言本身的特性。不過當你增加自己的型別時,你就可以有更多的控制力,因為你能選擇是否提供函式讓編譯器進行隱式型別轉換。

有兩種函式允許編譯器進行這些的轉換:單引數建構函式(single-argument constructors)和隱式型別轉換運算子。單引數建構函式是指只用一個引數即可呼叫的建構函式。該函式可以是隻定義了一個引數,也可以是雖定義了多個引數但第一個引數以後的所有引數都有預設值。

隱式型別轉換運算子只是一個樣子奇怪的成員函式:operator關鍵字,其後跟一個型別符號。你不用定義函式的返回型別,因為返回型別就是這個函式的名字。

explicit關鍵字是為了解決隱式型別轉換而特別引入的這個特性。如果建構函式用explicit宣告,編譯器會拒絕為了隱式型別轉換而呼叫建構函式。顯式型別轉換依然合法。

6。 自增(increment)、自減(decrement)運算子字首形式與字尾形式的區別

class UPInt { // unlimited precision intpublic: // 注意:字首與字尾形式返回值型別是不同的,字首形式返回一個引用,字尾形式返回一個const型別 UPInt& operator++() // ++字首 { //*this += 1; // 增加 i += 1; return *this; // 取回值 } const UPInt operator++(int) // ++字尾 { // 注意:建立了一個顯示的臨時物件,這個臨時物件必須被構造並在最後被析構,字首沒有這樣的臨時物件 UPInt oldValue = *this; // 取回值 // 字尾應該根據它們的字首形式來實現 ++(*this); // 增加 return oldValue; // 返回被取回的值 } UPInt& operator——() // ——字首 { i -= 1; return *this; } const UPInt operator——(int) // ——字尾 { UPInt oldValue = *this; ——(*this); return oldValue; } UPInt& operator+=(int a) // +=運算子,UPInt與int相運算 { i += a; return *this; } UPInt& operator-=(int a) { i -= a; return *this; } private: int i;}; int test_item_6(){ UPInt i; ++i; // 呼叫i。operator++(); i++; // 呼叫i。operator++(0); ——i; // 呼叫i。operator——(); i——; // 呼叫i。operator——(0); //i++++; // 注意:++字尾返回的是const UPInt return 0;}

無論是increment或decrement的字首還是字尾都只有一個引數,為了解決這個語言問題,C++規定字尾形式有一個int型別引數,當函式被呼叫時,編譯器傳遞一個0作為int引數的值給該函式。

字首形式有時叫做”增加然後取回”,字尾形式叫做”取回然後增加”。

當處理使用者定義的型別時,儘可能地使用字首increment,因為它的效率較高。

7。 不要過載”&&”, “||”,或”,”

int test_item_7(){ // if (expression1 && expression2) // 如果過載了運算子&&,對於編譯器來說,等同於下面程式碼之一 // if (expression1。operator&&(expression2)) // when operator&& is a member function // if (operator&&(expression1, expression2)) // when operator&& is a global function return 0;}

與C一樣,C++使用布林表示式短路求值法(short-circuit evaluation)。這表示一旦確定了布林表示式的真假值,即使還有部分表示式沒有被測試,布林表示式也停止運算。

C++允許根據使用者定義的型別,來定製&&和||運算子。方法是過載函式operator&&和operator||,你能在全域性過載或每個類裡過載。風險:你以函式呼叫法替代了短路求值法。函式呼叫法與短路求值法是絕對不同的。首先當函式被呼叫時,需要運算其所有引數。第二是C++語言規範沒有定義函式引數的計算順序,所以沒有辦法知道表示式1與表示式2哪一個先計算。完全可能與具有從左引數到右引數計算順序的短路計算法相反。因此如果你過載&&或||,就沒有辦法提供給程式設計師他們所期望和使用的行為特性,所以不要過載&&和||。

同樣的理由也適用於逗號運算子。逗號運算子用於組成表示式。一個包含逗號的表示式首先計算逗號左邊的表示式,然後計算逗號右邊的表示式;整個表示式的結果是逗號右邊表示式的值。如果你寫一個非成員函式operator,你不能保證左邊的表示式先於右邊的表示式計算,因為函式(operator)呼叫時兩個表示式作為引數被傳遞出去。但是你不能控制函式引數的計算順序。所以非成員函式的方法絕對不行。成員函式operator,你也不能依靠於逗號左邊表示式先被計算的行為特性,因為編譯器不一定必須按此方法去計算。因此你不能過載逗號運算子,保證它的行為特性與其被料想的一樣。過載它是完全輕率的行為。

8。 理解各種不同含義的new和delete

class Widget8 {public: Widget8(int widget8Size) {}}; void* mallocShared(size_t size){ return operator new(size);} void freeShared(void* memory){ operator delete(memory);} Widget8* constructWidget8InBuffer(void* buffer, int widget8Size){ return new(buffer) Widget8(widget8Size); // new運算子的一個用法,需要使用一個額外的變數(buffer),當new運算子隱含呼叫operator new函式時,把這個變數傳遞給它 // 被呼叫的operator new函式除了待有強制的引數size_t外,還必須接受void*指標引數,指向構造物件佔用的記憶體空間。這個operator new就是placement new,它看上去像這樣: // void * operator new(size_t, void* location) { return location; }} int test_item_8(){ std::string* ps = new std::string(“Memory Management”); // 使用的new是new運算子(new operator) //void * operator new(size_t size); // 函式operator new通常宣告 void* rawMemory = operator new(sizeof(std::string)); // 運算子operator new將返回一個指標,指向一塊足夠容納一個string型別物件的記憶體 operator delete(rawMemory); delete ps; // ps->~std::string(); operator delete(ps); void* buffer = operator new(50*sizeof(char)); // 分配足夠的記憶體以容納50個char,沒有呼叫建構函式 operator delete(buffer); // 釋放記憶體,沒有呼叫解構函式。 這與在C中呼叫malloc和free等同OA void* sharedMemory = mallocShared(sizeof(Widget8)); Widget8* pw = constructWidget8InBuffer(sharedMemory, 10); // placement new //delete pw; // 結果不確定,共享記憶體來自mallocShared,而不是operator new pw->~Widget8(); // 正確,析構pw指向的Widget8,但是沒有釋放包含Widget8的記憶體 freeShared(pw); // 正確,釋放pw指向的共享記憶體,但是沒有呼叫解構函式 return 0;}

new運算子(new operator)和new操作(operator new)的區別:

new運算子就像sizeof一樣是語言內建的,你不能改變它的含義,它的功能總是一樣的。它要完成的功能分成兩部分。第一部分是分配足夠的記憶體以便容納所需型別的物件。第二部分是它呼叫建構函式初始化記憶體中的物件。new運算子總是做這兩件事情,你不能以任何方式改變它的行為。你所能改變的是如何為物件分配記憶體。new運算子呼叫一個函式來完成必須的記憶體分配,你能夠重寫或過載這個函式來改變它的行為。new運算子為分配記憶體所呼叫函式的名字是operator new。

函式operator new通常宣告:返回值型別是void*,因為這個函式返回一個未經處理(raw)的指標,未初始化的記憶體。引數size_t確定分配多少記憶體。你能增加額外的引數過載函式operator new,但是第一個引數型別必須是size_t。就像malloc一樣,operator new的職責只是分配記憶體。它對建構函式一無所知。把operator new返回的未經處理的指標傳遞給一個物件是new運算子的工作。

placement new:特殊的operator new,接受的引數除了size_t外還有其它。

new運算子(new operator)與operator new關係:你想在堆上建立一個物件,應該用new運算子。它既分配記憶體又為物件呼叫建構函式。如果你僅僅想分配記憶體,就應該呼叫operator new函式,它不會呼叫建構函式。如果你想定製自己的在堆物件被建立時的記憶體分配過程,你應該寫你自己的operator new函式,然後使用new運算子,new運算子會呼叫你定製的operator new。如果你想在一塊已經獲得指標的記憶體裡建立一個物件,應該用placement new。

Deletion and Memory Deallocation:為了避免記憶體洩漏,每個動態記憶體分配必須與一個等同相反的deallocation對應。函式operator delete與delete運算子的關係與operator new與new運算子的關係一樣。

如果你用placement new在記憶體中建立物件,你應該避免在該記憶體中用delete運算子。因為delete運算子呼叫operator delete來釋放記憶體,但是包含物件的記憶體最初不是被operator nen分配的,placement new只是返回轉到給它的指標。

Arrays:operator new[]、operator delete[]

9。 使用解構函式防止資源洩漏

用一個物件儲存需要被自動釋放的資源,然後依靠物件的解構函式來釋放資源,這種思想不只是可以運用在指標上,還能用在其它資源的分配和釋放上。

資源應該被封裝在一個物件裡,遵循這個規則,你通常就能夠避免在存在異常環境裡發生資源洩漏,透過智慧指標的方式。

C++確保刪除空指標是安全的,所以解構函式在刪除指標前不需要檢測這些指標是否指向了某些物件。

10。 在建構函式中防止資源洩漏

C++僅僅能刪除被完全構造的物件(fully constructed objects),只有一個物件的建構函式完全執行完畢,這個物件才被完全地構造。C++拒絕為沒有完成構造操作的物件呼叫解構函式。

在建構函式中可以使用try catch throw捕獲所有的異常。更好的解決方法是透過智慧指標的方式。

如果你用對應的std::unique_ptr物件替代指標成員變數,就可以防止建構函式在存在異常時發生資源洩漏,你也不用手工在解構函式中釋放資源,並且你還能像以前使用非const指標一樣使用const指標,給其賦值。

std::unique_ptr的使用參考:https://blog。csdn。net/fengbingchun/article/details/52203664

11。 禁止異常資訊(exceptions)傳遞到解構函式外

禁止異常傳遞到解構函式外有兩個原因:第一能夠在異常傳遞的堆疊輾轉開解(stack-unwinding)的過程中,防止terminate被呼叫。第二它能幫助確保解構函式總能完成我們希望它做的所有事情。

12。 理解”丟擲一個異常”與”傳遞一個引數”或”呼叫一個虛擬函式”間的差異

你呼叫函式時,程式的控制權最終還會返回到函式的呼叫處,但是當你丟擲一個異常時,控制權永遠不會回到丟擲異常的地方。

C++規範要求被作為異常丟擲的物件必須被複制。即使被丟擲的物件不會被釋放,也會進行複製操作。丟擲異常執行速度比引數傳遞要慢。

當異常物件被複製時,複製操作是由物件的複製建構函式完成的。該複製建構函式是物件的靜態型別(static type)所對應類的複製建構函式,而不是物件的動態型別(dynamic type)對應類的複製建構函式。

catch子句中進行異常匹配時可以進行兩種型別轉換:第一種是繼承類與基類間的轉換。一個用來捕獲基類的catch子句也可以處理派生類型別的異常。這種派生類與基類(inheritance_based)間的異常型別轉換可以作用於數值、引用以及指標上。第二種是允許從一個型別化指標(typed pointer)轉變成無型別指標(untyped pointer),所以帶有const void*指標的catch子句能捕獲任何型別的指標型別異常。

catch子句匹配順序總是取決於它們在程式中出現的順序。因此一個派生類異常可能被處理其基類異常的catch子句捕獲,即使同時存在有能直接處理該派生類異常的catch子句,與相同的try塊相對應。不要把處理基類異常的catch子句放在處理派生類異常的catch子句的前面。

把一個物件傳遞給函式或一個物件呼叫虛擬函式與把一個物件作為異常丟擲,這之間有三個主要區別:第一,異常物件在傳遞時總被進行複製;當透過傳值方式捕獲時,異常物件被複製了兩次。物件作為引數傳遞給函式時不一定需要被複製。第二,物件作為異常被丟擲與作為引數傳遞給函式相比,前者型別轉換比後者要少(前者只有兩種轉換形式)。最後一點,catch子句進行異常型別匹配的順序是它們在原始碼中出現的順序,第一個型別匹配成功的catch將被用來執行。當一個物件呼叫一個虛擬函式時,被選擇的函式位於與物件型別匹配最佳的類裡,即使該類不是在原始碼的最前頭。

try catch介紹參考:https://blog。csdn。net/fengbingchun/article/details/65939258

13。 透過引用(reference)捕獲異常

透過指標捕獲異常不符合C++語言本身的規範。四個標準的異常——bad_alloc(當operator new不能分配足夠的記憶體時被丟擲);bad_cast(當dynamic_cast針對一個引用(reference)操作失敗時被丟擲);bad_typeid(當dynamic_cast對空指標進行操作時被丟擲);bad_exception(用於unexpected異常)——都不是指向物件的指標,所以你必須透過值或引用來捕獲它們。

std::exception的介紹參考:https://blog。csdn。net/fengbingchun/article/details/78303734

14。 審慎使用異常規格(exception specifications)

如果一個函式丟擲一個不在異常規格範圍裡的異常,系統在執行時能夠檢測出這個錯誤,然後一個特殊函式std::unexpected將被自動地呼叫(This function is automatically called when a function throws an exception that is not listed in its dynamic-exception-specifier。)。std::unexpected預設的行為是呼叫函式std::terminate,而std::terminate預設的行為是呼叫函式abort。應避免呼叫std::unexpected。

避免在帶有型別引數的模板內使用異常規格。

C++允許你用其它不同的異常型別替換std::unexpected異常,透過std::set_unexpected。

15。 瞭解異常處理的系統開銷

採用不支援異常的方法編譯的程式一般比支援異常的程式執行速度更快所佔空間也更小。

為了減少開銷,你應該避免使用無用的try塊。如果使用try塊,程式碼的尺寸將增加並且執行速度也會減慢。

16。 牢記80-20準則(80-20 rule)

80-20準則說的是大約20%的程式碼使用了80%的程式資源;大約20%的程式碼耗用了大約80%的執行時間;大約20%的程式碼使用了80%的記憶體;大約20%的程式碼執行80%的磁碟訪問;80%的維護投入於大約20%的程式碼上。基本的觀點:軟體整體的效能取決於程式碼組成中的一小部分。

17。 考慮使用lazy evaluation(懶惰計算法)

在某些情況下要求軟體進行原來可以避免的計算,這時lazy evaluation才是有用的。

18。 分期攤還期望的計算

over-eager evaluation(過度熱情計算法):在要求你做某些事情以前就完成它們。隱藏在over-eager evaluation後面的思想是如果你認為一個計算需要頻繁進行,你就可以設計一個數據結構高效地處理這些計算需求,這樣可以降低每次計算需求時的開銷。

當你必須支援某些操作而不總需要其結果時,lazy evaluation是在這種時候使用的用以提高程式效率的技術。當你必須支援某些操作而其結果幾乎總是被需要或不止一次地需要時,over-eager是在這種時候使用的用以提高程式效率的一種技術。

19。 理解臨時物件的來源

size_t countChar(const std::string& str, char ch){ // 建立一個string型別的臨時物件,透過以buffer做為引數呼叫string的建構函式來初始化這個臨時物件, // countChar的引數str被繫結在這個臨時的string物件上,當countChar返回時,臨時物件自動釋放 // 將countChar(const std::string& str, char ch)修改為countChar(std::string& str, char ch)則會error return 1;} #define MAX_STRING_LEN 64 int test_item_19(){ char buffer[MAX_STRING_LEN]; char c; std::cin >> c >> std::setw(MAX_STRING_LEN) >> buffer; std::cout<<“There are ”<

在C++中真正的臨時物件是看不見的,它們不出現在你的原始碼中。建立一個沒有命名的非堆(non-heap)物件會產生臨時物件。這種未命名的物件通常在兩種條件下產生:為了使函式成功呼叫而進行隱式型別轉換和函式返回物件時。

僅當透過傳值(by value)方式傳遞物件或傳遞常量引用(reference-to-const)引數時,才會發生這些型別轉換。當傳遞一個非常量引用(reference-to-non-const)引數物件,就不會發生。

C++語言禁止為非常量引用(reference-to-non-const)產生臨時物件。

臨時物件是有開銷的,所以你應該儘可能地去除它們。在任何時候只要見到常量引用(reference-to-const)引數,就存在建立臨時物件而繫結在引數上的可能性。在任何時候只要見到函式返回物件,就會有一個臨時物件被建立(以後被釋放)。

20。 協助完成返回值最佳化

class Rational20 {public: Rational20(int numerator = 0, int denominator = 1) {} int numerator() const { return 1; } int denominator() const { return 2; }};const Rational20 operator*(const Rational20& lhs, const Rational20& rhs){ // 以某種方法返回物件,能讓編譯器消除臨時物件的開銷:這種技巧是返回constructor argument而不是直接返回物件 return Rational20(lhs。numerator() * rhs。numerator(), lhs。denominator() * rhs。denominator());}int test_item_20(){ Rational20 a = 10; Rational20 b(1, 2); Rational20 c = a * b; return 0;}

一些函式(operator*也在其中)必須要返回物件。這就是它們的執行方法。

C++規則允許編譯器最佳化不出現的臨時物件(temporary objects out of existence)。

21。 透過過載避免隱式型別轉換

class UPInt21 { // unlimited precision integers classpublic: UPInt21() {} UPInt21(int value) {}}; const UPInt21 operator+(const UPInt21& lhs, const UPInt21& rhs) // add UPInt21+UPInt21{ return UPInt21(1);} const UPInt21 operator+(const UPInt21& lhs, int rhs) // add UPInt21+int{ return UPInt21(1);} const UPInt21 operator+(int lhs, const UPInt21& rhs) // add int+UPInt21{ return UPInt21(1);} int test_item_21(){ UPInt21 upi1, upi2; UPInt21 upi3 = upi1 + upi2; // 正確,沒有由upi1或upi2生成臨時物件 upi3 = upi1 + 10; // 正確,沒有由upi1或10生成臨時物件 upi3 = 10 + upi2; // 正確,沒有由10或upi2生成臨時物件 // 注意:註釋掉上面的operator+(UPInt21&, int)和operator+(int, UPInt21&)也正確,但是會透過臨時物件把10轉換為UPInt21 return 0;}

在C++中有一條規則是每一個過載的operator必須帶有一個使用者定義型別(user-defined type)的引數。

利用過載避免臨時物件的方法不只是用在operator函式上。

沒有必要實現大量的過載函式,除非你有理由確信程式使用過載函式以後其整體效率會有顯著的提高。

22。 考慮用運算子的賦值形式(op=)取代其單獨形式(op)

class Rational22 {public: Rational22(int numerator = 0, int denominator = 1) {} Rational22& operator+=(const Rational22& rhs) { return *this; } Rational22& operator-=(const Rational22& rhs) { return *this; }}; // operator+根據operator+=實現const Rational22 operator+(const Rational22& lhs, const Rational22& rhs){ return Rational22(lhs) += rhs;} // operator-根據operator-=實現const Rational22 operator-(const Rational22& lhs, const Rational22& rhs){ return Rational22(lhs) -= rhs;}

就C++來說,operator+、operator=和operator+=之間沒有任何關係,因此如果你想讓三個operator同時存在並具有你所期望的關係,就必須自己實現它們。同理,operator-, *, /, 等等也一樣。

確保operator的賦值形式(assignment version)(例如operator+=)與一個operator的單獨形式(stand-alone)(例如operator+)之間存在正常的關係,一種好方法是後者(指operator+)根據前者(指operator+=)來實現。

23。 考慮變更程式庫

不同的程式庫在效率、可擴充套件性、移植性、型別安全和其它一些領域上蘊含著不同的設計理念,透過變換使用給予效能更多考慮的程式庫,你有時可以大幅度地提供軟體的效率。

24。 理解虛擬函式、多繼承、虛基類和RTTI所需的程式碼

當呼叫一個虛擬函式時,被執行的程式碼必須與呼叫函式的物件的動態型別相一致;指向物件的指標或引用的型別是不重要的。大多數編譯器是使用virtual table和virtual table pointers,通常被分別地稱為vtbl和vptr。

一個vtbl通常是一個函式指標陣列。(一些編譯器使用連結串列來代替陣列,但是基本方法是一樣的)在程式中的每個類只要聲明瞭虛擬函式或繼承了虛擬函式,它就有自己的vtbl,並且類中vtbl的專案是指向虛擬函式實現體的指標。

你必須為每個包含虛擬函式的類的virtual table留出空間。類的vtbl的大小與類中宣告的虛擬函式的數量成正比(包括從基類繼承的虛擬函式)。每個類應該只有一個virtual table,所以virtual table所需的空間不會太大,但是如果你有大量的類或者在每個類中有大量的虛擬函式,你會發現vtbl會佔用大量的地址空間。

一些原因導致現在的編譯器一般總是忽略虛擬函式的inline指令。

Virtual table只實現了虛擬函式的一半機制,如果只有這些是沒有用的。只有用某種方法指出每個物件對應的vtbl時,它們才能使用。這是virtual table pointer的工作,它來建立這種聯絡。每個聲明瞭虛擬函式的物件都帶著它,它是一個看不見的資料成員,指向對應類的virtual table。這個看不見的資料成員也稱為vptr,被編譯器加在物件裡,位置只有編譯器知道。

關於虛擬函式表的介紹參考:https://blog。csdn。net/fengbingchun/article/details/79592347

虛擬函式是不能內聯的。這是因為”內聯”是指”在編譯期間用被呼叫的函式體本身來代替函式呼叫的指令”,但是虛擬函式的”虛”是指”直到執行時才能知道要呼叫的是哪一個函式”。

RTTI(執行時型別識別)能讓我們在執行時找到物件和類的有關資訊,所以肯定有某個地方儲存了這些資訊讓我們查詢。這些資訊被儲存在型別為type_info的物件裡,你能透過使用typeid運算子訪問一個類的type_info物件。

關於typeid的使用參考:https://blog。csdn。net/fengbingchun/article/details/51866559

RTTI被設計為在類的vtbl基礎上實現。

25。 將建構函式和非成員函式虛擬化

虛擬建構函式是指能夠根據輸入給它的資料的不同而建立不同型別的物件。虛擬複製建構函式能返回一個指標,指向呼叫該函式的物件的新複製。類的虛擬複製建構函式只是呼叫它們真正的複製建構函式。被派生類重定義的虛擬函式不用必須與基類的虛擬函式具有一樣的返回型別。如果函式的返回型別是一個指向基類的指標(或一個引用),那麼派生類的函式可以返回一個指向基類的派生類的指標(或引用)。

26。 限制某個類所能產生的物件數量

阻止建立某個類的物件,最容易的方法就是把該類的建構函式宣告在類的private域。

27。 要求或禁止在堆中產生物件

// 判斷一個物件是否在堆中, HeapTracked不能用於內建型別,因為內建型別沒有this指標typedef const void* RawAddress;class HeapTracked { // 混合類,跟蹤public: class MissingAddress {}; // 從operator new返回的ptr異常類 virtual ~HeapTracked() = 0; static void* operator new(size_t size); static void operator delete(void* ptr); bool isOnHeap() const; private: static std::list addresses;}; std::list HeapTracked::addresses; HeapTracked::~HeapTracked() {} void* HeapTracked::operator new(size_t size){ void* memPtr = ::operator new(size); addresses。push_front(memPtr); return memPtr;} void HeapTracked::operator delete(void* ptr){ std::list::iterator it = std::find(addresses。begin(), addresses。end(), ptr); if (it != addresses。end()) { addresses。erase(it); ::operator delete(ptr); } else { throw MissingAddress(); // ptr就不是用operator new分配的,所以丟擲一個異常 }} bool HeapTracked::isOnHeap() const{ // 生成的指標將指向“原指標指向物件記憶體”的開始處 // 如果HeapTracked::operator new為當前物件分配記憶體,這個指標就是HeapTracked::operator new返回的指標 const void* rawAddress = dynamic_cast(this); std::list::iterator it = std::find(addresses。begin(), addresses。end(), rawAddress); return it != addresses。end();} class Asset : public HeapTracked {}; // 禁止堆物件class UPNumber27 {private: static void* operator new(size_t size); static void operator delete(void* ptr);}; void* UPNumber27::operator new(size_t size){ return ::operator new(size);} void UPNumber27::operator delete(void* ptr){ ::operator delete(ptr);} class Asset27 {public: Asset27(int initValue) {} private: UPNumber27 value;}; int test_item_27(){ UPNumber27 n1; // okay static UPNumber27 n2; // also okay //UPNumber27* p = new UPNumber27; // error, attempt to call private operator new // UPNumber27的operator new是private這一點 不會對包含UPNumber27成員物件的物件的分配產生任何影響 Asset27* pa = new Asset27(100); // 正確,呼叫Asset::operator new或::operator new,不是UPNumber27::operator new return 0;}

禁止堆物件:禁止用於呼叫new,利用new運算子總是呼叫operator new函式這點來達到目的,可以自己宣告這個函式,而且你可以把它宣告為private。

28。 靈巧(smart)指標

// 大多數靈巧指標模板templateclass SmartPtr {public: SmartPtr(T* realPtr = 0); // 建立一個靈巧指標指向dumb pointer(內建指標)所指的物件,未初始化的指標,預設值為0(null) SmartPtr(const SmartPtr& rhs); // 複製一個靈巧指標 ~SmartPtr(); // 釋放靈巧指標 // make an assignment to a smart ptr SmartPtr& operator=(const SmartPtr& rhs); T* operator->() const; // dereference一個靈巧指標以訪問所指物件的成員 T& operator*() const; // dereference靈巧指標 private: T* pointee; // 靈巧指標所指的物件};

靈巧指標是一種外觀和行為都被設計成與內建指標相類似的物件,不過它能提供更多的功能。它們有許多應用的領域,包括資源管理和重複程式碼任務的自動化。

在C++11中auto_ptr已經被廢棄,用unique_ptr替代。

std::unique_ptr的使用參考:https://blog。csdn。net/fengbingchun/article/details/52203664

29。 引用計數

class String {public: String(const char* initValue = “”); String(const String& rhs); String& operator=(const String& rhs); const char& operator[](int index) const; // for const String char& operator[](int index); // for non-const String ~String(); private: // StringValue的主要目的是提供一個空間將一個特別的值和共享此值的物件的數目聯絡起來 struct StringValue { // holds a reference count and a string value int refCount; char* data; bool shareable; // 標誌,以指出它是否為可共享的 StringValue(const char* initValue); ~StringValue(); }; StringValue* value; // value of this String}; String::String(const char* initValue) : value(new StringValue(initValue)){} String::String(const String& rhs){ if (rhs。value->shareable) { value = rhs。value; ++value->refCount; } else { value = new StringValue(rhs。value->data); }} String& String::operator=(const String& rhs){ if (value == rhs。value) { // do nothing if the values are already the same return *this; } if (value->shareable && ——value->refCount == 0) { // destroy *this‘s value if no one else is using it delete value; } if (rhs。value->shareable) { value = rhs。value; // have *this share rhs’s value ++value->refCount; } else { value = new StringValue(rhs。value->data); } return *this;} const char& String::operator[](int index) const{ return value->data[index];} char& String::operator[](int index){ // if we‘re sharing a value with other String objects, break off a separate copy of the value fro ourselves if (value->refCount > 1) { ——value->refCount; // decrement current value’s refCount, becuase we won‘t be using that value any more value = new StringValue(value->data); // make a copy of the value for ourselves } value->shareable = false; // return a reference to a character inside our unshared StringValue object return value->data[index];} String::~String(){ if (——value->refCount == 0) { delete value; }} String::StringValue::StringValue(const char* initValue) : refCount(1), shareable(true){ data = new char[strlen(initValue) + 1]; strcpy(data, initValue);} String::StringValue::~StringValue(){ delete[] data;} // 基類,任何需要引用計數的類都必須從它繼承class RCObject {public: void addReference() { ++refCount; } void removeReference() { if (——refCount == 0) delete this; } // 必須確保RCObject只能被構建在堆中 void markUnshareable() { shareable = false; } bool isShareable() const { return shareable; } bool isShared() const { return refCount > 1; } protected: RCObject() : refCount(0), shareable(true) {} RCObject(const RCObject& rhs) : refCount(0), shareable(true) {} RCObject& operator=(const RCObject& rhs) { return *this; } virtual ~RCObject() = 0; private: int refCount; bool shareable; }; RCObject::~RCObject() {} // virtual dtors must always be implemented, even if they are pure virtual and do nothing // template class for smart pointers-to-T objects。 T must support the RCObject interface, typically by inheriting from RCObjecttemplateclass RCPtr {public: RCPtr(T* realPtr = 0) : pointee(realPtr) { init(); } RCPtr(const RCPtr& rhs) : pointee(rhs。pointee) { init(); } ~RCPtr() { if (pointee) pointee->removeReference(); } RCPtr& operator=(const RCPtr& rhs) { if (pointee != rhs。pointee) { // skip assignments where the value doesn’t change if (pointee) pointee->removeReference(); // remove reference to current value pointee = rhs。pointee; // point to new value init(); // if possible, share it else make own copy } return *this; } T* operator->() const { return pointee; } T& operator*() const { return *pointee; } private: T* pointee; // dumb pointer this object is emulating void init() // common initialization { if (pointee == 0) // if the dumb pointer is null, so is the smart one return; if (pointee->isShareable() == false) // if the value isn‘t shareable copy it pointee = new T(*pointee); pointee->addReference(); // note that there is now a new reference to the value }}; // 將StringValue修改為是從RCObject繼承// 將引用計數功能移入一個新類(RCObject),增加了靈巧指標(RCPtr)來自動處理引用計數class String2 {public: String2(const char* value = “”) : value(new StringValue(value)) {} const char& operator[](int index) const { return value->data[index]; } // for const String2 char& operator[](int index) // for non-const String2 { if (value->isShared()) value = new StringValue(value->data); value->markUnshareable(); return value->data[index]; } private: // StringValue的主要目的是提供一個空間將一個特別的值和共享此值的物件的數目聯絡起來 struct StringValue : public RCObject { // holds a reference count and a string value char* data; StringValue(const char* initValue) { init(initValue); } StringValue(const StringValue& rhs) { init(rhs。data); } void init(const char* initValue) { data = new char[strlen(initValue) + 1]; strcpy(data, initValue); } ~StringValue() { delete [] data; } }; RCPtr value; // value of this String2 }; int test_item_29(){ String s1(“More Effective C++”); String s2 = s1; s1 = s2; fprintf(stdout, “char: %c\n”, s1[2]); String s3 = s1; s3[5] = ’x‘; return 0;}

引用計數是這樣一個技巧,它允許多個有相同值的物件共享這個值的實現。這個技巧有兩個常用動機。第一個是簡化跟蹤堆中的物件的過程。一旦一個物件透過呼叫new被分配出來,最要緊的就是記錄誰擁有這個物件,因為其所有者——並且只有其所有者——負責對這個物件呼叫delete。但是,所有權可以被從一個物件傳遞到另外一個物件(例如透過傳遞指標型引數)。引用計數可以免除跟蹤物件所有權的擔子,因為當使用引用計數後,物件自己擁有自己。當沒人再使用它時,它自己自動銷燬自己。因此,引用計數是個簡單的垃圾回收體系。第二個動機是由於一個簡單的常識。如果很多物件有相同的值,將這個值儲存多次是很無聊的。更好的辦法是讓所有的物件共享這個值的實現。這麼做不但節省記憶體,而且可以使得程式執行更快,因為不需要構造和析構這個值的複製。

引用計數介紹參考:https://blog。csdn。net/fengbingchun/article/details/85861776

實現引用計數不是沒有代價的。每個被引用的值帶一個引用計數,其大部分操作都需要以某種形式檢查或操作引用計數。物件的值需要更多的記憶體,而我們在處理它們時需要執行更多的程式碼。引用計數是基於物件通常共享相同的值的假設的最佳化技巧。如果假設不成立的話,引用計數將比通常的方法使用更多的記憶體和執行更多的程式碼。另一方面,如果你的物件確實有具有相同值的趨勢,那麼引用計數將同時節省時間和空間。

30。 代理類

templateclass Array2D { // 使用代理實現二維陣列public: Array2D(int i, int j) : i(i), j(j) { data。reset(new T[i*j]); } class Array1D { // Array1D是一個代理類,它的例項扮演的是一個在概念上不存在的一維陣列 public: Array1D(T* data) : data(data) {} T& operator[](int index) { return data[index]; } const T& operator[](int index) const { return data[index]; } private: T* data; }; Array1D operator[](int index) { return Array1D(data。get()+j*index); } const Array1D operator[](int index) const { return Array1D(data。get()+j*index); } private: std::unique_ptr data; int i, j;}; // 可以透過代理類幫助區分透過operator[]進行的是讀操作還是寫操作class String30 {public: String30(const char* value = “”) : value(new StringValue(value)) {} class CharProxy { // proxies for string chars public: CharProxy(String30& str, int index) : theString(str), charIndex(index) {} CharProxy& operator=(const CharProxy& rhs) { // if the string is haring a value with other String objects, // break off a separate copy of the value for this string only if (theString。value->isShared()) theString。value = new StringValue(theString。value->data); // now make the assignment: assign the value of the char // represented by rhs to the char represented by *this theString。value->data[charIndex] = rhs。theString。value->data[rhs。charIndex]; return *this; } CharProxy& operator=(char c) { if (theString。value->isShared()) theString。value = new StringValue(theString。value->data); theString。value->data[charIndex] = c; return *this; } operator char() const { return theString。value->data[charIndex]; } private: String30& theString; int charIndex; }; const CharProxy operator[](int index) const // for const String30 { return CharProxy(const_cast(*this), index); } CharProxy operator[](int index) // for non-const String30 { return CharProxy(*this, index); } //friend class CharProxy;private: // StringValue的主要目的是提供一個空間將一個特別的值和共享此值的物件的數目聯絡起來 struct StringValue : public RCObject { // holds a reference count and a string value char* data; StringValue(const char* initValue) { init(initValue); } StringValue(const StringValue& rhs) { init(rhs。data); } void init(const char* initValue) { data = new char[strlen(initValue) + 1]; strcpy(data, initValue); } ~StringValue() { delete [] data; } }; RCPtr value; // value of this String30 }; int test_item_30(){ Array2D data(10, 20); fprintf(stdout, “%f\n”, data[3][6]); String30 s1(“Effective C++”), s2(“More Effective C++”); // reference-counted strings using proxies fprintf(stdout, “%c\n”, s1[5]); // still legal, still works s2[5] = ’x‘; // also legal, also works s1[3] = s2[8]; // of course it’s legal, of course it works //char* p = &s1[1]; // error, 通常,取proxy物件地址的操作與取實際物件地址的操作得到的指標,其型別是不同的,過載CharProxy類的取地址運算可消除這個不同 return 0;}

可以透過代理類實現二維陣列。

可以透過代理類幫助區分透過operator[]進行的是讀操作還是寫操作。

Proxy類可以完成一些其它方法很難甚至可不能實現的行為。多維陣列是一個例子,左值/右值的區分是第二個,限制隱式型別轉換是第三個。

同時,proxy類也有缺點。作為函式返回值,proxy物件是臨時物件,它們必須被構造和析構。Proxy物件的存在增加了軟體的複雜度。從一個處理實際物件的類改換到處理proxy物件的類經常改變了類的語義,因為proxy物件通常表現出的行為與實際物件有些微妙的區別。

31。 讓函式根據一個以上的物件來決定怎麼虛擬

32。 在未來時態下開發程式

未來時態的考慮增加了你的程式碼的可重用性、可維護性、健壯性,以及在環境發生改變時易於修改。

33。 將非尾端類設計為抽象類

34。 如何在同一程式中混合使用C++和C

名變換:就是C++編譯器給程式的每個函式換一個獨一無二的名字。在C中,這個過程是不需要的,因為沒有函式過載,但幾乎所有C++程式都有函式重名。要禁止名變換,使用C++的extern “C”。不要將extern “C”看作是宣告這個函式是用C語言寫的,應該看作是宣告這個函式應該被當作好像C寫的一樣而進行呼叫。

靜態初始化:在main執行前和執行後都有大量程式碼被執行。尤其是,靜態的類物件和定義在全域性的、名稱空間中的或檔案體中的類物件的建構函式通常在main被執行前就被呼叫。這個過程稱為靜態初始化。同樣,透過靜態初始化產生的物件也要在靜態析構過程中呼叫其解構函式,這個過程通常發生在main結束執行之後。

動態記憶體分配:C++部分使用new和delete,C部分使用malloc(或其變形)和free。

資料結構的相容性:在C++和C之間這樣相互傳遞資料結構是安全的——在C++和C下提供同樣的定義來進行編譯。在C++版本中增加非虛成員函式或許不影響相容性,但幾乎其它的改變都將影響相容。

如果想在同一程式下混合C++與C程式設計,記住下面的指導原則:(1)。確保C++和C編譯器產生相容的obj檔案;(2)。將在兩種語言下都使用的函式宣告為extern “C”;(3)。只要可能,用C++寫main();(4)。總用delete釋放new分配的記憶體;總用free釋放malloc分配的記憶體;(5)。將在兩種語言間傳遞的東西限制在用C編譯的資料結構的範圍內;這些結構的C++版本可以包含非虛成員函式。

35。 讓自己習慣使用標準C++語言

GitHub

:https://github。com/fengbingchun/Messy_Test