Effective C++ 读书笔记

文章目录

Effective C++ Read Log

前边6章的内容对刚开始用C++的人来说还是挺有意思的,但是第7-8章主要讲模板和元编程的话,通常对普通开发者意义没有那么大,因为平时也确实用不太上模板元编程:)。
同时这本书因为比较久远,在c++11出来之前就已经火了很多年了,里边有相当一部分内容是比较具有时代性的,但是在C++的软件开发思想上还是有一定理解的,值得一阅。

Chapter 1. Accustoming Yourself To C++

01. View C++ as a federation of languages

视C++为一个语言体系,主要是由C语言、Object-Oriented C++、Template C++、STL四个主要部分组成的体系

02. Prefer consts, enums, and inlines to #defines

尽量少用#define来定义变量

如果是单纯定义常量,最好用const变量或者enums来代替

如果是定义宏函数,最好是用inline函数(函数定义部分写在头文件里)来代替

03. Use const whenever possible

为成员函数提供const版本,并尽可能的让non-const版本在内部调用const版本,以减少代码量重复

尽可能将某些变量、成员函数、函数参数等声明为const,能帮助编译器识别错误用法

04. Make sure that objects are initialized before they’re used

构造函数最好是使用成员初始化列表,初始化的顺序最好按照类中的声明顺序排列

为内置类型变量手动初始化,C++编译器并不会保证初始化该类型对象

使用static关键词声明变量时,尽量在较小作用域中使用,以降低静态成员初始化顺序问题的影响

Chapter 2. Constructors, Destructors, And Assignment Operators

05. Know what functions C++ silently writes and calls

编译器会自动为未定义相关函数的class

提供默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数

06. Explicitly disallow the use of compiler-generated functions you do not want

明确需要禁止的成员函数

将该成员函数设置为delete

或者将访问权限声明为private

07. Declare destructions virtual in polymorphic base classes

如果某个类需要被继承,并且需要使用多态的特性

以防止析构时发生对象析构不充分,内存资源泄露的情况发生

必须要将其析构函数声明为virtual析构函数

08. Prevent exceptions from leaving destructors

不要在析构函数中throw任何异常,如果析构函数中调用的成员函数可能会抛出异常,那么尽量做异常处理,或者直接记录信息后吞掉异常

如果某些函数接口会抛出异常,那么应该提供成员函数接口来让客户手动执行该操作,而避免在析构过程中调用

09. Never call virtual function during construction or destruction

构造函数的执行顺序为基类->派生类,从上至下执行,因此在基类中若调用了virtual函数,那么只会执行当前基类版本的该函数

反之析构函数的执行顺序为派生类->基类,从下往上执行,因此在派生类中若调用了virtual函数,那么执行的是当前派生类版本的该函数

解决方案为,若必须在构造或析构函数中执行,切勿将该成员函数声明为virtual类型

10. Have assignment operators return references to *this

重载赋值运算符时,返回值为*this的引用,以支持连续运算操作

11. Handle assignment to self in operator=

重载赋值运算符时,应注意实参有可能为该对象本身,切记对该情况进行合理的处理

  A& A::operator=(cosnt A& a)
  {
    delete ptr;
    ptr = new Type(a.ptr);
    return *this;
  }
常见的问题形式如上,若实参为该对象本身,那么在delete操作调用之后,new操作并不能实现资源的拷贝
#+begin_src c++
  A& A::operator=(cosnt A& a)
  {
    if (&a == this) return *this;
    delete ptr;
    ptr = new Type(a.ptr);
    return *this;
  }
常见处理方法如上,这样通常可以满足要求,但是并不具备异常安全性,因为new操作符过程中可能会抛出异常
#+begin_src c++
  A& A::operator=(cosnt A& a)
  {
    if (&a == this) return *this;
    auto temp_ptr = new Type(a.ptr);
    delete ptr;
    ptr = temp_ptr
    return *this;
  }

最合理的处理方法如上,自我检测可加可不加,取决于发生自我赋值情况的频率

另外在重载赋值运算符时,若不需要对象深拷贝,或者参数为值传递的情况,可直接调用std::swap()函数进行对象内容的对调

12. Copy all parts of a object

重载赋值运算符时,切记对基类的成员部分也调用赋值运算符,方法为Base::operate=(param),以保证该对象能够完整拷贝

当拷贝构造和赋值运算符内部实现逻辑完全一样时,切忌互相调用,应声明第三个private函数,供前两者共同调用

Chapter 3. Resource Management

13. Use object to manage resources

