【C++必知必會】讀書筆記

C++ 必知必會

爲什麼再一次回頭看C++

自覺閱讀C++相關書籍也已不少。但是有些經典內容值得不斷去重複回顧。隨着項目開發經驗的增加以及代碼能力的增強,往常看起來很經典的知識,再一次回顧會產生很多優秀的想法。這是我閱讀《C++必知必會》的初衷。這本書我個人認爲涵蓋C++面試的所有知識,因此,各位可以通過本文來快速瞭解掌握這本書的乾貨,對於面試部分更加遊刃有餘。

C++正文內容

1.數據抽象

  • 類型:一組操作。
  • 抽象數據類型:一組具有某種實現的操作。

2.多態

  • 多態類型:帶有虛函數的class類型。
  • 多態對象:具有不止一種類型的對象。
  • 多態基類:爲滿足多態對象的使用需求設計的基類。
    編寫多態的原則:基類提供契約允許針對基類接口編寫的多態代碼對不同的特定情況起作用,同時對派生類的存在保持“健康的不知情”。

3.設計模式

  • 設計模式:一種描述面向對象設計的一種共享通用的術語方式。

4.STL

  • STL包括三大組件:容器,算法,迭代器、
    (1)容器:容納組織元素。
    (2)算法:執行操作。
    (3)迭代器:訪問容器中元素。

5.引用是別名而非指針

  • 引用與指針的區別
    (1)不存在空引用。
    (2)所有引用都要初始化。
    (3)一個引用永遠指向用來對其初始化的對象。
  • 注意:一個指向常量的引用採用字面值初始化時,其引用實際被設置爲指向“採用該字面值初始化”的一個臨時變量。本來臨時變量是在表達式末尾就被銷燬的。當這類臨時對象用於初始化一個常量引用時,在引用指向他們期間,臨時對象會一直存在。

6.數組形參

  • 退化:C++不存在數組形參,數組傳入時,實質只傳入指向首元素的指針。
  • 一些標準的數組傳入方式:
template<int n>
inline void average(int (&array)[n])
{
	average_n(array,n);
}

7.常量指針與指向常量的指針

  • const T* p1 : 指向常量T的指針。
  • T const * p2 : 指向常量T的指針。
  • T* const p3 : 常量指針,指向非常量T。

8.指向指針的指針

  • 指針數組作爲形參時,會退化爲指向指針的指針。
  • 函數需要改變傳遞給他指針的值:
    (1)將指針p移動到c的位置。
void scanTo(const char** p, char c)
{
	while(**p && **p!=c)
	{
		++*p;
	}
}

(2)C++更安全的做法是使用指針引用作爲函數形參。

void scanTo(const char*& p, char c)
{
	while(*p && *p!=c)
	{
		++p;
	}
}

9.新式轉型操作符

  • const_cast : 允許添加或移除表達式中類型的const或者volatile。
  • static_cast : 可跨平臺移植的轉型。將一個基類的指針或引用向下轉型爲一個派生類的指針或引用。
Shape* sp = new Circle;
Circle* cp = static_cast<Circle*>(sp);
  • reinterpret_cast : 從bit的角度看待一個對象。允許將一個東西看做完全不同的東西。
  • dynamic_cast : 用於執行從指向基類的指針安全的向下轉型爲指向派生類的指針。僅對於多態類型進行向下轉型。(被轉型的表達式的類型,必須是一個指向帶有虛函數的class類型的指針)同時,執行運行期檢查工作。

10.常量成員函數

  • 在類X的非常量成員函數中,this指針類型爲X *const,即指向非常量X的常量指針。
  • 在類X的常量成員函數中,this的指針類型爲const X* const,即指向常量X的常量指針。
  • 在類X的常量成員函數當中修改類的數據成員,標準做法是將數據成員修改成mutable,而不是const_cast。

