Effective C++

条款1:视C++为一个语言联邦

      刚开始的时候,C++只是C加上一些面向对象特性,C++最初的名字叫做C with class也反映了这一点。而现在的C++是一个支持多重范型的编程语言,一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。
      我们应该将C++看做是一个由相关语言组成的联邦而非单一的语言,其中包括C、Object-Orientd C++、Template C++以及STL。当我们从其中一个部分跳到另一个部分工作时,高效编程守则可能发生变化。例如在参数传递上,对于内置类型而言值传递通常比引用传递要高效,但是在Object-Oriented C++中,由于用户定义构造函数和析构函数的存在,引用传递则比较高效,template C++时更是如此。但是如果进入到STL中,由于迭代器和函数对象都是在C的指针上创造出来的,所以对STL的迭代器和函数对象而言,C语言的值传递效率更高。


条款2:尽量以const、enum、inline替换define

对于单纯的常量,最好以const对象或者enum替换宏定义

对于形似函数的宏,最好改用内联函数替换宏

宏的优点

增强代码的复用性、提高性能

宏的缺陷
  1. 调试问题:我们使用宏定义,可能导致出错时,难以排查,因为编译器就没有见过宏(宏名字并没有进入符号表),只见过宏替换之后的东西,并且可能导致代码量比较大,而使用const则可以比较好的解决这个问题。
  2. 作用域问题:我们无法利用宏定义创建一个class专属的常量,因为宏并不重视作用域,一旦被定义,那么它在其后的编译过程中有效(除非在某处卸载宏),因此宏不能提供任何的封装性。
  3. 宏函数容易出现一些坑,稍不注意就落了进去,建议使用内联函数代替掉宏函数。保证了作用域、效率、访问规则以及函数的类型安全性

       

说一下内联函数,他和宏定义有什么不一样
  1. 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且不能出现递归。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。

  2. 宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换,内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率

  3. 宏定义是没有类型检查的,无论对还是错都是直接替换,内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等

  4. 宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预编译的时候把所有的宏名替换,内联函数则是在编译阶段把所有调用内联函数的地方把内联函数插入。这样可以省去函数压栈退栈,提高了效率

  5. 宏函数不能访问类成员,内联函数可以访问类成员;

  6. 宏和内联函数都无法调试

静态整形常量可以没有定义
class Player
{
private:
	static const int num = 5;
}

        如果类内想要实现一个常量,那么我们最好加上static关键字,这样可以保证只有一个该常量,上述中的num只是一个声明式而不是一个定义式,通常C++对于任何东西都需要提供一个定义式,但是如果它是一个class专属常量又是一个static且为整数类型,则需要特殊处理。只要你不取他的地址,那么你就可以声明并使用而无需提供定义式。但是如果你取其地址或者编译器(错误的)坚持看到一个定义式,我们就必须提供一个定义式。

const int Player::NumTurns;//定义

        我们应该将其放入到源文件而不是头文件中,因为在声明的时候已经获得初值,因此定义时不可以再设初值。
        我们可以使用static const成员来定义数组,但是如果编译器不允许我们在类内直接对静态的const成员赋初值,那么我们就不能定义数组了,即便是在类内声明类外定义也不行。此时我们需要使用enum hack。其理论基础是:一个属于枚举类型的数值可当做int使用。

enum hack存在的原因
  1. 有时候比较像宏,而不是const,有时候正是你想要的,比如取一个const的地址是合法的,但是取一个enum的地址就不合法,而取一个宏定义的地址通常也不合法
  2. 如果你不想让别人获得一个指针或者引用指向你的某个整数常量,enum可以帮上忙
  3. 可以保证不会分配内存,const 就不一定了。宏定义也有可能导致内存的浪费。
  4. 实用主义,许多代码都是用到这个东西

条款3:尽可能的使用const

  1. 将某些东西声明为const可以帮助编译器侦测出错误用法。const可以施加与任何作用域内的对象、函数参数、函数返回类型、成员函数本体
  2. 编译器强制实施bitwise constness,但程序员编写的程序应该是logical const。
  3. 当const和non-const成员函数有着实质等价的实现时,另non-const版本去调用const版本可以避免代码重复。反之则不行
  4. 尽可能的使用const,你会为你的所为而高兴。

      const的一件奇妙的事情是,它允许你制定一个语义约束,而编译器会强制实施这项约束,它允许你告诉编译器和其他程序员某值应该保持不变。只要某个值不会变,你就应该表示出来,因为表示出来后可以获得编译器的帮助,确保这条约束不会被违反。
      STL迭代器以指针为基础构造出来的,所以迭代器的作用就像一个T*指针,声明迭代器为const就像声明指针为const一样,表示这个迭代器不能指向不同的东西,但是它所指的东西的值是可以改动的,如果你希望迭代器所指的东西也不能被改动,你需要使用的是const_iterator。

const与函数声明
  1. 修饰函数的返回值,往往可以降低因客户 错误而造成的错误,而又不至于放弃安全性和高效性

  2. 对于const参数而言,除非我们需要改动参数,我们都应该将参数设置为const

  3. 两个成员函数的常量性是可以作为重载的条件的
          函数的返回类型如果是一个内置类型,那么改动函数返回值从来就不是合法的,纵使合法,我们修改的不过是原对象的一个副本

const成员函数

      const成员函数存在是为了确定该成员函数可作用于const对象身上

  1. 他们使class接口比较容易被理解,这是因为,得知那个函数可以改动对象的内容而哪些函数不可以很重要
  2. 他们使操作const对象成为可能,这对编写高效的代码是个关键,因为我们通常传递对象的引用,这个技术可行的前提是,我们有const成员函数可以处理const对象。
常量指针?指针常量?

        如果关键字const出现在星号左边,表示被指物是常量,即常量指针;如果出现在星号右边,表示指针自身是常量,即指针常量。如果出现在星号的两边,表示被指物和指针两者都是常量。

bitwise const以及logical const

       编译器支持的是bitwise const,即成员函数只有在不更改对象任何成员变量时才可以说是const,也就是说它不更改对象内的任意一个bit。这种论点的好处是很容易侦测违反点:编译器只需要寻找成员变量的赋值动作即可,bitwise constness正是C++对常量性的定义,因此const成员函数不可以更改对象内的任何非静态的成员变量。
       logical const指的是,一个const函数可以更改它所处理对象的某些bit,但是不能在客户端侦测出来这种改变。往往我们需要logical const而不是bitwise const,这时我们可以使用C++中的一个关键字mutable来释放掉非静态成员变量的bitwise constness约束。

const版本和非const版本中存在大量的重复代码,该怎么办?

       你可能会说搞一个私有函数来完成这些动作呗,这样虽然可行,但是还是重复了一些代码,比如函数调用和两次return(有些牵强)。更好的方法是我们可以运用const成员函数实现其non-const函数。虽然转型是一个糟糕的想法,但是代码重复更是令我们头疼,所以两者取其轻。

const关键字
  • 修饰全局变量:C/C++略有不同,即C++的const修饰的全局变量可以作为属组的初始化的大小,而C不可以;两者中变量的值不能被修改
  • 修饰局部变量:代表局部变量的值不能被修改
  • 修饰类的成员变量:必须在初始化列表初始化,除此之外,引用类型的数据成员,没有默认构造函数的对象成员,如果存在继承关系,如果父类没有默认的构造函数,也必须在初始化列表中被初始化,初始化列表对数据成员的初始化顺序是按照数据成员的声明顺序严格执行的;
  • 修饰类的成员函数:一般放在成员函数的最后面,修饰的是类的成员函数中的隐藏参数this指针,代表不可以通过this指针修改类的数据成员。
  • 关于const还有一个问题就是传参和赋值的问题,一般来说,const修饰的变量是安全的,没有const修饰的变量是不安全的,一般在传参的时候,非const修饰的的变量可以传给const修饰的,而const修饰的不可以传给非const修饰的形参,这就相当于把安全的东西交给了不安全的人;而赋值的话更不用说了,const修饰的不可以传给没有const修饰的变量;

条款4 确定对象被使用前已经被初始化

  1. 对于无任何成员的内置类型,必须手动完成
  2. 而对于内置类型以外的任何其他东西,初始化的责任落在了构造函数身上。构造函数最好使用成员初始化列表,而不是在构造函数本体内使用赋值运算符,成员初始化的顺序与该成员变量在类中定义的次序有关,而与在初始化列表中的位置无关。
  3. 为了避免“跨编译单元的初始化次序”问题,应该以local static对象替换non-local static对象

      读取未初始化的值会导致不明确的行为,某些平台上,仅仅只是读取为初始化的值,就可能会让你的程序终止运行,更可能的情况是读入一些随机的bit污染了正在进行读取动作的那个对象,最终导致不可预测的程序的行为。

