effecitve c++ 笔记

Ch-1

Item 1: view C++ as a federation of languages

c++继承了c的部分,增加了面向对象的设计,增加了模板,以及标准模板库

这四个部分可以看做四种子语言,相对独立

Item 2: Prefer consts, enums, and inlines to #defines

  • 使用const变量替换掉#define,后者只是做简单替换,对编译器透明,可能会导致生成重复对象代码(obj文件?)
  • 相对于双重const的指针的char*字符串,更推荐使用const限定的string对象
  • 类成员常量(部分如int, char, bool等内置类型)如果不被取址,可以只声明,不定义

    1
    2
    3
    4
    5
    
      class GamePlayer {
      private:
          static const int NumTurns = 5; // constant declaration
          int scores[NumTurns]; // use of constant
      };

    如果需要取址,需要在实现文件中定义:

    1
    
      const int GamePlayer::NumTruns;

    除此之外,可以使用枚举变量替换常量,以兼容一些老的编译器,以支持用作数组声明:

    1
    2
    3
    4
    5
    6
    
      class GamePlayer {
      private:
          enum { NumTurns = 5 }; // "the enum hack" — makes
          // NumTurns a symbolic name for 5
          int scores[NumTurns]; // fine
      };
  • 使用内联模板函数替代#define实现的宏函数,以降低复杂度

Item 3: Use const whenever possible

  • 指针*之前的cosnt限定指向的数据,*之后的const限定指针变量本身
  • 迭代器的const等价于T* const,而const_iterator等价于const* T

    1
    2
    3
    4
    5
    6
    7
    
      const std::vector<int>::iterator iter = vec.begin();
      *iter = 10; // OK, changes what iter points to
      ++iter; // error! iter is const
    
      std::vector<int>::const_iterator cIter = vec.begin();
      *cIter = 10; // error! *cIter is const
      ++cIter; // fine, changes cIter
    

const 成员函数

  • bitwise constness(physical constness)含义是成员函数不应该修改对象的数据成员
  • logical constness在不违背逻辑的情况下,可以修改部分数据成员(使用mutable修饰)

    这两种constness是我们理解层面的概念,c++的const修饰符保证了大部分情况bitwise;如果要实现logical constness,需要使用mutable修饰符,让对应变量可以在const函数中被修改

    在一些不好的实现中,使用const修饰符还是不能保证bitwise:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
      class CTextBlock {
      public:
          char& operator[](std::size_t position) const 
          {
              // inappropriate (but bitwise const) 
              // declaration of operator[]
              return pText[position];
          }
    
      private:
          char *pText;
      };
    
      const CTextBlock cctb("Hello");
      char *pc = &cctb[0];
      *pc = 'J';

避免在const和非const成员函数中的代码重复

很多时候我们需要分别实现const和non-const版本的成员函数,这里面可能包含很多重复代码(比如debug相关代码、日志代码、数据完整性验证)

