Effective C++ 中文版

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

  1. 对于单纯常量,最好以const对象或enums替换#defines

  2. 对于形似函数的宏(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出现在星号左侧,表示被指物是常量;如果出现在星号右侧,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

  1. 当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”的两相结合。

7、模板与泛型编程

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值