我们如何判断对象的初始化动作什么时候会发生?

        如果使用的是C part of C++而且初始化可能导致运行期成本时,那么不保证会发生初始化,在C++的其余部分就会初始化。

        推荐:永远在使用对象之前先将他初始化,对于无任何成员的内置类型,必须手动完成。对于内置类型之外的任何其他东西,初始化的责任落在构造函数身上,我们应确保每一个构造函数都将对象的每一个成员初始化。

        在构造函数的函数体内的“初始化”其实就是赋值,C++规定,对象的成员变量的初始化动作发生在进入构造函数函数体之前,对于非内置类型,会在进入函数体之前调用其构造函数,获得初值。但这对于内置类型不一定成立。我们应该使用初始化列表来完成对象的初始化,这样做效率会高一些。对于大多数类型而言,比起先调用default构造函数然后再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有时甚至高效很多,对于内置类型,其初始化和赋值的成本相同,但是为了一致性最好也通过成员初始化列表来完成。最后,即便你想要默认构造一个对象,那么也可以为其直接指定默认值(无参构造)即可。

        有些情况下即便面对的成员属于内置类型,也需要使用初始化列表,例如,成员变量是const或者reference。他们就一定要被初始化而不是赋值。

        有些类拥有多个构造函数,每个构造函数都使用初始化列表且有多个成员,这种情况下可以适当遗漏那些赋值表现像初始化一样好的成员变量,改用他们的赋值操作,并将那些赋值操作移往某个函数供所有的构造函数使用(往往是私有的),这种做法在成员变量初始值由文件或数据库读入的时候特别有用。

        C++有着十分固定的成员初始化次序,base class更早于derived class被初始化,而class的成员变量按照其声明的次序被初始化。(所以初始化列表中最好和次顺序一致,要不然会搞晕不懂C++的人)

跨编译单元的初始化次序

        non-local static对象包括全局对象,命名空间中的对象,在类内的对象或在file作用域中的被定义为静态的对象。程序结束时,static对象会被自动销毁,也就是他们的析构函数会在main函数结束时被自动调用
        编译单元:指产出单一目标文件的那些源码,基本上就是 单一源码加上其所含入的头文件

        如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化。因为C++对于定义于不同编译单元内的non-local static对象的初始化次序没有明确定义。(因为决定他们的初始化顺序太难了)

        解决方法:将每一个non-local static对象搬到自己专属的函数中,这些函数返回一个指向该对象的引用。用户直接调用这些函数而不是直接涉及这些对象。因为C++保证函数内的局部静态变量会在该函数被调用期间首次遇到该对象的定义式时被初始化。如果你不调用该函数,那么该对象就不存在,这可是全局静态变量所没有的好处。


条款05 了解C++默默编写并调用了那些函数

1)默认构造函数
2)拷贝构造函数
3)赋值运算符函数
4)析构函数
5)取地址函数
6)const取地址函数

  1. 只有当这些函数被需要的时候,他们才会被编译器创建出来
  2. 默认的构造和析构函数给编译器一个地方用来放置“藏身幕后”的代码,比如调用基类或者非静态成员变量的构造和析构函数。
  3. 编译器产生的析构函数是非虚函数,除非这个类的父类有一个虚析构函数。
  4. 拷贝构造和赋值运算符只是单纯的将来源对象的每一个非静态成员变量拷贝到目标对象
  5. 如果声明了构造函数,那么编译器就不再为它创建默认构造函数了。这样做很重要,意味着如果你用心设计了一个类,其构造函数要求实参,你就不用担心编译器会给你添加一个无参构造函数从而覆盖掉你自己实现的版本。
  6. 拷贝构造函数中,对于有内置类型会以字节序进行拷贝,其他对象会调用其拷贝构造函数。
  7. 赋值运算符函数与拷贝构造函数的行为是一样的,唯一不同在于,只有当生成出的合法代码且有机会证明它有意义,其表现才会和拷贝构造函数一样。如果两个条件有一个不符合,那么编译器会拒绝生成赋值运算符函数。有以下三种情况不会生成:1)类中含有引用类型、2)类中含有常量类型、 3)基类将赋值运算符函数声明为私有(对于拷贝构造而言同样无法生成,即如果基类中的构造函数为私有,那么也无法构造子类对象)
什么时候一个空类不再是一个空类了?

        当C++处理过它之后,如果你没有声明,编译器就会为它声明一个copy构造函数、一个赋值操作符和一个析构函数,如果你一个构造函数也没有提供,那么编译器会为你声明一个默认构造函数。所有这些函数都是public且inline的。


条款6 若不想使用编译器自动生成的函数,就该明确拒绝

        如果你不希望类支持某一特定机能时,只要不声明对应函数就是了,但是这个策略对于copy构造函数和赋值操作符却不起作用,因为如果你不声明他们,那么当某些人去调用他们的时候,编译器会为你声明他们。

        我们解决该问题的关键是:所有编译器产生的函数都是public的,为阻止这些函数被创建出来,你得自行声明他们,但这里并没有什么需求使你必须将他们声明为public。因此你可以将copy构造函数或copy assignment操作符声明为private。即明确声明了一个成员函数,阻止了编译器擅自创建该函数,并且让该函数的访问权限为private,使得外部无法使用他们。
        上面那样做并不绝对安全,因为成员函数和友元函数还是可以访问到私有的拷贝函数,除非你将他们只声明而不实现,那么当任何人去调用的时候将会得到一个链接错误。现在如果你在类外使用拷贝,那么编译器会报错,如果你在类内的成员函数或者友元函数中使用拷贝,那么链接器将会报错。我们有个原则就是尽可能的将链接时错误移至编译器。我们可以实现一个类来完成该想法。

class Uncopyable{
protected:
	Uncopyable(){};
	~Uncopyable(){}
private:
	Uncopyable(const Uncopyable&);
	Uncopyable &operator=(const Uncopyable&);
};

        我们然后让不需要生成拷贝函数的类直接继承该类即可,如果任何人(甚至是成员函数或者友元)想要使用拷贝操作的时候,那么编译器尝试为该类生成一个拷贝函数,但是由于基类的拷贝函数为私有,所以编译器放弃生成该函数,在编译期报错。

        为了驳回编译器自动提供的功能,可以将相应的成员函数声明为private并且不予实现(防止成员函数或者友元函数调用),或者使用像uncopyable这样的基类(将链接时错误移至编译期间)


条款7 为多态基类声明虚析构函数

  • 带有多态性质的基类应该声明一个虚析构函数,即如果一个类带有任何虚函数,它就应该拥有一个虚析构函数
  • 类的设计目的如果不是作为基类使用,或者不是为了实现多态,就不应该声明析构函数为虚函数。

        C++指出,当派生类对象经由一个基类指针被删除,而该基类有一个非虚析构,那么结果是未定义的。实际上执行时通常发生的是对象的derived成员没有被销毁(派生类的析构函数没有被调用),但是其基类成员被销毁了,造成了资源泄漏。

        解决办法:给基类一个虚析构函数,那么以后通过基类指针或者引用删除该派生类的时候,就会销毁整个对象。

        建议:只有当类内含有虚函数就为它声明虚析构函数。

        如果一个类不含有虚函数,那么提供一个虚析构往往是一个馊主意。导致了在不同语言之间的不兼容性,并且对象的体积会增加。因为虚表需要占用空间啊。

        STL中的类是没有虚函数的,因此其析构函数不是虚函数,那么也就是说,如果我们继承了string,然后new出一个对象,通过一个string指针指向该对象,最后delete该对象,将会导致未定义行为。我们不应该继承STL容器或者任何其他带有非虚析构函数的类

        纯虚函数导致抽象类的产生,有时候使用抽象类可能会非常便利。当我们需要一个抽象类的时候,但又没有纯虚函数,这时,我们可以将析构函数声明为一个纯虚函数并提供一个实现。析构函数的运行方式是:最深层派生的那个类的析构函数最先被调用,然后是每个基类的析构函数被调用。编译器会在派生类的析构函数中创建一个对于基类析构函数的调用动作,所以你必须提供实现,否则编译器就会抱怨。

        对于“给基类一个虚析构函数”这个只适用于多态的基类身上,并不是所有基类的设计目的都是为了实现多态,如STL中的容器就不被设计为基类,更别提多态了。有些类的设计是作为基类使用,但是不为了多态用途,例如uncopyable类。


条款8 别让异常逃离析构函数

  • 析构函数绝对不要吐出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们或结束程序
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(非析构函数)执行该操作
  • 如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数之外的某个函数,因为析构函数吐出异常就是危险,总会带来“过早结束程序”或发生不明确行为的风险

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。

两个异常对于C++程序而言太多了。在两个异常存在的情况下,程序若不是结束执行就是导致不明确行为。

析构函数抛出异常的解决办法
  1. 如果程序遭遇一个在析构期间发生的错误后无法继续执行,强迫结束程序是一个合理选项。毕竟他可以阻止异常从析构函数传播出去(那将会导致不明确行为)。也就是说调用abort可以抢先制“不明确行为”于死地
  2. 将异常吞掉,一般来说,将异常吞掉是一个坏主意,因为它压制了“某些动作失败”的重要信息!然而有时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。

条款 9 绝对不要在构造和析构函数中调用virtual函数