11.編譯器在類當中放的東西

  • 如果一個類聲明瞭一個或多個虛函數,那麼編譯器將會爲該類的每一個對象插入一個指向虛函數表的指針。
  • 虛擬繼承(virtual inheritance):對象將會通過嵌入的指針,嵌入的偏移或其他非嵌入的信息來保持對其虛基類子對象(virtual base subobject)位置的跟蹤。即使類沒有聲明虛函數,還有可能被插入了一個虛函數表指針。
    注意:不管類的數據成員的聲明順序如何,編譯器都被允許重新安排他們的佈局。
  • POD(plain old data):爲了避免編譯器重新安排他們的佈局。使用struct的簡單數據結構。
    注意:不要使用memcpy標準內存塊複製函數來複制對象(用於複製存儲區)。
  • 對象的初始化或賦值,都會涉及到對象的構造函數,構造函數時編譯器建立隱藏機制的地方,該隱藏機制實現對象的虛函數等事物。向未初始化的存儲區中塞入一大把比特的做法往往會達到無法預計的結果(比特衝擊)。

12.賦值和初始化並不相同

  • 賦值和初始化本質上是不同的操作。
    (1)對於基礎內建類型,賦值和初始化時簡單的複製位。
    (2)對於用戶自定義類型,初始化與賦值的區別如下(以string爲例):
String::String(const char* init)
{
	if(!init) init = "";
	s_ = new char[strlen(init)+1];
	strcpy(s_, init);
}

String& String::operator = (const char* str)
{
	if(!str) str = "";
	char* tmp = strcpy(new char[strlen(str)+1], str);
	delete[] s_;
	s_ = tmp;
	return *this;
}
  • 賦值是析構後跟一個構造。

13.複製操作

  • 複製構造和複製賦值是兩種完全不同的操作。兩個操作總是被成對聲明。
X(const X&);
X& operator=(const X&);
  • 關於實現的兩種細節:
    (1)T是一個龐大而複雜的類,複製會導致不小的開銷。
    (2)通過交換X各自實現的指針會極大的加快複製的過程。例如Handle的實現機制。
  • Handle class : 句柄類是這樣一種類,主要由一個指向其實現的指針構成。
    注意:複製構造和複製賦值兩個函數是不同的函數,但是兩者應該互相兼容,產生的結果不應該有區別。
  • 標準的複製賦值實現:
Handle& Handle::operator=(const Handle& that)
{
	if(this!=&that)
	{
		//賦值
	}
	return *this;
}

14.函數指針

  • 聲明一個指向特定類型函數的指針:(無需顯式取得函數地址,編譯器知道隱式獲得函數地址,&操作符是可選的。)
void (*fp)(int);
(*fp)(12);
fp(12);

注意:非靜態成員函數的地址不是一個指針,因此不可以將一個函數指針指向非靜態成員函數。

  • 回調:一個可能的動作,這個動作在初始化階段設置,以便在對將來可能發生的事件作出反應時被調用。
    注意:一個函數指針指向內聯函數(inline)是合法的,但是通過函數指針調用內聯函數不會導致內聯函數調用,這是因爲編譯器無法在編譯器精確地確定將會調用什麼函數。

15.指向類成員的指針並非指針

  • 聲明一個指向成員的指針:(使用的是classname:: *)
int C::*pimC;
  • 指向成員的指針並不指向一個具體的內存位置,而是一個類的特定成員,而不是特定對象裏的特定成員。其是成員在類中的偏移量。
  • 存在從指向基類成員的指針到指向公有派生類成員的指針的隱式轉換,但是不存在從指向派生類成員的指針到指向其任何一個基類成員的指針轉換。
class Shape
{
	Point center_;
};
class Circle:public Shape
{
	double radius_;
};
Point Circle::*loc = & Shape::center_;
//Shape當中任何偏移量在Circle當中也是一個有效偏移量。

16.指向類成員函數的指針並非指針

  • 指向類成員函數的函數指針聲明方式:
void (Shape::*mf1)(Point) = &Shape::moveTo;
  • 指向一個指向成員函數的指針,通常不能實現爲一個簡單的指向函數的指針。一個指向成員函數的指針的實現自身必須存儲一些信息,諸如它所指向的成員函數是虛擬的還是非虛擬的,到哪裏去找到恰當的虛函數表指針。

