[关闭]
@zhengyuhong 2014-10-09T02:24:32.000000Z 字数 5586 阅读 1032

Effective C++

读书笔记 C++


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

C++是一个由相关语言组成的联邦,它支持过程、面向对象、泛型

  1. C语言的面向过程
  2. C++的面向对象:封装、继承、多态
  3. 泛型:模版编程,STL

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

  1. define不进行类型检查,在预编译过程进行替换
  2. define不重视作用域、生命期,譬如需要定义一个常量在类当中。define做不到,它会对整个源文件是可见的,直到遇到#undefined
  3. define不提供封装性,这也是C语言的特点,它本来就不提供封装性,所以属于C语言的define就没有封装性
  4. 对于类型函数的宏,最好用inline函数替换#define

条款3、尽可能使用const

const作用
1. 在类外部修饰全局变量或者命名空间中的变量
2. 修饰函数参数、返回值类型,区块作用域的static对象
3. 修饰类内部的成员变量,成员函数
const成员函数
  将const实施于成员函数的目的,是为了该成员函数可以作用于const对象,承诺不修改const对象、不修改const指针
  const、non-const成员函数避免重复,后者调用前者,并采用const_cast去常量性。(关于类型转换,安全的都可以隐式转换,不安全的需要显示,譬如去常量性const_cast,dynamic_cast向下转型)

条款4、确定对象在使用前已先被初始化

  在构造函数中对成员变量进行赋值,那不是初始化,那仅仅是伪初始化,初始化需要使用构造函数中的初始化列表,C++规定对象的成员变量的初始化动作发生在进入构造函数本体之前,初始化顺序与声明顺序一样。
  成员变量是const或者reference时,必须使用初始化列表
  跨编译单元的初始化顺序不能确定,运用单例模式以local static替换non-local static对象,具体详看@p31,这种手法是基于:C++保证,函数内中的local static 对象会在该函数第一次被调用时进行了初始化,并且保持记忆性,下一次调用直接返回,不必再初始化。

条款5、了解C++默认编写并调用哪一些函数

  1. class Empty{};
  2. 就好像写了如下:
  3. class Empty{
  4. public:
  5. Empty(){..};
  6. Empty(const Empty& rhs){...};
  7. ~Empty(){...}
  8. Empty& operator=(const Empty& rhs){...}
  9. };

  唯有当这些函数被需要时才会调用,如果整个源文件中都没有使用过拷贝函数或者赋值运算符,那么这两个函数C++编程器是不会实现的,因为它实现了,但是用户也没有用,浪费。
  当成员变量当中含有const、reference时,由于reference不能修改绑定对象,所以默认的赋值运算符是不可取的,C++编译器会禁止生成的,所以必须自行定义一个赋值运算符函数。

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

  为了驳回编译器自动提供的四个默认函数,可以将对象的成员函数声明为private并且不予以实现即可。继承Uncopyable这样的基类也是一种做法。

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

  1. 带有多态性的基类应该声明一个虚析构函数。如果类带有任何虚函数,那么它应该拥有一个虚析构函数
  2. 类的设计目的如果不是作为基类使用或者不是为了具备多态性,那么久不该声明虚析构函数,以免浪费内存空间。

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

条款9、绝对不在构造函数与析构函数中调用虚函数

  有一个观点是,在构造函数体中,仅仅是完成了基类的初始化(基类的初始化在初始化列表,进入构造函数体前已经完成了),派生类的初始化还没完成,所以当前对象只能算是基类的对象,调用的虚函数也只是对应的基类的函数,不均被多态性。同理,在析构函数中,派生类已经开始析构了,这也是已经不完全的派生类对象了,所以也是无法调用调用的虚函数的,只能算基类对象

条款10、令赋值操作符返回一个引用

条款11、在赋值运算符中处理自我赋值

  1. 需要添加“证同测试”来测试当前是否为自我赋值,若是自我赋值,直接返回
  2. 使用copy swap技术,先生成操作数的副本,然后再与this指针交换。