在构造和析构期间不要调用虚函数,因为这类调用从不下降至派生类(基类构造期间,虚函数绝不会下降到派生类阶层。取而代之会调用基类的函数,对象也就是一个基类类型对象)

原因

  1. 由于基类构造函数的执行更早于派生类构造函数,当基类构造函数执行时派生类的成员变量尚未初始化,如果此期间调用的虚函数下降至派生类阶层,因为派生类的函数几乎都会调用派生类的成员变量,又因为这些成员变量尚未初始化。要求使用对象内部尚未初始化的成分是危险的代名词,所以C++不让你这么做
  2. 在派生类对象的基类构造期间对象的类型是基类而不是派生类,不仅仅虚函数会被编译器解析至基类,若使用运行期类型信息也会把对象视为基类。对象在派生类构造函数开始执行前不会成为一个派生类对象。(因为我们一般使用初始化列表来完成成员变量的初始化)
  3. 同样的道理适用于析构函数。一旦派生类析构函数开始执行,对象内的派生类成员变量就呈现未定义值,所以C++视他们仿佛不存在。进入基类析构函数后对象就成为了一个基类对象,而C++的任何部分包括虚函数和dynamic_casts等等也就这么看待它。

解决方案:确定在构造函数和析构函数中都没有调用虚函数,而他们调用的所有函数也都服从该约束

由于你无法使用虚函数从基类向下调用,在构造期间,你可以通过“令派生类将必要的构造信息向上传递至基类构造函数”加以弥补。


条款 10 令operator=返回一个*this的引用

  • 为了实现连锁赋值,赋值操作符必须返回一个指向操作符左侧实参的引用
  • 不仅仅适用于标准的赋值运算符,也适用于其他赋值相关的运算符
  • 这并不是强制的,如果你不遵循它,代码一样可以通过编译,但是最好这么干

条款11 在operator=中处理“自我赋值”

  • 确保当对象自我赋值的时候operator=有良好的行为,其中技术包括“来源对象”和目标对象的地址是否相同、安排语句顺序做到异常安全、以及copy_and_swap
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
  • 发生的原因是因为自我赋值时合法的,并且由于别名的存在使得自我赋值并不是那么显而易见,并且只要两个对象来自同一个继承体系,他们甚至不需声明为相同类型就可能造成别名问题(因为一个基类的指针或引用可以指向子类的对象)
class Bitmap{};
class Widget{
private:
	Bitmap *pb;	
}

版本1(错误的)

Widget::operator=(const Widget &rhs)
{
	delete pb;
	pb = new BitMap(*rhs.pb);
	return *this;
}

无法解决自我赋值问题以及存在异常安全问题
版本2(加入了证同测试)

Widget::operator=(const Widget &rhs)
{
	if(this == &rhs) return *this;
	delete pb;
	pb = new BitMap(*rhs.pb);
	return *this;
}

可以解决自我赋值问题,但是不能解决异常安全问题,其中的异常可以发生在new Bitmap这一条语句中(可能因为内存不足),这样的话pb就会指向一块被删除了的BitMap。这样的指针有害,你无法安全的删除他们,甚至无法安全地读取他们。
版本3

Widget::operator=(const Widget &rhs)
{
	BitMap *pOrig = pb;
	pb = new BitMap(*rhs.pb);
	delete pOrig ;
	return *this;
}

可以解决自我赋值问题,可以解决异常安全问题,但是存在一定的效率问题,可以在程序的开头加入证同测试,应该考虑相同的概率,因为证同测试也要时间啊。
版本4

class Bitmap{};
class Widget{
private:
	void swap(Widget &rhs);
	Bitmap *pb;	
	Widget::operator=(const Widget &rhs)
	{
		Widget temp(rhs);
		swap(temp);
		return *this;
	}
}

条款 12 复制对象时勿忘其每一个成分

  • copying函数应该确保复制“对象内的所有成员变量”以及“所有基类成员”(通过调用基类内适当的拷贝函数)
  • 不要尝试以某个copying函数实现另一个copying函数,如果这两个函数有相近的代码,消除重复代码的做法是将其公共机能放进第三个函数中(经常命名为init并且访问权限是private的),并由两个coping函数共同调用

        默认的拷贝函数会为所有的成员执行拷贝动作,对于内置类型会按照字节序进行拷贝,对于非内置类型成员则会调用其拷贝构造函数。

        如果我们声明自己的copying函数,意思就是告诉编译器比并不喜欢缺省实现中的某些行为。编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的代码几乎必然出错时却不告诉你。例如当你增加了类中的成员个数,但是你却没有在拷贝函数中增加相应的代码,那么编译器不会提醒你。

        对于派生类的拷贝构造函数而言,我们必须在初始化列表中调用基类对应的拷贝构造函数,否则编译器将会调用默认构造函数,导致了使用拷贝构造出来的对象却不一样的情况。而对于赋值运算符来说,它从不企图修改基类中的成员变量,所以那些基类中的成员变量也从未发生改变,因此,在这种情况下我们应该手动调用基类的赋值运算符来修改基类的成员。

        任何时候只要你承担起为派生类写copying函数的重大责任时,必须很小心的复制其基类成分,那些成分往往是private,所以你无法直接访问他们,你应该让派生类的copying函数调用相应的基类函数(基类作用域::基类函数名(形参列表))


条款13 以对象管理资源

  • 为了防止资源泄漏,请使用RAII对象,他们在构造函数中获得资源并在析构函数中释放资源
  • 两个常被使用的RAII类分别是shared_ptr和auto_ptr。前者往往是比较好的选择,因为其copy行为比较直观。若选择auto_ptr,复制动作将会使它指向null。
  • 我们在一个函数中new一个对象,并将该指针返回出来是十分危险的
什么是资源?

资源:所谓资源就是一旦我们申请了它,那么将来就必须还给操作系统,如果不这样做就会导致资源泄漏

应用场景

本条款针对的资源是分配与堆内而后被用于单一区块或函数内,他们应该在控制流离开那个区块或者函数时被释放的资源。

以对象管理资源的关键点:
  1. 获得资源后立刻放进管理对象,
  2. 管理对象运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁(当执行流离开该对象的作用域)其析构函数自然会被调用,于是资源释放。

由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象,如果那样的话,对象被删除一次以上,将会产生未定义行为。为了预防这个问题,auto_ptr有一个不寻常的性质:若干通过copy构造函数或者copy assignment操作符复制他们,他们会变成NULL,而复制所得的指针将取得资源唯一的所有权。也正是因为此,STL中的容器无法通过auto_ptr进行管理

引用计数型智能指针(RCSP)持续追踪共有多个对象指向某笔资源,并在无人指向它时自动删除该资源,实现了类似垃圾回收的机制,但是无法打破环状引用。RCSP可以使用在STL上。

auto_ptr和shared_ptr两者在其析构函数中做delete而不是delete[]动作,因此在动态分配的对象数组中不适合使用auto_ptr以及shared_ptr。可悲的是,这样做编译器并不报错。我们也没有理由使用数组啊。vector那么好用,为啥用数组啊。。。


条款 14 在资源管理类中小心copying行为

  1. 复制RAII对象必须一并复制它所管理的资源,所以资源的“copy”行为决定RAII对象的copy行为
  2. 普遍而常见的RAII class copying行为是:
    • 禁止复制:继承uncopyable类
    • 对底层资源进行引用计数
      通常我们只需要使用一个shared_ptr将资源包装起来即可,但是在引用计数为0的时候会删除资源而不是释放资源,幸运的是shared_ptr允许指定所谓的删除器,删除器就是一个函数或者函数对象,当引用计数为0时就会被 调用(该机制在auto_ptr中不存在)。删除器是shared_ptr中一个可选的参数
    • 复制底层资源,比如一个字符串类,我们就需要复制堆空间以及重新设置类成员指针。
    • 转移底层资源的拥有权,使用auto_ptr可以轻松做到

在我们自己创建资源管理类的时候必须要考虑当一个RAII对象被复制的时候,会发生什么事?


条款15 在资源管理类中提供对原始资源的访问

  • 为什么会有以上问题?C APIs往往要求访问的是原始资源,所以每一个RAII 类应该提供一个“取得其所管理的资源”的办法
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便
    显式转换:提供一个get成员函数,用来执行显式转换,返回资源管理对象中管理的资源
    隐式转换:重载指针取值操作符(* ->)来完成隐式类型转换或者提供隐式转换函数

shared_ptr和auto_ptr都提供了一个成员函数get成员函数,用来执行显式转换,可以返回智能指针内部的原始指针。同时他们也重载了指针取值操作符,允许隐式转换至底部原始指针。
我们可以给我们设计的RAII类提供显式转换函数,比如get。。。或者是提供一个隐式转换函数,如下所示

operator 隐式转换的目标类型() const
{
	return f;
}

是否该提供一个显式转换函数 将RAII类转换为其内部资源指针,或者是提供一个隐式转换,答案取决于RAII被设计执行的特定工作,以及它被使用的情况。最佳设计很可能“让接口容易被正确使用,不易被误用”。通常来说提供显示转换函数比较受欢迎,因为它把出错的可能性降低到最小了。


