Effective C++

让自己习惯C++

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

可以将C++看作四部分组成,因为每个部分的思路都不同,分而治之效果更佳:
1.C语言。C++仍以C为基础
2.objected-oriented C++。面向对象编程,类、封装、继承、多态
3.template C++。C++泛型编程、模板元编程的基础
4.STL。容器、迭代器、算法

通常,内置类型通过值传递比引用效率更高;
而自定义类型通过引用比值传递效率更高。

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

#define最好只用于复杂变量名替换。

条款03:尽可能使用const

通常可以将const理解为只读权限。
使用const有以下好处:
1.让编译器参与帮助检测变量的权限。当使用了const声明变量后,就不必担心该变量被改变了,因为编译器会帮你检测;
2.让使用者明白。当参数中使用了const后,使用者会很容易知道这个参数是只读还是读写,这样就可以判断是入参还是出参了,如果不声明const,恐怕使用者都会进去看一眼。

const可以批量设置变量属性,如一个结构体被声明为const,这时结构体内成员都将成为只读权限。如果此时结构体内某个成员需要可写属性,就可以使用mutable关键字来解除限制,使其永久可写。

对于成员函数中使用后置const的行为,如void printall() const,主要是让使用者更有信心,证明这个函数只读不写。

条款04:确定对象被使用前已先被初始化

对于内置类型:
全局变量如果初始化时没被赋值,则会被初始化为默认值;
局部变量一定要被初始化,不然其内部存放的是上一次存放过的值,然而该值是未知的。
对于内置类型以外的类型:
初始化的责任落在构造函数身上。
初始化构造函数时建议使用初始化列表而不是在其体内使用赋值语句。

构造/析构/赋值运算

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

当没有声明时,编译器会自动为类创建默认构造函数、析构函数、复制构造函数和赋值运算符。

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

若不想使用编译器自动生成的函数,可将相应的成员函数申明为private并且不予实现。

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

如果一个类有任何虚函数,那么它就应该有虚析构函数。

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

析构函数不要抛出异常,如果析构函数中调用的函数可能抛出异常,析构函数应该捕捉并记录下来然后吞掉他(不传播)或结束程序。同时最好提供一个普通函数用来供用户执行可能异常的该操作。

条款09:绝不在构造和析构过程中调用虚函数

在构造函数和析构函数中不要去调用虚函数,因为子类在构造/析构时,会调用父类的构造/析构函数,此时其中的虚函数是调用父类的实现,但这是父类的虚函数可能是纯虚函数,即使不是,也可能不符合你想要的目的(是父类的结果不是子类的结果)。
如果想调用父类的构造函数来做一些事情,替换做法是:在子类调用父类构造函数时,向上传递一个值给父类的构造函数。

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

注意这只是个协议,但建议遵守。因为该形式被内置类型和标准库类型共同遵守。

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

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

由于变量有别名的存在(多个指针或引用只想一个对象),所以可能出现自我赋值的情况。比如 a[i] = a[j]*px=*py,可能是同一个对象赋值。
一般的解决办法是先检测再赋值。

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

复制构造函数和赋值构造函数要确保复制了对象内的所有成员变量和所有基类成分,这意味着你如果自定义以上构造函数,那么每增加成员变量,都要同步修改以上构造函数,且要调用基类的相应构造函数。

资源管理

条款13:以对象管理资源

为了确保一个对象在初始化后能够最终有效被delete,最好使用shared_ptr和auto_ptr,而前者更好,因为是基于引用计数机制,可以在复制时保持两个指针都指向同一对象,且只有两个指针都销毁时才delete。

这本书可能旧了,auto_ptr已经被废弃了。

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

如果对想要自行管理delete(或其他类似行为如上锁/解锁)的类处理复制问题,有以下方案,先创建自己的资源管理类,然后可选择:

  • 禁止复制,使用条款6的方法
  • 对复制的资源做引用计数(声明为shared_ptr),shared_ptr支持初始化时自定义删除函数(auto_ptr不支持,总是执行delete)
  • 做真正的深复制
  • 转移资源的拥有权,类似auto_ptr,只保持新对象拥有。

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