17.處理函數和數組聲明

  • 對於函數的聲明:
int* f1(); //返回值爲int*的函數
int (*f1)(); //一個指針,指向返回值爲int的函數。
int (*afp2[N])(); //一個具有N個元素的數組,元素類型爲指向“返回值爲int”的函數指針。
typedef void (*FP)();
FP afp3[N]; //一個具有N個“類型爲FP”的元素的數組,該類型與afp2相同
  • 對於數組的聲明:
int *a1[N]; //一個具有N個int*元素的數組。
int (*ap1)[N]; //一個指針,指向一個具有N個int元素的數組。
  • 聲明函數引用:
int aFunc(double);
int (&rFunc)(double) = aFunc;

18.函數對象

  • 函數指針的缺點:笨拙、危險且過時。解決:使用函數對象(function object)。
  • 函數對象:一個重載函數調用操作符()的普通的類對象。
  • 函數對象可以獲得虛函數指針的效果,這是通過創建一個帶有虛擬operator()的函數對象層次結構而實現的。
//通過虛函數來增加OOP的靈活性
class Func
{
public:
	virtual ~Func();
	virtual double operator()(double) = 0;
};
class NMFunc : public Func
{
public:
	NMFunc(double (*f)(double)):f_(f){}
	double operator()(double d){return f_(d);}
private:
	double (*f_)(double);
};
//函數使用實體
double integrate(Func& f, double low, double high);
double aFunc(double x) {}
int main()
{
	//如何利用多態性來靈活使用函數對象
	NMFunc g(aFunc);
	double area = integrate(g, 0.0, 2.7);
}

19.Command模式與好萊塢法則

  • Command模式實例:當一個用作回調的函數對象。
    注意:好萊塢法則:不要call我們,我們會calll你。
  • 標準的回調代碼:
class Button
{
public:
	Button(const string& label) : label_(label),action_(0){}
	void setAction(void (*newAction)()){
		action_ = newAction;
	}
	void onClick() const
	{
		if(action_) action_();
	}
private:
	string label_;
	void (*action_)();
};
  • Command模式:使用函數對象代替函數指針,將一個函數對象與“好萊塢法則”相結合使用。優勢:
    (1)函數對象可以封裝數據。
    (2)擁有一個相關的函數對象的層次結構。
    (3)可以處理類層次結構而不是較爲原始的、缺乏靈活性的結構(函數指針)。
class Action
{
public:
	virtual ~Action();
	virtual void operator()()=0;
	virtual Action* clone() const = 0; //原型模式
};
class PlayMusic : public Action
{
public:
	PlayMusic(const tring& songFile):song_(song){}
	void operator()();
private:
	MP3 song_;
};
//Button可以和任何一個Action對象進行協作
class Button
{
public:
	Button(const std::string& label):label_(label), action_(0){}
	void setAction(const Action* newAction){
		Action* temp = newAction->clone();
		delete action_;
		action_ = temp;
	}
	void onClick() const
	{
		if(action_) (*action_)();
	}
private:
	std::string label_;
	Action* action_;
};

注意:將clone命令與Command模式相結合(Prototype),可以獲得更大的靈活性

20.重載與重寫並不同

  • 重載:發生於同一個作用域內有兩個或更多個函數具有相同的名字但簽名不同時。
  • 重寫:派生類函數與基類虛函數具有相同的名字和簽名,派生類函數的實現會取代所繼承的基類函數的實現,以滿足對派生類對象的虛擬調用。重寫機制改變類的行爲而不是改變其接口。

