6. 继承与面向对象设计(Inheritance and Object-Oriented Design)

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

本条款告诉读者一个非常基本的继承思想:”public 继承”意味is唱。适用于base classes 身上的每一件事情一定也适用于derived classes 身上,因为每一个derived class 对象也都是一个base class 对象,但反之不然。

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

(1) derived classes 内的名称会遮掩base classes 内的名称。在public 继承下从来没有人希望如此。举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Base {
 private:
  int x;
 public:
  virtual void mfl() = 0;
  virtual void mfl(int);
  virtual void mf2();
  void mf3 ();
  void mf3(double);
};
class Derived: public Base {
 public:
  virtual void mfl();
  void mf3 ();
  void mf4 ();
  
};

base class 内所有名为mfl 和mf3的函数都被derived class 内的mfl 和mf3函数遮掩掉了。从名称查找观点来看,Base: :mfl 和Base: :mf3 不再被Derived继承!

(2) 为了让被遮掩的名称再见天日,可使用using 声明式或转变函数( forwarding

functions) 。举例说明:

[1] 使用using声明式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Base {
 private:
  int x;
 public:
  virtual void mfl() = 0;
  virtual void mfl(int);
  virtual void mf2();
  void mf3 ();
}
void mf3(double); class Derived: public Base {
 public:
  using Base::mfl; //使用using 声明式
  using Base: :mf3; //使用using 声明式
  virtual void mfl();
  void mf3 ();
  void mf4();
}
Derived d;
int x;
d.mf1 () ; //仍调用Derived: :mfl
d.mf1 (x); //调用Base: :mfl
d.mf2 () ; //调用Base: :mf2
d.mf3 ();//调用Derived: :mf3
d.mf3 (x); //调用Base: :mf3

[2] 使用转变函数

1
2
3
4
5
6
7
8
9
class Derived: private Base (
 public:
  virtual void mfl () //转变函数(forwading  function) ,
  { Base:: mfl ( );} //暗自成为inline
}

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

本条款告诉程序员:

(1) 接口继承和实现继承不同。在public 继承之下, derived classes 总是继承base class

的接口。

(2) pure virtual 函数只具体指定接口继承。(要求继承者必须重新实现该接口)

(3) 简朴的(非纯) impure virtual 函数具体指定接口继承及缺省实现继承(继承者可自己实现该接口也可使用缺省实现)。

(4) non-virtual 函数具体指定接口继承以及强制性实现继承。(继承者必须使用该接口的实现)

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Shape {
 public:
  virtual void draw( ) const = 0; //pure virtual 函数
  virtual void error(const std::string& msg); //简朴的(非纯) impure virtual 函数
  int objectID ( ) const;// non-virtual 函数
};
class Rectangle: public Shape { };
class Ellipse: public Shape { };

条款35: 考虑virtual函数以外的其他选择

本条款告诉程序员,当需要使用virtual 函数时,可以考虑其他选择。

Virtual函数的替代方案是:

(1) 使用non-virtual interface(NVI)手法。思想是:将virutal函数放在private中,而在public中使用一个non-virtual函数调用该virtual函数。优点是:用一个不能被子类重定义的函数,做一些预处理、后处理等,子类只需要在private中重新实现virtual函数即可。即:基类给出virtual函数的使用方法,而派生类给出virtual函数的使用方法。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class GameCharacter {
 public:
  int healthValue() const{                // 1. 子类不能重定义
    ...                               // 2. preprocess
    int retVal = doHealthValue();     // 2. 真正的工作放到虚函数中
    ...                               // 2. postprocess
   return retVal;
  }
  ...
 private:
  virtual int doHealthValue() const {   // 3. 子类可重定义
    ...
   }
};

(2) 将virtual函数替换为“函数指针成员变量”(这是Strategy设计模式中的一种表现形式)。优点是对象实例和派生类对象,可使用各种实现,也可在运行时随意改;缺点是:该函数不能访问类中的私有成员

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc); // default algorithm
class GameCharacter {
 public:
  typedef int (*HealthCalcFunc)(const GameCharacter&);
  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
  : healthFunc(hcf)
  {}
  int healthValue() const {
   return healthFunc(*this);
  }
  ...
 private:
  HealthCalcFunc healthFunc;
};