封装了资源管理类后,API有时候往往会要求直接使用其原始资源(作为参数的类型只能接受原始资源,不接受管理类指针),这时候就需要提供一个获取其原始资源的方法。有显式转换方法(如指针的->和(*)操作,也比如自制一个getXXX()函数),还有隐式转换方法(比如覆写XXX()取值函数)。显式操作比较安全,隐式操作比较方便(但容易被误用)。

不明觉厉。

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

new 对应 delete。
new a[4] 对应 delete [] a。
两者的使用必须对应

条款17:以独立语句将newed对象置入智能指针

如果有函数参数接收智能指针对象,那么该智能指针对象一定要在调用该函数前用独立语句去创建,否则在创建所指对象和用该对象绑定智能指针两个操作之间,可能插入一些操作(由于C++的独特性),这时候如果出异常,那么会造成创建的对象还没来得及用智能指针修饰,也就无法被自动回收了。

不明觉厉。

设计与声明

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

好的接口要容易被正确使用,不容易被误用,符合客户的直觉。

  • 促进正确使用的办法包括保持接口的一致性,既包括自定义接口之间的一致性,也包括与内置类型行为的相似一致性。
  • 阻止误用的办法包括建立新类型来限制该类型上的操作、束缚对象的值以及消除客户管理资源的责任,以此来作为接口的参数与返回类型。
  • shared_ptr支持定制删除函数,所以可以很方便的实现上述问题,以及防范DLL问题。

条款19:设计class犹如设计type

在设计class时,要考虑一系列的问题,包括:

  • 对象的创建和销毁(构造、析构)
  • 对象的初始化与赋值(构造、赋值操作符)
  • 复制操作(复制构造)
  • 合法值(约束条件)
  • 继承体系(注意虚函数)
  • 支持的类型转换(显示转换、类型转换操作符)
  • 成员函数和成员变量的可见范围(public/protected/private)
  • 是否用模板就能实现?

条款20:宁以传递const引用替换传递值

尽量用 常量引用类型 来作为函数的参数类型,这通常比较高效,也可以解决基类参数类型被赋值子类时引起的内容切割问题。
但对于内置类型和STL的迭代器与函数对象,通常编译器会对其专门优化,直接传值类型往往比较恰当。

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

虽然函数参数最好用引用值,但函数返回值却不要随便去用引用,这回造成很多问题,比如引用的对象在函数结束后即被销毁,或是需要付出很多成本和代码来保证其不被销毁且不重复,这大概率没有必要,就返回一个值/对象就好了。

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

切记将成员变量声明为private,这可以保证客户访问数据的一致性、可以细微划分访问控制、允许约束条件获得保证,并提供类作者充分的实现弹性来修改对其的处理,因为这保证了“封装性”,作者可以改变实现和对成员变量的操作,而不改变客户的调用方式。
protected并不比public更加具有封装性,因为protected修饰的成员变量一旦修改,也会造成子类的大量修改。

条款23:宁以非成员、非友元替换成员函数

宁可拿非成员非友元函数来替换成员函数。因为这种函数位于函数之外,不能访问类的private成员变量和函数,保证了封装性(没有增加可以看到内部数据的函数量),此外,这些函数只要位于同一个命名空间内,就可以被拆分为多个不同的头文件,客户可以按需引入头文件来获得这些函数,而类是无法拆分的(子类继承与此需求不同),因此这种做法有更好的扩充性。

条款24:若所有参数皆需类型转换,请为此采用非成员函数

如果你要为某个函数的所有参数(包括this所指对象本身)进行类型转换,那么该函数必须是个非成员函数。
举个例子,你想为一个有理数类实现乘法函数,支持与int类型的乘积,可以,因为传参int进去后会调用构造函数隐式转换为有理数类型,同时你想满足交换律,这时就会报错,因为int类型并没有一个函数用来支持你的有理数类做参数的乘法运算。解决方案是将该乘法运算函数作为一个非成员函数,传两个参数进去,这样不管你的int放在前面还是后面,都能作为参数被转换类型了。
但是,非成员函数不代表就一定成为友元函数,能够通过public函数调用完成功能的,就不该设为友元函数,避免权力过大造成麻烦。

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

由于swap函数如此重要,需要特别对他做出一些优化。
常规的swap是简单全复制三次对象进行交换(包括temp对象),如果效率足够就用常规版。
如果效率不够,那么给你的类提供一个成员函数swap,用来对那些复制效率低的成员变量(通常是指针)做交换。
然后,提供一个非成员函数的swap来调用这个成员函数,供别人调用置换。
对于类(非模板),为标准std::swap提供一个特定版本(swap是模板函数,可以特化)。
在使用swap时,记得 using std::swap,让编译器可以获取到标准swap或特化版本。编译器会自行从所有可能性中选择最优版本。

