Effective C++ 精華版筆記

文章目錄


01.讓自己習慣C++


01.視C++爲一個語言聯邦

C++支持多重泛型編程語言,支持面向過程、面向對象、函數形式、泛型形式、元編程形式。
內容:

  • C: 區塊、語句、預處理、內置數據類型、數組、指針等。
  • 帶有類的C: ,classes(構造函數和析構函數),封裝、集成、多態、虛函數等等
  • template C++:TMP,模板元編程
  • STL:容器、迭代器、算法以及函數對象
    C++是這四個次語言的組成

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

寧可編譯器代替預處理
1.報錯的時候會指向宏的內容,預處理會將宏進行替換

技巧:
1.常量字符串

	 const char* const authorName = "xxxxx";
	//聲明不變字符串
	const std::string authorName("Scott Meyers");

2.class專屬常量,爲了作用域

class GamePlayer{
	private :
		static const int NumTurns = 9;//定義該常量
		int socres[NumTurns] ;	//使用常量
}

3.需要取地址需要定義式
const int GamePlayer::NumTurns;由於聲明以賦值,所以無需繼續賦值

原因:
1.寧願編譯器替換預處理
2.使用宏定義,編譯器可能盲目的替換,沒有常量產生的代碼小
3.可以控制其域
4.enum比較像#define,因爲取地址的時候也違法
5.宏的括號,及算法優先級的煩惱

要點:

  • 對於單純常量,最好以const對象替換#defines
  • 對於形式函數的宏,最好用inline代替宏

03.儘可能使用const

const星號左邊:被指物體常量
const星號右邊:指針自身常量

  • 對單純遍歷,最好以const對象或enums替換#deines
  • 對於形式函數的宏,最好用inline替換爲#define

04.確定對象被使用前已被初始化

  • 變量都要初始化。
  • STL默認已經幫我們初始化好了。
  • 構造函數賦值的行爲不能稱之爲初始化,也不建議這麼寫
  • 推薦使用初始化成員列表方式初始化
  • 基類多構造函數,可以把數值初始化放到一個私有函數中

static分爲函數內static loical

static全局non-local

  • non-loacl static對象聲明如果 依賴另一個non-local static對象
    這樣不能明確順序是不好的,可以使用單例模式來解決這個問題

記住:
–爲內置型對象進行手工初始化,因爲C++不保證初始化他們
– 使用初始化列表初始化,不要在構造函數本體內使用賦值操作
– 爲免除“跨單元之初始化次序”,以local static 對象替換non-local static對象

2.構造/析構/賦值

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

  • 默認有構造函數,析構函數,拷貝構造函數

06.若不適用編譯器自動生成的函數,就該明確拒絕

  • 不適用拷貝構造
class HomeForSale {
public:
    ...
private:
    ...
    HomeForSale(const HomeForSale&);    //只有聲明而沒有定義
    HomeForSale& operator=(const HomeForSale&);
  • 爲了駁回編譯器自動(暗自)提供的機能,可以將相應的成員函數聲明爲private並且不予實現。使用像Uncopyable這樣的base class也是一種做法。(就是使用繼承的方法,讓父類私有化拷貝構造函數)

07.爲多態基類聲明virtual析構函數

  • 帶多態性質的基類應該聲明一個虛析構函數,如果類帶有任何虛函數,它就應該擁有一個虛析構函數

  • 類的設計目的如果不是作爲基類使用,或者不是爲了具備多態性,就不應該聲明虛析構函數。(浪費內存)
    – 32位計算中將佔用64bits~96bits
    – 64位佔用 64~128bits

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

  • 析構函數絕對不要吐出異常,如果一個析構函數調用的函數可能拋出異常,
    析構函數應該捕捉任何異常,然後吞下它們或者結束程序。