为防止内存泄露,尽量在对象构造函数中获取资源,并且在析构函数中释放资源

若临时获取资源,应尽量使用智能指针std::shared_ptr来进行内存管理,以防止忘记delete导致内存泄露

14. Think carefully about copying behavior in resource-managing classes

复制RAII对象时必须一并复制其所管理的资源,因此该资源的复制、获取、释放等行为决定了RAII对象的相关行为

常见的RAII对象实现机制为,对内部资源进行值拷贝,并且实施引用计数方式进行管理

15. Provide access to raw resources in resource-managing classes

每一个RAII类应提供能够访问所管理内部资源的接口

接口类型可为显式函数调用,此方式较为繁琐,但比较安全

或可为隐式转换调用,通过重载A::operator Type()操作符,进行隐式转换,使用方便,但是可能出现难以预测的问题

16. Use the same form in corresponding uses of new and delete

new和delete成对使用时注意调用形式

比如new [] 与 delete []

17. Store newed objects in smart pointers in standalone statements

如果已经考虑使用智能指针,那么尽量考虑将资源获取和智能指针对象定义放在一起,否则若期间触发异常,则可能会造成内存泄漏

Chapter 4. Designs and Declarations

18. Make interfaces easy to use correctly and hard to use incorrectly

任何接口如果要求用户必须记得做某些事情,那么这个接口的设计或许就不够合理

尽量保持接口的一致性,以及对内置类型的行为兼容

在设计接口时,考虑通过建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任

尽可能使用智能指针作为接口的资源类型返回值

19. Treat class design as type design

在设计一个类的时候,考虑如下问题:

  • 新class的对象如何被创建和销毁?
  • 对象的初始化和对象的赋值应该有什么样的差别?
  • 新class的对象如果被passed by value,意味着什么?
  • 什么情况下新class是valid对象,什么情况下是invalid对象?
  • 新的class需要配合某个继承约束吗?比如virtual函数?
  • 新的class需要支持什么样的类型转换?
  • 新的class需要重载什么样的操作符,需要声明哪些public函数?
  • 新的class需要声明什么样的private函数?
  • 新的class需要为成员配置哪种访问权限?
  • 新的class是一个类型特例还是一个类型族中的一员?若是后者则更应该定义为class template
  • 是否确实需要一个新的class?

20. Prefer pass-by-reference-to-const to pass-by-value

自定义类型作为函数参数时尽量用const引用类型传递,而尽量少的使用值传递方式,因为会创建临时变量,导致多次调用构造和析构函数

对于内置类型而言,使用值传递会比const引用类型传递高效,因为引用的底层实现是传递指针,内置类型直接传值即可

21. Don’t try to return a reference when you must return ao object

函数返回某些对象时,切忌传递临时变量的引用和指针,或者内部成员变量的引用和指针,重载运算符除外

尽量避免返回函数作用域内的static变量,可能会产生意料之外的结果

22. Declare data members private

从封装性的角度出发,任何成员变量都应该声明为private权限,尽量少用或不用protected来类的成员变量与函数

23. Prefer non-member non-friend functions to member functions

在考虑为某个类提供功能函数时,考虑该函数是否直接属于该类型的特性,否则应尽可能的使用外部非成员函数来实现相关功能

也即类本身应该只提供基本操作,参考代理模式,由代理类来完成较高层次的功能函数

24. Declare non-member functions when type conversions should apply to all parameters

如果需要为某个函数的所有成员提供隐式类型转换,那么务必将该函数声明为外部非成员函数

若需要进行双目运算符重载,请务必将其声明为外部非成员函数

25. Consider support for a non-throwing swap

利用C++函数查找的就近就优匹配原则,实现std::swap和类内置swap函数的最优选

Chapter 5. Inplementations

26. Postpone variable definitions as long as possible

尽可能将变量的定义和使用放在一起,否则若先定义变量,在实际使用变量之前抛出异常,则无端浪费构造+析构的开销

在循环体内部进行临时变量的构造和析构,与在循环体外部进行构造析构,内部进行赋值操作,两者相比,需要看该对象的特性来定

27. Minimize casting

旧式类型转换:

  • (Type)value
  • Type(value)

C++提供的新式类型转换:

  • const_cast(expression) 增加或删除变量的常量属性
  • dynamic_cast(expression) 实现运行时多态的动态类型转换
  • reinterpret_cast(expression) 执行低级转型,危险性较高
  • static_cast(expression) 实行强制性隐式转换

如果可以,尽量避免使用任何形式的类型转换,特别是注重效率时避免使用dynamic_cast