实现

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

直到使用它时才进行定义。
但对于循环,定义在循环体前还是定义在循环体内,哪一个比较好呢?
方法A:定义在循环体外

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

方法A:定义在循环体内

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

做法A:1个构造函数+1个析构函数+n个赋值操作;
做法B:n个构造函数+n个析构函数。

除非
(1)你知道赋值成本比“构造+析构”成本低,
(2)你正在处理代码中效率高度敏感的部分,
否则你应该使用做法B。

条款27:尽量少做转型操作

尽量避免使用转型cast(包括C的类型转换和C++的四个新式转换函数),特别是注重效率的代码中避免用dynamic_casts。如果一定要用,试着考虑无需转型的替代设计,例如为基类添加一个什么也不做的衍生类使用的函数,避免在使用时需要将基类指针转型为子类指针。
如果一定要转型,试着将其隐藏于某个函数后,客户调用该函数而无需自己用转型。
宁可使用C++新式转型,也不用用C的旧式,因为新式的更容易被注意到,而且各自用途专一。

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

避免让外部可见的成员函数返回handles(包括引用、指针、迭代器)指向对象内部(更隐私的成员变量或函数),即使返回const修饰也有风险。这一方面降低了封装性,另一方面可能导致其指向的对象内部元素被修改或销毁。

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

异常安全函数是指即使发生异常也不会泄露资源或者导致数据结构破坏,分三种保证程度:基本保证、强烈保证和不抛异常型。
只有基本类型才确保了不抛异常型。对于我们自己设计的函数,往往想要提供强烈保证,即一旦发生异常,程序的整个状态会回到执行函数前的状态,实现方法一般用复制一个副本然后执行操作,全部成功后再替换原对象的方式来实现。但这一操作有时对时间和空间的消耗较大,适用性不强。这种情况下可以提供基本保证。
函数提供的保证程度通常最高只等于其所调用的各个函数中的保证的最弱者——木桶理论。

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

inline还真的和宏很像,用一个名称替换一段代码。
类声明中的带有实现的成员函数就是内联函数。一般用于返回私有值。

只将inline用在小型、被频繁调用的函数身上。inline会带来体积增大的问题,此外,不要对构造函数、析构函数等使用inline,即使你自己在其中写的代码可能很少,编译器却会为他添加很多代码。
不要只因为模板函数出现在头文件,就将它们声明为inline,模板函数和inline并不是必须结对出现的。

条款31:将文件间的编译依存关系降至最低

为了增加编译速度,应该减少类文件之间的相互依存性(include),但是类内又常常使用到其他类,不得不相互依存,解决方案是:将类的声明和定义分开(不同的头文件),声明相互依存,而定义不相依存,这样当定义需要变更时,编译时不需要再因为依赖而全部编译。
基于此构想的两个手段是Handle classes和Interface classes。Handle classes是一个声明类,一个imp实现类,声明类中不涉及具体的定义,只有接口声明,在定义类中include声明类,而不是继承。而Interface classes是在接口类中提供纯虚函数,作为一个抽象基类,定义类作为其子类来实现具体的定义。

继承与面向对象设计

条款32:确定你的public继承是is-a关系

public继承意味着 is-a 关系,也就是要求,适用于基类身上的每一件事情,是每一件,也一定适用于衍生类身上。有时候,直觉上满足这一条件的继承关系,可能并不一定,比如,企鹅是鸟,但并不会飞。

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

就如函数作用域内的变量会掩盖函数作用域外的同名变量一样。
衍生类中如果声明了与基类中同名的函数(无论是虚、非虚,还是其他形式),都会掩盖掉基类中的所有同名函数,注意,是所有,包括参数不同的重载函数,都会不再可见。此时再通过子类使用其基类中的重载函数(子类没有声明接收该参数的重载函数时),都会报错。
解决方案一是使用using声明式来在子类中声明父类的同名函数(重载函数不需要声明多个),此时父类的各重载函数就是子类可见的了。二是使用转交函数,即在子类函数的声明时进行定义,调用父类的某个具体的重载函数(此时由于在声明时定义,成为inline函数),此举可以只让需要的部分父类重载函数于子类可见。

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