  • 如果客戶需要對某個操作函數運行期間拋出的異常做出反應,
    那麼class應該提供一個普通函數(而非在析構函數中)執行該操作。

09.絕不在構造和析構中調用virtual

eg:構造中父類調用了虛函數的print,會打印父類print
析構函數中,先析構子類後析構父類,所以調用虛函數,也會打印父類print函數
C++ 構造和析構禁止調用virtual

10. operator返回一個 reference to *this

int x,y,z;
x=y=z=15

將上述轉換爲下:

x=(y=(z=15))

推薦代碼如下:
適用於+=,-=,*=,等等

Widget& operator=(const Widget&rhs)
{
	...
	return *this
	...
}

11.在operator中處理"自我賦值"

可能導致指針被提前釋放的問題

//刪除this.pb意味着rhs.pb也可能是個野指針,代碼會產生問題
 Widget& Widget::operator=(const Widget& rhs)
 {
  delete pb;
  pb = new Bitmap(*(rhs.pb));
  return *this;
 }

解決方案

Widget& operator=(const Widget&rhs)
{
	if(this == &rhs)
	{
		return *this
	}
	delete pb;
	pb = new Bitmap(*(rhs.pb))
	return *this;
}
  • 確保當對象自我賦值時 operaotr=有良好的行爲,處理好源對象和目標對象的處理順序,以及 copy and swap
  • 確定任何函數如果操作一個以上的對象,而其中多個對象是同一個對象是,其還是正確。

12.複製對象時勿忘其每一個成分

eg:

class Defau {
public:
	int i = 10;
};
class A {
public:
	A() = default;
	A(const A&rhs):name(rhs.name),u(rhs.u){}
	A& operator = (const A&rhs) {
		this->name = rhs.name;
		this->u = rhs.u;
		return *this;
	}
	string name;
	Defau u;
};
 
class B : public A {
public:
	B():prio(3){}
	int prio;
	B(const B&ur):prio(ur.prio),A(ur){}
	B& operator =(const B&ur) {
		prio = ur.prio;
		A::operator=(ur);
		return *this;
	}
};
int main() {
	B b;
	b.u.i = 15;
	b.prio = 6;
	b.name = "dewdqw";
	B c(b);
	cout << c.u.i << endl;
	cout << c.prio << endl;
	cout << c.name;
}

Copying函數應該確保複製“對象內的所有成員變量”及“所有base class成分”。

不要嘗試以某個copying函數實現另一個copying函數。應該將共同機能放進第三個函數中,並由兩個copying函數共同調用。

3.資源管理

13.以對象管理資源

– 使用 auto_ptr /share_ptr

  • 爲防止資源泄漏,請使用RAII對象,它們在構造函數中獲得資源並在析構函數中釋放資源。

  • 兩個常被使用的RAII classes分別是tr1::shared_ptr和auto_ptr,前者通常是較佳選擇,因爲其copy行爲比較直觀。若選擇auto_ptr,複製動作會使它(被指物)指向null。

14.在資源管理類中小心coping行爲

RAII(資源獲得即是初始化)

  • 複製RAII對象必須一併複製它所管理的資源,所以資源copying行爲決定RAII對象的copying行爲

  • 普遍而常見的RAII class copying行爲是:抑制copying,施行引用計數法(shared_ptr思想),或者是轉移底部資源(auto_ptr思想)

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

  • 1、APIs往往要求訪問原始資源( raw resources),所以每一個 RAII class 應該提供一個“取得其所管理之資源”的辦法。

  • 2、對原始資源的訪問可能經由顯式轉換或隱式轉換。一般而言顯式轉換比較安全, 但隱式轉換對客戶比較方便。

eg:顯示轉換

#include "stdafx.h"
#include <iostream>
#include <memory>
using namespace std;
class myClass
{
public :
    myClass()
    {
        cout<<"myClass()"<<endl;
    }
    ~myClass()
    {
        cout<<"~myClass()"<<endl;
    }
    void printFunc()
    {
        cout<<"printFunc()"<<endl;
    }
int getType()const
    {
        return type;
    }
private:
    int type; 
};
myClass* creatMyClass()
{
    myClass *my=new myClass();
    return my;
}
void issue(myClass *my) { 
    delete my;
} 
int _tmain(int argc, _TCHAR* argv[])
{
    auto_ptr<myClass> apMy(creatMyClass());
    myClass* myC=apMy.get();//////auto_ptr 給我們提供的函數,用來訪問原始資源
    myC->printFunc();//////調用了myClass的方法
    return 0;
}

eg:隱式轉換
通過函數把share_ptr/auto_ptr,轉出一個目標值

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