条款16 成对使用new和delete时要采取相同形式

如果你在new表达式中使用[],必须在相应的delete表达式中使用[](对于内置类型同理,即便他们没有析构函数),如果你没有在new表达式中使用[],一定不要在相应的delete表达式中使用[]),否则两者将会导致未定义现象

我们对一个指针使用delete,唯一能够让delete知道内存中是否存在一个数组大小记录的方法就是:你来告诉它,如果你使用过delete时加上了中括号,delete便认为那是一个数组,否则它便认为指针指向单一对象。


条款 17 以独立的语句将新创建的对象置入智能指针

  • 以独立语句将新创建的对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以发觉的资源泄漏
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

我们怎么调用呢?

processWidget(new Widget, priority());
//这样是不行的,因为智能指针的构造函数是explicit的,无法进行隐式类型转换
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
//上面这种方法可以编译通过,但却有可能导致资源泄漏。因为对于以上两个形参的执行顺序是不确定的。
// C++完成以上工作的顺序是不确定的,但是可以保证的是new操作一定在执行shared_ptr的构造之前,若priority的执行插入在创建Widget和将该对象传入到智能指针中间并且抛出了异常,那么就导致资源泄漏
std::tr1::shared_ptr<Widget>pw(new Widget);
processWidget(pw, priority());
// 以上方法是正确的,因为编译器对于跨越语句的各项操作是没有重新排列的自由的。

条款 18 让接口更容易被正确使用,不易被误用

  • 好的接口很容易被正常的使用,不容易被误用,你应该在你的所有接口中努力达成这些性质
  • “促进正确使用”的方法包括接口一致性(size方法),以及与内置类型的行为兼容
  • “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户对资源管理的责任
  • tr1::shared_ptr支持定制型删除器,这可防范DLL问题。可被用来自动解除互斥锁
  • 理想上,如果客户企图使用某个接口而却没有获得他所预期的行为,这个代码就不该通过编译;如果代码通过了编译,那么他的行为就应该是客户想要的
  1. 所谓的软件设计,是令软件做出你希望它做的事情的步骤和做法,通常以颇为一般性的构想开始,最终演变成十足的细节,以允许特殊接口的开发,这些接口而后必须转换为C++声明式。
  2. 如果想要开发出一个容易被正确使用,不容易被误用的接口,我们首先必须考虑客户可能会做出什么样的错误。
  3. 许多客户端错误可以导入新的类型而获得预防。这样的话我们就可以限制该类型值的范围,比如一年只有12个月份,虽然我们可以拿一个int来存放,但是一定要限制它的取值范围。更好的做法是让Month类产生一组静态函数,这组静态函数返回代表12个月份的类对象,并私有化其构造函数。
  4. 预防客户错误的另一个办法是,限制类型内什么事可以做,什么不可以做,常见的限制是加上const。这样就可以防止出现类似a*b=c这样的错误了
  5. 让类型容易正确使用,不容易误用的准则是:除非有好理由,否则应该尽量令你定义的类型的行为与内置类型一致,避免无端地与内置类型不兼容,真正的理由是为了提供行为一致的接口,很少有其他性质比得上一致性更能导致接口被正确的使用,也很少有其他性质能比得上不一致性更加剧接口的恶化。
  6. 任何接口如果要求客户端必须记得做某些事情,就是有着“不正确使用的倾向”,因为客户端可能会忘记做哪些强制的事情。因此返回一个shared_ptr可以阻止一大部分人犯资源泄漏的错误。
  7. 在我们的设计中不应该出现有一个函数用来获取资源,另一函数用来释放资源。这样就是在强迫客户做某些重要的事情,而客户就有可能忘记做这些事情,因此,我们应该返回一个使用shared_ptr包裹的资源,并且恰当的设置其删除器,这样就可以防止资源泄漏了。
  8. tr1::shared_ptr有一个特别好的性质是:当引用计数变为0的时候,它会自动使用专属的删除器,因此可以消除另一个潜在的客户错误,即所谓的cross-DLL problem,这个问题发生在“对象在动态链接库中内创建,却在另一个动态链接库中被删除”。在许多平台上,这类跨DLL之new和delete的成对使用会导致运行期错误。shared_ptr没有这个问题,因为它缺省使用的删除器是来自shared_ptr所在的那个DLL的delete。
  9. shared_ptr比原始指针大且慢,而且使用辅助动态内存,但是在许多应用程序中这些额外的执行成本并不显著,然而其降低客户错误的成效确实有目共睹的。

条款19 设计class犹如设计type

C++ 就像其他OOP语言一样,当你定义一个新的类的时候,也就定义了一个新的类型,作为一个C++程序员来讲,你的许多时间主要用来扩张你的类型系统。这意味着你并不只是类的设计者,还是类型的设计者,重载函数和操作符、控制内核的分配和归还、定义对象的初始化和终结…全部在你手里。因此你应该呆着和“语言设计者当初设计语言内置类型时”一样的谨慎来研究类的设计。

  • 新类型的对象应该如何被创建和销毁?construct destruct operator new operator delete
  • 对象的初始化和对象的赋值该有什么样的差别?
  • 新类型如果以passed by value,意味着什么?copy构造函数用来定义一个类型的值传递该如何实现
  • 什么是新类型的合法值?对于类的成员变量而言,通常只有某些数值集合是有意义的,那些数值集合决定了你的类必须维护的约束条件,也决定了你的成员函数必须进行的错误检查工作、函数抛出的异常以及异常明细列
  • 新类型需要配合某个继承图系吗?作为子类,基类的虚函数和非虚函数需要我们加以考虑。作为基类,我们是否应该提供一个虚析构函数应该加以考虑
  • 新类型需要什么样的转换?
  • 什么样的操作符和函数对此新类型而言是合理的?
  • 什么样的标准函数应该驳回?声明private
  • 谁该取用新类型的成员?权限符以及friend
  • 什么是新类型的未声明接口?
  • 你的新类型有多么一般化?决定了你是定义一个类,还是定义一个类模板
  • 你真的需要一个新类型吗?

如何实现类型转换?

如果你希望允许类型T1被隐式转换为T2类型,就必须在class T1内写一个类型转换函数或者在class T2内写一个non explicit one argument的构造函数。


条款20 宁以pass-by-reference-to-const替换pass-by-value

  • 尽量以pass by reference to const替换pass by value。前者通常都比较高效,并可避免切割问题
  • 以上的规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,值传递往往比较合适

        缺省情况下C++以by value的方式传递对象至函数,除非你另外指定,否则函数参数都是以实际实参的拷贝为初值,而调用端所获得的也只是函数返回值的一个拷贝。这些拷贝都是通过copy构造函数产生的,因此值传递是可能是比较费时的操作。因此我们应该使用的是pass by reference-to-const。如下代码

修改前
bool validateStudent(Student s);
Student plato;
bool platoIsOk = validateStudent(plato);
修改后
bool validateStudent(const Student &s);

引用传递的效率高得多,因为没有任何构造函数和析构函数需要调用,修改后的const是很有必要的,原来的函数以值传递方式接受一个Student参数,因此调用者知道他们受到保护,函数内绝不会对实参Student作任何改变而只能对其副本进行修改,修改后的代码中以引用方式传递,将它声明为const是必要的,因为不这样做的话调用者就会担心该函数会修改传入的实参。

引用传递可以避免对象切割问题,当一个派生类对象以值传递方法传递给一个基类对象,那么基类的拷贝构造函数将会被调用,因而造成此对象的行为像一个派生类对象的任何特化性质被切割掉了,仅仅留下一个基类对象。这个并不应该让你感到惊讶,因为是基类拷贝构造函数创建了该对象啊,但是这绝对不是你想要的。

        总结:reference往往以指针实现出来,因此pass by reference通常意味真正传递的是指针。因此如果你有个对象属于内置类型,pass by value往往比 pass by reference的效率高些,对于内置类型而言,当你有机会采用pass-by-value或pass-by-reference-to-const时,选择pass-by-value并非没有道理。这个建议同样适用于STL的迭代器和函数对象,因为习惯上他们都被设计为pass by value。

并不是所有小型对象都是pass-by-value的候选人,因为这并不意味着其拷贝构造函数不昂贵,例如大多数的STL容器(往往只含有一个指针),并且自定义的小型对象也会长大啊。。。


条款21 必须返回对象时,别妄想返回其引用

  • 一旦程序员领略了值传递的效率问题后,往往变成十字军战士,一心一意根除值传递带来的种种邪恶,在坚定追求引用传递的纯度中,他们一定会犯下一个致命错误:开始传递一些引用指向并不存在的对象,这可不是什么好事
  • 绝对不要返回一个指向局部对象的指针或者引用,或者是一个堆上对象的指针或者引用,或者是一个局部静态的对象而又有可能同时需要多个这样的对象的引用或指针。
  • 如果我们返回一个函数中局部变量的引用,那么任何调用者只要对此函数的返回值做任何一点点的运用,就会产生未定义行为。任何函数如果返回一个局部对象的指针或引用都将一败涂地。
  • 一个必须返回新对象的函数的正确写法是:让那个函数返回一个新对象,当然你得承受构造和析构的成本,万一你无法承受,别忘了C++和其他所有编程语言一样,允许编译器实现者施行最优化,用来改善产出码的效率却不改变可观察的行为。

