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

- 注意:
Circle从Ellipse继承在概念上未必是最佳设计
2 Virtual Functions and Dynamic Binding¶
静态绑定 vs 动态绑定
- Static binding:按变量的声明类型决定调用哪个函数
- Dynamic binding:按对象的真实类型决定调用哪个函数
virtual开启动态绑定
含虚函数的对象通常含有一个隐藏指针 vptr,vptr 指向该类的虚函数表(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;
};

Shape、Ellipse、Circle各自有不同的 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对象会丢失,造成内存泄漏 - 但多态本身仍然成立,因为
elly和circ现在都指向同一个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;
};