  • 1.如果在new表達式中使用[],必須在相應的delete表達式中使用[]。如果
    在new表達式中不使用[],一定不要再相應的delete表達式中使用[]。
  • 2.new一個對象會有兩個行爲,第一個是內存被分配出來,第二是針對此內存會有一個
    或多個構造函數被調用。
  • 3.delete一個對象也會有兩個行爲,第一個是針對此內存會有一個或多個析構函數被調用,

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

以獨立語句將newed對象存儲於(置入)智能指針內。如果不這樣做,一旦異常拋出

有可能導致難以察覺的資源泄漏。

processWidget(std::trl::shared_ptr<Widget> (new Widget), property());

①執行new Widget
②調用proprety()函數
③調用tr1::shared_ptr的構造函數
那麼proprety()可以在第一個調用,可以第二個調用,也可以第三個調用。
如果函數異常了,new Widget的指針沒放入share_ptr,這樣就泄漏了.

4.設計與聲明

18.讓接口容易被正確使用,不易被勿用

struct Day {
    explicit Day(int d) : val(d) { }
    int val;
};

struct Month {
    explicit Month(int m) : val(m) { }
    int val;
};

struct Year {
    explicit Year(int y) : val(y) { }
    int val;
};

class Date {
public:
    Date(const Month& m, const Day& d, const Year& y);
    ...
};

Date d(30, 3, 1995);        //錯誤的類型!
Date d(Day(30), Month(3), Year(1995));  //錯誤的類型!
Date d(Month(3), Day(30), Year(1995));  //正確了!

eg:createInvestment使它返回一個tr1::shared_ptr並夾帶getRidOfInvestment函數作爲刪除器

std::tr1::shared_ptr<Investment> createInvestment()
{
    std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
    retVal = ...;    //令retVal指向正確的對象
    return retVal;
}

tr1::shared_ptr的一個優秀的性質是:
它會自動的使用它的“每個指針專屬的刪除器”

這樣的性質消除了一個潛在的可能錯誤:

“cross-DLL problem”
這個問題發生於“對象在動態鏈接庫(DLL)中被創建,但卻在另一個DLL內被delete銷燬”。在一些平臺上,這一類“跨DLL的new/delete成對運用”會導致運行期間錯誤。

然而tr1::shared_ptr就沒有這個問題,因爲它默認的刪除器是來自“tr1::shared_ptr誕生所在的那個那個DLL”的delete。舉個例子來說,如果Stock派生自Investment,而createInvestment的實現如下:

std::tr1::shared_ptr<Investment> createInvestment()
{
    return std::tr1::shared_ptr<Investment>(new Stock);
}

返回的那個tr1::shared_ptr可被傳遞給任何其他的DLLs,無需在意“cross-DLL problem”。這個指向Stock的tr1::shared_ptr會追蹤記錄“當Stock的引用次數變成0時該調用的那個DLL’s delete”。

  • 1,好的接口很容易被正確使用,不容易被誤用。應該在實現的所有接口中努力達成這些性質。
  • 2,“促進正常使用”的辦法包括接口的一致性,以及與內置類型的行爲兼容。
  • 3,“阻止誤用”的辦法包括建立新類型、限制類型上的操作,束縛對象值,以及消除用戶的資源管理責任。
  • 4,tr1::shared_ptr支持定製型刪除器(custom deleter)。這可防範DLL問題,可被用來自動解鎖互斥鎖(mutexes)等等。

19.設計class猶如設計type

靈魂十二問啊!!