条款22 将成员变量声明为private

  • 切记将成员变量声明为private,这可以赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证,并提供作者以充分的实现弹性
  • protected并不比public更具有封装性
为什么不采用public变量呢?
  1. 为了保证语法的一致性,如果成员变量不是public的,客户唯一能够访问对象的办法就是通过成员函数,如果public接口内的每样东西都是函数,客户就不需要在访问成员的时候考虑是否需要加上小括号。
  2. 使用函数可以让你对成员变量的处理有更精确的控制,如果你令成员变量为public,每个人都可以读写它,但是如果你以函数取得或者设置值,那么就可以实现“不准访问”、“只读访问”以及“读写访问”甚至可以有“只写访问”,如此细微的划分访问控制颇有必要,因为许多成员变量应该被隐藏起来,每个成员变量都需要一个get和set函数毕竟罕见
  3. 保证封装性,如果你通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而客户一点也不会知道类内部已经发生了变化。
  4. 将成员隐藏在函数接口的背后,可以为所有可能的实现提供弹性,例如可使成员变量被读或写时通知其他对象、可以验证类的约束条件以及函数的前提和时候状态、可以在多线程环境中执行同步控制…
为什么不使用protected成员变量?
  1. 实际上它和public成员变量的论点相同,虽然乍看起来不是那么一回事儿
  2. 语法一致性和细微划分访问控制理由对于protected成员仍然适用
  3. protected成员变量封装性是不是高过public成员变量?答案并非如此。成员变量的封装性和成员变量的内容改变(移除成员变量)时所破坏的代码数量成反比。假设有一个public成员变量,而我们最终取消了它,那么所有的客户码都会被破坏,因此public成员变量完全没有封装性。假设我们有一个protected成员变量,而我们最终取消了它,那么所有使用它的派生类都会受到破坏。因此,protected成员变量就像public成员变量一样缺乏封装性,因为在这两种情况下,如果成员变量被改变,都会有不可预知的大量代码受到破坏。一旦你将一个成员变量声明为public或protected而客户开始使用它,就很难改变那个成员变量所涉及的一切,太多代码需要重写、重新测试、重新编写文档、重新编译。从封装的角度来看,其实只有两种访问权限:private和其他
封装的重要性

封装的重要性比你最初见到它时还重要,如果你对客户隐藏成员变量(即封装),你可以确保类的约束条件总是获得维护,因为只有成员函数可以影响他们,进一步说,你保留了日后变更实现的权利,如果你不隐藏他们,你很快就会发现,即使拥有类源代码,任何改变public成员变量的能力还是极端受到束缚,因为那会破坏太多客户码,public意味着不封装,而几乎可以说,不封装意味着不可改变,特别是对被广泛使用的类来说,被广泛使用的类是最需要封装的对象。因为如果有封装,那么我们就可以改变底层实现而不影响接口的使用。


条款23 宁可拿non-member non-friend函数替换member函数

  • 宁可拿non-member non-friend函数替换member函数,这样做可以增加封装性,包裹弹性和技能扩充性。

面向对象守则要求,数据以及操作数据的那些函数应该被捆绑在一起,这样看起来成员函数是比较好的选择,不幸的是这个建议不正确。这是基于面向对象真实含义的误解,面向对象守则要求数据应该尽可能被封装,然而与直观相反,成员函数带来的封装性要比非成员非友元函数带来的封装性低。此外非成员非友元函数允许对类的相关技能提供较大的包裹弹性,而那最终导致较低的编译相依度,增加类的可延伸性

让我们从封装开始讨论,如果某些东西被封装,它就不再可见,越多东西被封装,越少人可以看到它。而越少人能够看到它,我们就有越大的弹性去改变它,因为我们的改变仅仅直接影响 看到改变的那些人,因此越多东西被封装,我们改变那些东西的能力也就越大,这就是我们首先推崇封装的原因:它使我们能够改变事物而只影响有限客户。这里考虑对象内的数据,越少的代码可以看到数据,越多的数据就被封装了,而我们也就越能自由地改变对象的数据。我们计算能够访问该数据的函数的个数作为一种粗糙的量测。越多函数可以访问它,数据的封装性就越低。因此我们可以看出如果一个成员函数和一个非成员非友元的函数提供相同的机能,那么导致较强封装性的是非成员非友元函数,因为它并不增加能够访问类内私有成员变量的函数的个数。以上讨论只适用于成员函数和非成员非友元函数,对于友元函数而言,他的封装性和成员函数的封装性是一致的。

在C++中,比较自然的做法是让non-member non-friend函数和它对应的类在同一个命名空间中。这样做并非仅仅为了看起来自然,要知道,命名空间和类不同,前者可以跨越多个源码文件而后者不能。将所有便利函数放在多个头文件内但隶属同一个命名空间,意味着客户可以轻松扩展这一组便利函数。他们需要做的只有将更多的非成员、非友元函数添加到此命名空间中。这是类无法提供的另一个性质,因为class定义式对客户而言是不能扩展的(继承得到的派生类无法访问基类的私有成员因此只是拥有一个次级身份)。


条款24 若所有参数都需要类型转换,必须采用非成员函数

  • 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。只有当参数位于参数列中,这个参数才是隐式类型转换的合格参与者,而this指代的那个对象绝对不是隐式转换合格参与者(成员函数不行)。

往往令类支持隐式类型转换通常是个糟糕的主意,当然这条规则有其例外,最常见的例外是建立数值类型时。

不能因为函数不该成为成员函数,就让其成为友元,成员函数的对立面是非成员函数,而不是友元函数。


条款25 考虑写出一个不抛出异常的swap函数

  • swap是一个有趣的函数,原来他只是STL的一部分,后来成为异常安全性编程的脊柱以及用来处理自我赋值问题。由于swap函数如此有用,适当的实现很有必要。然而在非凡的重要性之外它也带来了非凡的复杂度
  • std中的swap函数,只要需要交换的类型支持拷贝操作,缺省的代码就会帮你交换对象中的值。
  • 通常我们不能改变std命名空间中的任何东西,但可以为标准的template制造特化版本,使它专属于我们自己的类。
  • C++只允许对类模板进行偏特化,在函数模板上行不通的。当你打算偏特化一个函数模板时,通常是为它添加一个重载版本,但是不能对std命名空间中的函数模板进行重载,因为std是一个特殊的命名空间,其管理也比较特殊。客户可以全特化std内的templates,但不可以添加新的模板到std里面。std的内容完全是由C++标准委员会决定,标准委员会 禁止我们膨胀那些已经定义好的东西,这里的禁止(可能会让你沮丧),其实跨越红线的程序几乎仍可编译和执行,但他们的行为没有明确的定义,如果你希望你的软件有明确的行为,请不要添加任何新东西到std里面。
  1. 首先,如果swap的缺省实现码对你的类或类模板提供可接受的效率,那么你不需要做额外的事,任何尝试交换该类型对象的人都会获得(良好的运行)缺省版本。
  2. 如果swap缺省实现办的效率不足(那几乎总是意味着你的类或者模板使用了某种pimpl手法),按照以下步骤处理
  3. 提供一个public swap成员函数,让它高效的交换目标类型的两个对象(该函数绝对不应该抛出异常,因为它往往是用来保证异常安全性的)
  4. 在你的类或者模板所在的命名空间内提供一个非成员的swap函数,并令他调用步骤3中的swap成员函数
  5. 如果你正在编写一个类而不是类模板,那么应该为你的类特化std::swap,并令他调用你的swap成员函数
  6. 最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见。然后不加任何命名空间修饰,赤裸裸的调用swap。

条款26 尽可能延后变量定义式的出现时间

  • 尽可能延后变量定义式的出现,这样做可以增加程序的清晰度并改善程序效率
  • 只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你就要承受构造成本,当执行流离开该变量的作用域时,你就要承受析构成本,即使这个变量最终并未被使用,仍然需要这些成本,所以你应该尽量避免这种情形。
  • 你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造和析构非必要对象,还可以避免无意义的默认构造行为。以“具有明显意义的初始值”将变量初始化带有说明变量的目的。

对于循环中的变量定义我们怎么处理,是定义在循环中,还是定义在循环之外

方案一:

Widget w;
for(int i = 0; i < n; i++)
{
	w = ?;
}

方案二:

for(int i = 0; i < n; i++)
{
	Widget w(?);
}

       对以上两种方案进行分析,方案一将会导致一次构造、一次析构以及n次赋值运算,而方案二将会导致n次构造和n次析构。除当你知道“赋值成本比构造+析构成本低且正在处理代码效率高度敏感的部分”情况外可以使用方案一,但是方案一导致w的作用域比方案二大,有时对于程序的可理解性和易维护性造成冲突。


