【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; //预定义转换
  • 多重继承下可以出现的布局:
    【C++必知必会】读书笔记_第1张图片
  • 无论哪种布局,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对象。
    【C++必知必会】读书笔记_第2张图片
  • 使用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::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消除歧义

你可能感兴趣的:(迈向游戏引擎工程师)