(3) 以tr1::function成员变量替换virtual函数,这允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。这种方式比上面的函数指针更灵活、限制更少:[1]返回值不一定是int,与其兼容即可; [2]可以是function对象; [3]可以是类的成员函数。

(4) 继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。这种方式最大的优点是:可以随时添加新的算法。举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class GameCharacter;
class HealthCalcFunc {
 public:
  ...
  virtual int calc(const GameCharacter& gc) const
   { ... }
  ...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
 public:
  explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)
   : pHealthCalc(phcf)
    {}
  int healthValue() const {
   return pHealthCalc->calc(*this);
 }
  ...
private:
HealthCalcFunc *pHealthCalc;
};

条款36: 绝对不要重新定义继承而来的non-virtual函数

本条款告诫程序员:绝不要重新定义继承而来的non-virtual函数,因为这不仅容易造成错误,而且是一种自相矛盾的设计。 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class B{
 public:
  void func(){ cout<<“B”;}
};
class D:public B{
 public
  void func() { cout<<“D”;}
};

下面是对B和D的使用:

1
2
3
4
5
D dObject;
B* basePtr = &dObject;
D* dOjbectPtr = &dObject;

看下面这两种调用方式:

1
2
3
basePtr->func();
dOjbectPtr->func();

你会发现打印结果为:

B

D

解释:在C++继承中,virtual函数是动态绑定的,调用的函数跟指针或者引用实际绑定的那个对象有关,而non-virtual函数是静态绑定的,调用的函数只跟声明的指针或者引用的类型相关。

此外,继承者自己重新实现了non-virtual函数的行为是自相矛盾的。Non-virtual函数是用于同时指定函数接口和函数实现的,既然你想只继承函数接口,就应该定义为non-virtual的。

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

该条款告诫程序员:绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-你唯一应该覆写的东西-却是动态绑定。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Shape{
 public:
  enum Color{RED,GREEN,BLUE};
  virtual void draw(Color color = RED)const = 0;
  ...
};
class Circle:public Shape{
 public:
  //竟然改变缺省参数值
  virtual void draw(Color color = GREEN)const{ ... }
};
Shape* pc = new Circle;
pc->draw(); //注意调用的是: Circle::draw(RED),也就是说,此处的draw函数是基类和派生类的“混合物”。

为什么缺省参数是静态绑定而不是动态绑定呢?主要原因是运行效率。如果是动态绑定,程序员使用起来很方便,但会降低运行效率,C++做了取舍,结果就是现在这样。

条款38:XXXXXXXXXXXXXXXXXXX

条款39:明智而审慎地使用private继承

(1)如果class之间的继承关系是private。编译器不会自动将一个derived class对象转化为一个base class对象。由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原来是protected或public属性。

(2)private继承意味is-implemented-in-terms-of,它的级别比组合低,当derived class需要protected base class或者需重新定义继承而来的virtual class时,设计才是合理的。

(3)与复合不同 ,private继承可以使empty base空间最优化。举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Empty{}; //empy class
clsss HoldsAnyInt{
 private:
  int x;
  Empty e;
};//这个的大小为>sizeof(int),Empty空对象需要安插一个char到空对象,并且有齐位需求。
class HoldsAnyInt::private Empty{
 private:
  int x;
}; //这个sizeof大小为sizeof(int)
补充:
class HoldsAnyInt::private Empty{
 private:
  int cal() = 0;
  int x;
}; //这个sizeof大小为8, 实际上为size(int) + sizeof(vptr)

条款40:明智而审慎地使用多重继承

(1) 多重继承比单一继承复杂。他可能导致新的歧义性,以及virtual继承的需要

(2) Virtual继承会增加大小、速度、初始化复杂度等等成本。如果virtual base classed不带任何数据,将是最具使用价值的情况。

(3) 多重继承最正当用途是:其中一个设计“public 继承某个interface class”和“priavte继承某个协助实现的class”的两相结合。

7. 模板与泛型编程(Templates and Generic Programming)

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

(1) class和templates都支持接口(interfaces)和多态(polymorphism)。

(2) 对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。