条款 27 尽量少做转型动作

  • C++规则的设计目标之一是,保证“类型错误”绝不可能发生,理论上如果你的程序很干净地通过编译,就表示它并不企图在任何对象身上执行任何不安全、无意义的操作。这是一个极具价值的保证,别轻易忽略它。转型会破坏类型系统
  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,尝试不需要使用转型的设计
  • 如果转型是必要的,那么应该尝试将其隐藏在某个函数背后,客户随后可以调用该函数,而不是将转型直接放在他们自己的代码中。
  • 宁可使用C++风格的转型,不要使用旧式转型。前者很容易辨识出来,并且也比较术业有专攻
  • 单个对象可能拥有一个以上的地址,尤其是多重继承中,我们通常应该避免做出“对象在C++中如何布局”的假设。当然更不应该以此假设为基础执行任何转型动作,那么几乎必然产生未定义行为。即便我们成功了,也会带来可移植性的问题(与编译期有关)。
  • 当我们发现自己打算使用转型时,那就是一个警告信号,你可能正将局面发展至错误的方向上。如果你使用的是dynamic_cast更是如此。因为dynamic_cast的许多实现版本都非常慢。

转型语法

  1. (T)expression
  2. T(expression)
  3. C++提供的转型

使用新式转型的好处

  1. 他们很容易在代码中辨识出来,因此得以简化“找出类型系统在哪里被破坏”的过程。
  2. 各转型动作的目标越窄化,编译器越可能诊断出错误的运用。(例如用其他cast去除const)
  3. 宁可使用C++新式转型,不要使用旧式转型。只有当使用一个explicit构造函数将一个对象传递给一个函数时
class Widget{
	explicit Widget(int size);
};
void doSomeWork(const Widget &w);
doSomeWork(Widget(15));//C语言风格
doSomeWork(static_cast<Widget>(15));//C++风格
这两种风格C语言风格可能好一些

类型转换真的什么都没有做吗

       许多程序员相信,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型,这是错误的概念。任何一个类型转换往往真的令编译器编译出运行期间执行的代码。例如将整形转型为double然后进行除法运算以及将一个派生类对象指针转型为基类指针,这两个指针的值往往不相等,而是存在一定的偏移。

如何在派生类中虚函数中正确的调用基类中对应的虚函数?

class Window {
public:
	virtual void onResize() {};
};

class SpecialWindow : public Window {
public:
	virtual void onResize() {
		static_cast<Window>(*this).onResize();
	}
};

以上做法看似正确,但实际并不是对当前的派生类对象执行onResize操作,而是在转型动作建立的一个以*this对象的基类成分构造的副本身上执行onResize。也就是说这样调用基类onResize函数不会对派生类对象产生任何影响,可能会导致派生类专属的成员发生了变化而基类成员没有变化的奇怪情况。
正确做法:

class Window {
public:
	virtual void onResize() {};
};

class SpecialWindow : public Window {
public:
	virtual void onResize() {
		Window::onResize();
	}
};

替代dynamic_cast的方法

  1. 使用容器并在其中存储直接指向派生类对象的指针(往往是智能指针)
  2. 可以在基类提供虚函数做派生类想做的事情(空函数)。
  3. 出现一连串的dynamic_cast肯定是不好的(体积大,执行速度慢,且需要经常变动)

条款28 避免返回handles指向对象内部成分

  • 避免返回handle指向对象内部,遵守这个条约可增加封装性,帮助const成员函数的行为更像一个const,并将发生dangling handle的可能性降到最低。

  • 成员的封装性最多只等于“返回其”引用的函数的访问级别,

  • 如果const成员函数传出一个引用,而这个引用所指的数据域对象自身有关联,而该数据又被存储于对象之外,那么这个函数的调用者可以修改该数据。这正是bitwise constnes的一个附带结果。

  • 引用、指针和迭代器都是所谓的handles,而返回一个代表对象内部数据的handle,随之而来的便是“减低对象封装性”的风险,以及导致虽然调用const成员函数却造成对象状态被更改的风险

  • 通常我们认为,对象的内部就是指它的成员变量,但其实不被公开使用的成员函数也是对象“内部”的一部分。因此也应该留心不要返回他们的handles,这意味着你绝对不该令成员函数返回一个指向“访问级别较低”的成员函数。如果你那么做,后者的实际访问级别就会被提高到如同访问级别较高的成员函数,因为客户可以得到一个指针指向那个访问级别较低的函数,然后通过那个指针访问它。

  • 即便我们给返回号码牌的成员函数的返回值加上const修饰,但是依然会产生问题,如下:

dangling handles 问题
       什么dangling handle?dangling handles是指该handle所指向的对象已经不复存在了,最常见的就是函数的返回值。因为函数返回的对象是临时的,只在当前行有效。

       这就是为什么函数如果返回一个handle代表对象内部成分总是危险的原因。不论这所谓的handle是个指针还是一个引用,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里唯一的关键是,有个handle被传出去了,一旦如此就有handle比起所指对象的生存周期更长的风险。


条款29 为异常安全而努力是值得的

  • 异常安全的代码要求:1.不会泄露任何资源;2.不允许数据败坏
  • 异常安全函数即使发生异常也不会泄露任何资源或允许任何数据败坏,这样的函数分为三种:基本型、强烈性、不抛异常型
  • 强烈保证往往能够以copy and swap实现出来,但强烈保证并非对所有函数都有可实现或具备现实意义(导致较大的复杂度)。
  • 函数提供的异常安全保证通常最高值等于其所调用各个函数的异常安全保证中最弱的那一个
  • 一个一般性规则是这么说的:较少的代码就是较好的代码,因为出错机会比较少,而且一旦有所改变,被误解的机会也比较少。而异常安全性一般可以达到此目的(例如智能指针的使用)
  • 除非面对的是不具有异常安全性的传统代码,否则我们都应该提供异常安全性
  • 智能指针的reset函数用于重新设置内部维护的指针,并且会释放掉就的指针对应的资源。
异常安全函数提供以下三个保证之一

基本保证:如果异常被抛出,程序内的任何事物仍然保持有效状态,没有任何对象或数据结果因此而败坏,所有对象都处于一种内部前后一致的状态(类的约束条件都依旧满足),然而程序的状态却不可预料(but是一种合法状态)。
强烈保证:如果异常被抛出,程序状态不改变,调用这样的函数需有这样的认识,如果函数成功,就是完全成功,如果函数失败,程序会恢复到调用函数之前的状态。
不抛掷保证:承诺绝不抛出异常,因为他们总是能够完成他们原先承诺的功能,作用于内置类型身上的所有操作都提供nothrow保证。

空白异常明细

       如果一函数带着空白的异常明细说明该函数抛出异常,将是严重错误,会有你意想不到的函数被调用(set_unexpected用来指定那个意想不到的函数)。该函数不提供任何异常保证,函数的声明式并不能够告诉你他是否正确、是否可移植、是否是高效的以及是否是异常安全的,这些所有的性质由函数实现决定,与声明无关。

copy and swap策略

       当你打算修改对象时先做出一个副本,然后在哪个副本上进行一切必要的修改,若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)

        copy and swap策略是对对象状态作出全有或者全无改变的一个很好的办法,但一般而言它并不能保证整个函数有强烈的异常安全性。原因如下:1.倘若在该函数内调用了其他没有提供强烈异常安全性的函数。2.即便该函数调用的所有函数都是异常安全的,也很难提供强烈的异常安全性,因为存在连带效应。

       综上所述,提供强烈异常安全性是很难得,并且可能会导致时间复杂度和空间复杂度的攀升。因此,我们应当尽量提供强烈的异常安全性,但强烈保证并不是在任何时刻都可能。那么这个时候就要考虑基本保证了。

如何选择异常抛出类型

       不抛出异常肯定是很爽了,但是现实中很少有代码可以做到这一点,所以如果可能的话请提供nothrow保证,但对大部分函数而言,抉择往往落在基本保证和强烈保证之间。当强烈保证不切实际的时候就提供基本保证。


条款30 透彻了解inline的里里外外

含义:编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序执行的效率。

  • 将大多数内联函数限制在小型、被频繁调用的函数身上,让程序的速度提升机会最大化
  • 大部分编译器拒绝将过于复杂的函数内联,而对所有的虚函数都不会内联,因为虚函数意味着直到运行期才确定调用哪个函数,而inline意味着执行前,先将调用动作替换为被调用函数的本体。
  • 一个表面看似inline的函数是否真的会inline,取决于你的建构环境,主要取决于编译器,幸运的是大多数编译器提供了一个诊断级别:如果他们无法将你要求的函数内联化,会给你一个警告信息。
  • 虽然有时候编译器有意愿内联某个函数,但是可能为该函数生成一个函数本体,比如取该函数的地址。因为它无法让一个函数指针指向一个不存在的函数。编译器通常不对通过函数指针而进行调用进行内联,这意味着对于内联函数的调用可能内联,也可能不内联,取决于调用的实施方式。
  • 实际上构造函数析构函数往往不是内联函数的合适人选,因为构造中存在对于基类进行构造以及对成员属性进行构造的代码,看似很短,实则很长。
  • 如果有个异常在对象构造期间被抛出,该对象已经构造好的那一部分会被自动销毁