  • 1.新的type對象該如何被創建銷燬? 這會影響到class的構造函數和析構函數以及內存的分配函數和釋放函數的設計。

  • 2.對象初始化和對象賦值應該有什麼差別不要混淆初始化和賦值,這將決定構造函數和賦值操作符的行爲。

  • 3.新type對象如果被值傳遞,這將意味着什麼? copy構造函數用來定義一個type的值傳遞應該如何實現。

  • 4.什麼是新type的合法值? 對於成員變量而言,只有有些數值集是有效的。

  • 5.你的新type需要配合繼承圖系嗎?

  • 繼承:受到基類的束縛

  • 被繼承:影響你所聲明的函數——尤其是析構函數——是否爲virtual。

  • 6. 你的新type需要什麼樣的轉換?

  • 7.什麼樣的操作符核函數對此新的type是合理的? 這個問題將決定你的class聲明哪些函數

  • 8.什麼樣的標準函數應該被駁回 這些正是你必須聲明爲private的

  • 9.誰改取用新type的成員? 這些幫助決定那些成員爲public,哪些爲protedted,哪些爲private。也幫助決定哪一個class和/或function應該是友元,以及將他們嵌套到另一個之內是否合理。

  • 10.什麼是type的“未聲明接口”? 他對效率、異常安全性以及資源運用提供何種保證?你講在這些方面提供的保證將爲你的class實現代碼加上相應的約束條件

  • 11.你的type有多麼一般化 或許你並非定義了一個新type,而是定義了一整個types家族。如果果真如此,你就不應該定義一個新的class,而是定義一個新的class template(模板類)

  • 12.你真的需要一個新type嗎? 如果只是定義一個新的子類以便爲既有的class添加機能,說不定單純定義一個或多個non-number函數或者templates更能夠達到目標。

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

儘量傳引用,效率高。不需要修改的 引用就給個const
不包含STL的迭代器和函數對象

  • 1,儘量以pass-by-reference-to-const替換pass-by-value。前者通常比較高效,並可以避免切割問題(slicing problem)。
  • 2,以上的規則並不適用於內置類型,以及STL的迭代器和函數對象。對它們而言,pass-by-value往往比較適當。

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

該錯誤嘗試過了,不在解釋。新手易犯錯誤

絕不要返回pointer或reference指向一個local stack對象,或返回reference指向一個heap-allocated對象,或返回pointer指向一個local static對象而有可能同時需要多個這樣的對象。

22.將成員變量聲明爲private

  • 1,切記將成員變量聲明爲private。這可以賦予客戶訪問數據的一致性、可細微劃分訪問控制、允諾約束條件獲得保證,並提供class作者以充分的實現彈性。
  • 2,protected並不比public根據封裝性。
    一旦你將一個成員變量聲明爲public或protected而用戶開始使用它,就很難改變這個變量所涉及的一切!總結說來,其實只有兩種訪問權限:
    private(提供封裝)
    其他(不提供任何封裝)

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

封裝性優先於類的內聚邏輯
採用namespace可以對內聚性進行良好的折中。
“愈多東西被封裝,愈少人可以看到它,而愈少人看到它,我們就有愈大的彈性去改變它,因爲我們的改變僅僅影響看到改變的那些人或事物”
code:

void ClearWebBrowser(WebBrower& w)
{
    w.ClearCach();
    w.ClearHistory();
    w.RemoveCookies();
}
  • 儘量用non-member non-friend函數替換member函數。可以增加封裝性、包裹彈性(packaging flexibility)、和機能擴充性。

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

https://www.bbsmax.com/A/lk5aEoE051/
不太理解

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

  • todo

5.實現

26.儘可能延後變量定義式的出現時間

  • 儘可能延後變量定義式。這樣做可增加程序清晰度和改善效率。
  • 用到變量在定義就行,防止沒有必要的生命成本

27.儘量少做類型轉換動作

C風格的轉型動作:

(T)expression

函數風格的轉型動作:

T(expression)

C++ 提供四種新類型轉換

