Effective C++条款34:继承与面向对象之(区分接口继承和实现继承)

一、继承中接口的处理方式

  • 作为类的设计者,对于基类的成员函数可以大致做下面三种方式的处理:
    • ①纯虚函数:基类定义一个纯虚函数,然后让派生类去实现
    • ②非纯虚的virtual虚函数:基类定义一个非纯虚的virtual虚函数,然后让派生类去重写覆盖(override)
    • ③普通的成员函数:基类定义一个普通的成员函数,并且不希望派生类去隐藏
  • 本文依次介绍上面这三种设计的原理。下面定义一个类,作为本文讲解的基础:
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 {};

二、纯虚函数

  • 这是文章开始提到的第一种情况:派生类只继承基类的成员函数的接口(纯虚函数),派生类自己实现纯虚函数
  • 纯虚函数的一些特征:
    • ①拥有纯虚函数的类不能实例化
    • ②拥有纯虚函数的类,其派生类必须实现该纯虚函数
  • 在上面,我们的纯虚函数为:
class Shape {
public:
    virtual void draw()const = 0; //纯虚函数
};

class Rectangle :public Shape {};
class Ellipse :public Shape {};
  • 其中涉及纯虚函数的目的为:
    • Shape是所有图形类的基类,其提供一个draw()的画图函数,但是由于其派生类(矩形、圆等)的画图方式都是不一样的,因此无法为draw()函数提供一种默认缺省行为,因此Shape将draw()定义为纯虚函数, 让其派生类去自动实现
  • 演示案例:
class Shape {
public:
    virtual void draw()const = 0;
};

class Rectangle :public Shape {
public:
    virtual void draw()const {
        std::cout << "Rectangle" << std::endl;
    }
};
class Ellipse :public Shape {
public:
    virtual void draw()const {
        std::cout << "Ellipse" << std::endl;
    }
};

int main()
{
    //Shape *ps = new Shape;    //错误,不能实例化
    Shape *ps1 = new Rectangle;
    Shape *ps2 = new Ellipse;

    ps1->draw(); //调用Rectangle::draw()
    ps2->draw(); //调用Ellipse::draw()

    return 0;
}
  • 纯虚函数虽然用途有限,但是它可以实现一种机制,为非纯虚的virtual虚函数提供更平常安全的缺省实现(见下面的介绍)

三、非纯虚的virtual虚函数

演示案例(介绍优点的)

  • 先来看一个virtual函数的演示案例
  • 假设某航天公司设计一个飞机继承体系,该公司现在只有A型和B型两种飞机,代码如下:
class Airport {}; //机场

class Airplane {  //飞机的基类
public:
    virtual void fly(const Airport& destination) {
        //飞机飞往指定的目的地(默认行为)
    }
};

//A、B两个派生类
class ModelA :public Airplane {};
class ModelB :public Airplane {};
  • fly()函数被声明为virtual函数,因为A和B两个飞机具有相同的默认飞行行为,因此在Airplane类的fly()函数中定义这种默认飞行行为,然后让A和B继承。这样的好处是:
    • 将所有性质搬到到base class中,然后让两个class继承
    • 避免代码重复,并提升未来的强化能力,减缓长期维护所需的成本

演示案例(介绍以纯虚函数代替虚函数)

  • 上面的A和B都有默认的飞行行为,因此在Airport的fly()函数中定义了这份默认行为
  • 但是如果航天公司增加了一个C型的飞机,如下:
class ModelC :public Airplane {};
  • 如果C型飞机可能不适合默认的飞行行为,那么可能就会误调用Airplane中的fly()函数,例如:
Airport PDX;

//C型飞机不具有fly()的行为,但是却调用了fly()
Airplane *pa = new ModelC;
pa->fly(PDX);
  • 这是设计上的概念错误,我们可以修改代码,切断“virtual函数”和其“默认实现”之间的连接。代码如下:
class Airport {}; //机场
class Airplane {
public:
    virtual void fly(const Airport& destination) = 0;
protected:
    void defaultFly(const Airport& destination) {
        //飞机飞往指定的目的地(默认行为)
    }
};

class ModelA :public Airplane {
public:
    virtual void fly(const Airport& destination) {
        defaultFly(destination);
    }
};
class ModelB :public Airplane {
public:
    virtual void fly(const Airport& destination) {
        defaultFly(destination);
    }
};