条款12、拷贝对象时勿忘其每一个成分

  如果用户自定义了自己的拷贝函数,意思告诉编译器不喜欢它提供的函数,编译器就会以儒学方式回敬:当自定义拷贝函数中有出错的情况,它却不会告诉你,譬如漏了拷贝某些成员变量
  派生类拷贝函数需要调用基类对应的拷贝函数对基类的成员变量进行初始化,这时候也是在初始化列表当中Base(rhs),rhs是派生类对象,这时候会自动向上类型转换,这是安全的转换,简称切割。
  同理,在赋值运算符时也需要调用基类的赋值运算符对基类部分成员变量进行赋值

条款13、以对象管理资源

  1. void foo(){
  2. int* p = new int[100];
  3. ...//假如在此处出现了异常,那么已经提早返回,导致后面的的delete p无法执行,造成内存泄露
  4. delete p;
  5. }

解决方法是:
1. 获得资源后立刻放入管理对象内譬如:

  1. std::auto_ptr<int> p(new int[100]);

当离开当前作用域时,管理对象(RAII)自动释放资源,这样子就不用担心异常中断了delete p
2. 管理对象是运用在离开作用域时调用析构函数的策略来释放资源的。
  除了auto_ptr,还有share_ptr,后者是一个引用计数型智慧指针,记录由多少个对象指向某笔资源,并在无对象指向资源时释放资源。

条款14、在资源管理类中小心拷贝行为

当一个RAII对象被复制时,会发生什么事?

  1. 禁止复制,许多RAII对象被复制时不合理的,将拷贝函数声明为private
  2. 对底层资源使用引用计数法,就像share_ptr
  3. 复制底层资源,深层拷贝,让新的RAII管理一份资源
  4. 转移底层资源所有权,就像auto_ptr

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

为了顾及原来的接口,所以必须提供管理对象内中的原始指针或者提供类型转换函数譬如get**(),或者隐式转换函数,这是一个成员函数

  1. operator 目标类型(){
  2. return 目标类型对象
  3. }

我个人比较认同显式转换,就是使用一个get函数获得指向资源的指针,误用概率更低。

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

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

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

譬如工厂函数不要返回原始指针,应该返回一个智能指针,如此一来资源释放的任务由智能指针完成,不必担心用delete还是free或者忘记使用delete或者free。

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

条款20、宁以const引用参数代替引用参数

以引用方式传递基类接口时可以避免切割

  1. void f(base& b){
  2. ...
  3. }
  4. derived d;
  5. f(d);//不会产生切割对象

为了防止对象被修改,可以使用const引用

  1. void f(const base& b){
  2. ...
  3. }

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

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

提高封装性,用户访问数据的一致性

条款23、宁以non-member、non-friend替换member函数

成员变量应该是private,否则有无限函数可以访问它,无封装性可言
以non-member、non-friend替换member函数提高封装性、包裹弹性、机能扩展性

条款24、若所有参数皆需要类型转换,请为此采用non-member函数

譬如一个有理数类:

  1. class Rational{
  2. public:
  3. Rational(int numerator = 0, int denominator = 1);
  4. int getNumerator()const;
  5. int getDenominator()const;
  6. const Rational operator+(const Rational& rhs)const;
  7. private:
  8. int _numerator,_denominator;
  9. }
  10. Rational oneHalf(1,2);
  11. Rational one(2*oneHalf);//错误

解决方案:在Rational的命名空间下定义一个non-member函数

  1. Rational operator=(int lhs, const Rational& rhs);

如此以来Rational one(2*oneHalf);就可以查找到相应的函数了

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

  1. 当std::swap对自定义类型效率不高时,提供一个public swap成员函数,并确定这个成员函数不抛出异常
  2. 如果提供了一个member,也该提供一个同命名空间下non-member的swap来调用前者。对于自定义类型,可以特化std::swap
  3. 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何命名空间修饰 参考@p108
  4. 千万不要尝试在std内添加某些对于std是全新的东西,就是不能违背一个常理吧。

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

如此可以增加源代码的清晰度并且改善效率