如果非要进行类型转换,尽量使用C++提供的新式类型转换,尽量规避使用C型旧式转换

28. Avoid returning “Handles” to internal objects

尽量避免成员函数返回内部低级访问权限数据的引用,这样会导致封装性被降低

如果考虑成员函数返回内部成员的引用,则理应为返回值声明为const type func(para);

29. Strive for exception-safe code

满足异常安全性的函数需要满足以下条件:

  • 不能泄露任何资源
  • 不能破坏任何既有数据,如果有异常抛出,程序内的任何事物仍然保存有效状态,并未被修改或损坏
  • 保证程序一致性,如果函数调用成功,则完全成功,如果调用失败,则理应回退到调用前的状态

30. Understand the ins and outs of inlining

inline函数的生命和定义必须被放在头文件中

inline关键词只是申请使用inline形式进行编译,但是具体是否真正inline是由编译器做决定

谨记真正的inline函数无法被调试,程序出错时难以找出问题所在

31. Minimize compilation dependencies between files

对标准库或者第三方库等几乎不做修改的文件,直接#include即可

对自建类型等需要经常修改的class、struct,使用前置声明的形式,降低代码间依存性,提高修改代码后的编译效率

如果使用指针或者引用就能够声明接口,就不要直接使用对象

使用工厂模式以及代理模式,结合多态特性来降低代码间的耦合度,毕竟修改代码比单纯使用代码更加频繁

考虑提供只含有前置声明的头文件以供其他类使用,参考标准库中文件的实现形式,降低代码间依存性

  namespace std{
    class XXX;
    class XXX;
    class XXX;
    ......
  }

Chapter 6. Inheritance and Object-Oriented Design

32. Make sure public inheritance models “is-a”

切记,public继承意味着"is-a",适用于base class的所有操作,都可以应用在derived class身上

33. Avoid hiding inherited names

在继承基类时,派生类的某个成员函数可能会覆盖掉基类的同名方法,也即override

此时,若在派生类中想要使用基类的该方法,需要使用using声明,或者显式调用基类的某个方法base::func()

34. Differentiate between inheritance of interface and inheritance of implementation

C++的继承一般分为以下几种:

  • 纯虚基类(接口继承),派生类必须实现相关纯虚接口
  • 非纯虚函数(可选型实现继承),派生类可选择不继承虚函数,则会直接调用基类相关函数
  • 非虚函数(强制性实现继承),派生类直接继承相关成员函数,或重写覆盖

35. Consider alternatives to virtual functions

virtual函数的替代方案如下:

  • 使用non-virtual interface(NVI),Template Method模板方法中的一种特殊形式,在函数中间接调用class的其他函数
      class GameCharacter {
      public:
          int healthValue() const {
              int retVal = doHealthValue();
              return retVal;
          }
      private:
          virtual int doHealthValue() const {
              int ret = 0;
              return ret;
          }
      };
    

···

  • 将virtual函数替换为函数指针成员变量,属于Strategy设计模式的一种形式
    class GameCharacter;
    int defaultHealthCalc(const GameCharacter& gc);
    class GameCharacter {
    public:
	    typedef int (*HealthCalcFunc) (const GameCharacter&);
	    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
	        : healthFunc(hcf) {}
	    int healthValue() const {
	        return healthFunc(*this);
	    }
    private:
    	HealthCalcFunc healthFunc;
    };

同一个类的不同对象可以有不同的生命值计算函数,使得其可以在运行时进行修改

  • 用std::function<>成员变量替换virtual函数,
      class GameCharacter;
      int defaultHealthCalc(const GameCharacter& gc);
      class GameCharacter {
      public:
          typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
          explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
              : healthFunc(hcf) {}
          int healthValue() const {
              return healthFunc(*this);
      }
      private:
      	HealthCalcFunc healthFunc;
      };    
    
  • 将virtual函数替换为另一套继承体系中的virtual函数,属于传统Strategy设计模式的实现形式
      class GameCharacter;
      class HealthCalcFunc {
      public:
     	 	virtual int calc(const GameCharacter&) const { return 0; }
      };
      HealthCalcFunc defaultHealthCalc;
      class GameCharacter {
      public:
          explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
              : pHealthCalc(phcf) {}
          int healthValue() const { return pHealthCalc->calc(*this); }
      private:
      	HealthCalcFunc* pHealthCalc;
      };
    

virtual函数的替代方案包括NVI方法以及Strategy设计模式(策略模式)的多种形式,NVI方法本身是一种特殊的Template Method模式(模板方法模式)

