Skip to content

Chapter 6 Polymorphism

1 Basic Idea

多态(Polymorphism)

  • 允许把派生类对象当作基类对象来使用
  • 这种把派生类当基类看待的过程称为向上转型(upcast)
  • 真正调用哪个成员函数,在运行期根据对象的实际类型决定
图形程序的抽象层次
  • Shape:公共属性如 center,公共操作如 move()render()resize()
  • EllipseCircleRectangle:在公共接口上实现各自行为

  • 注意:CircleEllipse 继承在概念上未必是最佳设计

2 Virtual Functions and Dynamic Binding

静态绑定 vs 动态绑定
  • Static binding:按变量的声明类型决定调用哪个函数
  • Dynamic binding:按对象的真实类型决定调用哪个函数
  • virtual 开启动态绑定

含虚函数的对象通常含有一个隐藏指针 vptrvptr 指向该类的虚函数表(vtable),调用虚函数时,运行期通过 vptr 找到正确版本。

通过基类指针调用
void render(Shape* p) {
    p->render(); // 调用与实际对象类型匹配的 render()
}

void func() {
    Ellipse ell(10, 20);
    Circle circ(40);

    ell.render();
    circ.render();
    render(&ell);
    render(&circ);
}
示例
基态 Shape
class Shape { 
public:
    Shape();
    virtual ~Shape();
    virtual void render();
    void move(const Point&);
    virtual void resize(); 
protected:
    Point center;
};

派生 Ellipse 类
class Ellipse: public Shape{
public:
    Ellipse(float major, float minor);
    virtual void render();
protected:
    float major_axis,;
    float minor_axis;
};

派生 Circle 类
class Circle: public Ellipse{
public:
    Circle(float radius);
    virtual void render();
    virtual void resize();
    virtual float radius();
protected:
    float area;
};

  • ShapeEllipseCircle 各自有不同的 vtable
  • 同名虚函数在不同派生类中可以对应不同实现

3 Object, Pointers and References Preserve Polymorphism

对象赋值会发生切片

Ellipse elly(20F, 40F);
Circle circ(60F);
elly = circ;

(&elly)->render(); // Ellipse::render()
  • Circle 中超出 Ellipse 的那部分数据会被切掉(slice off)
  • 例如 Circle 特有的 area 不会复制到 elly
  • elly 自己的 vptr 仍指向 Ellipse 的 vtable

指针和引用保留多态特性

Ellipse* elly = new Ellipse(20F, 40F);
Circle* circ = new Circle(60F);
elly = circ;

elly->render(); // Circle::render()
  • 原来 elly 指向的 Ellipse 对象会丢失,造成内存泄漏
  • 但多态本身仍然成立,因为 ellycirc 现在都指向同一个 Circle
void func(Ellipse& elly) {
    elly.render();
}

Circle circ(60F);
func(circ); // 调用 Circle::render()
  • 引用和指针一样,也能保留动态绑定

4 Overriding, Overloading and Covariant Return Types

  • 覆盖(Overriding):派生类重新定义虚函数函数体,现代 C++ 推荐使用 override 明确表达这是在覆盖基类版本。
Example
class Base {
public:
    virtual void func();
};

class Derived : public Base {
public:
    void func() override;
};
复用基类逻辑
void Derived::func() {
    cout << "In Derived::func!";
    Base::func(); // 调用基类版本
}
  • 返回类型放宽(covariant return type):若 D 公有继承自 B,则 D::f() 可以把返回类型从 B* / B& 收窄为 D* / D&,仅适用于指针和引用
示例
class Expr {
public:
    virtual Expr* newExpr();
    virtual Expr& clone();
    virtual Expr self();
};

class BinaryExpr : public Expr {
public:
    virtual BinaryExpr* newExpr(); // ok
    virtual BinaryExpr& clone();   // ok
    virtual BinaryExpr self();     // Error
};
  • 基类中若有一组重载虚函数,派生类若覆盖其中一个,同名其他重载也会被隐藏,实践上应把整组重载都补齐
示例
class Base {
public:
    virtual void func();
    virtual void func(int);
};

class Derived : public Base {
public:
    virtual void func() {
        Base::func();
    }
    virtual void func(int) { /* ... */ }
};

多态设计相关建议

  • 不要重新定义继承来的非虚函数:非虚函数是静态绑定,没有动态分派
  • 不要重新定义继承来的默认参数值:默认参数也是静态绑定

5 Virtual Destructors and Abstract Classes

  • 如果一个类可能被继承,并且你会通过基类指针删除对象,那么基类析构函数必须是 virtual
如果不是 virtual?
Shape* p = new Ellipse(100.0F, 200.0F);
delete p;

如果 Shape::~Shape() 不是 virtual,那么只会调用 Shape::~Shape()Ellipse::~Ellipse() 不会执行,资源释放不完整。

示例
class A {
public:
    A() { f(); }          // 基类构造函数中调用虚函数 f()
    virtual void f() { cout << "A::f()"; }  // 基类虚函数 f
};

class B : public A {
public:
    B() { f(); }          // 派生类构造函数中调用 f()
    void f() { cout << "B::f()"; }  // 派生类重写虚函数 f
};

当执行 B b; 时,构造顺序严格遵循先基类 A 构造,再派生类 B 构造」,最终输出为 A::f()B::f()

  • 抽象类(Abstract Classes)通常用于建模,可以强制派生类遵守接口,定义接口而不提供完整实现,适用于当前信息不足无法给出通用实现或只想提供接口继承的场景。
Protocol / Interface Class
  • 除析构函数外,所有非静态成员函数均为纯虚函数
  • 析构函数是虚函数,且通常为空实现
  • 不含非静态数据成员
  • 可以含静态成员
  • 纯虚函数语法是 virtual 返回类型 函数(...) = 0;,只要类中存在至少一个纯虚函数,该类就是抽象类
  • 抽象类通常只作为基类使用,用来统一接口而不直接创建对象。
纯虚函数与抽象类
class Shape {
public:
    virtual ~Shape() {}
    virtual void render() = 0; // 纯虚函数
    virtual void resize() = 0;
};

// Shape s; // error:抽象类不能直接实例化
接口类示例
class CDevice {
public:
    virtual ~CDevice() {}

    virtual int read(...) = 0;
    virtual int write(...) = 0;
    virtual int open(...) = 0;
    virtual int close(...) = 0;
    virtual int ioctl(...) = 0;
};