  • const_cast (expression)
    • 通常是將對象的長良性消除,是唯一有此能力的C++ style轉型操作符
  //1. 指針指向類 
  const A *pca1 = new A; 
  A *pa2 = const_cast<A*>(pca1); //常量對象轉換爲非常量對象 
  pa2->m_iNum = 200; //fine 
	   //2. 指針指向基本類型 
  const int ica = 100; 
  int * ia = const_cast<int *>(&ica); 
  *ia = 200; 
     
     3.常量引用轉爲非常量引用 
    A a0; 
  const A &a1 = a0; 
  A a2 = const_cast<A&>(a1); //常量引用轉爲非常量引用 

	
    //常量對象被轉換成非常量對象時出錯 
  const A ca; 
  A a = const_cast<A>(ca); //不允許 
  
  const int i = 100; 
  int j = const_cast<int>(i); //不允許 

     const int a = 1;//允許
	int * b = const_cast<int*>(&a);
	*b = 2;
	
	const int a = 1;//允許
	int & b = const_cast<int&>(a);
	b = 2;

網上看到的一個比較好的代碼

const string& shorter(const string& s1, const string& s2) {
	return s1.size() <= s2.size() ? s1 : s2;
}

string& shorter(string& s1, string& s2) {
	//重載調用到上一個函數,它已經寫好了比較的邏輯
	auto &r = shorter(const_cast<const string&>(s1), const_cast<const string&>(s2));
	//auto等號右邊爲引用,類型會忽略掉引用
	return const_cast<string&>(r);
}

  • dynamic_cast(expression)
    • 是將一個基類對象指針(或引用)轉換到繼承類指針,dynamic_cast會根據基類指針是否真正指向繼承類指針來做相應處理。(基類轉子類)
    • 前提條件:當我們將dynamic_cast用於某種類型的指針或引用時,只有該類型含有虛函數時,才能進行這種轉換。否則,編譯器會報錯。
      dynamic_cast運算符的調用形式如下所示:
dynamic_cast<type*>(e)  //e是指針
dynamic_cast<type&>(e)  //e是左值
dynamic_cast<type&&>(e)//e是右值

e能成功轉換爲type*類型的情況有三種:

  • 1)e的類型是目標type的公有派生類:派生類向基類轉換一定會成功。

  • 2)e的類型是目標type的基類,當e是指針指向派生類對象,或者基類引用引用派生類對象時,類型轉換纔會成功,當e指向基類對象,試圖轉換爲派生類對象時,轉換失敗。

  • 3)e的類型就是type的類型時,一定會轉換成功。

如果一條dynamic_cast語句的轉換目標是指針類型並且轉換失敗了,會返回一個空指針,則判斷條件爲0,即爲false;如果轉換成功,指針爲非空,則判斷條件爲非零,即true。

補充:dynamic_cast 有個普通版本來說,他會調用strcmp,用以比較class名稱,所以效率較低

  • reinterpret_cast(expression)
    T必須是一個指針、引用、算術類型、函數指針或者成員指針。它可以把一個指針轉換成一個整數,也可以把一個整數轉換成一個指針(先把一個指針轉換成一個整數,再把該整數轉換成原類型的指針,還可以得到原先的指針值)。

    之前一篇博客介紹的這個用法,使用不是很多,點擊前往
    參考代碼,不好用的東西,慎用吧。reinterpret_cast體現了 C++ 語言的設計思想:用戶可以做任何操作,但要爲自己的行爲負責。