21.Template Method

  • Template Method : 基類設計者爲派生類設計者提供清晰指示的一種方式,“如何實現基類當中所規定的契約”。好萊塢法則的良好例子。
  • 關於OOP的一些基礎公約:
    (1)基類可以自由通過公有成員函數指定與外界的契約關係。通過保護成員函數爲派生類指明額外的細節。
    (2)私有成員函數也可以作爲類實現的一部分。數據應該指定爲私有。
    (3)如果基類成員是非虛擬的,那麼基類設計者就爲以該基類爲根所確立的層次結構指明瞭一個不變式(invariant)。派生類不應該用同名的派生類成員去隱藏基類的非虛函數。
    (4)虛函數和純虛函數指定的操作,其實現可以由派生類通過重寫機制定製。一個非純虛函數提供了默認實現,並且不強求派生類一定要重寫他。純虛函數必須重寫。
  • Template Method : **被實現爲一個公有非虛函數,它調用被保護的虛函數。**派生類必須接受他所繼承的非虛基類函數所指明的全部實現,同時可以重寫公有函數調用的保護虛函數。
  • 我的理解:
    (1)公有非虛函數:作爲一個傳遞給派生類的模板方法流程。
    (2)保護虛函數:每個類依據當前類的功能,重寫這些方法細節,從而調用模板函數的時候,達到不同類有不同效果的目的。

22.名字空間

  • 名字空間:對全局作用域的細分。
  • using從名字空間中導入名字,使他們在using指令的作用域內,不需要限定就可以訪問,同時作用域一直延續到函數體末尾。

23.成員函數查找

  • 成員函數查找的步驟與機制:
    (1)編譯器查找函數的名字。
    (2)從候選者當中選擇最佳匹配函數。
    (3)檢查是否具有訪問該匹配函數的權限。
  • C++非虛類的繼承,派生類如果有一個與基類一樣的函數,會造成隱藏關係。

24.實參相依的查找

  • 實參相依的查找(Argument Dependent Lookup,ADL):當查找一個函數調用表達式中的函數名字時,編譯器會用到“包含函數調用實參的類型”的名字空間中檢查。

25.操作符函數的查找

這部分存疑。

26.能力查詢

  • 存在的問題:在不知道面對對象的精確類型,不知道其是否具備某種能力的情況下, 需要能力查詢。如下所示,當檢查很多個shape是否具備rollable的能力時,就需要能力查詢了。
//接口類,Interface class,一個虛析構函數和一組虛函數指明Rollable對象能做什麼。
class Rollable
{
public:
	virtual ~Rollable();
	virtual void roll()=0;
};
//要繼承的類
class Shape
{
public:
	virtual ~Shape();
	virtual void draw() const = 0;
};
//圓形可以滾動
class Circle : public Shape, public Rollable
{};
//方形不能滾動
class Square : public Shape
{};
  • 能力查詢:C++中的能力查詢是通過對不相關的類型進行dynamic_cast轉換而表達的。這種用法被稱爲橫向轉換(cross-cast)。
  • 若無法轉換,dynamic_cast將會失敗,並且返回一個空指針。
Shape* s = getSomeShape();
Rollable* roller = dynamic_cast<Rollable*>(s);
if(roller != nullptr)
	roller->roll();

27.指針比較的含義

  • C++中,一個對象可以有多個有效的地址,因此,指針比較不是關於地址的問題,而是關於對象同一性的問題。
class Shape{};
class Subject{};
class ObservedBlob : public Shape, public Subject{};
ObservedBlob* ob = new ObservedBlob;
Shape* s = ob; //預定義轉換
Subject* subj = ob; //預定義轉換
  • 多重繼承下可以出現的佈局:
    多重繼承對象佈局
  • 無論哪種佈局,ob,s,subj都指向同一個ObservedBlob對象,因此編譯器必須確保ob與s,subj比較結果均爲true。
ob == subj
//可以等價爲下式:
//(delta爲subject子對象在ObservedBlob對象中的偏移量。)
ob ? (ob + delta == subj) : (subj == 0)
  • 在處理指向對象的指針或引用時,必須小心避免丟失類型信息。
void* v = subj;
if(ob == v)
 //兩者不相等,因爲丟失了類型信息而不知道delta是什麼。

28.虛構造函數與Prototype模式

  • 爲什麼要使用克隆:
    (1)你必須對正在處理的對象的精確類型保持不知情。
    (2)你不希望改變被clone的原始對象。
    (3)不希望收到原始對象改變的影響。
  • 解決方法:Prototype模式:利用虛函數clone來提供克隆能力,類似於專門類型的工廠模式,製造一個適當的產品,同時允許調用代碼對上下文和產品類的精確類型保持不知情。