(3) 对template参数而言,接口是隐式的(implicit),奠基于有效表达式;多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。

条款42:了解typename的双重定义

(1) 声明template参数时,前缀关键字class与typename可互换。

例如:

1
2
3
4
5
6
7
8
9
10
11
template <class T> //or template <typename T>
void swap(T& obj1, T& obj2) {
  T temp(obj1);
  obj1 = obj2;
  obj2 = temp;
}

(2) 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initailization list(成员初值表列)内以作为base class修饰符。

例如,你必须:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tempalte <typename C>
void print2nd(const C& container)//打印容器内的第二元素
{
  if( containter.size() >= 2 ){
   typename C::const_iterator iter( containter.begin() );
   ++iter;
   int value = *iter;
   std::cout << value;
  }
}

解释:template内出现的名称如果相依于某个template参数,称之为从属名称;如果从属名称在class内呈嵌套状,称为嵌套从属名称。在上面的例子中,C::const_iterator就是嵌套从属名称。编译器并不知道 const_iterator是个类型,除非你告诉编译器,不然它以为这是C中的static成员变量或者是global变量。

但需要注意一下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
class Derived:public Base<T>::Nested{ //base class list中不允许出现"typename"
 public:
  explicit Dervied(int x) : Base<T>::Nested(x){ //成员初始化列表中不允许"typename"
  typename Base<T>::Nested temp; //既不在base class list也不在初始化列表中,作为一个base class修饰符需加上typename.
  ...
  }
  ...
};

条款43:学习处理模板化基类内的名称

本条款给出了以下问题的解决方案:当基类是模板化的类时,派生类应该怎样调用基类中的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
template<typename Company>
class MsgSender{
 public:
  ...
  void sendClear(const MsgInfo& info){
   std::string msg;
   ...//根据info产生信息
   Company c;
   c.sendClearText(msg);
  }
  void sendSecret(const MsgInfo& info){...} //这里调用的是c.sendEncrypted.
};
template <typename Company>
class LoggingMsgSender:public MsgSender<Comany>{
 public:
  ...
  void sendClearMsg(const MsgInfo& info){ //为避免"名称遮掩"现象的发生,采用了一个不同的名称
   ...// record status information before sending message
   sendClear(info);
   ...//record status information after sending message.
  }
  ...
};

以上代码直接编译会报错:抛出了”sendClear不存在”的抱怨。解决方法有以下三个:

(1) 在base class函数调用动作之前加上”this->”:

1
2
3
4
5
6
7
8
9
10
11
template <typename Company>
void LoggingMsgSender<Company>::sendClearMsg(const MsgInfo& info){
  ...
  this->sendClear(info); //ok
  ...
}

(2) 使用using声明式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>{
 public:
  //这里的情况不是base class名称被derived class名称遮掩,而是编译器不进入base base
  //作用域查找,于是我们通过using声明式告诉它,请它这么做
  using MsgSender<Company>::sendClear;//告诉编译器,请它假设sendClear位于base class内
  ...
  void sendClearMsg(const MsgInfo& info){
   ...
   sendClear(info);//ok
   ...
  }
};

(3) 明明白白指出被调用函数位于base class内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>{
 public:
  ...
  void sendClearMsg(const MsgInfo& info){
  ...
  MsgSender<Company>::sendClear(info); //ok
  ...
 }
 ...
};

条款44:将与参数无关的代码抽离template

(1) Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。

(2) 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。

举个例子,假设现在你要为固定尺寸的矩阵编写一个template类,该类声明要支持矩阵的逆运算,可以采用下面代码:

1
2
3
4
5
6
7
8
9
10
11
template <typename T, std::size_t n> //矩阵元素类型T,尺寸大小为n
class SquareMatrix{
 public:
   ...
   void invert(); //逆运算
};

这样定义,声明以下两个对象会产生不同的代码,造成代码膨胀:

1
2
3
SquareMatrix<double,5> square1;
SquareMatrix<double,10> square2;

减小代码膨胀的方法是采用以下定义:

1
2
3
4
5
6
7
8
9
10
11
template <typename T > //矩阵元素类型T
class SquareMatrix{
 public:
  ...
  void invert(std::size_t n); //把尺寸大小n作为参数
};