#include <iostream>
using namespace std;
class A
{
public:
    int i;
    int j;
    A(int n):i(n),j(n) { }
};
int main()
{
    A a(100);
    int &r = reinterpret_cast<int&>(a); //強行讓 r 引用 a
    r = 200;  //把 a.i 變成了 200
    cout << a.i << "," << a.j << endl;  // 輸出 200,100
    int n = 300;
    A *pa = reinterpret_cast<A*> ( & n); //強行讓 pa 指向 n
    pa->i = 400;  // n 變成 400
    pa->j = 500;  //此條語句不安全,很可能導致程序崩潰
    cout << n << endl;  // 輸出 400
    long long la = 0x12345678abcdLL;
    pa = reinterpret_cast<A*>(la); //la太長,只取低32位0x5678abcd拷貝給pa
    unsigned int u = reinterpret_cast<unsigned int>(pa);//pa逐個比特拷貝到u
    cout << hex << u << endl;  //輸出 5678abcd
    typedef void (* PF1) (int);
    typedef int (* PF2) (int,char *);
    PF1 pf1;  PF2 pf2;
    pf2 = reinterpret_cast<PF2>(pf1); //兩個不同類型的函數指針之間可以互相轉換
}
  • static_cast(expression)
    ①用於類層次結構中基類(父類)和派生類(子類)之間指針或引用的轉換。
    進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的;
    進行下行轉換(把基類指針或引用轉換成派生類表示)時,由於沒有動態類型檢查,所以是不安全的。
    ②用於基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性也要開發人員來保證。
    ③把空指針轉換成目標類型的空指針。
    ④把任何類型的表達式轉換成void類型。

可以將non_const轉爲const,相反則不能(相反使用const_cast)

小結:

  • 如果可以,儘量避免轉型,特別是注重效率的代碼避免使用dynamiy_cast
  • 如果轉型是必要的,試着將它隱藏域某個函數背後,客戶隨後可以調用該函數,而不需將轉型放進他們代碼內
  • 寧可使用C++ style(新式)轉型,不要使用舊式轉型。

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

class Point{ 
public: 
    Point(int x, int y); 
    ... 
    void setX(int newVal); 
    void setY(int newVal); 
    ... 
}; 

struct RectData{ 
    Point ulhc; 
    Point lrhc; 
}; 

class Rectangle{ 
    ... 
    Point& upperLeft()const {return pData->ulhc;} 
    Point& lowerRight()const {return pData->lrhc;} 

private: 
    std::tr1::shared_ptr<RectData> pData; 
};

上述代碼是不好的,因爲客戶可以通過upperLeft或是lowerRight返回一個內部的引用,進而修改了內部值

使用如下是更好的

class Rectangle{ 
    ... 
    const Point& upperLeft()const {return pData->ulhc;} 
    const Point& lowerRight()const {return pData->lrhc;} 

private: 
    std::tr1::shared_ptr<RectData> pData; 
};

第二個例子

class GUIObject{...}; 

const Rectangle boundingBox(const GUIObject& obj);
GUIObject *pgo; 

... 

const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());//取得一個指針指向外框左上點

boundingBox(*pgo).函數調用結束後會析構Rectangle ,導致代碼產生問題。

避免返回 handle(包括 reference、指針、迭代器)指向對象內部。遵守這個條款可增加封裝性,幫助 const 成員函數的行爲像個 const,並將發生“虛吊號碼牌”的可能性降至最低。

29.爲異常安全而努力是值得的

30.透徹瞭解inling的裏裏外外

inline的代碼會提高效率,編譯器會最優化,但也可能會導致程序體檢太大

class Person
{
public:
int age() const{return theAge;} //一個隱喻的inline申請
private:
 int theAge;
}
  • 將template 生命爲inline,所有的具現化才都是inlined,否則不是。
  • 虛函數inlinehi沒有作用的
  • 編譯器會把inline函數copy多份,這也是爲啥會程序會變大
  • 如果f是程序庫內的inline函數,客戶修改了inline函數,所有用到inline的都會重新編譯
    那麼如果不是inline函數,只需要重連接一下。

請記住:

  • 將大多數inline限制在小型、被頻繁調用的函數身上。這可使日後的調試過程和二進制
    升級更容易,也可以使潛在的代碼膨脹問題最小化,使程序的速度提升機會最大化。
    • 不要只因爲function template出現在文件,就將它inline

