Effective C++ 中文版
explicit:显式的 implicit: 隐式的
预备知识
1、继承关键字说明:
- public:可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问;
- protected:可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问;
- private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象、访问。
1、让自己习惯C++
命名习惯:
//lhs:left hand side(左手端)
//rhs:right hand side(右手端)
//指向一个T型的对象的指针一般命名为:pt,意思是“pointer to T"
Widget* pw; //pw="ptr to Widget"
class Airpland;
Airplane* pa; //pa="ptr to Airplane"
条款 02:尽量以const,enum,inline替换#define
-
对于单纯常量,最好以const对象或enums替换#defines
-
对于形似函数的宏(macros),最好改用inline函数替换#defines
条款 03:尽可能使用const
char greeting[]="hello";
char* p=greeting; //non-const pointer,non-const data
const char* p=greeting; //non-const pointer,const data
char* const p=greeting; //const pointer,non-const data
const char* const p=greeting;//const pointer,const pointer
说明:如果关键字const出现在星号左侧,表示被指物是常量;如果出现在星号右侧,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复
条款 04:确定对象被使用前已先初始化
- 为内置型对象进行手工初始化,因为C++不保证初始化它们
- 构造函数最好使用成员初值列表,而不要在构造函数内进行赋值操作。初值列表的成员变量,其排列顺序应该和它们在class中的声明次序相同
- 为免除“跨编译单元之间的初始化”问题。请以local static对象替换non-local-static对象。
2、构造、析构、赋值运算
条款 05:了解C++默默编写并调用哪些函数
空类:class Empty { };此时编译器会为它声明一个默认的构造、析构、赋值等函数。好比你写了如下函数:
class Empty {
public:
Empty() {...} //default构造函数
Empty(const Empty& rhs) {...}//copy构造函数
~Empty() {...}//析构函数,编译器产生的析构函数都是non-virtual
Empty& operator=(const Empty& rhs) {...}//copy assignment操作符
}
条款 06:若不想使用编译器自动生成的函数,就应该明确拒绝
- 为了拒绝编译器自动提供的功能,可将相应的成员函数声明为private,并且不予实现。
条款 07:为多态基类声明virtual析构函数
1、多态中如果基类的析构函数不是虚函数造成的后果:当derived class对象经由一个base class指针被删除时,实际执行时对象的derived成分没有被销毁,只有base class成分会被销毁,因此这就造成了”局部销毁“的现象,形成难以察觉的内存泄漏问题。
解决方法:base class的析构函数声明为虚函数。
- polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,他就应该拥有一个virtual析构函数
- classes的设计目的如果不是作为base classes使用,或者不是为了具备多态性,则就不应该声明virtual析构函数
条款 08:别让异常逃离析构函数
- **析构函数绝对不要吐出异常。**如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序
- 如果客户需要对某个函数运行期间抛出的异常做出反应,则class应该提高一个普通函数执行该操作(而不是放在析构函数中执行)
条款 09:绝不在构造和析构函数中调用virtual函数
相关链接:C++不要在构造函数和析构函数中调用虚函数 - 云+社区 - 腾讯云 (tencent.com)
为什么不要在构造函数和析构函数中调用虚函数? - jiayouwyhit - 博客园 (cnblogs.com)
- 在构造和析构期间不要调用virtual函数,因为此类调用不会下降至derived class
- 问题:为何父类(基类)的析构函数一定也要写成虚函数:唯有这样,当delete一个指向子类对象的父类指针时,才能保证系统能够依次调用子类的析构函数和父类的析构函数,从而保证对象(父指针指向的子对象)内存被正确地释放。
条款 10:令operator=返回一个reference to *this
条款 11:在operator=中处理“自我赋值”
- 确保当对象自我赋值时,operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序,以及copy-and-swap
条款 12:复制对象时勿忘其每一个成分
- Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同部分放进第三个函数中,通过调用实现功能复用。
3、资源管理
条款13:以对象管理资源
- 为防止资源泄露,请“以对象管理资源”。它们在构造函数中获得资源并在析构函数中释放资源
- 两个常被使用的(Resource Acquisition Is Initialization)RAII classes分别是shared_ptr和auto_ptr。前者通常是比较好的选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向nullptr。
条款 14:在资源管理类中小心copying行为
- 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为
- 普遍的RAII class copying行为是:抑制copying、采用引用计数法(reference counting)。
条款 15:在资源管理类中提供对原始资源的访问
- APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理的资源”的方法
- 对原始资源的访问可能经由显式转化或隐式转化。一般而言显示转化比较安全,但隐式转化对客户方便。
条款 16:成对使用new和delete时要采取相同形式
- 如果你在new表达式中使用[],必须在相应的delete表达式也使用[]。如果你在表达式中不使用[],一定不要在相应的delete表达式中使用[]
条款 17:以独立语句将newed对象置入智能指针
- 以独立语句将newed对象存储于(置于)智能指针内。如果不这样做,一旦出现异常抛出,则可能造成难以察觉的内存泄露
//以下语句可能会出现内容泄露
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority);
/*由于编译器可能对表达式的参数调用顺序可调整,那么它有可能会执行以下顺序
1、执行“new Widget”
2、调用priority
3、调用tr1::shared_ptr构造函数
此时如果priority函数调用出现异常,则“new Widget”返回的指针将会遗失,这样它就不能成功放入tr1::shared_ptr中,从而造成后期可能存在的内存泄露*/
//解决方法:使用分离语句
std::tr1::shared_ptr<Widget> pw(new Widget); //在单独语句内以智能指针存储
processWidget(pw,priority()); //这个调用动作绝不会造成内存泄露
4、设计与声明
“让接口容易被正确使用,不容易被误用”
条款18:让接口容易被正确使用,不易被误用
- shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变为0时将被调用的“删除器”。
- shared_ptr还有一个很好的性质:它会自动使用它的“每个指针专属的删除器”。
- “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
- tr1::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁。
条款 19:设计class犹如设计type
条款 20:宁以pass-by-reference-to-const替换pass-by-value
- 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较合适
条款 21:必须返回对象时,别妄想返回其reference
- 绝不要返回一个指向local stack对象的pointer或reference,或返回一个指向heap-allocated对象的reference,或返回一个指向local static对象而有可能同时需要多个这样对象的reference。
条款 22:将成员变量声明为private
- 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
- protected并不比public更具封装性。
条款 23:宁以non-member、non-friend替换member函数
- 这样做的目的是增加封装性、包裹弹性和机能扩充性(它毕竟没有增加“能够访问class内的private成分”的函数数量)
- 将所有便利函数放在多个头文件但隶属同一个命名空间,意味着客户可以轻松扩展这一便利函数。
条款 24:若所有参数都需要类型转换,请为此采用non-member函数
条款 25:考虑写出一个不抛出异常的swap函数
- 典型的swap函数的实现:
namespace std{
template<typename T> //std::swap的典型实现
void swap(T& a,T& b){ //置换a和b的值
T tmp(a);
a=b;
b=tmp;
}
}
1、 c++只允许对类模板偏特化,不允许对函数模板偏特化。
2、 std是一个特殊的命名空间,它允许全特化任何templete,但是不允许添加任何templete。
- 关于pimpl类:提供一个member swap,并提供一个non-member swap来调用前者,最后特化std::swap()
- 关于pimpl模板类:创建一个包括类定义的namespace,并在此命名空间之中构建non-member swap。
- 关于调用,针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间修饰符”。
5、实现
条款 26:尽可能延后变量定义式的出现时间
- “尽可能延后”的真正意义:你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延迟这部分定义直到能够给它实参进行初始化为止。例如以下代码是一个比较好的书写规范:
std::string encryptPassword(const std::string& passwordd)
{
... //检查长度
std::string encrypted(password); //通过copy构造函数
//定义并初始化
encrypt(encrypted);
return encrypted;
}
条款 27:尽量少做转型工作
C++提供了四种新式转型:
const_cast<T>(expression); //常被用来将对象的常量性去除cast away the constness
dynamic_cast<T>(expression);//常用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型
reinterpret_cast<T>(expression);//意图执行低级转型
static_cast<T>(expression);//用来强迫隐式转换
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。
- 如果转型是必须的,试着将其隐藏于某个函数背后。客户可以随时调用该函数而不需将转型放进他们自己的代码中
- 宁可使用C++ style(新式)转型,不要使用旧式转型。前者很容易辨识出来。
条款 28:避免返回handle指向对象内部成分
- 避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数行为像个const,并将发生“虚吊号码牌”(指针指向一个已被销毁的对象)的可能性降到最低。
条款 29:为“异常安全”而努力是值得的
当异常抛出时,带有异常安全性的函数会:1. 不泄露任何资源。2. 不破坏任何数据
- 异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构遭到破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型
- “强烈保证”往往能够以copy-and-swap实现出来。但“强烈保证”并非对所有函数都可实现或具备显示意义
- 函数提供“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全保证”中的最弱者(木桶效应)
条款 30:透彻了解Inlining的里里外外
大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining,而对所有的virtual函数调用也不进行inlinin(因为virtual函数意味着“等待”,只有在运行期才能确定调用哪个函数)
- 将大多数inlining限制在小型、被频繁调用的函数身上。着可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化
- 不要因为function templates出现在头文件中就将它们声明为inline。
备注:如果函数不是inline函数,那么它有个优点是当该函数发生修改时,客户端只需要重新连接就好,这远比重新编译整个文件负担少很多
条款 31:将文件间的编译依存关系降至最低
- 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。
- 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否设计templates都适用。
6、继承与面向对象设计
条款 32:确定你的public继承模仿出is-a关系
- “public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象
条款 33:避免遮掩继承而来的名称
- 如果我们继承base class并加上重载函数,而我们又希望重新定义或覆写(推翻)其中的一部分,那么则必须为那些原本会被遮掩的每一个名称引入一个using声明,防止我们希望继承的函数被遮掩
class Derived:public Base{
public:
using Base::mf1;//让Base class内名为mf1和mf3的所有东西
using Base::mf2;//在Derived作用域内都可见(并且Public)
void mf3();
}
- 当我们仅需要继承Base中某一个特殊函数而非全部函数时(就是说不要要所有重载函数),可以采用转交函数(forwarding function),不能采用using声明,因为using声明会让Base中所有相同名称都可见(Base也有重载)。这也为那些无法进行using声明的旧编译器提供了一条解决方法。
class Derived:private Base{
public:
virtual void mmf1()//转交函数
{ Base::mf1(int);}//默认成为inline。
}
- derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
- 为了让被遮掩的名称可见,可使用using声明式或转交函数。
条款 34:区分接口继承和实现继承
- 接口继承和实现继承不同。在public继承下,derived classes总是继承base class的接口
- pure virtual函数只继承接口
- impure virtual函数是继承接口和一份缺省实现
- non-virtual函数是继承接口以及强制性实现继承
条款 35:考虑virtual函数以外的其它选择
- 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹低访问性(private或protected)的virtual函数。
- 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
- 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
- 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现方法。
条款 36:绝不重新定义继承而来的non-virtual函数
原因:调用一个derived class函数(该函数在base class有一个实现,在derived class内也有一个实现,且它是non-virtual函数)。那么此时声明一个derived class对象,程序调用会出现诡异的一面:
class B{
public:
void mf(); //non-virtual函数
}
class D:public B{
public:
void mf(); //它遮盖了B::mf;
}
D x;
B* pB=&x;
D* pD=&x;
pB->mf(); //调用B::mf
pD->mf(); //调用D::mf
//愿意解释:可以看到,都是同一对象x,但由于指向的指针不一样,导致函数调用不用(而非简单的派生类覆盖了基类)
//就是说:决定因素不在于自身,而在于“指向该对象的指针”当初的声明类型
条款 37:绝不重新定义继承而来的缺省参数值
- NVI(non-virtual interface)手法:令base class内的一个public non-virtual函数调用private virtual函数,后者可被derived classes重新定义。
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——我们唯一应该覆写的东西却是动态绑定。
条款 38:通过复合模拟出has-a或“根据某物实现出”
- 复合(composition)的意义和public继承完全不同
- 在应用域(application domain),复合意味着has-a(有一个)。在实现域(implementation domain),复合意味着is-implemented-in-terms-of(根据某物实现出)
条款 39:明智而审慎地使用private继承
- Private继承意味着is-implemented-in-terms of(根据某物实现出)。它通常比复合(composition)的级别低。但当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
- 和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者来讲很重要
条款 40:明智而审慎地使用多重继承
- 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承地需要。
- virtual继承会增大空间、速度、初始化(赋值)复杂度等成本。如果virtual base classes不带任何数据,将是最具有实用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相结合。