通常情况下我们可以在non-const函数中调用const函数,反过来不行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  class TextBlock {
  public:

  const char& operator[](std::size_t position) const // same as before
  {
      return text[position];
  }
  char& operator[](std::size_t position) // now just calls const op[]
  {
      return 
          const_cast<char&>( 
                            // cast away const on
                            // op[]'s return type;
                            static_cast<const TextBlock&>(*this)[position]
                            // add const to *this's type;
                            // call const version of op[]
                             );
  }

Item 4: Make sure that objects are initialized before they're used

  • 为简单处理,所有的变量都需要初始化,优先使用构造函数列表来初始化成员函数(老的编译器需要程序员保证顺序)
  • 函数内的static变量可以当做单例使用

Ch-2

Item 5: Know what functions C++ silentlywrites and calls

如果没有声明,编译器会隐式生成构造、拷贝构造和析构函数。但如果遇到一些意外情况,编译器会报错:

  • 成员变量是引用或者带有const修饰,不会生成拷贝构造函数
  • 基类拷贝构造函数被声明为private
  • 等等

Item 6: Explicitly disallow the use of compiler-generated functions you do not want

如果不希望编译器自动生成部分函数,可以在private下面声明他们,然后不实现

特别的,如果一个类不希望被拷贝,可以考虑私有继承 Uncopyable 类:

1
2
3
4
5
6
7
8
  class Uncopyable {
  protected: // allow construction
      Uncopyable() {} // and destruction of
      ~Uncopyable() {} // derived objects...
  private:
      Uncopyable(const Uncopyable&); // ...but prevent copying
      Uncopyable& operator=(const Uncopyable&);
  };

Item 7: Declare destructors virtual in polymorphic base classes

  • 需要用作多态的类型的类需要虚析构函数
  • 不用于派生的类不建议使用虚析构函数,一方面增加了一个虚表的开销,另一方面数据结构和c等外部语言不兼容

Item 8: Prevent exceptions from leaving destructors

  • 析构函数不应该异常,如果发生了,要么终止程序,要么吞掉所以异常
  • 某些可能抛异常的清理操作尽量放在独立的函数(类似于close或者destroy这样的),由外部调用并处理异常

Item 9: Never call virtual functions during construction or destruction

对象构造和析构的时候调用虚函数会存在无定义行为,通常这种需求可以转换为在构造时传递参数或者使用对应类的静态构建函数

Item 10: Have assignment operators return a reference to *this

赋值操作符最好返回*this(所有内置类型都遵守这个惯例),连等的操作就是通过这样实现的

Item 11: Handle assignment to self in operator=

赋值操作符需要考虑到自我赋值的情况,一般来说构建一个临时变量,然后swap

也可以做分支判断,如果是自己就直接返回。但一般这种操作是低频的,而且会带来取址、缓存命中和管线的开销。

Item 12: Copy all parts of an object

拷贝函数包括构造拷贝函数和赋值操作符,如果要自己实现拷贝函数,需要覆盖掉所有可能的情况(特别是在后面增加成员的时候),编译器是不会报错的

两个拷贝函数无法相互调用,要实现代码复用,只能在增加一个private的公共代码函数

Ch-3 Resource Management

资源就是一种一旦你使用了,就需要返回(给系统)的东西。如果不还,就会出问题

Item 13: Use objects to manage resources.

通常从API申请到资源之后,返回的是指针(或者句柄),例如:

1
2
3
4
5
6
  void f()
  {
      Investment *pInv = createInvestment(); // call factory function
      // use pInv
      delete pInv; // release object
  }

函数f中第一行申请了pInv,最后一行释放了pInv。在中间的使用过程中,如果出现了异常,最后一行可能执行不到,一个比较好的办法就是把pInv放到局部对象来管理,在栈清理时候自动调用对象析构释放资源,auto_ptr就是这个作用:

1
2
3
4
5
6
7
  void f()
  {
      std::auto_ptr<Investment> pInv(createInvestment()); // call factory
      // function
      ... // use pInv as
      // before
  }

由于auto_ptr在拷贝时候的语义很奇怪,后面被废弃了,而是使用unique_ptr替代,unique_ptr不允许拷贝,只允许使用std::move显式转移,并且支持deleter和数组

这种资源管理方式经常叫做RAII(Resource Acquisition Is Initialization)

unique_ptr独占一个资源,如果要共享,可以使用shared_ptr。shared_ptr使用引用计数来维护声明周期,在引用计数降为0时,调用deleter

引用计数会带来循环引用的问题,所以可以考虑使用弱引用(weak_ptr)来解决,weak_ptr在使用时必须先转为shared_ptr

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

一般来说涉及资源管理拷贝的情况分为四种:

  • 禁止拷贝 比如说锁,拿到一把锁之后,极少可能性会把锁的资源管理类进行复制:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      class Lock {
      public:
      explicit Lock(Mutex *pm)
      : mutexPtr(pm)
          { lock(mutexPtr); } // acquire resource
          ~Lock() { unlock(mutexPtr); } // release resource
      private:
          Mutex *mutexPtr;
      };
  • 对下层资源计算引用计数 当拷贝资源管理类时,增加一个引用计数,类似于shared_ptr。当引用计数降为0时,调用deleter做清理操作

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
      class Lock {
      public:
          explicit Lock(Mutex *pm) // init shared_ptr with the Mutex
              : mutexPtr(pm, unlock) // to point to and the unlock func
          { // as the deleter
              lock(mutexPtr.get()); // see Item 15 for info on "get"
          }
      private:
          std::tr1::shared_ptr<Mutex> mutexPtr; // use shared_ptr
      }; // instead of raw pointer
    
  • 拷贝下层资源 如果希望复制多份资源时候,就需要拷贝下层资源,各个资源管理类需要自行清理掉自己管理的资源副本,简称“深拷贝”,std::string就是这样做的
  • 转移下层资源的拥有权 类似于auto_ptr或者unique_ptr这样的独占管理类

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

资源管理类都会提供API访问下层资源,c++支持显式或者隐式,前者安全,后者方便:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  class Font {
  public:
      operator FontHandle() const { return f; } // implicit conversion function
  private:
      FontHandle f; // the raw font resource

  };

  Font f1(getFont());

  FontHandle f2 = f1; // oops! meant to copy a Font
  // object, but instead implicitly
  // converted f1 into its underlying
  // FontHandle, then copied that

f1被清理的时候,会释放掉底层资源,也就是f2会变成野指针

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

new/delete和new []/delete []成对使用

Item 17: Store newed objects in smart pointers in standalone statements.

1
  processWidget(std::shared_ptr<Widget>(new Widget), priority());

以上代码可能会出现内存泄露的风险:

  1. 执行new Widget
  2. 调用priority()
  3. 调用std::shared_ptr

如果在第二步出现异常,会导致第一步申请的资源得不到释放。其原因还是在于c++没有定义代码执行的顺序,保险的方案是把资源申请独立出来:

1
2
3
4
  std::shared_ptr<Widget> pw(new Widget); // store newed object
  // in a smart pointer in a
  // standalone statement
  processWidget(pw, priority()); // this call won't leak

Ch-4 Designs and Declarations

本章主要讲述c++类型设计和声明

Item 18: Make interfaces easy to use correctly and hard to use incorrectly

  • 接口使用专用类型,而不是简单的int等内置类型

    1
    2
    3
    4
    5
    
      class Date {
      public:
          Date(int month, int day, int year);
          ...
      };
    1
    2
    3
    4
    5
    6
    7
    8
    
      class Date {
      public:
          Date(const Month& m, const Day& d, const Year& y);
          ...
      };
      Date d(30, 3, 1995); // error! wrong types
      Date d(Day(30), Month(3), Year(1995)); // error! wrong types
      Date d(Month(3), Day(30), Year(1995)); // okay, types are correct
    
  • shared_ptr之类的deleter可以避免跨dll时new/delete不一致导致的问题
  • factory创建对象时候,直接返回智能指针而不是裸指针可以减少用户手动RAII的工作量

Item 19: Treat class design as type design

类的设计相当于类型设计,需要考虑到一下一些因素:

对象是如何创建和删除

这将影响到构造和析构函数的设计,也影响到内存分配的选择

如何区分对象的初始化和赋值

这个区别实际上就是构造函数和拷贝函数的区别

设计的对象按值传递时有什么含义

拷贝构造函数决定了按值传递的实现

类型对值的合法性作何限定

数据成员的值组合,只有一部分是合法的,这些组合决定了类型需要维护的不变量,而这些不变量决定了怎样做错误检查,并影响异常的实现

类型是否适合继承图

如果继承于已有的类,这些类会有一些束缚,例如它们的函数是否虚化?如果允许别的类继承自己的类,自身的函数是否要虚化,尤其是析构函数?

类型能够做哪些类型转换

能否转换?隐式的?显式的?

类型支持哪些操作符

+,-,*,/……

哪些标准函数需要禁用

声明为private并只做声明不实现

谁能够访问类型的成员

这将决定public、protected和private的使用

类型有哪些隐性接口

例如提供了哪些保证(guarantees),包括对性能、异常安全以及资源使用(锁、动态内存分配),这些保证将是在实现时候的约束

通用性怎么样

是否考虑用模板

真的需要一个新的类型

也许只需要定义成非成员函数或者模板就能实现?

Item 20: Prefer pass-by-reference-to-const to pass-by-value

  • 自定义类型即便很小,也建议传const引用,因为自定义变量很可能随着时间有变化
  • 传引用(指针)一定能够放到寄存器的,而其他类型不一定了(看编译器)
  • 对于内置类型、STL迭代器和函数对象类型,传值更合适

Item 21: Don't try to return a reference when you must return an object

避免返回栈上对象的引用、函数静态成员以及new出来的对象

Item 22: Declare data members private

  • unencapsulated means unchangeable
  • protected成员也是违背封装的,跟public一样

Item 23: Prefer non-member non-friend functions to member functions

  • 非友元并且非成员函数不会增加访问私有变量的接口,提高了封装性,注意一定是非友元并且非成员
  • 这种函数一般都属于 convenience functions ,即便没有这些函数,用户可以组合成员函数来实现功能
  • 对c++来说,这样的函数往往可以写到命名空间里面,然后按功能放在不同的头文件里面,提供了强大的可扩展性。很多标准库就是这样组织的,使用成员函数无法实现这种组织方式

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

1
2
3
4
5
6
  const Rational operator*(const Rational& lhs, // now a non-member
                           const Rational& rhs) // function
  {
      return Rational(lhs.numerator() * rhs.numerator(),
                      lhs.denominator() * rhs.denominator());
  }

如果*操作符作为Raitonal的成员函数,第一个参数就定死为this了,无法处理int * Ratinal的情况,除此之外,不必是友元函数。

Item 25: Consider support for a non-throwing swap

要实现一个自定义swap,三步:

  1. 写一个交换数据用的成员函数
  2. 在当前同一个命名空间里面提供一个非成员版的swap函数
  3. 如果swap一个类(非类模板),特化std::swap,让它调用已实现的swap成员函数

需要注意swap不能抛异常

ch-5 Implementations

Item 26: Postpone variable definitions as long as possible

一些局部变量放到需要的时候才初始化,避免不必要的构造开销

循环变量的定义位置跟构造函数和赋值操作的性能相关,如果构造的开销小于赋值,则放在里面

Item 27: Minimize casting

几种cast:

  1. const_cast 用来去掉const属性,其他cast都不行
  2. dynamic_cast 一般用来将基类安全地转换为派生类,有较大的运行时开销
  3. reinterpret_cast 下层转换,常用来把指针转为int,高级代码尽量不要用
  4. static_cast 用于强制隐式转换

旧的转换方案也是可以用的,一般用于函数传参。新的cast一方面方便grep查找,另一方面便于编译器优化

cast方案可能会被误用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  class Window { // base class
  public:
      virtual void onResize() { ... } // base onResize impl
      ...
  };

  class SpecialWindow: public Window
  {
  public:
      virtual void onResize() {
          static_cast<Window>(*this).onResize(); // doesn't work!
          ...
      }
      ...
  };

这里使用static_cast实际上创建了一个临时对象,复制了一份*this,正确方法应该是:

1
2
3
4
5
6
7
8
  class SpecialWindow: public Window {
  public:
      virtual void onResize() {
          Window::onResize(); // call Window::onResize
          ... // on *this
      }
      ...
  };

如果这里用指针,会有无限递归的问题

dynamic_cast可以通过容器或者虚函数来避免,一定不要写出级联的dynamic_cast代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
  VPW winPtrs;

  for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
  {
      if (SpecialWindow1 *psw1 = 
          dynamic_cast<SpecialWindow1*>(iter->get())) { ... }
      else if (SpecialWindow2 *psw2 =
               dynamic_cast<SpecialWindow2*>(iter->get())) { ... }
      else if (SpecialWindow3 *psw3 =
               dynamic_cast<SpecialWindow3*>(iter->get())) { ... }
   }
  • 尽量避免转换,特别是 dynamic_cast
  • 如果一定要做转换,尽量隐藏到函数中,而不是让客户代码自己做转换
  • 优先使用c++风格的转换,更加明确

Item 28: Avoid returning "handles" to object internals

不要返回任何对象内部的句柄(引用、指针或者迭代器),一方面破坏封装,另一方面生命周期不可预期。

Item29: Strive for exception-safe code.

尽量写异常安全代码,异常安全有三个级别:

  • the basic guarantee: 如果发生异常,程序还是一个合法的状态,但是状态不可预期
  • the strong guarantee: 完全成功或者完全失败,类似于原子操作
  • the nothrow guarantee: 永不抛异常

异常安全代码至少要保证以上至少一项,难度依次递增

the strong guarantee 函数通常可以使用 copy-and-swap 的方式来实现

在写异常安全函数中,如果调用了其他函数,一般要求安全等级不低于当前函数,不然很难保证安全。

如果函数调用了很多不同异常安全等级的子函数,该函数的安全等级不会高于最低的子函数

Item 30: Understand the ins and outs of inlining.

只在真正有需要的地方,对函数进行内联,避免滥用

inline函数一旦发布出去,后期有修改的话,所有调用到该函数的代码都需要重新编译

Item31: Minimize compilation dependencies between files.

默认c++类对接口和实现的分离做得并不好

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  class Person {
  public:
      Person(const std::string& name, const Date& birthday,
             const Address& addr);
      std::string name() const;
      std::string birthDate() const;
      std::string address() const;

  private:
      std::string theName; // implementation detail
      Date theBirthDate; // implementation detail
      Address theAddress; // implementation detail
  };

这几个私有成员往往会要求include自己的头文件,比如 <string>, date.h, address.h 之类的, 一旦这些头文件有改动,Person类以及使用Person类的文件都会重编译

当然可以用 forward declaration 的方式来避免include头文件,但是会带来两个问题:

  1. string是一个typedef,不能预先声明
  2. 如果要定义,会涉及分配空间的问题,编译器不能决定该分配多少空间

解决这个问题的方法就是把Person写成句柄类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  #include <string> // standard library components shouldn't be forward-declared
  #include <memory> // for tr1::shared_ptr; see below

  class PersonImpl; // forward decl of Person impl. class
  class Date;       // forward decls of classes used in
  class Address;    // Person interface

  class Person {
  public:
      Person(const std::string& name, const Date& birthday,
             const Address& addr);
      std::string name() const;
      std::string birthDate() const;
      std::string address() const;

  private:
      std::tr1::shared_ptr<PersonImpl> pImpl;
 
  };

在PersonImpl类里面做真正的实现,接口一一对应。

实现分离的关键在于将对定义的依赖,替换为对声明的依赖。可能的话,尽量让头文件自我依赖;如果做不到,也只依赖于其他文件的声明,而不是定义

遵循以下原则:

  • 在可以用指针或者引用的情况下,尽量避免直接使用对象
  • 只要可能,依赖类的声明而不是类的定义 声明一个函数是不需要定义的:

    1
    2
    3
    4
    
      class Date;
    
      Date today();
      void clearAppointmets(Date d);

    以上Date未定义,但是依然是合法的,只有在真正需要的地方,再include必要的定义

  • 对定义和声明提供分开的两份头文件,两份文件保持一致 使用库的客户需要include声明文件,而不是手动 forward declare

另一种方法,是让Person成为一个抽象基类,也就是接口类。相对于.net或者java,c++的接口类可以支持实现一些非虚函数,以便所有继承类能够使用

客户可以调用静态 factory 函数来构建Person子类,并返回指针。

句柄类和接口类都解决了声明和定义的耦合问题,代价就是多一级访问的开销,这两种方法都不能使用inline(inline会把头文件的代码插入到所有调用的地方)

Ch-6. Inheritance and Object-Oriented Design

Item 32: Make sure public inheritance models "is-a."

public继承最关键的一点: is-a

一些生活中的概念并不能直接建模,它们并不满足 is-a 的原则:企鹅不能公有继承鸟(鸟会飞),正方形不能公有继承长方形(长方形允许调整单边长度)

public继承要求,任何基类的方法,都能够无歧义地应用到派生类

Item 33: Avoid hiding inherited names

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  class Base {
  private:
      int x;
  public:
      virtual void mf1() = 0;
      virtual void mf1(int);
      virtual void mf2();
      void mf3();
      void mf3(double);

  };

  class Derived: public Base {
  public:
      virtual void mf1();
      void mf3();
      void mf4();
  };

上面代码中,派生类的mf1和mf3名称会隐藏掉基类的名称,以下代码是编译不过的:

1
2
3
4
  Derived d;

  d.mf1(10);
  d.mf4(1.0);

即便不用不合法的调用,如果不继承基类的重载函数,也是违背了public继承的 is-a 原则。解决办法就是在public下面使用using Base::mf1和using Base::mf3将对应的名称暴露出来。

1
2
3
4
5
6
7
8
  class Derived: public Base {
  public:
      using Base::mf1; // make all things in Base named mf1 and mf3
      using Base::mf3; // visible (and public) in Derived's scope
      virtual void mf1();
      void mf3();
      void mf4();
  };

但是在私有继承的时候,可能需要暴露一部分基类的名称到public里面,如果用using,会导致所有的基类名称都暴露出去了,这里可以考虑forwarding function

1
2
3
4
5
6
  class Derived: private Base {
  public:
      virtual void mf1() // forwarding function; implicitly
      { Base::mf1(); } // inline (see Item 30)

  };

PS:using是受public和private的限制的

Item 34: Differentiate between inheritance of interface and inheritance of implementation

1
2
3
4
5
6
  class Shape {
  public:
      virtual void draw() const = 0;
      virtual void error(const std::string& msg);
      int objectID() const;
  };

一般来说,继承包含三种,如上面代码:

  1. 继承接口 基类的纯虚函数,必须由派生类实现,纯虚函数只用来指定接口。 此时基类也可以写一个同名的默认实现(不会被继承,也无法被默认使用),派生类调用Base::xxx()来使用,这样要求派生类显式调用,可以避免增加一些有变化的派生类时,忘了重写默认函数
  2. 继承实现 基类的普通虚函数,派生类继承接口和一个默认的实现
  3. 非虚函数 指定了接口,并且带上一个强制不变的实现

Item 35: Consider alternatives to virtual functions

虚函数有很多替代的方案

  1. non-vertual interface idiom (NVI idiom) public接口使用非虚函数实现,通过调用真正有功能的虚函数来实现多态,相当于wrapper。 可以为private,派生类虽然无法调用这些虚函数(private),但是可以重载,这里不冲突。重定义一个虚函数决定了 how something is to be done ,而调用一个虚函数,决定了 when it will be done ,分开来看 这种方案只是 Template Method 设计模式的一个例子
  2. function pointer data members 使用函数指针数据成员替代虚函数。在类构造时候,传入函数指针,并用这些函数实现接口功能。缺点在于这些函数只能访问公共接口,而不能直接访问数据成员
  3. tr1::function data members 相对于函数指针,提供更加灵活的动态实现
  4. virtual functions in another hierarchy (Strategy Pattern) 将两个继承体系进行组合

Item 36: Never redefine an inherited non-virtual function

如题,违反了公有继承的原则

Item 37: Never redefine a function's inherited default parameter value

虚函数是动态绑定,但是默认参数却是静态绑定。在调用虚函数时,会根据当前指针类型决定默认参数。

要保证接口的一致性,所有派生类在重定义的时候,需要指定同样的默认参数,但是这样在改的时候很容易改漏掉部分函数。解决办法是使用NVI,用非虚函数实现接口,指定默认参数,然后在虚函数中实现功能,虚函数不设默认参数

Item 38: Model "has-a" or "is-implemented-in-terms-of" through composition

复合(composition)是类型之间的一种关系,是一种完全不同于公有继承的建模

人、交通工具、视频帧这样的属于应用领域(application domain),复合意味着"has-a"的关系。

缓冲区、互斥锁、搜索树这样的属于软件的实现领域(implementation domain),符合意味着 is-implemented-in-terms-of 关系

例如一个Person类,它包含了名字、地址、电话号码等,这既是 has-a 的关系:

1
2
3
4
5
6
7
8
9
  class Person {
  public:
      ...
  private:
      std::string name; // composed object
      Address address; // ditto
      PhoneNumber voiceNumber; // ditto
      PhoneNumber faxNumber; // ditto
  };

又比如用list实现的set模板类,可以说set是根据list实现而出的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  template<class T> // the right way to use list for Set
  class Set {
  public:
      bool member(const T& item) const;
      void insert(const T& item);
      void remove(const T& item);
      std::size_t size() const;
  private:
      std::list<T> rep; // representation for Set data
  };

Item 39: Use private inheritance judiciously

私有继承意味着 is-implemented-in-terms-of ,只有在软件实现中有意义,而在软件设计中并没有。私有继承往往可以使用 item 38 中的composition来取代:

  • 私有继承允许派生类重写基类的私有函数(即便不能访问),而composition却是通过私有变量来实现的,派生类无法重载
  • 私有继承可能会include必要的实现头文件,会导致增加编译时间,composition却可以只声明,并在实现中使用

私有继承唯一值得使用的情况是,继承一个没有数据成员(包括虚函数)的基类,可以省掉一个最小内存空间,而这是composition做不到的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  class Empty {}; // has no data, so objects should
  // use no memory
  class HoldsAnInt { // should need only space for an int
  private:
      int x;
      Empty e; // should require no memory
  };

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

第一种方式使用composition,sizeof(HoldsAnInt) > sizeof(int); 而第二种使用私有继承,借助于 empty base optimization (EBO)优化策略 ,sizeof(HoldsAnInt) = sizeof(int)

Item 40: Use multiple inheritance judiciously

多重继承可能存在命名冲突,这时候需要带上对应类型的名字,例如mp.BorrowableItem::checkOut();

出现菱形继承的时候,通常使用虚基类来避免基类重复,但是会带来开销,所以尽量不要用虚基类,平常使用非虚继承就好了。如果一定要用到虚基类,尽量避免在其中放入数据,这样就不会遇到初始化、赋值时候诡异的问题了

多重继承最常规的用法:公有继承一个接口类的同时,需要私有继承一个类来辅助实现