class Meal
{
public:
	virtual ~Meal();
	virtual void eat() = 0;
	virtual Meal* clone() const = 0;
};

class Spaghetti : public Meal
{
public:
	Spaghetti(const Spaghetti&);
	void eat();
	Spaghetti * clone() const
	{
		return new Spaghetti(*this);
	}
};
Meal* m = thatGuyMeal();
Meal* myMeal = m->clone();

29.Factory Method模式

  • 問題:例如,依據Employee的相關類型的實例,要生成對應類型的HRInfor對象。
    在這裏插入圖片描述
  • 使用enum或者dynamic_cast判斷Employee的對象類型,然後生成HRinfo,是一種很差的實現方式。
  • 解決方法:在基類當中提供一種getInfo的純虛函數。並在其派生類當中重寫相應的實現方法。瞭解員工的只有員工自己。
  • Factory Method本質:基類提供一種虛函數“HOOK”,用於生產適當的產品。每一個派生類重寫繼承的虛函數。從而具有使用一個位置類型對象來生成另一個位置類型的對象。

30.協變返回類型

  • 函數重寫規則:一個重寫的函數與其被重寫的函數必須具有相同的返回類型。
  • 協變返回類型:B是一個類類型,一個基類虛函數返回B*,那麼重寫一個派生類函數可以返回D*。其中D公有派生於B。如果基類虛函數返回B&,那麼重寫的派生類函數可以返回一個D&。
class Shape
{
public:
	virtual Shape* clone() const = 0;
};

class Circle : public Shape
{
public:
	virtual Circle* clone() const;
};
  • 優勢:協變返回機制避免使用易於出錯的轉型操作來“重新”提供類型信息。

31.禁止複製

  • 訪問修飾符(public,protected,private)可以用於表達和執行高級約束技術,指明一個類可以被怎樣使用。
  • 這些技術中最常見的一種是不接受對象的複製操作,這通過將其複製操作聲明爲private同時不爲之提供定義而做到。
  • 將複製構造函數和複製賦值操作符聲明爲private是必不可少的,否則編譯器就會偷偷地將他們聲明爲公有、內聯的成員。通過聲明爲private,就謝絕了編譯器的干預。

32.禁止或強制使用堆分配

  • 在某些情況指明一些特定類的對象不應該分配到Heap上,確保該對象的析構函數一定被調用。維護本體對象的引用計數的句柄對象就屬於這種對象。
  • 具有自動存儲區的類的局部對象,其析構函數會自動調用,具有靜態存儲區的類的對象亦然。Heap上分配的對象則必須顯式銷燬。
  • 指明對象不應該分配到堆上的方式:將堆內存分配定義爲不合法。將operator new和operator delete定義爲protected。
  • 強制對象分配到堆上的方式:將相應的類的析構函數聲明爲private。

33.placement new

  • placement new 是 operator new的一個標準的重載版本,位於全局名字空間中,與我們通常看到的operator new不同,語言明令禁止用戶替換掉placement new,而普通的operator new和operator delete是可以進行修改的。
  • placement new並不實際分配任何存儲區,僅僅返回一個(可能)指向已經分配好空間的指針。正因爲調用placement new並沒有分配空間,所以不要對其進行delete操作。在對象生命期結束的時候調用對象析構函數。
//創建數組
const int numComs = 4;
SPort* comPorts = new (comAddr) SPort[numComs];

//銷燬
int i = numComs;
while(i)
	comPorts[--i].~SPort();

34.特定於類的內存管理

  • 我們無法對new操作符和delete操作符做什麼,其行爲是固定的,但是可以改變他們所調用的operator new 和 operator delete,以滿足其特殊要求。
  • operator new、operator delete最好出現。
  • 在一個new表達式中分配一個類型爲Handle的對象時,編譯器首先會在Handle的作用域內查找一個operator new,如果沒有找到,將會使用全局作用域中的operator new。

