1.数据抽象
构造函数、析构函数、复制构造以及转换(不带explicit单参数构造和转换操作符)。
易于正确使用,难以错误使用。
不要让实现影响类型的接口,wrap函数对实现进行封装。
2.多态
多态类型就是带有虚函数的类类型。
多态对象就是一个具有不止一种类型的对象。
多态基类就是一个为了满足多态对象的使用需求而设计的基类。
接口类:通常来说,这样的类没有非静态数据成员,没有声明构造函数,由一个虚析构函数和一个或一组纯虚函数组成。
3.设计模式
它为特定上下文中常见的设计问题提供解决方案,并描述了这种解决方案的结果。
设计模式可以像描述算法设计一样高效、毫无歧义地描述面向对象设计。
设计模式常常被描述为“微架构”,它们可以与其他模式进行组合从而生产一个新的架构。
4.STL
容器、算法和迭代器。
思想体现在:容器与在容器上执行的算法之间无需彼此了解(通过迭代器)。
容器是对数据结构的一种抽象。
算法是对函数的一种抽象。
迭代器提供一种使容器与算法协同工作的机制。
5.引用是别名而非指针
c++编译器通常采用指针的方式实现引用。
引用:不存在空引用、所有引用都要初始化、引用永远指向用来对它初始化的对象。
一个指向非常量的引用是不可以用字面值或临时值进行初始化的,然而一个指向常量的引用可以。
当一个指向常量的引用采用一个字面值来初始化时,该引用实际上被设置为指向“采用该字面值初始化”的一个临时位置。在引用指向它们期间,这些临时对象会一直存在。
6.数组形参
退化:即数组退出成指向其首元素的指针,丢失边界(函数参数会退化成一个函数指针,可以保持其参数类型和返回类型)。
//使用引用限定接受含有特定数量的元素的数组 void Average(int (&T_array)[6]); //模板泛化,编译器推到Nun值 template<int Num> void Average(int(&T_array)[Num]) { AverageImpl(T_array,Num); }
7.常量指针与指向常量的指针
const修饰符修饰的是基础类型还是指针修饰符*。
引用指针天然是一个常量指针。
8.指向指针的指针
使用指向指针的引用代替使用指向指针的指针(函数参数)。
将一个指向非常量的指针转换为一个指向常量的指针是合法的,但不可以将一个指向“指向非常量的指针”的指针转换为一个指向“指向常量的指针”的指针。
9.新式转型操作符
const_cast:允许添加或移除表达式中类型的const或者volatile修饰符(只影响类型修饰符)。
reinterpret_cast:从位bit的角度来看待一个对象,从而允许将一个东西看作另一个完全不同的东西。
static_cast:可跨平台移植的转型,最常见的情况是,它用于将一个继承层次结构中的基类的指针或引用,向下转型为一个派生类的指针或引用。
dynamic_cast:通常用于执行从指向基类的指针安全地向下转型为指向派生类的指针(不同于static_cast,dynamic_cast仅用于对多态类型进行向下转型),失败则返回nullptr。对引用类型执行向下转换,失败则抛出一个std::bad_cast异常而不是仅仅返回一个空指针(不存在空引用)。
向下转型:reinterpret_cast通常只是将基类指针假装成一个派生类指针而不改变其值,而static_cast则将执行正确的地址操作(见28,多重继承的对象布局)。dynamic_cast,被转型的表达式的类型,必须是一个指向带有虚函数的类类型的指针,并在执行期检查工作,来判断转型的正确性。
10.常量成员函数的含义
类x的非常量成员函数中,this指针的类型为 x* const。
类x的常量成员函数中,this指针的类型为 const x* const。
对成员函数的this指针类型加上常量修饰,就可以解释函数重载解析是如何区分一个 成员函数的常量和非常量版本。
11.编译器会在类中放东西
虚函数表指针的位置是随机的。
如果使用虚拟继承,对象将会通过嵌入的指针、嵌入的偏移或其他非嵌入的信息来保持对虚基类子对象位置的跟踪,因此,即便类没有声明虚函数,其中还是有可能被插入一个虚函数表指针。
不管类的数据成员的声明顺序如何,编译器都被允许重新安排它们的布局(POD也不一定保证)
对象的构造函数是编译器建立隐藏机制的地方,该隐藏机制实现对象的虚函数以及诸如此类的东西(简单比特复制可能会破坏内部结构)。
12.赋值和初始化
赋值发生于赋值时,除此之外,遇到所有其他的复制情形均为初始化,包括声明、函数返回、参数传递以及捕获异常中的初始化。
赋值运算符操作可以采用先构造再swap的方式来保证异常安全性。
13.复制操作
复制构造(copy construction)、复制赋值(copy assignment)以及swap。
Class(const Class&)、Class& operator=(const Class&)以及swap(Class&)。复制构造和swap异常安全,从而保证复制赋值操作异常安全(this!=&Class)。
14.函数指针
将一个函数的地址初始化或赋值给一个指向函数的指针时,无需显示地取得函数地址,编译器知道隐式地获得函数地址。
为了调用函数指针所指向的函数而对指针进行解引用操作也是不必要的。
一个函数指针指向内联函数是合法的(没用),然而,通过函数指针调用内联函数将不会导致内联式的函数调用,因为编译器无法在编译期精确地确定将会调用什么函数。
函数指针持有一个重载函数的地址是合法的(依照指针类型选取重载的函数类型)。
15.指向类成员的指针并非指针(size:指针大小)
一个常规的指针包含一个地址。
一个指向成员的指针并不指向一个具体的内存位置。它指向的是一个类的特定成员,而不是指向一个特定对象里的特定成员。(一种可能的实现,通常最清晰的做法是将指向数据成员的指针看作为一个偏移量)大多数编译器将指向数据成员的指针实现为一个整数,其中包含被指向的成员的变异量,另外加上1(加1是为了让值0可以表示一个空的数据成员指针)。这个偏移量说明一个特定的成员的位置距离对象的起点有多少个字节(且一般通用,并不特指某对象)。
逆变性:与指向类成员的指针情况相反,存在从指向基类成员的指针到指向公有派生类的成员的隐式转换,但不存在从指向派生类成员的指针到指向其任何一个基类成员的指针的转换(基类包含派生类数据成员是错误的)。
16.指向成员函数的指针并非指针(size:指针的两倍)
指向一个常量成员函数:type (class::*Func)()const= &class::Func;
逆变性:即存在从指向基类的成员函数的指针到指向派生类成员函数指针的预定义转换,反之则不然。
解引用需要一个对象或者对象指针,一个指向成员函数的指针的现实自身必须存储一些信息,比如它所指向的成员函数是虚拟还是非虚拟,到哪里去找到适当的虚函数指针,从函数的this指针加上或减去的一个偏移量,以及可能还有其他一些信息。
17.处理函数和数组声明
函数和数组修饰符的优先级比指针修饰符的优先级高
//声明一个函数 int *func(); //定义一个函数指针 int (*func_p)();
18.函数对象
函数对象类型重载函数调用操作符(),来创建类似于函数指针的东西。
通过创建带有虚拟operator()的函数对象层次结构来获取虚函数指针的效果(带状态的函数对象、函数指针以及指向成员函数的指针)。
19.Command模式与好莱坞法则
当一个函数对象用作回调(好莱坞法则)时,就是一个Command模式的实例。
好莱坞法则:作为框架,知道何时该去干一些事情,但具体干什么,一无所知。作为框架的客户,知道当发生一个特定的事情时,应该干些什么,但不知道何时去干这件事情(共同构成一个完整的应用程序)。
优点:可以封装数据、函数对象可以通过虚拟成员表现出动态行为、可以处理类层次结构而不用去处理较为原始的结构。
20.STL函数对象
比较器:类似小于操作符(less-than-like)的操作。
判断式(predicate):询问关于单个对象的真/假问题的操作。
使用函数对象作为比较器/判断式,可能操作(前提本身是内联)将被内联处理,而使用函数指针则不允许内联。原因在于,当一个函数模板实例化时,编译器知道比较器/判断式的类型,从而使它知道operator() 将被调用,接着使它可以内联函数,最后使它可以内联对嵌套的比较器/判断式函数的调用。一个函数指针指向内联函数是合法的(没用),然而,通过函数指针调用内联函数将不会导致内联式的函数调用,因为编译器无法在编译期精确地确定将会调用什么函数。
21.重载与重写并不相同
重载:当同一个作用域内的两个或更多个函数名字相同但是签名不同时,就会发生重载(函数的签名由它所声明的参数的数目和类型构成)。
重写:当派生类函数和基类虚函数具有相同的名字和签名时,就会发生重写。
22.Template Method模式
基类成为函数为非虚拟的,那么基类设计者就为以该基类为根所确立的层次结构指明了一个不变式(invariant)。
虚函数和纯虚函数指定的操作,其实现可以由派生类通过重写机制定制(非纯虚函数提供一个默认实现,纯虚函数强制在具体派生类中重写)。
Template Method模式介于非虚函数提供的“占有它或离开它”和虚函数提供的“如果你不喜欢就替换掉所有东西”这两种机制之间。
Template Method确立了其实现的整体架构,同时将部分实现延迟到派生类中进行。通常来说,Template Method被实现为一个公有非虚函数,它调用被保护的虚函数。派生类必须接受它所有继承的非虚基类的函数所指明的全部实现,同时还可以通过重写该公有函数所调用的保护的虚函数,以有限的方式来定制其行为。
23.名字空间
using指令:作用域一直延伸到作用域结束(这种“可用”又不能算是绝对的可用,其实际效果相当与该名字空间中的名字被声明在全局作用域之中,而非局限于using指令所在的作用域中)。
using声明:它通过一个真正的声明提供对名字空间中名字的访问。
匿名空间:避免声明具有静态链接的函数和变量的新潮方式。
24.成员函数查找
首先,编译器查找函数名字,接着,从可用候选者中选择最佳匹配函数,最后,检查是否具有访问该匹配函数的权限。
一旦在内层作用域中找到一个名字,编译器就不会到外层作用域中继续查找该名字(内层作用域会隐藏外层作用域中相同的名字,函数名和变量名等同效果)。
25.实参相依的查找(Argument Dependent Lookup,ADL)
当查找一个函数调用表达式中的函数名字时,编译器也会到“包含函数调用实参的类型”的名字空间中检查(只限于当前查找)。
ADL是关于函数如何被调用的一个属性(并不是重载)。
26.操作符函数查找
对重载操作符的中缀调用执行一个退化形式的ADL,即当确定将哪些函数纳入重载解析考虑范围时,中缀操作符中左参数的类(可能只有一个左参数,没有右参数)的作用域和全局作用域被考虑在内。
27.能力查询
dynamic_cast通称为横向转型,因为它试图在一个类层次结构中执行横向转换,而不是向上或向下转换,失败返回nullptr。
28.指针比较的含义
class Observed : public Shape,public Subject{}
一个对象可以有多个有效的地址,因此,指针比较不是地址问题,而是对象同一性问题。
不管哪种布局,ob、s和subj都指向同一个Observed对象,编译器通过将参与比较的指针值之一调整一定的偏移量来完成这种比较。
指向void的指针丢失类型信息(地址比较为原始地址比较)。
29.Prototype模式与虚构造函数
virtual Type* Clone()const;
30.Factory Method 模式
本质在于,基类提供一个虚函数“挂钩”,用于生产适当的“产品”。每一个派生类可以重写继承的虚函数,为自己生产适当的产品。
实际上,我们具备了使用一个未知类型的对象来产生另一个未知类型的对象的能力。
31.协变返回类型
对于一个重写的虚函数必须与它重写的函数具有相同的返回类型(派生类是一个(is-a)基类,所以基类的返回类型可以写成派生类)。
返回某个类类型,其完整定义必须在此函数声明之前,因为编译器必须知道对象的布局,才能执行适当的地址操作(指针的比较含义)。
32.禁止复制
通过将其复制操作声明为private同时不为之提供定义而做到(c++11:delete、default)。
33.制造抽象基类
声明一个纯虚函数、将构造函数以及复制构造函数声明为protected、将析构函数声明为protected。
34.禁止或强制使用堆分配
禁止:将operator new、operator delete、operator new[]以及operator delete[]声明为protected。
强制:将析构声明为private,同时提供公有销毁对象的方法。
35.placement new
const size_t n= sizeof(std::string)* BUFSIZE; string *buf= static_cast<std::string*>(::operator new (n)); void Append(std::string T_buf[],int& T_szie,const std::string& T_val) { new (&T_buf[T_size++]) std::string(T_val); } void CleanupBuf(std::string T_buf[],int T_size) { while(T_size) { T_buf[--T_size].~string(); ::operator delete(T_buf); } }
36.特定于类的内存管理
在一个new表达式中分配一个对象时,编译器首先会在对象的作用域内查找一个operator new,如果没有,它将会使用全局作用域中的operator new。
new和delete操作符并不意味着使用堆或自由存储区内存,使用new操作符唯一能表明的是名为operator new的函数将被调用,且该函数返回一个指向某块内存的指针(标准、全局的operator new 和operator delete的确是从堆上分配内存,但成员operator new和operator delete可以做它们想做的任何事情)。
37.数组分配
通过new表达式隐式地调用array new时,编译器常常会略微增加一些内存请求(所请求的额外空间一般由运行期内存管理器来记录关于数组的一些信息,这些信息包括分配的元素个数、每个元素的大小等,对于以后回收内存是必不可少的,不过,编译器未必为每一个数组分配都请求额外的内存空间,并且对于不同的数组分配而言,额外请求的内存空间大小也会发生变化)。
通过使用placement new代替array new。
38.异常安全公理
以尽可能小的一组公理为起点来证明一些简单的定理,然后再使用这些辅助定理去证明后续更复杂、更有意义的定理。
异常是同步的:异常是同步的并且只能发生在函数调用的边界(诸如预定义类型的算数操作、预定义类型的赋值以及其他低层操作不会导致异常发生),即函数调用可能会抛出异常。如果程序在某一点抛出一个异常,那么在该异常被处理之前程序不会继续往下执行,即程序执行流必须等待异常处理完成。
对象的销毁是异常安全的:析构函数、operator delete以及operator delete[]不会抛出异常。
交换操作不会抛出异常。
39.异常安全的函数
做任何可能会抛出异常的事情时,不先改变对象的状态,而是等事情结束且没发生异常时再改变对象的状态,以不会抛出异常的操作作为结束。
try...catch通常是在代码和第三方的库之间以及代码和操作系统之间的模块分界处。
40.RAII(resource acquisition is initialization,资源获取即初始化)
利用对象生命期的概念来控制程序的资源,如内存、文件句柄、网络链接以及审计追踪(audit trail)等。
如果希望对某个重要资源进行追踪,那么创建一个对象,并将资源的生命期和对象的生命期相关联。
边缘性情形:调用abort或者exit的情形以及因抛出的异常从未被捕获而导致不确定的情形,析构函数无法确保被调用。
一个对象的构造函数按照其基类的继承列表中声明的顺序来初始化各个基类的子对象,接着按照数据成员声明的顺序来初始化各数据成员,然后执行构造函数的本体。
一个对象的析构函数先执行析构函数本体,接着按与声明相反的顺序销毁对象的数据成员,最后是按与声明相反的顺序销毁对象的基类子对象。
41.new、构造函数和异常
new操作符实际上执行两个不同的操作,首先调用一个名为operator new的函数来分配一些存储区,然后调用一个构造函数将未被初始化的存储区变成一个对象。
在调用new操作符时编译器在构造函数发生异常时候它将会调用与执行分配任务的operator new相对应的 operator delete。
42.智能指针
重载->、*、->*、++、--、+、-、+=、-=、[]、构造、赋值运算符
重载->,此操作符必须被重载为一个成员函数,并且具有一个非同寻常的性质,即当它被调用时,它并不被消耗掉。
43.auto_ptr
对于auto_ptr而言,赋值和初始化斌不是真正的复制操作,它们实际上是将对底层对象的控制权从一个auto_ptr转移到另一个auto_ptr(可以将赋值或初始化操作的右参数视作“源”,而将左参数视作“接收器sink”,底层对象的控制权从源传递给接收器)。
不能是资源数组(调用delete)。
44.指针算术
指针算术总是依照所指对象的大小比例进行的,自增或自减是增加或减少sizeof(type)个字节(void*不支持指针算术因为不知道某个void*所指向的对象的类型)。
同一类型的两个指针可以进行减法运算,结果为参与运算的两个指针之间的元素个数,如果第一个指针大于第二个指针,结果为正,反之则相反(结果类型 ptrdiff_t)。
45.模板术语
模板参数列表:模板的声明,
模板实参列表:模板的特化,
模板名字:简单的标识符,
模板id:指附带有模板实参列表的模板名字,
模板的特化是指把一套模板实参提供给一个模板时所得到的东西(模板特化可能会也可能不会导致模板发生实例化)。
编译器根据主模板的声明来检查类模板特化,如果模板实参和主模板相匹配,编译器将会查找与模板实参有着最佳匹配的完全特化或者局部特化(主模板的完全特化或局部特化必须采用与主模板相同数量和类型的实参进行实例化,但它的模板参数列表并不需要具有和主模板相同的形式)。
46.类模板显示特化
通常根据主模板提供的接口编写泛型代码,并预期任何特化版本将至少具有这些能力(但标准并不做要求)。
完全特化:模板参数列表为空,但是要特化的模板实参则附加在模板名字后面,类模板显示特化版本其实并不是一个模板,因为此时没有剩下任何未指定的模板参数。
47.模板局部特化
不能对函数模板进行局部特化。
局部特化类似完全特化,但是它的模板参数列表是非空的。与完全特化一样,类模板名字是一个模板id而不是一个简单的模板名字。
局部特化是一个模板,它的参数类型并没有完全被确定,它只是部分地确定。
完全特化版本> 局部特化版本> 主模板。
48.类模板成员特化
特化主模板成员函数的一个子集(主模板函数的接口必须和进行成员特化的模板的相应接口精确匹配)。
如果已存在局部特化,那么显示特化就必须符合局部特化中成员的接口。
49.利用typename消除歧义
编译器无法确定一个嵌套名字的类型,它就会假定嵌套的名字是一个非类型的名字。typename关键字可以明确地告诉编译器,接下来的限定名字是一个类型名字。
50.成员模板
成员模板就是一个自身是模板的成员。
51.采用template消除歧义
STL配置器 。
typedef typename A::template rebind<Node>::other NodeAlloc;
52.针对类型信息的特化
在编译期询问明显的问题并进行相应的操作。
template <typename T> class Stack { public: ~Stack() { cleanup(typename IsPtr<T>::Result()); } private: void clenup(Yes) { for (;;) delete *list; } void cleanup(No){} };
53.嵌入的信息类型
参数类型、返回值以及迭代器类型等。
unary_function/binary_function。
typename从属类型:告诉编译器,嵌套的名字是类型名,而不是其他什么嵌套名字。
54.traits类
traits类是一个关于某个类型的信息的集合,是在泛型算法和不遵从算法期望的约定的类型之间,放入一个遵从约定的中间层。(即不是直接从容器类型自身获得信息,而是从中间层traits类获取信息)。
//特化 template<> struct ContainerTraits<ClassName> { typedef int Elem; typedef Elem Temp; typedef Elem* Ptr; }; //局部特化 template<typename Type> struct ContainerTraits<Type*> { typedef Type Elem; typedef Elem Temp; typedef Type* Ptr; };
55.模板的模板参数
一个模板可以带有一个自身就是一个模板名字的参数。
通过自身的实现解决元素和容器类型之间的协调问题。
56.policy
典型用法,模板被实例化并采用模板的模板参数。
在泛型程序中,常常要做出关于实现和行为的策略决策,这些决策可以作用策略进行抽象和表示。
57.模板实参的推导
类模板必须被显示地特化(编译器能够从函数实参的类型推导出实参类型和返回值类型,用函数模板实参推导机制可间接地特化类模板)。
函数模板,编译器根据函数调用中的实参类型推导出模板实参(编译器在模板实参的推导期间只检查函数实参的类型,对它可能的返回类型并不作检查,因此,想要让编译器指定返回类型就必须告诉它,如果编译器可以自行推导出实参,那么模板实参列表中靠后的模板实参就可以省略)。
template <typename A1,typename A2,typename R> class PFun : public std::binary_function<A1,A2,R> { public: explicit PFun(R (*fp)(A1,A2)):fp_(fp){} R operator()(A1 a1,A2 a2)const { return fp_(a1,a2); } private: R (*fp_)(A1,A2); }; template <typename A1,typename A2,typename R> inline PFun<A1,A2,R> makePFun(R(*pf)(A1,A2)) { return PFun<A1,A2,R>(pf); }
58.重载函数模板
函数模板和非模板函数之间的主要区别在于对实参隐式转换的支持度。
非模板函数允许对实参进行广泛的隐式转换,从内建转换到用户自定义转换(借助未标识explicit和转换操作符)。
对函数模板,由于编译器必须基于调用的实参类型来执行模板参数推导,因此只支持一些琐细的隐式转换,包括涉及外层修饰(T到const T、const T 到T)、引用以及数组和函数退化的指针等。
59.SFINAE(substitution failure is not an error,替换失败并非错误)
当试图使用函数模板实参推导机制在多个重载函数模板和非模板函数中进行选择时,编译器可能会尝试到一些失败的特化。
作为函数模板实参推导的一个组成部分,编译器将会忽略“一级”cv修饰符(类模板局部特化不会),也会忽略引用修饰符。且不会将带有重载指针操作符的自定义类型误认为是一种指针类型,因为在函数模板实参的推导过程中,编译器对实参只会施用一套极有限的转换规则清单,用户自定义转换并不在此清单之列。
实际上,类模板局部特化和函数模板重载在技术上有着极其密切的关系,c++标准其实就是根据它们中的一个来定义另一个选择算法。
typedef char True; typedef struct {char a[2];} False; template <typename T> True IsPtr(T*); False IsPtr(...); #define is_ptr(e) (sizeof(IsPtr(e))== sizeof(True)) //在这里,可以使用is_ptr来确定一个表达式的类型是否为一个指针,这是通过结合使用函数模板实参推导和SFINAE达成的。编译器将会匹配函数模板,否则它将会匹配带有省略号形参的
//非模板函数,SFINAE可以确保以非指针类型来匹配模板isPtr的尝试不会导致编译期错误 template <typename T> struct IsClass { template <class C> static True isClass(int C::*); template <typename C> static False isClass(...); enum{r= sizeof(IsClass<T>::isClass<T>(0))}; }; //is class template <class C> True hasIterator(typename C::iterator const*); template <typename T> False hasIterator(...); #define has_iterator(C) (sizeof(hasIterator<C>(0))== sizeof(True)) //has iteratoc template <typename T1,typename T2> struct CanConvert { static True canConvert(T2); static False canConvert(...); static T1 makeT1(); enum {r= sizeof(canConvert(makeT1()))== sizeof(True)}; };
61.只实例化要用的东西
如果实际上并没有调用一个模板的成员函数,那么该成员就不会被实例化,如果没有调用一个已声明的函数(或试图获取其地址),那么就不需要定义它。
用来特化一个类模板的模板实参时,并不一定要使得该类模板的所有成员函数都被合法地实例化。
63.可选关键字
当声明operator new、operator delete、array new以及array delete成员时,static关键字是可选的,因为这些函数是隐式静态(因为operator new被调用于类对象构造之前,而operator delete则被调用于类对象析构之后,这两种情形下,对象均不处于有效的状态,因此无this指针可用)。