Public继承由两部分组成:接口(interface)继承和实现(implementation)继承.

class Shape{
public:
    virtual void draw() const = 0;//纯虚函数
    virtual void error(const std::string& msg);//非纯虚函数
    int ObjectID() const;//非虚函数
    ....
};

class Rectangle:public Shape{...};
class Ellipse:public Shape{...};

上述代码中,类Shape是一个抽象类,因为draw()是纯虚函数,所以客户不能创建Shape的实体。
纯虚函数的作用是:让派生类只继承函数接口,而派生类必须提供实现;
非纯虚函数的作用是:让派生类继承函数接口,并提供给派生类默认的实现方法(实现继承),当派生类不打算复写方法时,将应用基类提供的默认方法;
非虚函数的作用是:派生类不能复写该函数。

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

虚函数(本质是希望子类的实现不同)的替代方案:

  • 用public的非虚函数来调用private的虚函数具体实现,非虚函数必须为子类继承且不得更改,所以它决定了何时调用以及调用前后的处理;虚函数实现可以在子类中覆写,从而实现多态。
  • 将虚函数替换为函数指针成员变量,这样可以对同一种子类对象赋予不同的函数实现,或者在运行时更改某对象对应的函数实现(添加一个set函数)。
  • 用tr1::function成员变量替换虚函数,从而允许包括函数指针在内的任何可调用物搭配一个兼容于需求的签名式。
  • 将虚函数也做成另一个继承体系类,然后在调用其的类中添加一个指针来指向其对象。

本条款的启示为:为避免陷入面向对象设计路上因常规而形成的凹洞中,偶尔我们需要对着车轮猛推一把。这个世界还有其他许多道路,值得我们花时间加以研究。

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

不要重新定义继承而来的非虚函数,理论上,非虚函数的意义就在于父类和子类在该函数上保持一致的实现。

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

不要重新定义一个继承而来的函数(虚函数)的缺省参数的值(参数默认值),因为函数是动态绑定(调用指针指向的对象的函数实现),但参数默认值却是静态绑定(指针声明时的类型所设定的默认参数,比如基类设定的)。这会导致两者不对应,比如:
Base *p = new SubClass();

条款38:通过复合表示 has-a 或者“根据某物实现出”的关系

注意 has-a 和 is-a 的区分。如果是 is-a 的关系,可以用继承,但如果是 has-a 的关系,应该将一个类作为另一个类的成员变量来使用,以利用该类的能力,而不是去以继承它的方式使用。

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

Private继承意味着“根据某物实现出”,而不是 is-a 的关系。与上面的复合(has-a)很像,但比复合的级别低。当衍生类需要访问 protected 基类的成员,或需要重新定义继承而来的虚函数时,可以这么设计。
此外,private继承可以让空基类的空间最优化。

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

多重继承确实有正当使用场景,比如public继承某个接口类的接口(其接口依然是public的),private继承某个类的实现来协助实现(继承来的实现为private,只供自己用)。
虚继承会增加大小、速度、初始化(及赋值)复杂度等成本,如果虚基类不带任何数据,将是最具使用价值的情况。

模板与泛型编程

条款41:了解隐式接口和编译期多态

类和模板都支持接口和多态。
类的接口是显式定义的——函数签名。多态是通过虚函数在运行期体现的。
模板的接口是隐式的(由模板函数的实现代码所决定其模板对象需要支持哪些接口),多态通过模板具现化和函数重载解析在编译期体现,也就是编译期就可以赋予不同的对象于模板函数。

条款42:了解typename的双重意义

声明模板的参数时,前缀关键字 class 和 typename 可互换,功能相同。
对于嵌套从属类型名称(即依赖于模板参数类型的一个子类型,例如迭代器),必须用typename来修饰,但不能在模板类的基类列和初始化列表中修饰基类。

条款43:学习处理模板化基类内的名称

如果基类是模板类,那么衍生类直接调用基类的成员函数无法通过编译器,因为可能会有特化版的模板类针对某个类不声明该接口函数。
解决方法有:

  • 在调用动作前加上“this->”
  • 使用using声明式来在子类中声明基类的该接口
  • 明确指出被调用的函数位于基类:Base