35.數組分配

  • 大多數C++程序員都知道在分配和歸還內存時保持數組和非數組形式的操作符的匹配。
  • 數組的分配和歸還不同域非數組。new表達式使用array new。dleete表達式使用array delete。其與operator new的對應方式如下:
void *operator new(size_t) throw(bad_alloc);//operator new
void *operator new[](size_t) throw(bad_alloc);//array new
void operator delete(void*) throw();//operator delete
void operator delete[](void*)throw();//array delete
  • 對於數組的情形,調用的是全局array new和array delete。一般來說,對於數組的分配和釋放,一般定義“僅轉發對全局形式的調用”的局部形式,可以讓事情變得更清晰:
class Handle
{
public:
	void* operator new(size_t);
	void operator delete(void*);
	void* operator new[](size_t n)
	{
		return ::operator new(n);
	}
	void operator delete[](void* p)
	{
		::operator delete(p);
	}
};
  • 通過new表達式隱式的調用array new時,編譯器常常會略微增加一些內存請求,這部分額外內存空間一般用於運行期內存管理器(runtime memory manager)記錄關於數組的一些信息。這些信息(分配的元素個數,每個元素的大小)對於以後分配內存時必不可少的。
arrT = new T[5];
//請求的內存量:5*sizeof(T) + delta

注意:編譯器未必會給每一個數組分配都請求額外的內存空間,並且對於不同的數組分配而言,額外請求的內存空間大小也會發生變化。

36.異常安全公理

  • 處理異常安全問題:從異常安全的組件開始構建異常安全的代碼。但是簡單的將一組異常安全的組件或函數調用組合起來,並不能確保所得的結果就是類型安全的。
  • 公理1 : 異常時同步的。
    (1)異常是同步的並且只能發生於函數調用的邊界。因此諸如預定義類型的算數操作、預定義類型(尤其是指針)的賦值以及其他底層操作不會導致異常發生(他們可能會導致產生某種信號或中斷,但這些東西都不是異常)。
    (2)

37.異常安全的函數

  • 當一個被拋出的異常從throw表達式轉向catch子句的時候,經過的路徑,任何一個部分執行的函數在從執行堆棧上移除其激活記錄之前,都必須清理其所控制的重要資源。
  • 異常安全的函數:首先去做任何可能會拋出異常的事情(但不會改變對象重要的狀態),然後使用不會拋出異常的操作作爲結束。
void Button::setAction(const Action* newAction)
{
	//先做可能會拋出異常的操作
	Action* tmp = newAction->clone();
	//再釋放關鍵資源(析構不會拋出任何異常)
	delete action_;
	action_ = tmp;
}
  • 編寫正確的異常安全的代碼很少使用try語句。
  • 在關鍵的位置使用異常安全檢查:(確實希望檢查一個傳遞的異常類型)
    (1)代碼與第三方庫之間。
    (2)代碼和操作系統之間的模塊分界處。

38.RAII

  • RAII:資源獲取即初始化(resource acquisition is initialization)。利用C++對象生命週期的概念來控制程序資源。例如:內存、文件句柄、網絡連接、審計追蹤。
class Resource{};
class ResourceHandle
{
public:
	explicit ResourceHandle(Resource* aResource):r_(aResource){}//獲取資源
	~ResourceHandle(){
		delete r_;
	}
	Resource* get(){
		return r_;
	}
private:
	ResourceHandle(const ResourceHandle&);
	ResourceHandle& operator=(const ResourceHandle&);
	Resource* r_;
};
  • 使用RAII的初衷:引用的資源在發生異常的情況,往往不會執行完後面的語句。這會導致內存泄漏,從而導致嚴重的問題。
    注意:當RAII對象被分配到堆上的時候,只有顯式的delete掉這個對象,才能夠釋放。