条款45:运用成员函数模板接受所有兼容类型

本条款告诉你,怎样编写成员函数模板。从下面例子说起:

怎样支持以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
class SmartPtr{
 public:
  explicit SmartPtr(T* realPtr);//智能指针通常以原始指针完成初始化
  ...
};
SmartPtr<Top> top1_smart_ptr = SmartPtr<Middle>(new Middle);
SmartPtr<Top> top2_smart_ptr = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> const_top2_ptr = top1_smart_ptr;

一个比较好的方案是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//根据SmartPtr<U>创建一个SmartPtr<T>,其中T是基类,U是T的派生类
template <typename T>
class SmartPtr{
 public:
  template <typename U>
  SmartPtr(const SmartPtr<U>& other) :held_ptr_( other.get() ){...} //这里就完成子类向父类的隐式转换过程.
  T* get()const{ return held_ptr_;}
  ...
 private:
  T* held_ptr_; //这是SmartPtr持有的内置指针.
};

上述中的SmartPtr构造函数便是成员函数模板(member function template),得出的结论是:

(1) 请使用member function template(成员函数模板)生成”可接受所有兼容类型”的函数。

(2) 如果你声明member template用于”泛化copy构造”或”泛化assignment操作”,你还需要声明正常copy构造函数和copy assignment操作符。(不然编译器会为你生成默认的copy构造函数和copy assignment操作符)

条款46:需要类型转换时请为模板定义非成员函数

本条款告诉程序员,当你需要进行类型转化时,为了避免麻烦,最好将模板定义为非成员函数(如friend函数)。

条款47: 请使用traits classes 表现类型信息

条款48: 认识template 元编程

8. 定制new和delete(Customizing new and delete)

条款49: 了解new-handler 的行为

(1) set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时被调用。

(2) No-throw new 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出bad_alloc异常。

条款50: 了解new 和delete 的合理替换时机

有许多理由需要写个自定的口new 和delete ,包括改善效能、对heap 运用错误进

行调试、收集heap 使用信息。

条款51: 编写new和delete时需固守常规

operator new内应该有一个无穷循环,并在其中尝试分配内存,如果分配失败,就调用new handler。它也应该有能力处理0 bytes申请(对于标准库中的new操作符,当用户申请0bytes,会返回1bytes的空间)。class版本还需要处理“比正确大小更大的(错误)申请”。

需要注意的是,operator new成员函数会被derived classes继承,也就是说, base class的operator new可能被调用以分配derived class对象。因此 derived class的 operator new的代码建议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Base{
 static void* operator new(std::size_t size) throw( std::bad_alloc );
 ...
};
struct Derived:public Base{...};
Derived* p = new Derived;//call Base::operator new.
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
  if( size != sizeof(Base) ){
  return ::operator new( size ); //call standard operator new version.
}
...
}

条款52: 写了placement new 也要写placement delete

1. 当你写一个placement new,请确定也写出对应的placement delete。如果没这样做,你的程序可能会出现微弱时断时续的内存泄漏;

2. 当你写placement new和placement delete时,请确定不要无意识的(非故意的)遮掩了全局范围默认的new/delete版本。

9. 杂项讨论(Miscellany)

条款53: 不要轻忽编译器的警告

条款54: 让自己熟悉包括TR1在内的标准程序库

本条款告诉程序员:

1.C++标准程序库的主要机能由STL、iostreams、locales组成,并包括C99标准程序库。

2.TR1添加了智能指针、一般化函数指针、hash-based容器、正则表达式,以及另外10个组件的支持。

3.TR1自身只是一个规范。为获得tr1提供的好处,你需要一份实物。一个好的实物来源是boost。

条款55: 让自己熟悉Boost

原创文章,转载请注明: 转载自董的博客

本文链接地址: http://dongxicheng.org/cpp/effective-cpp-part2/

