Effective C++ - Inheritance and Object-Oriented Design

Effective C++ - Inheritance and Object-Oriented Design

前言:OOP面向对象编程(继承,单一继承,多重继承,public/protected/private,virtual/non-virtual,继承的查找规则,virtual函数是最佳选择吗,等等)有哪些坑?


  • Effective C - Inheritance and Object-Oriented Design
    • 确定你的public继承是is-a关系
    • 避免遮掩继承而来的名称
    • 区分接口继承和实现继承
    • 考虑virtual函数以外的其他选择
      • 1 Non-Virtual Interface NVI
      • 2 Function Pointers 实现 Strategy 模式
      • 3 tr1function 实现 Strategy 模式

1 确定你的public继承是is-a关系

Make sure public inheritance models “is-a”.

例子:

class Person {
    // ...
};

class Student: public Person {
    // ...
};

每个学生都是人,但并非每个人都是学生。人的概念比学生更一般化,学生是人的一种特殊形式。

这个论点,只对public继承才成立。只有当Student以public形式继承Person,C++的行为才如上述描述。private继承的意义与此完全不同,至于protected继承,其意义更加困惑。

请记住
public继承意味is-a。适用于base classes身上的每一件事情一定适用于derived classes身上,因为,每一个derived class对象也都是一个base class对象。

2 避免遮掩继承而来的名称

#include 

class Base {
public:
    virtual void f1() = 0;
    virtual void f1(int) {
        std::cout << "virtual void Base::f1(int)\n";
    }
    virtual void f2() {
        std::cout << "virtual void Base::f2()\n";
    }
    void f3();

};

class Derived: public Base {
public:

    // 让Base class内名为f1的函数在Derived class作用域内可见,如果不这样声明,下面d.f1(1)会找不到
    using Base::f1;

    virtual void f1() {
        std::cout << "virtual void Derived::f1()\n";
    }
    void f3() {
        std::cout << "void Derived::f3()\n";
    }
};

int main()
{
    Derived d;

    d.f1();
    d.f1(1); // error ?
    d.f2();
    d.f3();

    return 0;
}
/*
virtual void Derived::f1()
virtual void Base::f1(int)
virtual void Base::f2()
void Derived::f3()
 */

请记住
* derived classes内的名称会遮掩base classes内的名称。在public继承下,正常是不希望被遮掩的。
* 为了让遮掩的名称可见,可以使用using声明来到达目的(如上述例子)。

3 区分接口继承和实现继承

Differentiate between inheritance of interface and inheritance of implementation.

表面上直接了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:

  • 函数接口(function interfaces)继承
  • 函数实现(function implementations)继承
class Shape {
public:
    // 三种被继承的接口
    virtual void draw() const = 0;
    virtual void error(const std::string& msg);
    int objectID() const;
    // ...
};

class Rectangle: public Shape {
    // ...
};

class Ellipse: public Shape {
    // ...
};

Shape是一个抽象class,它的pure virtual函数draw使它成为一个抽象class。所以客户不能够创建Shape class的实体,只能创建其derived classes的实体。

三种以public继承的接口,含义是不一样的:

  1. 成员函数接口总是会被继承。
  2. 声明一个pure virtual函数的目的是,为了让derived classes只继承函数接口。
  3. 声明简朴的impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
  4. 声明non-virtual函数的目的,是为了令derived classes继承函数的接口,和一份强制性实现。

4 考虑virtual函数以外的其他选择

Consider alternatives to virtual functions.

class GameCharacter {
public:
    virtual int healthValue() const;  // 返回游戏中人物的健康指数,derived classes可以重新定义此函数
    // ...
};

healthValue并未被声明为pure virtual,这暗示我们将会有个计算健康指数的缺省算法

一些替代方案:

4. 1 Non-Virtual Interface (NVI)

就是,令客户通过public non-virtual成员函数间接调用private virtual函数。这样做的好处是,可以在public non-virtual函数(也就是virutal函数的wrapper函数)中完成一些事前和事后的工作。

class GameCharacter {
public:
    int healthValue() const { // 返回游戏中人物的健康指数,derived classes不重新定义此函数

        do_something_before();
        int ret = doHealthValue();   // 做真正的工作
        do_something_after();
    }

private:
    virtual int doHealthValue() const { // derived classes可以重新定义它
        // 缺省实现
    }
};

4.2 Function Pointers 实现 Strategy 模式

这种方法的思路是,人物健康指数的计算与人物类型无关。这样的计算完全不需要人物这个成分。例如,我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算。

class GameCharacter;   // 前置声明 forward declaration

int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter {
public:
    typedef int (*HealthCalcFunc) (const GameCharacter&);
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {
        // init
    }

    int healthValue() const {
        return healthFunc(*this);
    }

    // ...

private:
    HealthCalcFunc healthFunc;   // 函数指针
};

这种实现更加具有弹性:

  1. 同一个人物类型的不同实例,可以有不同的健康计算函数。

  2. 某已知人物的健康指数计算函数,可以在运行时期变更。

事物都有两面性:

一般而言,唯一能够解决,需要以non-member函数访问class的non-public成分的办法就是:弱化class的封装。例如,class可声明那个non-member函数为friends,或是为其实现某一部分提供public访问函数。

运用函数指针替换virtual函数。其优点是,每个对象可各自拥有自己的健康计算函数和可在运行时期改变计算函数;而缺点,是可能必须降低类的封装性。

4.3 tr1::function 实现 Strategy 模式

一旦习惯了templates,以及它们对隐式接口的使用,基于函数指针的做法看起来便过分苛刻而死板了。

为什么要求“健康指数的计算”必须是函数,而不能是某种”像函数的东西”,例如,函数对象。如果我们不再使用函数指针,而是改用一个类型为tr1::function的对象,这些约束就全部挥发不见了。这样的对象,可以持有任何可调用物(也就是,函数指针、函数对象、成员函数指针)。

TODO

你可能感兴趣的:(C/C++,C++沉思录)