39.new、構造函數和異常

  • 爲了編寫完美的異常安全代碼,保持對任何分配的資源的跟蹤並且時刻準備着當異常發生時釋放他們,是必不可少的。
  • 可以將代碼組織成無需回收資源的方式,也是用資源句柄來回收資源。
  • new操作符實際上執行兩個不同的操作,首先調用一個名爲operator new的函數來分配一些存儲區,然後調用一個構造函數來將未被初始化的存儲區變成一個對象。判斷這兩個操作究竟是哪個產生異常時很重要的。
  • 編譯器可以處理new所產生的異常。當new出現異常,會調用與執行分配任務的operator new相對應的operator delete。

40.智能指針

  • 智能指針:使用類的構造函數、析構函數和複製操作符所提供的能力,來跟蹤對它所指向的東西的訪問,內建指針無能爲力。
  • 智能指針的feature:
    (1)重載->與*操作符,從而可以採用標準指針語法來使用他們。
    (2)通常使用類模板來實現。
template<typename T>
class CheckedPtr
{
public:
	explicit CheckedPtr(T* P):p_(P){}
	~CheckedPtr(){delete p_;}
	T* operator ->(){return get();}
	T& operator *(){return *get();}
private:
	T* p_;
	//返回前檢查指針爲空
	T* get(){
		if(!p_)
		{
			throw NullCheckedPointer();
		}
		return p_;
	}
	CheckedPtr(const CheckedPtr&);
	CheckedPtr& operator=(const CheckedPtr&);
};
  • 對智能指針的調用相當於下式:
(s.operator->())->draw();

41.auto_ptr

  • auto_ptr : 標準庫提供了一個資源句柄模板,以便滿足很多需要使用資源句柄的場合。
  • auto_ptr 在離開作用域時,其析構函數會釋放它所指向的任何東西。如手工編寫的資源句柄所做的那樣。
  • auto_ptr 的賦值和初始化並不是真正的複製操作,他們實際上是將底層對象的控制權從一個auto_ptr轉移到另一個auto_ptr。可以將初始化的右參數作爲“源”,而左參數作爲“接收器sink”。 底層對象的控制權從源傳遞給接收器。
  • 不應該使用auto_ptr的場景:
    (1)永遠都不應該被用作容器元素。(因爲容器假定元素遵從普通複製語義,而不是auto_ptr複製語義。)
    (2)一個auto_ptr不應該指向數組。因爲其析構的時候,使用operator delete進行操作。(vector和string是一個更好的替代)

42.模板術語

  • 模板參數:用於模板聲明。
  • 模板實參:模板的特化當中。
  • 模板名字:簡單的標識符。
  • 模板id:帶有模板實參列表的模板名稱。
  • 模板的特化:以一套模板實參供應給一個模板時得到的東西。特化可以顯式進行,或隱式進行。
template<typename T>//模板參數列表
class Heap; //模板名字
template<typename T>
class Heap<T*>;//模板id

template<typename T>//T是一個模板參數
class Heap{};

Heap<double> dHeap;//double是一個模板實參

43.類模板顯式特化

  • 主模板:僅僅被聲明,用於特化。
template<typename T> class Heap;
  • 顯式特化版本:模板參數列表是空的,要特化的模板實參附加在模板名字後面。這種特化稱爲“完全特化”(沒有剩下任何未指定的模板參數)。
template<>
class Heap<const char*>
  • 編譯器依據主模板的聲明來檢查類模板特化。如果模板實參和主模板相匹配,編譯器將會查找一個可以精確匹配模板實參的顯式特化。
  • 經過特化的版本可以在其中添加新函數,這是合法的。用戶依據主模板提供的接口編寫泛型代碼,並且預期任何特化版本至少具有某些能力。

44.模板局部特化

  • 不能對函數模板進行局部特化。我們只能重載函數。
  • 顯式特化用於以一套精確的實參來定製類模板。而局部特化的語法類似於完全特化,但是其模板參數列表是非空的。
template <typename T>
class Heap<T*>{};