内联函数的优点
       Inline函数看起来像函数,动作像函数 ,比宏好的多,可以调用他们又不需要蒙受函数调用所招致的额外开销。远不止与此,实际上编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,所以你内联一个函数或许编译器就一次有能力对他执行语境进行最优化,大部分编译器绝不会对一个“outline”函数调用实行如此优化
内联函数的缺点
       内联函数背后的整体观念是将对每个内联函数的调用都用函数本体替换,因此过渡热衷于inline会造成程序体积太大,即使使用虚拟内存,inline造成的代码膨胀也会导致额外的换页行为,降低指令高速缓存装置的击中率,以及伴随这些而来的效率损失。换个角度说,如果inline的函数体积本身很小,那么inline之后产生的代码体积可能比函数调用所产生的代码量更小,如真如此,将函数inline确实可能导致较小的目标码和较高的指令告诉缓存装置的击中率。此外,调试器对于inline函数束手无策(你怎么在一个不存在的代码中插入断点),多数编译器禁止在调试的时候inline
内联的使用
       Inline只是对编译器的一个申请,不是强制命令,这项申请可以隐喻的提出,也可以明确提出,隐喻的方式是将函数定义与类内。通常是成员函数,但是如果友元函数也可以定义在类内,如果真是这样,他们也是可以被隐喻声明为inline。明确声明inline函数的做法是在其定义式前加上关键字inline。

inline和template
       不要因为函数模板出现在头文件,就将他们声明为inline。内联函数实现一定被放在头文件中,因为大多数构建环境在编译时期进行内联,而为了将一个函数调用替换为被调用函数的本体,编译器必须知道那个函数是什么样子。Template通常也放在头文件中,因为它一旦被使用,编译器为了将它具现化,需要知道他长什么样子。所以Inline和template是没有必要关联的,所以如果在使用模板函数的时候,需要内联的时候就内联,在不恰当的时候内联会导致代码膨胀以及效率问题

库的设计
       程序库的设计者必须评估将函数声明为内联的冲击,内联函数无法随着程序库的升级而升级。换句话说如果f是程序库内的一个内联函数,客户将“f函数本体”编进其程序中,一旦程序库的设计者决定改变f,所有用到f的客户端程序都必须重新编译,这往往不是大家想看到的。然而如果不是内联函数,一旦该函数有任何修改,客户端只要重新链接就可以了,远比重新编译的负担少的多,如果程序库采用动态链接,升级版函数深刻可以不知不觉地被应用程序吸纳。

注意点

  1. inline是一种以空间换时间的做法,省去调用函数时的额外开销,所以代码很长或者有循环/递归的函数不适宜使用作为内联函数
  2. inline对于编译器而言是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联
  3. inline不建议声明和实现分离,分离会导致链接错误。因为inline被展开,就没又函数地址了,链接就会找不到

条款32 确定你的public继承模塑出is-a关系

  • 以C++ 进行面向对象编程时,最重要的一个规则是:public继承意味着is-a的关系。
  • 如果你令class D 以public形式继承class B,你就是在告诉C++编译器说:每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更加一般的概念,而D比B表现出更特殊化的概念,你主张“B对象可派上用场的任何地方,D对象一样可以派上用场”。因为每一个D对象都是一个B对象,反之如果你需要一个D对象,B对象无法效劳,因为虽然每个D对象都是一个B对象,反之并不成立。
  • Public继承意味着is-a,适用于基类对象身上的每一件事情都一定适用于派生类身上,因为每一个派生类对象都是一个基类对象
  • public继承意味着is-a,适用于base身上的每一件事情一定也适用于derived身上,因为每一个derived对象也都是一个base对象。

条款33 避免遮掩继承而来的名称

  • 内层作用域的名称会隐藏外围作用域的名称
  • 我们知道,当一个派生类成员函数引用了基类中的某个成员时,编译器可以找出我们所引用的东西,因为派生类继承了声明于基类内的所有东西,实际的机制是派生类的作用域被嵌套在基类作用域内
  • C++的名称遮掩规则所做的唯一事情就是:遮掩名称,至于名称所指的事物是不是相同类型,这并不重要
  • 即便基类和派生类中函数有不同的参数类型也适用,而且不论函数是不是虚函数,通用!
  • 派生类内的名称会遮掩基类内的名称,在public继承下从来没有人希望这样
  • 为了让被遮掩的名称再见天日,可使用using声明或转交函数
什么是转交函数

如果基类存在一组重载的虚函数,而我们只想要其中一个,那么我们可以使用在私有继承,然后在派生类中定义一个与基类中重载函数同名的方法,然后在其内部使用作用域运算符的方式来访问基类中我们想要的那个函数。

class Window {
public:
	virtual void onResize() {};
};

class SpecialWindow : private Window {
public:
	virtual void onResize() {
		Window::onResize();
	}
};

条款34 区分接口继承和实现继承

表面上看似直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成,函数接口继承和函数实现继承。

纯虚函数的特性:他们必须被任何继承了该纯虚函数所在抽象类的派生类重新声明,而在抽象类中通常不提供定义,可以有定义,但是调用方式必须是p->Base::func();p是一个派生类对象指针
基类中声明一个纯虚函数的目的是为了让派生类只继承函数接口

非纯虚函数的特性:非纯虚函数在基类中会提供一份实现代码,派生类可以覆盖它
基类中声明非纯虚函数的目的是让派生类可以继承该函数的接口和缺省实现

非虚函数的特性:非虚函数意味着它并不打算在派生类中有不同的行为。实际上一个非虚函数所表现出的不变性凌驾于其特异性,因为他表示不论派生类变得多么特异化,非虚函数的行为都不可以改变。所以他决不应该在派生类中被重新定义。
基类中声明非虚函数的目的是为了令派生类继承函数的接口以及一份强制性实现

如果我们将基类中的方法声明为非纯虚函数后,那么其派生类就会得到一个接口以及一份默认的实现,这意味着派生类就有机会犯错,例如忘记覆写该虚函数,此时我们可以得到如下解决方案,那就是在基类中设计一个纯虚函数作为接口,并且使用普通成员函数来提供一份默认实现,这样就实现了强制派生类实现接口,并且只有当设计者明确使用默认实现的时候才会使用。虽然以上做法实现了接口和实现的分离,但是却又可能导致命名空间的污染,因此我们可以利用纯虚函数可以提供一份实现来解决这个问题,这样既完成了接口和实现的分离,又避免了命名空间的污染。


条款35 考虑virtual函数以外的其他选择

  • 虚函数的替代方案包括NVI手法以及策略模式的多种形式

  • 将机能从成员函数转移到类外部函数,带来的一个缺点是,非成员函数无法访问类的非共有成员

  • tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物(函数指针、函数对象、成员函数)

  • 使用NVI手法,那是是一个特殊形式的Template method设计模式,它以public非虚成员函数包裹较低访问性的虚函数

  • 将虚函数替换为“函数指针成员变量”,这是策略设计模式的一种分解表现形式

  • 以tr1::function成员变量替换虚函数,因而允许使用任何可调用物(函数指针,函数对象,成员函数)搭配一个兼容于需求的签名式,这也是策略设计模式的某种形式

  • 将继承体系内的虚函数替换为另一个继承体系内的虚函数这是策略设计模式的传统实现方法


条例36 绝不重新定义继承而来的非虚函数

  • 非虚函数是静态绑定,虚函数是动态绑定(需要注意的是,只有当虚函数通过指针或者引用来进行调用的时候才是动态绑定)
  • 绝对不要重新定义继承而来的非虚函数

条款37 绝不重新定义继承而来的缺省参数值

  • 你只能继承两种函数:虚函数和非虚函数,然而重新定义一个继承而来的非虚函数永远是错误的,所以我们将本次讨论局限在“继承一个带有缺省参数值的virtual”

  • 所谓对象的静态类型就是他在程序中被声明时所采用的类型,对象的动态类型则是指目前所指对象的类型

  • 虚函数是动态绑定的,而缺省参数值却是静态绑定的

  • 如果我们不遵循该条款那么就会发现,调用一个定义于派生类的虚函数的同时,却使用基类为它所指定的缺省参数值

  • 原因是虚函数是动态绑定(晚绑定)的,而缺省参数值却是静态绑定(早绑定)的,并且我们使用指针或者引用去调用

  • C++之所以这样做,是为了提高运行期效率,否则将会降低效率并使得编译器复杂化

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而虚函数是动态绑定的

  • 我们可以通过NVI手法来解决该问题


条款38 通过复合塑模楚has-a或者“根据某物实现出”

  • 复合的意义和public继承完全不同
  • 在应用域,复合意味着has-a,而在实现域,复合意味着根据某物实现出
  • 当我们使用public继承的时候,一定要看对于基类的操作是否都可以应用到子类对象身上,如果不是,那么放弃public继承这个思想