《Effective C++》读书笔记(第二部分)相关推荐

  1. Effective C++读书笔记 摘自 pandawuwyj的专栏

    Effective C++读书笔记(0)       Start   声明式(Declaration):告诉编译器某个东西的名称和类型,但略去细节.   std::size_t numDigits(i ...

  2. Effective Java读书笔记(二)

    Effective Java 读书笔记 (二) 创建和销毁对象 遇到多个构造器参数时要考虑使用构建器 创建和销毁对象 何时以及如何创建对象? 何时以及如何避免创建对象? 如何确保它们能够适时地销毁? ...

  3. Effective STL 读书笔记

    Effective STL 读书笔记 标签(空格分隔): 未分类 慎重选择容器类型 标准STL序列容器: vector.string.deque和list(双向列表). 标准STL管理容器: set. ...

  4. more effective c++和effective c++读书笔记

    转载自http://bellgrade.blog.163.com/blog/static/83155959200863113228254/,方便日后自己查阅, More Effective C++读书 ...

  5. 《计算传播学导论》读书笔记——第二章文本分析简介

    <计算传播学导论>读书笔记--第二章文本分析简介 第一节 文本分析研究现状 常用文本挖掘技术 第二节 文本分析与传播学研究 (一)为什么文本挖掘技术逐渐受到传播学者的关注 (二)不同文本分 ...

  6. Effective Java 读书笔记(七):通用程序设计

    Effective Java 读书笔记七通用程序设计 将局部变量的作用域最小化 for-each 循环优于传统的 for 循环 了解和使用类库 如果需要精确的答案请避免使用 float 和 doubl ...

  7. Effective Java读书笔记完结啦

    Effective Java是一本经典的书, 很实用的Java进阶读物, 提供了各个方面的best practices. 最近终于做完了Effective Java的读书笔记, 发布出来与大家共享. ...

  8. Effective C++ 读书笔记 Item1-Item4

    目录 守则01:把C++看做一个语言的集合,而不是单一的语言 守则02:尽量使用const, enum, inline, 减少宏变量#define的使用 守则03: 尽可能使用const关键字 守则0 ...

  9. In-memory Computing with SAP HANA读书笔记 - 第二章:SAP HANA overview

    本文为In-memory Computing with SAP HANA on Lenovo X6 Systems第二章SAP HANA overview的读书笔记. 本章最重要的部分是SAP HAN ...

  10. Effective Java 读书笔记(一)

    前言: 开个新的坑位,<effective java>的读书笔记,之后有时间会陆陆续续的更新,读这本书真的感触满多,item01和item02就已经在公司的项目代码中看到过了.今天这篇主要 ...

最新文章

  1. andy the android ppt,新概念同步测试1.ppt
  2. java cygwin 乱码_windows10乱码怎么解决
  3. java激光推送ios_关于ios极光推送server端注意的地方
  4. springboot jpa sql打印_SpringBoot集成Spring Data JPA以及读写分离
  5. 一个搜集大量网页特效的网站
  6. 【跃迁之路】【425天】程序员高效学习方法论探索系列(实验阶段182-2018.04.06)...
  7. vue开发环境的搭建流程
  8. 学生签到系统c代码_C语言之学生管理系统代码(完整)
  9. 中债估值 收益率_收债
  10. AI_Drug: 分子生成模型之VAE(一)
  11. linux 常用的shell脚本
  12. win 10 读写EFI分区
  13. 数学图形之单叶双曲面
  14. 一场官司,终于让人们看清了苹果
  15. 今年春节,租个“人设”回家?
  16. 流程图分级、分类、分层
  17. Python+Excel制作精美壁纸日历,任意DIY
  18. 关于C语言中随机函数的使用详解
  19. 王咏刚《AI的产品化和工程化挑战》
  20. 字面量、对象字面量、函数字面量、函数定义

热门文章

  1. 刘强东:第四次零售革命意义将超互联网
  2. matlab绘制三维图形现状,MATLAB绘制三维图形
  3. mysql5.6 replication_MySQL5.6 Replication主从复制(读写分离) 配置完整版
  4. 为Windows git 配置比较工具为beyond compare
  5. Spring Cloud Alibaba - 15 微服务之间使用Feign实现参数的透传
  6. Redis进阶-无所不知的info命令诊断redis
  7. JVM-05垃圾收集Garbage Collection(中)【垃圾收集算法】
  8. Spring-AOP @AspectJ切点函数之@within()和@target
  9. Imageloader7-获取图片需要显示的大小
  10. 心中有“树”:数据结构之树详解