31.將文件的編譯依賴關係降到最低

6.繼承與面向對象設計

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

public意味着,基類的事情也一定適用於子類

33.避免遮掩繼承而來的名稱

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

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();// ok 調用 Derived::mf1
d.mf1(x);//error  Derived::mf1掩蓋了Base::mf1
d.mf2();// ok 調用Base::mf2()
d.mf3();//ok 調用Derived::mf3()
d.mf3(x);//error Derived::mf3遮掩了 Base::mf3

1.派生類內的名稱會遮掩基類內的名稱。
2.可以使用using 聲明式或者轉交函數。
-純虛函數只繼承接口
-虛函數既繼承接口,也提供了一份默認實現;調用基類虛函數使用Base::fun()
-普通函數既繼承接口,也強制繼承實現。這裏假定討論的成員函數都是public的。
NVI:該設計是令客戶通過public non-virtual成員函數間接調用private virtual函數,稱爲non-virtual interface(NVI)手法。
它是模板方法設計模式的一個獨特表示;相當對virtual函數進行一層的包裝,可以稱爲是virtual函數的外覆器它是模板方法設計模式的一個獨特表示;相當對virtual函數進行一層的包裝,可以稱爲是virtual函數的外覆器(warpper).
non-virtual函數,採用就近調用原則。virtual函數系動態綁定,而缺省參數值卻是靜態綁定。
Base* ps = new Derived;ps->func(defaultParam);ps的靜態類型就是Base*,而動態類型則是Derived*。

35.考慮virtual函數以外的其他選擇

36.絕不重新定義繼承而來的non-virtual

37.絕不重新定義繼承而來的non-virtual function

38.通過複合塑模出has-a或根據某物實現出

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

private繼承根據某物實現

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

多重繼承可能導致歧義性,帶有virtual會增加成本。可以做虛接口繼承類或是private繼承

7.模板與泛型編程

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

對於 classes(類),interfaces(接口)是 explicit(顯式)的並以 function signatures(函數識別特徵)爲中心的。polymorphism(多態性)通過 virtual functions(虛擬函數)出現在運行期。

class Widget {
public:
   Widget();
   virtual ~Widget();
   virtual std::size_t size() const;
   virtual void normalize();
   void swap(Widget& other); // see Item 25
   ...
};
void doProcessing(Widget& w)
{
   if (w.size() > 10 && w != someNastyWidget) {
   	Widget temp(w);
   	temp.normalize();
   	temp.swap(w);
   }
}

Templates及泛型編程,包含顯示接口和運行期多態。也包含了
隱式接口和編譯器多態。


template<typename T>
void doProcessing(T& w)
{
	if (w.size() > 10 && w != someNastyWidget) {
		T temp(w);
		temp.normalize();
		temp.swap(w);
	}
}

T的隱式接口似乎有這些約束
1、它必須提供一個size函數,該函數返回一個整數值。
2、必須支持一個operator!= 函數,用於比較兩個T對象。這裏我們假設someNastyWidget類型爲T

從上述代碼我們可以知道:
1、w支持哪種接口,由模版執行於w上的操作決定。如本例中w必須支持size,normalize,swap,copy構造、不等比較等(雖然不完全正確)。這組表態式(對該模板而言必須有效編譯)是T必須支持的一組隱式接口。
2、涉及w的調用,可能實例化模板。“以不同的模板參數實例化函數模版”會導致不同的函數調用。這就是所謂的編譯期多態。
類似於“哪一個重載函數被調用”(發生在編譯期)和“哪一個虛函數被綁定”(運行期)。

需要記住的:
1、類和模版都支持接口和多態。
2、類接口是顯示的,以函數聲明爲中心。多態通過虛函數發生於運行期。
3、對模版參數而言,接口是隱式的,基於有效表達式。多態通過模版實例化和函數重載解析,發生於編譯期。

42.瞭解tyename的雙重含義

  1. 在template的聲明式中,typename的使用和class完全相同,即以下兩種聲明方式完全相同