条款39 明智而审慎的使用private继承

  • private继承意味着“根据某物实现出”,它通常比复合的级别低,但是当派生类需要访问基类的protected成员或者重新定义继承而来的虚函数时,这么设计师合理的
  • 和复合不同,private继承可以造成empty base最优化,这对致力于“对象尺寸最小化”的程序开发者而言很重要
私有继承将会产生那些结果
  1. 如果类之间的继承关系是private,编译期不会自动将一个派生类对象转换为一个基类对象
  2. 由私有继承而来的所有成员,在派生类中都会变成private属性,即便他们在基类中原本是protected或者public属性
私有继承的含义

私有继承意味着“根据某物实现出”,如果你让某个类私有继承另一个类,你的目的是为了使用基类中已经备妥的某些特性,而不是因为派生类和基类存在任何观念上的关系,private纯粹是一种实现技术,它意味着只有实现部分备继承,接口不封应略去。

class Timer {
public:
	explicit Timer(int tickFrequency);
	virtual void onTick() const;  //该函数会被以一定频率调用
};

class Window : private Timer{
private:
	virtual void onTick();
};

从上面的代码可以看出,我们想要以一定频率汇报Window类中的信息,但是我们不能通过public继承Timer类,因为我们发现这两个类之间并不是is-a的关系,因此使用private继承,以做到“根据某物实现出”的功能。

如何与复合进行区分使用

复合同样能够达到“根据某物实现出”的目的,我们应当尽量使用复合,只有当protected成员和虚函数牵扯进来的时候使用私有继承,以及EOB技术(Empty base optimization)


条款40 明智而审慎的使用多重继承

  • 多重继承比单一继承复杂,他可能导致新的歧义性,以及对虚拟继承的需要
  • 虚拟继承会增加大小、速度、初始化(以及赋值)复杂度的成本,如果虚基类不带任何数据,那将是最具使用价值的情况
  • 多重继承的正当用途:当设计public继承一个接口类和private继承某个协助实现的类
  • 虚基类的初始化任务由继承体系中最底层的类负责




条款49 了解new-handler的行为

  • STL容器所使用的堆内存是由容器所拥有的分配器对象管理,而不是被new和delete直接管理
  • 当operator new无法满足某一内存分配需求时,他会抛出异常,但是当他抛出异常以反映一个未被满足的内存需求之前,它会调用一个客户指定的错误处理函数,一个所谓的new-handler
  • 为了指定这个“用来处理内存不足”的函数,客户必须调用set_new_handler。其参数是一个指针,指向operator new无法分配足够内存时被调用的函数,其返回值也是一个指针,指向set_new_handler被调用前正在执行的那个new-handler函数。
  • 当operator new无法满足内存申请时,他会不断调用new-handler函数,直到找到足够内存。
  • 一个设计良好的new-handler函数必须满足条件:1,让更多的内存可以被使用。2,安装另一个new-handler。3,卸除new-handler。4,抛出bad_alloc的异常。5,不返回
  • C++并不支持类专属的new-handlers,但是我们可以自己实现出这种行为。只需要令每一个类提供自己的set_new_handler和operator new即可。其中set_net_handler使客户得以指定类专属的new-handler,至于operator new则确保在分配类对象内存的过程中以类专属的new-handler替换全局的new-handler
  • 如果按照上面方法指定了类的new-handler后,那么operator new需要按照如下做法实现:1.调用标准的set_new_handler将类内的new-handler设置为全局的new-handler。2.调用全局的operator new执行实际的内存分配,如果分配失败,全局的operator new会调用全局的new-handler(上一步设置的),如果全局的operator new最终无法分配足够的内存,会抛出一个bad_alloc异常,在此情况下应该将全局的new-handler恢复为原始的global new handler。然后再传播该异常。3.如果global operator new能够分配足够一个类对象所用内存的时候,operator new会返回一个指针指向分配的内存,类的析构函数会管理global new-handler,他会自动将operator new调用前的那个global new-handler恢复回来。
  • 直至1993年,C++都还要求operator new必须在无法分配足够内存时返回空指针,新一代的operator new则应该抛出bad_alloc异常,但是因为好多程序在编译器开始支持新规范之前写出来的,为了兼容之前的代码,C++标准委员会提供了nothrow 形式的operator new。
  • 使用 nothrow new只能保证operator new不抛掷异常,不保证像new 类名这样的表达式不导致异常。其实这个nothrow版本的operator new对于现在写代码没有什么用。。。
  • nothrow new是一个颇为局限的工具,因为它只适用于内存分配,后续的构造函数调用还是可能抛出异常的。

条款50 了解new和delete的合理替换时机

为什么要替换掉系统提供的operator new 和 operator delete呢?
  1. 运来检测运用上的错误
  2. 为了收集动态分配内存的使用统计信息
  3. 为了增加分配和归还的速度
  4. 为了降低缺省内存管理器带来的空间额外开销
  5. 为了弥补缺省分配器中的非最佳齐位
  6. 为了将相关对象成簇集中
  7. 为了获取非传统的行为

条款51 编写new和delete时需要固守常规

  • 实现一致性operator new必须返回正确的值,内存不足时必须调用new-handler函数,必须有对零内存需求的准备,还需避免不慎掩盖正常形式的new。
  • operator new的返回值十分单纯,如果它有能力供应客户申请的内存,就返回一个指针指向那块内存,如果没有那个能力,就抛出一个bad_alloc异常,然而operator new函数也并不是非常单纯,因为operator new实际上不止一次尝试分配内存并且在每次失败后调用new handler函数,只有当指向new handler函数的指针是null,operator new才会抛出异常。
  • 即使用户要求0字节,operator new也要返回一个合法的指针
  • 如果我们想要获得当前的new_handler,那么我们只能使用set_new_handler的返回值,然后再通过该函数将其设置回去
  • 在operator new里面包含一个死循环,只有当内存被成功分配,或者new-handler抛出异常或者返回(并不是return哦)才能跳出该死循环
  • operator new成员函数是会被派生类继承的,即基类的operator new可能被调用用以分配派生类对象。处理这种情况的方法是,在基类的operator new中判断该类情况,如果确定是这种情况,那么直接使用标准的operator new。
  • operator new应该内含一个无限循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new handler,它也应该有能力处理0字节的申请,类专属版本还应该能够处理比正确大小更大的申请。
  • operator delete应该在收到空指针时不作任何事,类专属版本还应该处理不正常大小的内存释放工作(通过调用全局的operator delete)
  • 如果即将被删除的对象派生自某个基类并且该基类每有虚析构函数,那么C++传递给operator delete的参数数值可能不正确

条款52 写了placement new也要写placement delete

  • 当你写了一个placement operator new,请确定也写出了对应的placement operaotr delete,如果没有这样做,你的程序可能会发生内存泄露
  • 当你声明placement new 和placement delete的时候,请确定不要无意识地遮掩了他们的正常版本
  • Widget *pw = new Widget;在以上代码中共有两个函数被调用,一个是用以分配内存的operator new,一个是Widget的默认构造函数,假设其中第一个函数调用成功,第二个函数却抛出异常。既然这样,步骤一中分配的内存必须归还给操作系统并恢复原样,否则就会造成内存泄漏。在这个时候,客户没有能力归还内存,因为如果Widget构造函数抛出异常,pw尚未被赋值,客户手上也就没有指向这块应该被归还内存的指针,取消步骤一并恢复旧观的责任因此就落在C++运行期系统身上
  • 运行期系统会调用步骤一所调用的operator new相应的operator delete版本,前提是它知道哪一个operator delete应该被调用。如果目前面对的是拥有正常签名的new和delete,这并不是问题。然而如果使用非正常的operator new,也就是带有附加参数的operator new(这其实就是placement new),问题就浮出水面了。运行期系统寻找参数个数和类型都与operator new相同的某个operator delete,如果找到,那就是它的调用对象。如果找不到,那么就什么也不做(内存泄漏了)
  • 如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这便是个所谓的placement new。如果operator delete接受额外参数,便称为placement delete。
  • 如果使用placement new创建对象成功,销毁该对象时,delete调用的额是正常形式的operator delete,而不是其placement版本,placement delete只有在伴随placement new调用而触发的构造函数出现异常时才会被调用,对一个指针施行delete绝不会导致调用placement delete。这意味着如果要对所有与placement new相关的内存泄漏宣战,我们必须同时提供一个正常的operator delete(用于构造期间无任何异常被抛出)和一个placement版本(用于构造期间有异常被抛出)。后者的额外参数必须和operator new一样。
  • 由于成员函数的名称会遮掩其外围作用域中相同的名称,你必须小心避免让类专属的operator new遮盖客户期望的其他operator new。假设你有一个基类,其中声明唯一一个placement new,客户端会发现他们无法使用正常形式的new(遮盖了全局的operator new),同理在派生类中声明operator new同样会遮盖基类版本和全局版本
  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值