将一些操作放在函数外部,声明为非成员函数是,带来的缺点就是无法访问class的私有成员

函数指针可用std::function<>模板库代替,提供了更高的灵活性

36. Never redefine an inherited non-virtual function

派生类重写覆盖父类的普通非虚函数,会导致多态无法按原有意愿做出反应

  class Base
  {
  public:
      void func(); // Base version
  };
  class Derived: public Base
  {
  public:
      void func(); // Derived version
  };
  Derived *pD = new Derived;
  Base *pB = pD;
  pD->func(); // Derived version;
  pB->func(); // Base version;

指向父类的指针,即便它其实本身是一个子类对象,当调用一个被子类重写了的非虚函数时,会出现父类指针或引用会调用父类的该函数
子类指针或引用会调用子类的该函数,多态的特性也就无法体现

37. Never redefine a function’s inherited default parameter value

永远不要重新定义父类函数参数的默认值,参考以下代码情景:

  class Shape {
      virtual void func(int a = 0);
  };
  class Rectangle: public Shape {
      virtual void func(int a = 1);
  };
  class Circle: public Shape {
      virtual void func(int a = 0);
  };

  Shape* pR = new Rectangle; // 静态类型为Shape*,动态类型为Rectangle
  Shape* pC = new Circle; // 静态类型为Shape*,动态类型为Circle
  pR->func() // a = 0; default a by Shape::func(int a = 0);
  pC->func() // a = 0; default a by Shape::func(int a = 0);

静态类型即为编译时类型,动态类型为运行时类型,因此,继承多态的应用主要是利用动态类型的特性

但是函数的默认参数值却依赖于静态类型,因此pR调用函数时若含有默认函数,则会使用Shape*也即静态类型的默认参数值

38. Model “has-a” or “is-implemented-in-terms-of” through composition

当无法通过继承来合理的利用现有的class时,尝试通过"has-a"或者"is-implemented-in-terms-of"的关系,
使用聚合模式来提供一些函数接口,这些接口具体是调用其内部成员的相关函数接口

39. Use private inheritance judiciously

private继承相当于隐去了父类的所有接口,无论是虚函数还是非虚函数
只保留了父类的成员变量,也即只继承了父类的实现部分,抛弃了接口部分

同时,private继承会使得多态被禁用,也即子类的指针或引用无法转换成父类

private继承意味着is-implemented-in-terms-of(根据某物实现出),当出现这种情况时,通常建议使用38条中的聚合模式来实现

不得不使用private继承的情况通常为极限内存需求情况:

  class Empty{};
  class HoldsInt: private Empty
  {
  private:
      int x;
  };

这样的继承形式,会通过EBO(empty base optimization: 空白基类最优化)来使得class HoldsInt的内存大小等于一个int的大小

当需要处理两个class的关系时,若其不存在任何的"is-a"关系,同时其中一个class需要访问另一个class的protected成员,
或者需要重新定义另一个class的某些virtual函数,或许可以尝试使用private继承形式

40. Use multiple inheritance judiciously

注意相同函数(非重载情况)的歧义冲突,注意菱形继承

采用虚继承方式解决菱形继承问题,但是要注意,虚继承会增加大小、降低速度、增加初始化等成本
因此非必要不使用虚继承,如果必须使用虚继承,尽量不要在基类中添加成员变量

多继承加继承权限的妙用:

  class IPerson {
  public:
      virtual ~IPerson();
      virtual std::string name() const = 0;
      virtual std::string birthDate() cosnt = 0;
  };
  class DatabaseID;
  class PersonInfo {
  public:
      explicit PersonInfo(DataBaseID pid);
      virtual ~PersonInfo();
      virtual const char* theName() const;
      virtual const char* theBirthDate() const;
      virtual const char* valueDelimOpen() cosnt;
      virtual const char* valueDelimClose() cosnt;
  };
  class CPerson: public IPerson, private PersonInfo {
  public:
      explicit CPerson(DatebaseID pid)
      : PersonInfo(pid) {}
      virtual std::string name() const {
      return PersonInfo::theName();
      }
      virtual std::string birthDate() const  {
      return PersonInfo::theBirthDate();
      }
  private:
      virtual const char* valueDelimOpen() cosnt { return ""; };
      virtual const char* valueDelimClose() cosnt { return ""; };
  };

class CPerson私有继承class PersonInfo,因此CPerson可以使用PersonInfo的大部分接口,同时也能够嫁接继承到class IPerson的成员函数

Chapter 7. Templates and Generic Programming