条款27、尽量少做转型动作

  1. 尽量少转型,
  2. 如必须,使用显式示转型函数,如get**()显式调用转型函数
  3. 使用新式转型,static_cast,const_cast,dynamic_cast,...

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

像智能指针为了兼容旧接口,有必要返回指针内部资源的接口,其他尽量不能返回内部接口,因为如此就是破坏了封装性

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

条款30、右侧了解inlining的里里外外

记住,inline只是对编译器的一个申请,不是强制命令
在类内部定义的函数体也只是一个隐喻的inline申请,不属于强制命令
在类外部定义的函数体有inline声明就是一个显式inline申请。

条款31、将文件的编译依存关系降到最低

继承与面向对象

虚函数意味着接口必须被继承,(函数声明就是接口,实现就是函数定义),当然虚函数的实现也可以被继承,但是不强制
非虚函数意味着接口与实现必须被继承

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

  1. public继承意味着is-a,适用于基类的每一个事情概念都应该适合与派生类,因为每一个派生类是基类的一个特殊情况

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

  1. 派生类里的名称会遮掩基类同名的成员变量、成员函数。在public继承体系下没有人希望如此
  2. 为了让被遮掩的名称重见天日,可以使用using声明式,或者使用转交函数,具体看@p160

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

纯虚函数只指定了具体的接口(函数声明),但是没有具体实现(函数定义)

  1. class Airplane{
  2. public:
  3. vidtual void fly(const Airport& destination) = 0;
  4. }

简朴的纯虚函数还提供了一个实现

  1. class Airplane{
  2. public:
  3. virtual void fly(const Airport& destination) = 0;
  4. }
  5. void Airplane::fly(){
  6. ...;
  7. }
  8. Airplane plane;//错误,有纯虚函数,不能实例化

派生类使用缺省的fly

  1. class Aplane:public Airplane{
  2. public:
  3. virtual void fly(const Airport& destination){
  4. Airplane::fly(destination);//显式使用缺省行为
  5. }
  6. }
  7. //纯虚函数仅仅继承接口,即便有实现也不会继承的。

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

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

在前面也说到了,在派生类重新定义域基类同名成员函数会遮掩基类的成员函数,这是public继承所不希望看到的,尽量不能重新定义非虚函数。
如果需要重新定义一个函数,那么证明了派生类的行为与基类的行为不一样,所以需要重新定义,那么使用一个虚函数也是一个不错的选择,还可以使用多态性。当然如果不需要使用多态性的话那么考虑重新定义一个非虚函数还是可以考虑一下,没有太绝对的必然。

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

虚函数是动态绑定的,而缺省函数时静态绑定的,那么就在编译时已经根据指针或者引用的类型绑定了缺省参数了,所以多态调用时的默认参数就是跟着指针或者引用的,那就是根据基类的参数了,那么跟实际不相符,所以不应该重新定义缺省参数值。

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

复合的关系还是比较容易理解,就是类中包含了一个成员变量
至于根据某物实现出这个通常是通过private继承,然后在类内部调用了private基类的接口(就是public函数)
尽量使用复合,必要时才使用private继承

条款39、明智而谨慎地使用private继承

  1. private继承意味着“根据某物实现出”,它通常比复合的级别低,但当派生类需要访问protected base class的成员或者需要重新定义继承而来的virtual函数时,这时候使用private继承
  2. 和复合不同,private继承可以造成empty base最小化,这对致力于对象尺寸最小化的程序可开发者而言可能很重要。
  3. class derived: private base{},将派生类对象赋值给base类型不会进行切割,它们不是一个is-a关系,只有public继承才会在赋值时切割

条款40、明智而谨慎地使用多重继承

个人认为多重继承就是在继承接口方面有很大作用,在其他方面,我没有太多深入理解,特别涉及虚继承的时候,比较麻烦,这些内容在《深入探索C++对象模型》中有详细讲解。

条款41~条款55关于模版、泛型、元编程,没有深入理解,以后理解深刻了再写读书笔记

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注