(1)和類模板的完全特化不同,局部特化是一個模板,template關鍵字和參數列表是不可缺少的。
(2)與完全特化不同,這版本的Heap參數類型並沒有完全確定,僅僅是部分地確定爲T*。而T是一個未指定的類型。這就是爲什麼它是局部特化的原因。

  • 局部特化版本優先於主模板被使用,完全特化版本優先於局部特化。最具體、限制性最強的候選者將被選擇。
  • 主模板的完全特化版本或者局部特化必須採用與主模板相同數量和類型的實參進行實例化,但它的模板參數列表並不需要具有和主模板相同的形式。
//示例1
template<typename T, int n>
class Heap<T[n]>;

Heap<float*[6]> h8;

//示例2:局部特化,帶有兩個參數的非成員函數的指針對Heap進行特化
template<typename R, typename A1, typename A2>
class Heap<R (*)(A1,A2)>

45.類模板成員特化(這部分沒太看懂)

  • 類模板的完全特化或局部特化全然是單獨的實體,他們不從主模板繼承任何藉口或實現。一個特化版本僅僅是從主模板繼承了一套有關接口或行爲的“期望”,因爲用戶根據主模板的接口編寫泛型代碼時,通常期望所編寫的代碼同樣可以處理特化的情形。
  • 一個完全特化或局部特化通常必須重新實現主模板所具備的所有能力。
  • 關鍵的兩點:
    (1)除了成員函數外,類模板的其他成員可以被顯式特化,這包括靜態成員和成員模板。
    (2)顯式特化和顯式實例化的區別常常混淆。顯式特化是爲模板或模板成員提供定製版本的一種手段,這種定製版本不同於對主模板的隱式實例化所得到的東西。顯式實例化僅僅是明確告訴編譯器去實例化一個成員,所得結果與隱式實例化所得的東西是一致的。
template void Heap<double>::push(const double&);

46.利用typename消除歧義

  • 簡單的非標準容器模板:
//簡單的非標準容器模板:以嵌套類型名字嵌入關於他們自己的信息
template <typename T>
class PtrList
{
public:
	typedef T* ElemT;
	void insert(ElemT);
};
//嵌套名字允許很容易地訪問被PtrList模板認可的元素類型。
typedef PtrList<State> StateList;
StateList:ElemT currentState = 0;
  • 一般來說,如果T當中有嵌套的Elem名字,那麼在模板當中直接進行T的嵌套名字的訪問,編譯器會將其認爲一個非類型名字。因爲編譯器無法確定嵌套名字的類型。
  • 使用typename關鍵字可以明確告訴編譯器,接下來的限定名字是一個類型名字,從而允許編譯器正確地解析模板。
//嵌套層次最深的名字E是一個類型名字。
typename A::B::C::D::E

總結:必須明確告訴編譯器,某個嵌套的名字是一個類型名字。這樣編譯器才能正確解析。

47.成員模板

  • 一個類模板可以有成員模板,一個成員模板就是自身是模板的成員:
template<typename T>
class SList
{
public:
	template<typename In>
	SList(In begin, In end);
};

//成員模板的實現
template<typename T> //針對一個SList
template<typename In> //針對一個成員
SList<T>::SList(In begin, In end):head_(0)
{
	while(begin!=end)
	{
		push_front(*begin++);
	}
	reverse();
}

//T是double
//In是vector<double>::iterator
SList<double> data2(rd2.begin(), rd2.end());
  • 編譯器會根據需要執行實參推導並實例化構造函數模板。
  • 在STL當中,這是一個常見的構造函數模板應用,以便允許一個容器可以通過從任一個數據源中抽取一系列值進行初始化。
  • 成員模板另一個常見應用:生成類似複製操作的構造函數和賦值操作符。
template <typename T>
class SList
{
public:
	//這是爲了防止S與T一樣的情況下,編譯器自己編寫一個複製操作。
	SList(const SList& that);
	SList& operator=(const SList& rhs);
	//成員複製函數,當S與T不是一個類型的時候。
	template<typename S>
	SList(const SList<S>& that);
	template<typename S>
	SList& operator=(const SList<S>& rhs);
};
  • 任何非虛的成員函數都可以寫成模板。成員模板不能使虛擬的。

48.使用template消除歧義

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