条款01:视C++为一个语言联邦(View C++ as a federation of languages.)
-
对于内置类型而言,pass-by-value通常比pass-by-reference高效;
-
对于用户自定义的类,pass-by-reference-to-const往往更好,对于TemplateC++时尤其如此;
-
对于迭代器和STL中的函数对象,pass-by-value守则再次适用。
条款02:尽量以const,enum,inline替换#define(Prefer consts,enums, and inline to #defines.)
编译过程:.c文件--预处理-->.i文件--编译-->.o文件--链接-->bin文件
-
使用const替换#define来定义常量,对于常量指针,必须写两次const;
-
如果想让这个常量的作用域限制于class中,最好加上static以确保至多只有一份实体;
-
如果需要一个类中的常量(例如作为数组的指定大小)而编译器不支持“static整数型常量”完成“in class 初值设定”,可改用“enum hack”补偿做法;
-
使用inline(或者template inline)函数替换#define宏函数;
条款03:尽可能使用const(Use const whenever possible)
-
如果将迭代器声明为const,实际上就像声明指针为const一样(即声明一个T* const指针)
-
令函数返回一个常量值,例如operator*的实现
-
对于参数,除非有需要改动参数或local对象,否则请将它们声明为const;
-
如果两个函数,只是常量性不同,可以被重载
-
对于常量函数,编译器只认准bitwise constness,但是我们在使用时,应该使用概念上的常量性(可以通过mutable方法)
-
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复;
条款04:确定对象被使用前已先被初始化(Make sure that objects are initialized before they're used)
-
使用成员初值列进行初始化;
-
内置类型的初始化与赋值成本相同,但为了保持一致性,最好也在成员初值列进行初始化;
-
对于拥有多个构造函数的class,可以合理的遗漏那些”赋值表现像初始化一样好“的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常时private),供所有构造函数调用
-
成员初值列的成员变量总是以其声明次序被初始化,即使它们在成员初值列以不同的次序出现(很不幸那是合法的)也不会有任何影响,为了避免检阅者迷惑,最好在成员初值列以其声明次序列出成员变量;
-
所谓编译单元,是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件;
-
如果某编译单元内的某个non-local static 对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对”定义于不同编译单元内的non-local static 对象“的初始化次序并无明确定义。
-
为免除”跨编译单元之初始化次序“问题,请以local static对象替换non-local static对象;
条款05:了解C++默默编写并调用哪些函数(Know what functions C++ sliently writes and calls)
- 编译器会为空类声明一个copy构造函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。所有这些函数都是public且inline;
- 编译器默认产出的析构函数是个non-virtual;
- C++不允许”让reference改指向不同对象“,所以如果你打算在一个”内涵reference成员“的class内支持赋值操作,你必须自己定义copy assignment操作符。面对”内含const成员“的classes,编译器的反应也一样。
- 如果某个base class将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment;
条款06:若不想使用编译器自动生成的函数,就应该明确拒绝(Explicitly disallow the use of compiler-generated functions you do not want.)
- 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数(例如copy构造函数和copy assignment操作符)声明为private并且不予实现。这样当客户企图拷贝对象时,编译器会阻挠他,但如果不慎在member函数或frind函数之内那么做,轮到连接器发出抱怨。将这个连接器错误移至编译器也是可能的,就是设计一个专门为了阻止这些动作而设计的base class,在这个类中将这些动作设为private;
- 函数参数名称并非必要,只不过大家都愿意写出来,不写也可以;
条款07:为多态基类声明virtual析构函数(Declare destructors virtual in polymorphic base classes.)
- 任何class只要带有virtual函数都几乎确定也应该有个非virtual函数;
- 当class不含virtual函数,令其析构函数为virtual往往是个馊主意;
- 有时候希望某个类成为抽象类,但是又没有纯虚函数,此时可以把析构函数声明为纯虚析构函数。这里有个窍门:你必须为这个pure virtual析构函数提供一份定义(因为编译器会在深层的派生类的析构函数会中创建对基类的虚构函数的调用)。
- Classes的设计目的如果不是作为base classes使用(例如标准string 和STL容器),或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。
条款08:别让异常逃离析构函数(Prevent exceptions form leaving destructors.)
- 如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理选项;吞下异常是另一个可行方案;
- 如果在析构函数中关闭数据库连接有可能发生异常,那么提供一个关闭数据库的函数给用户使用,同时保留在析构函数中关闭数据库的做法,这样的目的是给用户第一手处理异常的机会。(如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。)
条款09:绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruction.)
- 绝不在构造函数和析构函数过程中调用virtual函数;有些构造函数通过调用init()函数进行初始化,而在init()函数中调用了虚函数,这种比较潜藏而且暗中为害,因为它通常不会引发编译器和连接器的抱怨;
- 如何确保每次一有某个继承体系上的对象被创建,就会有适当版本的初始化方法被调用呢,很显然,在构造函数内对着对象调用virtual函数是一种错误做法;一种做法是将初始化函数变为non-virtual方法,然后要求deviered class 传递必要信息给base class的构造函数,然后那个构造函数就可以安全的调用non-virtual的初始化函数了;
条款10:令operator=返回一个reference to *this(Have assignment operators returne a reference to *this.)
- 令赋值(assignment)操作符(包括赋值相关运算,比如+=)返回一个reference to *this。
条款11:在operator=中处理“自我赋值”(Handle assignment to self in operator=.)
- 欲阻止错误赋值,传统做法是藉由operator=最前面的一个证同测试(如果是自我赋值,就不做任何事),这种做法行得通但仍不具备异常安全性;
- 更好的做法是在复制新对象前别删除原有对象(先记住原先指针,然后new新对象,最后删除原先对象)如果你很关注效率,可以再次把“证同测试”放回函数起始处;
- 另一种做法是使用“copy and swap”技术;
条款12:复制对象时勿忘其每一个成分(Copy all parts of an object)
- Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分“。(1、复制所有local成员变量;2、调用所有base classes内的适当copying函数)
- 不要尝试以某个copying函数实现另一个copy函数。应该将共同机能放进第三个函数中(这样的函数往往是private而且常被命名为init),并由两个copying函数共同调用。
条款13: 以对象管理资源(use objects to manage resources)
- 把资源放进对象内,可以依赖C++“析构函数自动调用机制”确保资源被释放。
- 为防止资源泄露,使用RAII对象,它们在构造函数中获得资源,在析构函数中释放资源。
RAII:Resource Acquisition Is Initialization,“资源取得的时机便是初始化时机”,也就是以对象管理资源。 - shared_ptr和auto_ptr两者都在其析构函数中做delete而不是delete[]动作,那意味着在动态分配而得的身上使用shared_ptr或auto_ptr是个馊主意(尽管可叹的是它们仍能通过编译)
- 你也许会发现,并没有针对“C++动态分配数组”而设计的类似shared_ptr和auto_ptr那样的东西,因为vector或string总是能够代替它,如果你一定需要,看看BOOST吧。
- 常用的两个RAII类分别是shared_ptr和auto_ptr(智能指针)。
auto_ptr复制动作会使它(被复制物)指向null,即“受auto_ptr管理的资源没有一个以上的auto_ptr同时指向它”。shared_ptr(引用计数型智能指针)可以多对一,在无人指向的时候自动删除,但不能打破环状引用。
条款14: 在资源管理类中小心 copying 行为(Think carefully about copying behavior in resource-managing classes)