Effective C++ 第六章--继承与面向对象设计笔记

    • 条款32确定你的public继承塑模处is-a关系
    • 条款33避免遮掩继承而来的名称
    • 条款34区分接口继承和实现继承
    • 条款35考虑virtual函数以外的其他选择
      • 藉由Non-Virtual Interface手法实现Template Method模式
      • 藉由Funciton Pointers实现Strategy策略模式
      • 藉由tr1function完成Strategy模式
      • 古典的Strategy模式
    • 条款36绝不重新定义继承来的non-virtual函数
    • 条款37绝不重新定义继承而来的缺省参数值
    • 条款38通过复合塑模处has-a或根据某物实现出
    • 条款39明智而审慎的使用private继承
    • 条款40明智而审慎的使用多重继承

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

  • “public继承”意味着is-a。适用于每个基类身上的每一件事一定也适用于派生类身上,因为每一个派生类对象也都是一个基类对象

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

派生类继承自基类,会产生命名作用域嵌套。派生类内部自成一个作用域,如果派生类使用某个名字,首先会在派生类命名作用域查找。如果派生类内部没有该名字,才会在基类的作用域类查找该名字。如果基类也没有,再去namespace,甚至全局作用域。

派生类继承自基类后,如果定义一个函数,函数名名与基类中函数名相同,那么基类中的所有同名函数会被覆盖。无论参数是否一致。因为这是命名规则。

如果派生类继承基类,基类有同名且重载多次的函数,派生类希望重新定义或者覆盖其中一部分,使用using。使用using可以使基类一部分和派生类重载函数同时存在,而不致使基类所有同名函数全部被覆盖。

如:

class base {
public:
    void fun(int i) { std::cout<<"base"<<std::endl; }
};

class derived : public base{
public:
    using base::fun;
    void fun() { std::cout<<"derived"<<std::endl; }
};

如上用法,则不会产生全部覆盖。派生类可以根据不同参数选择调用基类或是自己的函数。

有时候派生类不想继承基类的全部函数,这时候,不能用公有继承,因为公有继承是is-a。此时使用using不可行,因为using会使基类中所有同名函数都被派生类可见。我们需要使用private继承加转交函数来实现。

class derived : private base {  //私有继承,只继承实现
public:
    virtual void fun() { base::fun(); }
};

上面代码仅为示意,以这种形式书写,就可调用基类某个函数。

  • 派生类内的名称会遮掩基类内的名称。在公有继承下从来没有人希望如此。
  • 为了让遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)。

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

  • 接口继承和实现继承不同。在公有继承下,派生类总是继承基类的接口。
  • 纯虚函数只具体指定接口继承
  • 虚函数(非纯)具体指定接口继承及缺省实现继承。(意思是虚函数自身定义了一份默认实现,如果子类不重写的话,那使用的就会是这份基类的实现)。
  • 普通函数具体指定接口继承以及强制性实现继承。(公有继承下普通函数是不能被重写的,虽然没有语法错误,但不符合is-a,所以是强制性实现)。

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

藉由Non-Virtual Interface手法实现Template Method模式

大概意思就是virtual函数要作为private函数,使用一个non-virtual函数调用private virtual函数。

比如:

class game_character {
public:
    int health_value() const {
        ...           //做一些前期工作
        int ret = do_health_value();   //做真正的工作
        ...           //做一些后期处理
        return ret; 
    }
private:
    virtual int do_health_value() const {  //派生类可重新定义它,只不过不能使用
        ...
    }
};

这一设计,又称non-virtual interface(NVI)手法。它是所谓Template Method设计模式的一种形式。上面的non-virtual函数可以称作wrapper。

NVI手法的一个优点在于可以做事前,事后的工作。比如事前加锁,事后解锁,assert验证等。

藉由Funciton Pointers实现Strategy(策略)模式

实际上就是针对不同需求使用不同函数指针进行回调,不赘述。

藉由tr1::function完成Strategy模式

这个实际上是std::function,或者说boost::function,这本书比较老,所以当时还没有。

std::function相对函数指针的优势在于全能!不仅可使用函数,也可食用函数对象,甚至成员函数。不过要注意与std::bind的配合,绑定成员函数需要改该型对象指针。

古典的Strategy模式

利用纯代码实现策略模式:

class game_character; //前向生命

class health_calc_func {
public:
    virtual int calc(const game_character& gc) const 
    { ... }
};
health_calc_func default_health_calc;

class game_character {
public:
    explicit game_character(health_calc_func* phcf = &default_health_cal) : health_calc_(phcf)
    {}
    int health_value() const {
        return health_calc_->calc(*this); 
    }
private:
    health_calc_func* health_calc_;
};

上述就是策略模式(策略模式真平易近人),用上述方法只要为health_calc_func继承体系纳入一个派生类,就可以添加一个新的算法了。

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

这个不必说,因为要符合is-a。

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

代码验证:

class base {
public:
    virtual void fun(int i = 3) { std::cout<<i<<std::endl; }
};

class derived : public base {
public:
    virtual void fun(int i = 2)  {  std::cout<<"derived"<<std::endl; std::cout<<i<<std::endl; }
};

int main()
{
    base *b = new derived;
    b->fun();
    return 0;
}

上述代码打印的结果是:

derived  3

这是完全错误的结果,调用了派生类函数,却打印出基类的默认值。

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。

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

复合有has-a和”is-implement-in-terms-of”两种。has-a就是某物有某个成员,比如教室,成员变量就是桌子等,这个好理解。主要来说后者,意思是根据某物实现出。比如自己实现一个数据结构集合set,我们使用链表list来做它的底层实现,那么不应该用继承自list(这会构成is-a,明显不符合),应该把list作为set的成员变量。成员函数在链表上进行相应的简单操作就可以实现set的功能,所以说set是根据list实现出。

  • 复合(composition)的意义和public继承完全不同。
  • 在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合一位is-implemented-in-terms-of(根据某物实现出)。

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

  • private继承意味着”根据某物实现出”。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合(composition)不同,private继承可以造成empty base最优化。这对致力于”对象尺寸最小化”的程序库开发者而言,可能很重要。(这意思是,基类如果是空类,非空派生类继承基类,基类的那一个char占位字节会被去掉。实际上,这本书太老了,即便不使用private继承,目前所有编译器都会针对这种情况优化,公有继承也同样。所以这里是错的。

我们可以使用复合来代替私有继承,如下:

class base {
private:
    virtual void timer() { std::cout<<"base"<<std::endl; }
};

class derived {
public:
    void call() { timer_.timer(); }
private:
    class inside_class : public base {
    public:
        virtual void timer() { std::cout<<"inside_timer"<<std::endl; }
    };  
    inside_class timer_;
    //如果derived继承自base,此处可以重写:
    //virtual void timer() { std::cout<<"derived"<<std::endl; } 
    //注意本段代码derived并没有继承base,上面一行代码仅为说明:如果继承,基类私有虚函数虽然能重写,但是是不可调用的。 
};

int main()
{
    derived d;
    d.call();
    return 0;
}

并且利用上述技巧我们可以实现不能被派生类定义的虚函数(注意去掉注释):
如果要类A继承某个类,要实现它的虚函数,我们使用一个成员类公有继承它,并包含一个该成员类的对象。我们就可以在成员类中重写改虚函数,调用该虚函数通过成员类对象即可。那么,往后如果某个类B继承类A,那么它是不可以定义上述虚函数的,因为那是类A私有对象的函数。

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

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节设计”public继承某个Interface class”和”private 继承某个协助实现的class”的两相结合。

你可能感兴趣的:(继承)