class ModelC :public Airplane {
public:
    virtual void fly(const Airport& destination) {
        //C型飞机不可以使用默认飞行行为,因此定义自己的飞行方式
    }
};
  • 现在C型飞机,或者别的添加的飞机就不会意外继承默认的飞行行为了(因为我们将默认的飞行行为封装到一个defualtFly函数中了),自己可以在fly中定义飞行行为了
  • 注意,在A和B的类的fly()函数中,对defaultFly()函数做了一个inline调用(见条款30,inline和virtual函数之间的交互关系)

演示案例(以纯虚函数代替虚函数的另一种实现)

  • 上面我们将fly()接口和实现(defaultFly()函数)分开来实现,有些人可能会反对这样做,因为这样会因过度雷同的函数名称而引起class命名空间污染
  • 如果不想将上述两个行为分开,那么可以为纯虚函数进行定义,在其中给出defaultFly()函数的相关内容。例如:
class Airport {}; //机场
class Airplane {
public:
    //实现纯虚函数
    virtual void fly(const Airport& destination) = 0 {
        //飞机飞往指定的目的地(默认行为)
    }
};

class ModelA :public Airplane {
public:
    virtual void fly(const Airport& destination) {
        Airplane::fly(destination);
    }
};
class ModelB :public Airplane {
public:
    virtual void fly(const Airport& destination) {
        Airplane::fly(destination);
    }
};

class ModelC :public Airplane {
public:
    virtual void fly(const Airport& destination) {
        //定义自己的飞行方式
    }
};
  • 这个设计实现的功能和上面的演示案例是一样的,只不过在派生类的fly()函数中用纯虚函数Airplane::fly替换了独立函数Airplane::defaultFly。这种合并行为丧失了“让两个函数享有不同保护级别”的机会:例如上面的defaultFly()函数从protected变为了public

四、普通的成员函数

  • 最后来看看Airplane的普通成员函数
class Shape {
public:
    int objectID()const; //普通成员函数,不希望派生类隐藏
};

class Rectangle :public Shape {};
class Ellipse :public Shape {};
  • 设置普通的成员函数的目的:
    • 意味着基类不希望派生类去隐藏这个成员函数
    • 实际上一个普通的成员函数所表现的不变性凌驾其特异性,因为它表示不论派生类变得多特特异化,它的行为都不可以改变
  • 在上面的代码中:
    • 每个Shape对象都有一个用来产生对象识别码的函数
    • 此识别码总是采用相同计算方法,该方法有Shape::objectID的定义式决定,任何派生类都不应该尝试改变其行为
  • 由于普通成员函数代表的意义是不变性凌驾特异性,所以它绝不该在派生类中被重新定义(这也是条款36所讨论的一个重点)

五、class设计常犯的两个错误

  • “纯虚函数、非纯虚的virtual虚函数、普通的成员函数”之间的差异,使得指定你想要派生类继承的东西,针对于不同的函数,经验不足的class设计者最常犯的两个错误如下

第一个错误

  • 第一个错误是将所有函数声明为“普通的成员函数”,这使得派生类米有多余空间进行特化工作
  • non-virtual析构函数尤其会带来问题(见条款7)
  • 当然,如果一个类不打算作为基类,那么将所有函数声明为“普通的成员函数”是可以的。但是如果该类会作为基类,那么可以适当的声明一些virtual函数(见条款7)
  • 如果你当心virtual函数的成本,那么可以参阅80-20法则(也可参阅条款30):
    • 这个法则为:一个典型的程序有80%的执行时间花费在20%的代码身上
    • 这个法则意味着,平均而言你的函数调用中可以有80%是virtual而不冲击程序的大体效率。所以当你担心virtual函数的成本之前,先将精力放在那举足轻重的20%代码上,它才是真正的关键

第二个错误

  • 第二个错误是将所有成员函数声明为virtual
  • 有时候这样做是正确的,例如条款31的Interface classes。然而某些函数就是不该在派生类中被重新定义,因此你应该将那些函数声明为non-virtual的

六、总结

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口
  • pure virtual函数只具体hiding接口继承
  • 简朴的(非纯)impure virtaul函数具体指定接口继承及缺省实现继承
  • non-virtual函数具体指定接口继承以及强制性实现继承
发布了1504 篇原创文章 · 获赞 1063 · 访问量 43万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104785357