模板元编程这一部分平时比较难用得上,后边也看不太懂,没遇到过这种情况就很难理解

41. Understand implicit interfaces and compile-time polymorphism

普通class和templates都支持接口和多态,一个是运行时多态,一个是编译期多态

对class而言,接口是显式指定的,多态通过virtual声明,发生于运行时

对templates而言,接口一般是隐式指定的,基于函数内的实际表达式,多态通过template实例化和函数重载解析,发生于编译期

42. Understand the two meanings of typename

声明模板时,typename和class是等价的

  template<typename T>
  void func(T &t);
  template<class T>
  void func(T &t);

typename有其独特的用处,在模板内部,针对模板类型的嵌套类,需要前置使用typename声明其为某种类型

template<typename T>
  class Temp: public Base<T>
  {
  public:
      Temp();
      typename Base<T>::B b;
      typename T::A a;
  };

43. Know how to access names in templatized base classes

考虑如下模板类继承情况:

  template<typename Company>
  class LoggingMsgSender: public MsgSender<Company>
  {
  public:
      void sendClearMsg(const MsgInfo& info)
      {
          sendClear(info);    // Error
      }
  };

这段代码无法通过编译,因为编译器在这个类中无法找到sendClear()函数的信息,编译器在解析基类之前并不知道基类中是否含有此成员函数

解决办法有如下三种:

  template<typename Company>
  class LoggingMsgSender: public MsgSender<Company>
  {
  public:
      void sendClearMsg(const MsgInfo& info)
      {
          this->sendClear(info);    // 显式指定sendClear()函数是继承自基类的
      }
  };

  template<typename Company>
  class LoggingMsgSender: public MsgSender<Company>
  {
  public:
      using MsgSender<Company>::sendClear;
      void sendClearMsg(const MsgInfo& info)
      {
          sendClear(info);    // 假设sendClear()函数是继承自基类的
      }
  }template<typename Company>
  class LoggingMsgSender: public MsgSender<Company>
  {
  public:
      void sendClearMsg(const MsgInfo& info)
      {
          MsgSender<Company>::sendClear(info);    // 显式指定sendClear()函数是继承自基类的
      }
  };

44. Factor parameter-independent code out of templates

templates按照实际调用实例化自动生成多个class和多个函数,所以任何template代码都不应该与某个造成膨胀的template参数产生相依关系,
所谓膨胀就是代码膨胀,大量只有少数差异的重复代码

因非类型模板参数而造成的代码膨胀,往往可以消除,通过以函数参数或者class成员变量来替换template的特定参数

因为类型参数而造成的代码膨胀,往往可以降低,做法是让带有完全相同二进制表述的实例化类型共享二进制代码,但是不在代码中实现

45. Use member function templates to accept “all compatible types”

使用成员函数模板生成“可接受所有兼容类型”的函数

如果声明的成员函数模板是用于拷贝构造或者赋值运算符,那么还需要声明正常的拷贝构造和赋值运算符,否则编译器会生成默认的版本

46. Define non-member functions inside templates when type conversions are desired

在编写一个class template时,如果需要提供一个具备所有参数隐式类型转换的函数,则需要将该函数在类内声明和定义,并且声明为friend函数

  template<typename T> class Rational;
  template<typename T>
  const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);

  template<typename T>
  class Rational
  {
  public:
      friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
      {
          return doMultiply(lhs, rhs);
      }
  };

  template<typename T>
  const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
  {
      return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
  }

另外需要注意的是,涉及到模板的源代码,用.hpp文件存储声明和定义,不要拆开到.h和.cpp中

47. Use traits classes for information about types

48. Be aware of template metaprogramming

Chapter 8. Customizing New and Delete

49. Understand the behavior of the new-handler

50. Understand when it makes sense to replace new and delete

51. Adhere to convention when writing new and delete

52. Write placement delete if you write placement new

类内重载new和delete操作符的时候,会自动隐藏掉外部全局作用域的操作符,因此理应在类内提供全局作用域的new和delete版本

Chapter 9. Miscellany

53. Pay attention to compiler warnings

严肃对待编译器发出的警告信息,尽量使得自己的程序在编译过程中不要报出任何一个警告

但是也不要过度依赖编译器的警告能力,并不是所有的问题编译器都可以准确定位,并且不同的编译器提供的检测也不尽相同

54. Familiarize yourself with the standard library, including TR1

熟悉C++标准库模块,以及其他的第三方库模块,C++11之后似乎大部分第三方库比较出彩的功能都被整合到标准库了

55. Familiarize yourself with Boost

同上

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值