template<typename T>
template<class T>

然而typename還有其他用途:指明嵌套從屬類型名稱.

template <typename C> 
void print2nd(const C& container) 
{ 
    if (container.size() >= 2) 
    { 
        C::const_iterator iter(container.begin()); 
        ++iter; 
        int value = *iter; 
        std::cout << value; 
    } 
}

嵌套從屬名稱,可能會導致解析困難,出現二義性。比如:C::const_iterator* x; 有兩種意思。
一是:模版形參C中有個靜態字段const_iterator,然後計算它與x的乘積;
二是:模版形參C中有個嵌套類型const_iterator,定義指向它的指針。默認情況下,C++編譯器按第一種意思解釋,也就是說,把它當成靜態字段,而不是類型。如果我想告訴編譯器,這是個嵌套類型,該怎麼辦?使用typename C::const_iterator* x;

請記住

  • 聲明template參數時,前綴關鍵字class和typename可互換
    請使用typename標識嵌套從屬類型的名稱;但不得在base class lists(基類列)或
  • member initalization list(成員初值列)內以它作爲base標識符

43.學習模板化基類內名稱

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

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

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

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

48.認識template元編程

X

8.定製new和delete

49.瞭解new-handler的行爲

operator new無法滿足內存申請時,它會不斷調用new-handler函數,直到找到足夠內存。
標準函數如下:

namespace std
{
	typedef void (*new_handler)();
	new_handler set_new_handler(new_handler p) throw;
}

eg


void outOfMen()
{
	std::cerr<<"unable tosatisfy requesest for memeory \n"
	std::abort();
}

int main()
{
	std::set_new_handler(outOfMem);
	int *p = new int[100000000000L]
}

設計一個new-handler函數必須做以下事情:
1.讓更多內存可以被使用,以便於下一次new可以分配內存
2.set_new_handler傳遞一個可以替換原有的函數指針,以便內存申請
3.set_new_handler傳遞空指針,這樣以便於拋出異常
4.拋出bad_alloc的異常,會在存儲所求處拋出異常
5.不返回,通常調用abort或exit

50.瞭解new和delete的合理替換時機

51.編寫new和delete時固守常規

52.寫了placement new 也要寫placement delete

9.雜項討論

53.不要忽略編譯期的警告

class B {
public:
virtual void f() const;
};

class D: public B {
public:
virtual void f();
};

以上代碼會有警告,原因參考50

1、嚴肅對待編譯器發出的警告信息,因爲編譯器作者對所發生的事情有更好的領悟,儘量爭取“無任何警告”。

2、不要過度依賴編譯器的警告,因爲不同編譯器對待事情的態度不同。

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

C++ 98標準:STL、Iostreams、國際化支持、數值處理、異常階層體系、C89標準程序庫。

TR1 14個新組件:智能指針(shared_ptr和tr1:weak_ptr)、tr1:function、tr1::bind、Hash tables/
正則表達式、Tuples、tr1::array、tr1::men_fn、reference_wrapper、隨機數、數學特殊函數、C99兼容擴展
第二組TR1:Type traits、tr1::result_of.

請記住
1、C++標準程序的主要機能是由STL、iostreams、locales組成,幷包含C99標準程序庫;
2、TR1添加了諸如智能指針、一般化函數指針、哈希容器、正則表達式及其他10個組件;
3、TR1只是一個規範,爲了熟悉此規範你必須熟悉源碼,Boost是個不錯的源碼庫。

55.讓自己熟悉Boost

Boost很多的特性都可能會加入C++標準,Boost
包含 文字字符串與文本處理、容器、函數對象和高級編程、泛型編程、模板元編程
等等
1、Boost是一個社羣,一個網站。它致力於免費、源碼開放、同僚複審的C++程序開發。Boost在C++標準化過程中扮演了重要的角色。
2、Boost提供了許多TR1組件實現品,以及其他許多程序庫。

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