前言
"多态"是 C++ 面向对象编程中最重要的概念之一,也是初次接触时最容易和继承、重载、模板混在一起的概念。
这篇文章不从术语定义开始死记,而是沿着一个实际问题逐步推导:
text
我们想用同一套代码处理不同种类的对象
↓
普通函数为什么做不到自动选择行为
↓
virtual怎样让调用在运行时发生分派
↓
如何用抽象类设计统一接口
↓
怎样正确管理多态对象的生命周期
↓
编译器通常怎样通过虚函数表实现它
↓
多继承、RTTI、智能指针和工程设计
↓
什么时候应该使用动态多态,什么时候不应该
理解完整条主线后,virtual 就不再是一个孤立的关键字,而是一整套类型抽象、运行时分派和对象生命周期机制的入口。
第一章:什么是多态
1.1 从"同一个动作,不同表现"说起
Polymorphism,中文叫"多态"。
这个单词可以拆成:
text
poly = 多种
morph = 形态
在程序设计中,多态可以通俗地理解为:
使用同一种调用方式,面对不同类型的对象时,表现出不同的行为。
例如,程序中有三种图形:
text
圆形 Circle
矩形 Rectangle
三角形 Triangle
它们都可以执行"绘制"操作:
cpp
circle.draw();
rectangle.draw();
triangle.draw();
调用形式都叫 draw(),但实际绘制过程不同:
text
Circle::draw() 绘制圆形
Rectangle::draw() 绘制矩形
Triangle::draw() 绘制三角形
"相同接口,不同实现"就是多态最直观的表现。
1.2 C++ 中常见的两类多态
C++ 中常见的多态可以分成两大类:
text
静态多态 Static Polymorphism
在编译期决定调用哪个实现
动态多态 Dynamic Polymorphism
在运行时决定调用哪个实现
这里的"静态"和"动态"不是说对象会不会移动,而是在说:
编译器在什么时候确定最终调用目标?
第二章:先简单区分静态多态与动态多态
2.1 函数重载属于静态多态
Overload,中文叫"重载"。
cpp
#include <iostream>
#include <string>
void print(int value) {
std::cout << "整数:" << value << '\n';
}
void print(const std::string& value) {
std::cout << "字符串:" << value << '\n';
}
int main() {
print(10);
print(std::string{"hello"});
}
编译器看到参数类型后,就能在编译阶段决定:
text
print(10)
→ print(int)
print(std::string{"hello"})
→ print(const std::string&)
程序运行前,调用目标已经确定,因此属于静态多态。
2.2 模板也可以实现静态多态
cpp
template<class T>
void drawObject(const T& object) {
object.draw();
}
调用:
cpp
drawObject(circle);
drawObject(rectangle);
编译器会分别生成适用于 Circle 和 Rectangle 的代码。选择仍然发生在编译期。
2.3 动态多态解决什么问题
假设程序运行时才知道应该创建哪一种图形:
text
用户输入1 → 创建圆形
用户输入2 → 创建矩形
配置文件变化 → 创建另一种图形
插件加载成功 → 创建插件提供的图形
此时对象的具体类型可能无法在编译时确定。
我们希望把不同对象统一放在一起:
text
一组图形
Circle
Rectangle
Triangle
然后只写一次循环:
cpp
for (const auto& shape : shapes) {
shape->draw();
}
循环不关心每个对象究竟是什么图形。运行到某个对象时,再调用该对象真正的 draw()。
这就是动态多态要解决的核心问题。
第三章:没有 virtual 时发生了什么
3.1 先建立继承关系
Inheritance,中文叫"继承"。
先定义一个公共基类:
cpp
#include <iostream>
class Shape {
public:
void draw() const {
std::cout << "绘制普通图形\n";
}
};
class Circle : public Shape {
public:
void draw() const {
std::cout << "绘制圆形\n";
}
};
这里:
text
Shape → 基类 Base Class
Circle → 派生类 Derived Class
public Shape 表示 Circle 公开继承 Shape。
从类型关系上可以说:
text
Circle is a Shape
圆形是一种图形
公开继承会保留这种"是一种"的对外关系,因此外部代码可以把 Circle* 转换成 Shape*。protected 或 private 继承主要用于实现复用,不会向普通外部代码公开同样的替换关系,通常不用于建立常规多态接口。
3.2 基类指针可以指向派生类对象
cpp
Circle circle;
Shape* pointer = &circle;
这种转换叫:
text
Upcasting
= 向上转换
把更具体的 Circle* 转换成更抽象的 Shape*,通常可以隐式完成。
现在调用:
cpp
pointer->draw();
输出却是:
text
绘制普通图形
为什么不是"绘制圆形"?
因为基类的 draw() 还不是虚函数。编译器主要根据表达式的静态类型 Shape* 决定调用:
cpp
Shape::draw()
这种在编译期确定调用目标的方式叫:
text
Static Binding
= 静态绑定
= Early Binding
= 早绑定
第四章:virtual 让调用目标推迟到运行时
4.1 第一个完整的动态多态程序
只需要把基类函数声明为虚函数:
cpp
#include <iostream>
class Shape {
public:
virtual void draw() const {
std::cout << "绘制普通图形\n";
}
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "绘制圆形\n";
}
};
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "绘制矩形\n";
}
};
void paint(const Shape& shape) {
shape.draw();
}
int main() {
Circle circle;
Rectangle rectangle;
paint(circle);
paint(rectangle);
}
输出:
text
绘制圆形
绘制矩形
paint() 的参数始终是:
cpp
const Shape& shape
但它接收到 Circle 时调用 Circle::draw(),接收到 Rectangle 时调用 Rectangle::draw()。
这种机制叫:
text
Dynamic Binding
= 动态绑定
Runtime Dispatch
= 运行时分派
Late Binding
= 晚绑定
4.2 实现经典动态多态的三个条件
教科书中通常把经典动态多态总结为三个条件:
- 存在继承关系。
- 基类声明虚函数。
- 通过基类指针或基类引用调用虚函数。
cpp
Circle circle;
Shape* pointer = &circle;
Shape& reference = circle;
pointer->draw(); // Circle::draw()
reference.draw(); // Circle::draw()
更准确地说,关键是:
对多态对象进行虚函数调用时,调用目标可以根据对象的动态类型决定。
即使直接通过对象表达式调用虚函数,语言规则仍然保留虚调用语义;只是当编译器明确知道对象类型时,往往可以直接确定目标并进行优化。
第五章:静态类型、动态类型与动态分派
5.1 两种类型必须分清
看下面的代码:
cpp
Circle circle;
Shape* pointer = &circle;
pointer 涉及两种类型:
text
静态类型 Static Type
Shape*
编译器从变量声明中直接看到的类型
动态类型 Dynamic Type
Circle
pointer当前真正指向对象的类型
调用普通成员函数时,静态类型非常重要。
调用虚函数时,运行时还会考虑动态类型。
cpp
pointer->draw();
可以通俗地理解为:
text
编译器看到pointer是Shape*
↓
确认draw是虚函数
↓
运行时查看pointer真正指向什么对象
↓
发现真实对象是Circle
↓
调用Circle::draw()
5.2 基类引用同样保留动态类型
cpp
Circle circle;
Shape& shape = circle;
shape.draw(); // Circle::draw()
引用没有复制对象,它只是给原对象提供另一个访问入口,因此原对象仍然是 Circle。
第六章:override、final 与正确覆盖
6.1 什么是覆盖
Override,中文常叫"覆盖"或"重写"。
派生类使用相同函数签名,重新实现基类虚函数:
cpp
class Shape {
public:
virtual void draw() const;
};
class Circle : public Shape {
public:
void draw() const override;
};
基类第一次声明时写 virtual。派生类中的函数即使不再写 virtual,仍然是虚函数:
cpp
void draw() const;
但强烈推荐写:
cpp
void draw() const override;
因为 override 会让编译器检查"这个函数是否真的覆盖了基类虚函数"。
6.2 一个容易发现不了的错误
cpp
class Shape {
public:
virtual void draw() const;
};
class Circle : public Shape {
public:
void draw() override; // 错误:少了const
};
Circle::draw() 和 Shape::draw() const 不是同一个函数签名。
写了 override 后,编译器立即报错。如果不写,程序可能悄悄定义出一个新的同名函数,导致通过基类引用调用时仍然进入基类实现。
常见的不匹配原因包括:
- 参数类型不同;
- 参数数量不同;
- 成员函数末尾的
const不同; - 引用限定符不同;
noexcept规则不兼容。
关于 noexcept,可以先记住:
text
基类虚函数承诺noexcept
派生类覆盖不能取消这个承诺
基类虚函数没有承诺noexcept
派生类可以给出更严格的noexcept承诺
例如:
cpp
class Base {
public:
virtual void run() noexcept;
};
class Derived : public Base {
public:
void run() noexcept override;
};
6.3 final 禁止继续覆盖
Final,中文可以理解为"最终的、不可继续扩展的"。
禁止更下层派生类继续覆盖某个函数:
cpp
class Circle : public Shape {
public:
void draw() const final;
};
上面的面积计算使用了 C++20 的 <numbers>。如果使用较早标准,可以自行定义合适精度的圆周率常量。
禁止一个类继续被继承:
cpp
class Circle final : public Shape {
};
final 可以表达清晰的设计意图,也可能帮助编译器判断虚函数的最终目标。
第七章:重载、覆盖与名字隐藏
这三个概念名称相近,但不是一回事。
7.1 重载:同一作用域中的同名函数
Overload,重载:
cpp
void print(int value);
void print(double value);
编译器根据参数决定调用哪个版本。
7.2 覆盖:派生类重新实现虚函数
Override,覆盖:
cpp
class Base {
public:
virtual void print(int value);
};
class Derived : public Base {
public:
void print(int value) override;
};
7.3 隐藏:派生类同名声明遮住基类名字
Name Hiding,中文叫"名字隐藏"。
cpp
class Base {
public:
void print(int value);
void print(double value);
};
class Derived : public Base {
public:
void print(const char* value);
};
Derived 中的同名 print 会隐藏基类的整组 print。
cpp
Derived object;
object.print(10); // 可能无法找到期望的Base::print(int)
可以使用 using 把基类重载重新引入:
cpp
class Derived : public Base {
public:
using Base::print;
void print(const char* value);
};
记忆方法:
text
重载:同一个作用域,参数不同
覆盖:继承体系中,重新实现虚函数
隐藏:派生类同名声明遮住基类名字
第八章:为什么多态通常使用指针或引用
8.1 按值复制会发生对象切片
cpp
Circle circle;
Shape shape = circle;
这叫:
text
Object Slicing
= 对象切片
可以把派生类对象想象成:
text
Circle对象
┌─────────────────┐
│ Shape基类部分 │
├─────────────────┤
│ Circle派生部分 │
└─────────────────┘
执行:
cpp
Shape shape = circle;
目标对象的类型就是 Shape,只能容纳基类部分:
text
shape对象
┌─────────────────┐
│ Shape基类部分 │
└─────────────────┘
Circle 的派生部分被"切掉"了。
因此:
cpp
shape.draw();
调用的是 Shape::draw(),因为 shape 已经是一个真正、独立的 Shape 对象。
8.2 多态参数不要按值接收
错误或不符合多态目的的写法:
cpp
void paint(Shape shape) {
shape.draw();
}
传入 Circle 时会复制成 Shape,产生切片。
正确做法通常是:
cpp
void paint(const Shape& shape) {
shape.draw();
}
如果允许空值,也可以使用指针:
cpp
void paint(const Shape* shape) {
if (shape != nullptr) {
shape->draw();
}
}
8.3 多态容器不能直接存基类对象
cpp
std::vector<Shape> shapes;
shapes.push_back(Circle{});
这里同样发生对象切片。
多态集合通常存储智能指针:
cpp
std::vector<std::unique_ptr<Shape>> shapes;
后面会详细讲解。
第九章:纯虚函数、抽象类与接口
9.1 基类一定要提供默认实现吗
并不是所有图形都存在一种有意义的"通用绘制方法"。
如果 Shape 只负责规定接口,不提供默认实现,可以把函数声明为纯虚函数:
cpp
class Shape {
public:
virtual void draw() const = 0;
virtual double area() const = 0;
virtual ~Shape() = default;
};
Pure Virtual Function,中文叫"纯虚函数"。
语法:
cpp
virtual 返回类型 函数名(参数) = 0;
这里的 = 0 不是把函数赋值为数字零,而是 C++ 表示纯虚函数的特殊语法。
9.2 什么是抽象类
Abstract Class,中文叫"抽象类"。
只要类中还有纯虚函数没有得到最终实现,这个类就是抽象类,不能直接创建对象:
cpp
Shape shape; // 编译错误
但可以声明它的指针和引用:
cpp
Shape* pointer = nullptr;
Shape& reference = someShape;
派生类实现所有纯虚函数后,才能成为可实例化的具体类:
cpp
#include <numbers>
class Circle : public Shape {
public:
explicit Circle(double radius)
: radius_(radius) {}
void draw() const override {
std::cout << "绘制圆形\n";
}
double area() const override {
return std::numbers::pi
* radius_
* radius_;
}
private:
double radius_;
};
9.3 接口类
Interface,中文叫"接口"。
C++ 没有单独的 interface 关键字。通常用只包含抽象操作和虚析构函数的抽象类表达接口:
cpp
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() = default;
};
接口主要说明"对象能做什么",而不关心对象内部怎样完成。
9.4 纯虚函数也可以拥有定义
纯虚函数仍然可以在类外提供函数体:
cpp
class Shape {
public:
virtual void draw() const = 0;
};
void Shape::draw() const {
std::cout << "执行公共绘制准备工作\n";
}
派生类仍然需要覆盖它,才能让类变为具体类;但派生类可以显式调用这个公共实现:
cpp
void Circle::draw() const {
Shape::draw();
std::cout << "绘制圆形\n";
}
这不是最常用的写法,但它说明:
text
"纯虚"主要表示派生类必须提供最终覆盖,
不等于这个函数绝对不能有定义。
第十章:虚析构函数------多态对象生命周期的底线
10.1 危险场景
假设派生类持有资源:
cpp
class Shape {
public:
virtual void draw() const = 0;
~Shape() {
std::cout << "销毁Shape\n";
}
};
class Circle : public Shape {
public:
Circle()
: data_(new int[100]) {}
~Circle() {
delete[] data_;
std::cout << "销毁Circle\n";
}
void draw() const override {}
private:
int* data_;
};
然后:
cpp
Shape* shape = new Circle;
delete shape;
如果基类析构函数不是虚函数,通过基类指针删除派生类对象会产生:
text
Undefined Behavior
= 未定义行为
程序可能只执行 Shape 析构函数,派生类资源不能正确释放;也可能出现其他不可预测结果。
10.2 正确写法
cpp
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
现在:
cpp
Shape* shape = new Circle;
delete shape;
析构顺序为:
text
Circle::~Circle()
↓
Shape::~Shape()
常用设计原则是:
如果一个类准备作为多态基类使用,通常应当提供 public 虚析构函数。
10.3 什么时候可以使用受保护的非虚析构函数
还有一种较少见但合理的设计:
cpp
class Base {
protected:
~Base() = default;
};
它表达:
text
允许继承Base,
但禁止外部通过Base*执行delete。
可以把常见原则总结成:
多态基类的析构函数要么 public 且 virtual,要么 protected 且 non-virtual。
这条经验通常称为:
text
Public virtual or protected non-virtual destructor
= 公开虚析构,或者受保护的非虚析构
10.4 纯虚析构函数也必须有定义
析构函数可以声明为纯虚函数:
cpp
class Shape {
public:
virtual ~Shape() = 0;
};
但必须在类外提供定义:
cpp
Shape::~Shape() = default;
原因是销毁派生类对象时,最后仍然需要执行基类析构部分。
第十一章:构造和析构期间为什么不能依赖动态多态
11.1 基类构造期间,派生类还没有准备好
cpp
class Base {
public:
Base() {
run();
}
virtual void run() {
std::cout << "Base::run\n";
}
};
class Derived : public Base {
public:
void run() override {
std::cout << "Derived::run\n";
}
};
创建:
cpp
Derived object;
基类构造函数中的 run() 调用的是:
text
Base::run
而不是 Derived::run。
构造顺序是:
text
先构造Base部分
↓
再构造Derived部分
执行 Base 构造函数时,Derived 的成员尚未构造完成。如果此时进入 Derived::run(),它可能访问尚未初始化的成员,因此 C++ 限制了构造期间的虚分派。
11.2 析构期间也是同样道理
析构顺序与构造相反:
text
先析构Derived部分
↓
再析构Base部分
进入 Base 析构函数时,Derived 部分已经被销毁,因此虚调用不会重新进入派生类实现。
11.3 实用建议
不要在构造函数和析构函数中依赖虚函数完成派生类特有行为。
尤其不要在构造或析构期间调用没有可用基类实现的纯虚函数。这种设计可能导致未定义行为、链接错误或运行时终止。
如果初始化过程确实需要派生类参与,可以考虑:
- 构造完成后显式调用初始化函数;
- 使用工厂函数分阶段创建;
- 把所需数据作为构造参数传给基类;
- 使用非虚辅助函数完成基类自己的初始化。
第十二章:编译器通常怎样实现虚函数
12.1 vptr 与 vtable
主流 C++ 编译器通常使用两个结构实现动态分派:
text
vptr
= Virtual Table Pointer
= 虚函数表指针
vtable
= Virtual Function Table
= 虚函数表
可以把多态对象的常见布局粗略理解成:
text
Circle对象
┌────────────────────────┐
│ vptr ─────────────────────→ Circle的虚函数表
│ Shape基类成员 │ draw → Circle::draw
│ Circle成员radius_ │ area → Circle::area
└────────────────────────┘ 析构 → Circle::~Circle
虚函数表通常保存一组函数地址。同一种动态类型的对象通常共享对应的虚函数表,每个对象内部保存指向表的指针。
12.2 一次虚函数调用的简化过程
cpp
Shape* shape = new Circle{2.0};
shape->draw();
可以粗略理解为:
text
1. 通过shape找到Circle对象
2. 从对象中读取vptr
3. 通过vptr找到Circle的虚函数表
4. 在draw对应的槽位取得函数地址
5. 调用Circle::draw()
虚函数在不同虚函数表中通常占据相应位置:
text
Shape虚函数表
槽位0 → Shape::draw
槽位1 → Shape::area
槽位2 → Shape析构
Circle虚函数表
槽位0 → Circle::draw
槽位1 → Circle::area
槽位2 → Circle析构
这解释了为什么调用方只知道 Shape*,仍然能够找到 Circle 的实现。
12.3 这是常见实现,不是标准强制布局
C++ 标准规定动态多态应该表现出什么行为,但通常不规定对象必须怎样布局。
因此:
text
vptr和vtable
是主流编译器的常见实现方式
具体放在哪里、有几个、怎样排列
属于编译器和ABI的实现细节
ABI 是:
text
Application Binary Interface
= 应用二进制接口
它规定编译后的模块怎样在二进制层面协作,例如对象布局、函数调用约定、名字修饰和虚函数表结构。
12.4 虚函数带来的典型成本
常见成本包括:
- 对象中可能增加一个或多个虚函数表指针;
- 调用虚函数通常多一次间接寻址;
- 间接调用可能增加内联难度;
- 多态对象经常经由指针访问,可能降低缓存局部性。
但"有虚函数就一定很慢"是不准确的。
现代编译器可能执行:
text
Devirtualization
= 去虚拟化
如果编译器能够证明真实类型,就可能把虚调用优化成普通直接调用,甚至继续内联。
例如:
cpp
Circle circle;
circle.draw();
编译器明确知道对象是 Circle,通常不需要真的查虚函数表。
性能是否重要,应通过实际测量判断,而不是看到 virtual 就下结论。
第十三章:虚函数中的特殊语言规则
13.1 默认参数不是动态绑定
虚函数本体可以动态绑定,但默认参数根据调用点看到的静态类型决定。
cpp
class Base {
public:
virtual void print(int value = 1) const {
std::cout << "Base: " << value << '\n';
}
};
class Derived : public Base {
public:
void print(int value = 2) const override {
std::cout << "Derived: " << value << '\n';
}
};
Derived object;
Base& reference = object;
reference.print();
输出:
text
Derived: 1
原因:
text
函数体
根据动态类型选择Derived::print
默认参数
根据静态类型Base&选择1
因此不要在覆盖函数中修改默认参数。更稳妥的做法是让默认值只出现在对外的非虚接口中。
13.2 访问权限与虚函数分派是两回事
派生类可以覆盖基类的 private 虚函数:
cpp
class Base {
public:
void run() {
execute();
}
private:
virtual void execute() {
std::cout << "Base\n";
}
};
class Derived : public Base {
private:
void execute() override {
std::cout << "Derived\n";
}
};
cpp
Derived object;
object.run(); // Derived
访问控制检查"调用者能不能直接访问这个名字",虚分派决定"已经形成虚调用后进入哪个最终覆盖函数"。两者不是同一阶段。
这种写法引出了:
text
NVI
= Non-Virtual Interface
= 非虚接口模式
基类提供稳定的 public 非虚函数,内部调用 protected 或 private 虚函数,把流程控制和可定制步骤分开。
13.3 限定名调用会关闭动态分派
cpp
Circle circle;
Shape& shape = circle;
shape.draw(); // 动态分派到Circle::draw
shape.Shape::draw(); // 显式调用Shape::draw
写出 Shape::draw() 相当于明确指定目标,不再进行正常动态分派。
13.4 构造函数不能是虚函数
创建对象之前必须先知道要分配多大空间、使用哪种布局,因此构造函数不能声明为虚函数:
cpp
// 错误
virtual Shape();
如果运行时需要创建不同类型对象,通常使用工厂函数,后面会讲。
13.5 静态成员函数不能是虚函数
虚函数依赖具体对象及其动态类型,静态成员函数不依赖任何对象,也没有 this 指针,因此不能是虚函数:
cpp
// 错误
virtual static void create();
13.6 函数模板不能直接成为虚函数
cpp
class Base {
public:
template<class T>
virtual void process(const T& value); // 错误
};
虚函数表需要一组可以确定的函数槽位,而函数模板可以针对无数类型产生不同实例,二者模型不匹配。
需要区分:
text
类模板中的普通成员函数
可以是虚函数
成员函数模板本身
不能是虚函数
例如下面是允许的:
cpp
template<class T>
class Base {
public:
virtual void process(const T& value) = 0;
};
当 Base<int> 实例化时,process(const int&) 是确定的普通虚函数。
13.7 友元函数不能直接是虚函数
Friend Function,中文叫"友元函数"。
友元函数不是类的成员函数,而虚函数必须是非静态成员函数,所以友元函数不能直接声明为虚函数。
第十四章:协变返回类型与多态复制
14.1 什么是协变返回类型
Covariant Return Type,中文叫"协变返回类型"。
覆盖函数的返回类型通常需要一致,但类类型的指针或引用存在特殊规则:
cpp
class Shape {
public:
virtual Shape* cloneRaw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
Circle* cloneRaw() const override {
return new Circle(*this);
}
};
基类返回 Shape*,派生类可以返回更具体的 Circle*。
协变主要适用于相关类类型的原生指针或引用,不会自动扩展到所有模板包装类型。
14.2 构造函数不能虚,但复制需求仍然存在
假设只有一个基类指针:
cpp
const Shape& shape = getShape();
我们不知道它实际是 Circle 还是 Rectangle,但希望复制出同样真实类型的新对象。
普通复制:
cpp
Shape copy = shape;
会产生切片,而且抽象基类甚至不能这样创建。
常用解决方案是:
text
Virtual Constructor Idiom
= 虚拟构造函数惯用法
它不是真的让构造函数变成虚函数,而是通过虚函数 clone() 创建正确类型。
14.3 使用智能指针实现 clone
cpp
#include <memory>
class Shape {
public:
virtual std::unique_ptr<Shape> clone() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
explicit Circle(double radius)
: radius_(radius) {}
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Circle>(*this);
}
void draw() const override {
std::cout << "绘制圆形\n";
}
private:
double radius_;
};
使用:
cpp
std::unique_ptr<Shape> copy =
original->clone();
虽然接口返回 std::unique_ptr<Shape>,内部真正创建的是 Circle。
unique_ptr<Circle> 可以移动转换成 unique_ptr<Shape>。这里不能把派生类返回类型直接改成 unique_ptr<Circle> 来形成协变,因为 unique_ptr<Circle> 和 unique_ptr<Shape> 是两个不同模板实例,不属于语言规定的协变返回类型。
第十五章:用智能指针管理多态对象
15.1 为什么优先使用 unique_ptr
直接写:
cpp
Shape* shape = new Circle{2.0};
需要手动:
cpp
delete shape;
一旦中间发生异常、提前返回或控制流变复杂,就容易忘记释放。
更推荐:
cpp
std::unique_ptr<Shape> shape =
std::make_unique<Circle>(2.0);
unique_ptr 表示:
text
Exclusive Ownership
= 独占所有权
同一时刻主要由一个 unique_ptr 负责对象生命周期,离开作用域时自动删除对象。
基类必须有虚析构函数,才能保证 unique_ptr<Shape> 正确删除 Circle。
15.2 建立多态对象集合
cpp
#include <memory>
#include <vector>
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(
std::make_unique<Circle>(2.0)
);
shapes.push_back(
std::make_unique<Rectangle>(3.0, 4.0)
);
for (const auto& shape : shapes) {
shape->draw();
std::cout << shape->area() << '\n';
}
容器保存的都是:
cpp
std::unique_ptr<Shape>
但每个指针管理的真实对象可以不同。
text
unique_ptr<Shape> → Circle
unique_ptr<Shape> → Rectangle
unique_ptr<Shape> → Triangle
这正是动态多态最常见的工程形态。
15.3 工厂函数
Factory Function,中文叫"工厂函数"。
它把"创建哪一种具体对象"的判断集中起来:
cpp
#include <memory>
#include <stdexcept>
#include <string_view>
std::unique_ptr<Shape>
createShape(std::string_view type) {
if (type == "circle") {
return std::make_unique<Circle>(2.0);
}
if (type == "rectangle") {
return std::make_unique<Rectangle>(
3.0,
4.0
);
}
throw std::invalid_argument{
"unknown shape type"
};
}
调用方只依赖抽象接口:
cpp
auto shape = createShape("circle");
shape->draw();
调用方不需要知道具体类的构造细节。
15.4 什么时候使用 shared_ptr
shared_ptr 表示:
text
Shared Ownership
= 共享所有权
多个 shared_ptr 可以共同拥有一个对象:
cpp
std::shared_ptr<Shape> shape =
std::make_shared<Circle>(2.0);
auto anotherOwner = shape;
最后一个所有者销毁时,对象才被释放。
只有真正存在共享生命周期语义时才应使用 shared_ptr。不能因为"不知道谁负责释放"就默认使用共享所有权;那往往只是所有权设计不清晰。
还要注意循环引用:
text
A通过shared_ptr拥有B
B又通过shared_ptr拥有A
双方引用计数永远不能归零。此时一方通常应使用:
text
weak_ptr
= 弱引用智能指针
weak_ptr 可以观察 shared_ptr 管理的对象,但不增加强所有权计数。
第十六章:RTTI、dynamic_cast 与 typeid
16.1 什么是 RTTI
RTTI 是:
text
Run-Time Type Information
= 运行时类型信息
它让程序在运行时查询多态对象的真实类型,相关工具主要包括:
text
dynamic_cast
typeid
std::type_info
16.2 向上转换通常是安全的
cpp
Circle circle;
Shape* shape = &circle;
这是从派生类向基类转换,通常可以隐式完成。
16.3 向下转换可能失败
Downcasting,中文叫"向下转换"。
cpp
Shape* shape = getShape();
假设 Circle 还提供了自己特有的 setRadius() 成员函数。如果想判断 shape 是否真的指向 Circle,可以使用:
cpp
if (Circle* circle =
dynamic_cast<Circle*>(shape)) {
circle->setRadius(10.0);
}
指针版本转换失败时返回:
cpp
nullptr
引用版本失败时抛出:
text
std::bad_cast
cpp
try {
Circle& circle =
dynamic_cast<Circle&>(*shape);
circle.setRadius(10.0);
} catch (const std::bad_cast&) {
std::cout << "不是Circle\n";
}
用于向下转换的基类通常必须是多态类型,也就是至少拥有一个虚函数。
16.4 为什么不能随意使用 static_cast 向下转换
cpp
Shape* shape = new Rectangle{3.0, 4.0};
Circle* circle =
static_cast<Circle*>(shape);
static_cast 不会进行这种运行时类型检查。shape 实际指向 Rectangle,却被当成 Circle 使用,后续行为可能产生未定义行为。
如果真实类型不确定,应使用 dynamic_cast;如果程序频繁需要向下转换,则应该重新检查接口设计。
16.5 typeid
cpp
#include <typeinfo>
if (typeid(*shape) == typeid(Circle)) {
std::cout << "真实类型是Circle\n";
}
当表达式引用的是多态对象时:
cpp
typeid(*shape)
可以得到动态类型。
但:
cpp
typeid(shape)
查询的是指针表达式本身的类型,也就是 Shape*,不是所指对象的动态类型。
16.6 少写类型判断,多设计虚函数
如果程序到处都是:
cpp
if (dynamic_cast<Circle*>(shape)) {
...
} else if (
dynamic_cast<Rectangle*>(shape)
) {
...
}
说明调用方仍然在手动区分每一种具体类型。
通常更好的方式是把变化行为放进虚函数:
cpp
shape->draw();
shape->area();
不过 dynamic_cast 并非绝对错误。它在插件接口、编辑器对象查询、多继承横向转换和确实需要派生类能力的边界位置仍然有合理用途。
16.7 智能指针怎样安全转换
对于 shared_ptr,标准库提供了配套转换函数:
cpp
std::shared_ptr<Shape> shape =
std::make_shared<Circle>(2.0);
std::shared_ptr<Circle> circle =
std::dynamic_pointer_cast<Circle>(
shape
);
if (circle) {
circle->setRadius(10.0);
}
std::dynamic_pointer_cast 内部进行安全的运行时类型检查。失败时返回空 shared_ptr,成功时得到一个共享同一控制块的新智能指针。
不要为了转换而写出:
cpp
// 不推荐:容易制造重复所有权和重复释放
std::shared_ptr<Circle>(
static_cast<Circle*>(shape.get())
);
对于 unique_ptr,标准库没有完全对应的 dynamic_pointer_cast。因为转换失败时必须明确谁继续拥有原对象,所有权处理更敏感。通常应优先通过良好的虚函数接口避免向下转换;确实需要转换时,要专门设计清晰的所有权转移函数。
16.8 多继承中的横向转换
Cross Cast,中文可以叫"横向转换"。
假设一个具体对象同时实现两个接口:
cpp
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() = default;
};
class Serializable {
public:
virtual void save() const = 0;
virtual ~Serializable() = default;
};
class Document
: public Drawable,
public Serializable {
public:
void draw() const override {}
void save() const override {}
};
已知一个 Drawable*,可以询问同一个真实对象是否也实现 Serializable:
cpp
Drawable* drawable = getDrawable();
if (auto* serializable =
dynamic_cast<Serializable*>(
drawable
)) {
serializable->save();
}
转换不是沿着单条继承链简单向下走,而是在同一个最派生对象的不同基类接口之间移动,因此叫横向转换。
第十七章:多重继承、菱形继承与虚继承
17.1 多重继承
Multiple Inheritance,中文叫"多重继承"。
一个类可以同时继承多个基类:
cpp
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() = default;
};
class Serializable {
public:
virtual void save() const = 0;
virtual ~Serializable() = default;
};
class Document
: public Drawable,
public Serializable {
public:
void draw() const override {
std::cout << "绘制文档\n";
}
void save() const override {
std::cout << "保存文档\n";
}
};
这类"继承多个纯接口"的设计通常比继承多个带复杂状态的基类更容易控制。
17.2 多态子对象与指针调整
多重继承对象内部可能包含多个基类子对象:
text
Document对象
┌─────────────────────┐
│ Drawable基类子对象 │
├─────────────────────┤
│ Serializable子对象 │
├─────────────────────┤
│ Document自己的成员 │
└─────────────────────┘
把 Document* 转成不同基类指针时,指针数值可能需要调整:
cpp
Document document;
Drawable* drawable = &document;
Serializable* serializable = &document;
两个基类子对象未必位于相同地址。
编译器可能生成一小段辅助代码调整 this 指针,这类辅助代码常被称为:
text
Thunk
= 调整跳板或辅助调用代码
具体布局属于 ABI 实现细节。
17.3 菱形继承问题
Diamond Inheritance,中文叫"菱形继承"。
text
Object
/ \
Drawable Named
\ /
Widget
示例:
cpp
class Object {
public:
int id = 0;
};
class Drawable : public Object {};
class Named : public Object {};
class Widget
: public Drawable,
public Named {};
Widget 中包含两份 Object 基类子对象:
cpp
Widget widget;
// 有歧义:到底访问哪一份Object::id?
// widget.id = 10;
17.4 虚继承解决共享基类问题
Virtual Inheritance,中文叫"虚继承"。
cpp
class Drawable
: virtual public Object {};
class Named
: virtual public Object {};
现在 Widget 最终只共享一份 Object 子对象。
需要特别区分:
text
虚函数 virtual function
解决运行时函数分派
虚继承 virtual inheritance
解决多条继承路径共享同一基类子对象
它们都使用 virtual 关键字,但解决的是完全不同的问题。
虚继承还会让对象布局和构造责任更复杂:最派生类负责初始化虚基类。
第十八章:单分派、双分派与访问者模式
18.1 普通虚函数是单分派
Dispatch,中文叫"分派",表示根据某些信息选择最终函数。
C++ 普通虚函数主要是:
text
Single Dispatch
= 单分派
例如:
cpp
shape->draw();
最终调用哪个 draw(),主要根据 shape 所指对象的动态类型决定。
18.2 行为同时依赖两个动态类型怎么办
假设要处理不同图形之间的碰撞:
text
Circle与Circle碰撞
Circle与Rectangle碰撞
Rectangle与Rectangle碰撞
结果同时依赖左右两个对象的真实类型。
这种需求叫:
text
Double Dispatch
= 双分派
C++ 没有直接内置通用的双分派语法,常见解决办法包括:
- Visitor Pattern,访问者模式;
- 两次虚函数调用;
dynamic_cast组合;std::variant与std::visit;- 手工类型表。
18.3 访问者模式的核心思想
Visitor Pattern,中文叫"访问者模式"。
它通过两次动态调用,让操作同时感知对象类型和访问者类型。经典结构大致如下:
cpp
class Circle;
class Rectangle;
class ShapeVisitor {
public:
virtual void visit(const Circle&) = 0;
virtual void visit(const Rectangle&) = 0;
virtual ~ShapeVisitor() = default;
};
class Shape {
public:
virtual void accept(
ShapeVisitor& visitor
) const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void accept(
ShapeVisitor& visitor
) const override {
visitor.visit(*this);
}
};
第一次分派:
text
shape->accept(visitor)
根据shape动态类型进入Circle::accept
第二次调用:
text
visitor.visit(*this)
选择visit(const Circle&)
访问者模式适合"类型集合相对稳定、操作经常增加"的场景,但代码结构较重,不应为了展示技巧而滥用。
第十九章:动态多态中的设计原则
19.1 多态不等于"有继承就行"
真正合理的继承应该表达:
text
Derived is a Base
= 派生类确实是一种基类
如果只是想复用代码,组合往往更合适:
cpp
class Report {
private:
Formatter formatter_;
};
这叫:
text
Composition
= 组合
常见经验是:
优先考虑组合;只有在需要真正的可替换类型关系时,再使用 public 继承。
19.2 里氏替换原则
LSP 是:
text
Liskov Substitution Principle
= 里氏替换原则
通俗地说:
任何需要基类对象的地方,换成派生类对象后,程序的基本语义仍然应该合理。
例如基类承诺:
text
resize(width, height)
可以独立修改宽和高
某个派生类却强制宽高永远相等,就可能破坏调用方预期。语法上能继承,不代表语义上适合继承。
19.3 开闭原则
OCP 是:
text
Open-Closed Principle
= 开闭原则
含义是:
text
对扩展开放
对修改关闭
有了稳定的 Shape 接口,增加 Triangle 时,绘制循环通常不必修改:
cpp
for (const auto& shape : shapes) {
shape->draw();
}
19.4 依赖倒置原则
DIP 是:
text
Dependency Inversion Principle
= 依赖倒置原则
高层代码依赖抽象接口,不直接依赖具体实现:
cpp
void render(const Shape& shape) {
shape.draw();
}
render() 依赖 Shape 抽象,而不是写死 Circle。
19.5 接口隔离原则
ISP 是:
text
Interface Segregation Principle
= 接口隔离原则
不要让所有派生类被迫实现一个巨大接口。
与其:
cpp
class Object {
public:
virtual void draw() = 0;
virtual void save() = 0;
virtual void print() = 0;
virtual void send() = 0;
};
不如拆分成:
text
Drawable
Savable
Printable
Sendable
具体类型只实现自己真正支持的能力。
第二十章:动态多态常见设计模式
动态多态是许多经典设计模式的基础。
20.1 策略模式
Strategy Pattern,中文叫"策略模式"。
把可替换算法抽象成统一接口:
cpp
class SortStrategy {
public:
virtual void sort(
std::vector<int>& values
) const = 0;
virtual ~SortStrategy() = default;
};
程序运行时可以替换不同排序策略。
20.2 状态模式
State Pattern,中文叫"状态模式"。
把不同状态下的行为放进不同状态对象,而不是写一大串 if-else。
20.3 模板方法模式
Template Method Pattern,中文叫"模板方法模式"。
这里的"模板"不是 C++ template 关键字,而是"流程模板"。
基类定义固定流程,虚函数提供可变化步骤:
cpp
class Task {
public:
void execute() {
prepare();
run();
finish();
}
virtual ~Task() = default;
private:
void prepare() {
std::cout << "公共准备\n";
}
virtual void run() = 0;
void finish() {
std::cout << "公共收尾\n";
}
};
这也是 NVI 的一种典型使用方式。
20.4 工厂方法和抽象工厂
Factory Method,工厂方法,用于把对象创建过程放到可扩展接口中。
Abstract Factory,抽象工厂,用于创建一组相互关联的对象。
20.5 观察者、命令、装饰器
text
Observer Pattern
观察者模式
通过统一接口通知不同观察者
Command Pattern
命令模式
把操作封装成多态对象
Decorator Pattern
装饰器模式
在保持接口一致的情况下组合额外行为
设计模式不是必须照搬的代码模板,而是对常见设计问题的命名和经验总结。
第二十一章:什么时候不应该使用动态多态
动态多态很有价值,但不是唯一选择。
21.1 静态多态
如果具体类型在编译期已知,可以使用模板或 Concepts:
cpp
template<class T>
void drawObject(const T& object) {
object.draw();
}
优点:
- 容易内联;
- 不要求共同基类;
- 可以保留值语义;
- 编译期可以进行更强约束。
代价:
- 模板实现通常需要放在头文件;
- 可能增加编译时间和代码体积;
- 运行时不容易替换未知类型。
21.2 std::variant
如果所有可能类型在编译期形成一个封闭集合:
cpp
using ShapeValue =
std::variant<Circle, Rectangle>;
可以使用:
cpp
std::visit(
[](const auto& shape) {
shape.draw();
},
value
);
variant 适合:
text
类型种类有限且已知
希望使用值语义
不想进行堆分配
动态多态则更适合:
text
类型集合开放
未来可以增加新派生类
运行时需要替换实现
调用方只依赖稳定接口
21.3 类型擦除
Type Erasure,中文叫"类型擦除"。
它把具体类型隐藏在统一包装对象之后。标准库中的典型例子是:
cpp
std::function<void()>
它可以保存函数指针、Lambda 和函数对象,调用方不必知道真实类型。
类型擦除同样可能在内部使用虚函数,但不会把继承要求暴露给使用者。
21.4 普通组合
如果只是想复用实现或替换某个内部部件,组合往往比继承清晰:
cpp
class Renderer {
public:
explicit Renderer(
std::unique_ptr<Backend> backend
)
: backend_(std::move(backend)) {}
private:
std::unique_ptr<Backend> backend_;
};
这里 Renderer 不需要"是一种 Backend",而是"拥有一个 Backend"。
第二十二章:性能、ABI 与工程边界
22.1 堆分配不是虚函数的必然要求
动态多态经常和 new、智能指针一起出现,但虚函数本身不要求对象必须位于堆上:
cpp
Circle circle{2.0};
Shape& shape = circle;
shape.draw();
这里没有任何堆分配。
堆分配通常是因为:
- 对象需要超出当前作用域;
- 需要把不同大小的派生对象放进同一容器;
- 需要转移或共享所有权;
- 运行时才决定创建哪种对象。
22.2 二进制兼容问题
虚函数表和对象布局与 ABI 密切相关。
在共享库或插件接口中,下面的变化可能破坏二进制兼容:
- 增加、删除或调整虚函数;
- 改变继承关系;
- 修改类成员布局;
- 更换编译器或不兼容的编译选项;
- 改变运行库配置。
跨动态库边界还要明确:
text
对象由谁创建?
对象由谁销毁?
双方是否使用兼容的运行库?
异常是否允许跨边界传播?
STL类型能否安全跨边界?
接口如何进行版本管理?
大型插件接口常使用工厂函数和配套销毁函数,或者使用 PImpl 等技术降低 ABI 耦合。
PImpl 是:
text
Pointer to Implementation
= 指向实现的指针
它把实现细节隐藏在单独对象中,减少头文件依赖和公开对象布局变化。
22.3 优化时先测量
动态多态可能带来间接调用成本,但程序性能通常还取决于:
- 算法复杂度;
- 内存访问模式;
- 缓存命中率;
- 堆分配次数;
- 数据布局;
- 编译器优化能力;
- 实际调用频率。
如果某个虚函数内部执行大量工作,一次间接调用的成本可能微不足道。如果是在极高频、极短小的循环内调用,则可能值得比较静态多态、连续数据布局或批处理方案。
第二十三章:常见错误与排查方法
23.1 忘记虚析构函数
cpp
class Base {
public:
virtual void run() = 0;
~Base() = default; // 危险
};
多态基类通常应写:
cpp
virtual ~Base() = default;
23.2 忘记 override
cpp
void draw(); // 可能因为少了const而没有覆盖
推荐:
cpp
void draw() const override;
23.3 按值传递基类
cpp
void process(Shape shape);
会发生切片。改为:
cpp
void process(const Shape& shape);
23.4 使用 vector<Base>
cpp
std::vector<Shape> shapes;
无法保留派生类部分。通常改为:
cpp
std::vector<std::unique_ptr<Shape>>
shapes;
23.5 在构造函数中期待调用派生类覆盖
基类构造期间不会按完整派生类进行虚分派。把需要派生类参与的操作移到构造完成之后。
23.6 覆盖函数使用不同默认参数
函数实现动态绑定,默认参数静态绑定,容易产生意外组合。默认参数应保持一致,或者放在统一非虚接口中。
23.7 到处使用 dynamic_cast
偶尔的边界类型查询是合理的;如果核心业务逻辑到处判断具体类型,应考虑把行为移入虚函数或重新划分接口。
23.8 为了复用代码而滥用继承
继承首先表达类型关系,不只是代码复用工具。仅仅"内部要用另一个类的功能"时,优先考虑组合。
23.9 混淆虚函数与虚继承
text
虚函数
运行时选择函数
虚继承
菱形继承中共享基类子对象
23.10 认为虚函数一定需要堆对象
栈对象同样可以通过基类引用进行动态分派。是否使用堆,取决于生命周期和所有权,不取决于 virtual 本身。
第二十四章:一个完整的多态程序
下面把抽象接口、虚析构、智能指针、工厂函数和多态集合串起来。
完整示例使用 C++20 的 std::numbers::pi,可以使用 -std=c++20 编译。
cpp
#include <iostream>
#include <memory>
#include <numbers>
#include <stdexcept>
#include <string_view>
#include <vector>
class Shape {
public:
virtual void draw() const = 0;
virtual double area() const = 0;
virtual std::unique_ptr<Shape>
clone() const = 0;
virtual ~Shape() = default;
};
class Circle final : public Shape {
public:
explicit Circle(double radius)
: radius_(radius) {}
void draw() const override {
std::cout
<< "绘制半径为"
<< radius_
<< "的圆形\n";
}
double area() const override {
return std::numbers::pi
* radius_
* radius_;
}
std::unique_ptr<Shape>
clone() const override {
return std::make_unique<Circle>(
*this
);
}
private:
double radius_;
};
class Rectangle final : public Shape {
public:
Rectangle(double width, double height)
: width_(width),
height_(height) {}
void draw() const override {
std::cout
<< "绘制"
<< width_
<< "×"
<< height_
<< "的矩形\n";
}
double area() const override {
return width_ * height_;
}
std::unique_ptr<Shape>
clone() const override {
return std::make_unique<Rectangle>(
*this
);
}
private:
double width_;
double height_;
};
std::unique_ptr<Shape>
createShape(std::string_view type) {
if (type == "circle") {
return std::make_unique<Circle>(2.0);
}
if (type == "rectangle") {
return std::make_unique<Rectangle>(
3.0,
4.0
);
}
throw std::invalid_argument{
"unknown shape type"
};
}
void printShape(const Shape& shape) {
shape.draw();
std::cout
<< "面积:"
<< shape.area()
<< "\n\n";
}
int main() {
std::vector<std::unique_ptr<Shape>>
shapes;
shapes.push_back(
createShape("circle")
);
shapes.push_back(
createShape("rectangle")
);
for (const auto& shape : shapes) {
printShape(*shape);
}
auto copiedShape =
shapes.front()->clone();
std::cout << "复制出的图形:\n";
printShape(*copiedShape);
}
观察它们之间的关系:
text
Shape抽象类
规定所有图形都必须提供哪些行为
↓
Circle和Rectangle
实现各自不同的行为
↓
createShape工厂
在运行时创建具体对象
↓
unique_ptr<Shape>
统一管理不同派生对象的生命周期
↓
vector<unique_ptr<Shape>>
把不同图形放进同一个集合
↓
shape->draw()和shape->area()
根据真实对象类型进行运行时分派
↓
clone()
在不知道具体类型时完成多态复制
这就是现代 C++ 中一套完整、常见的动态多态使用方式。
第二十五章:把全部知识连成一条主线
现在回到最开始的问题:我们为什么需要动态多态?
text
不同类型拥有相同概念上的操作
↓
使用public继承表达"派生类是一种基类"
↓
基类用virtual声明可变化行为
↓
派生类用override提供具体实现
↓
基类指针或引用保留对象动态类型
↓
运行时根据动态类型选择最终覆盖函数
↓
抽象类和纯虚函数用于只规定接口
↓
虚析构保证通过基类正确销毁派生对象
↓
unique_ptr管理独占所有权
↓
工厂函数负责运行时创建具体对象
↓
clone解决多态对象复制问题
↓
RTTI在确有需要时查询真实类型
↓
多继承和虚继承处理更复杂的类型关系
↓
设计原则保证继承在语义上仍然合理
最后需要牢牢记住的不是某个语法,而是下面四个问题:
问题一:调用为什么进入派生类函数
因为基类把函数声明为虚函数,调用表达式对应的对象具有派生类动态类型,运行时选择了最终覆盖函数。
问题二:对象为什么不能按基类值传递
因为按值复制会创建独立基类对象,派生部分被切掉,动态类型也随之丢失。
问题三:对象由谁销毁
所有权必须清晰。优先使用 unique_ptr;确实共享生命周期时使用 shared_ptr;多态基类通常需要虚析构函数。
问题四:这里真的需要动态多态吗
如果类型集合开放、运行时需要替换实现、调用方只依赖稳定接口,动态多态通常很合适。
如果类型在编译期已知,或者类型集合固定并且希望保留值语义,则模板、Concepts、std::variant、类型擦除或普通组合可能更合适。
结语
C++ 动态多态的核心可以浓缩成一句话:
通过抽象基类规定统一接口,通过虚函数让不同派生对象在运行时表现出各自行为,再用正确的所有权和生命周期设计安全地管理这些对象。
virtual 只是入口。真正完整的动态多态还包括:
- 继承关系是否合理;
- 静态类型和动态类型的区别;
- 覆盖、重载和隐藏;
- 对象切片;
- 抽象类与纯虚函数;
- 虚析构和对象生命周期;
- 构造析构期间的分派规则;
- 虚函数表的常见实现原理;
- 默认参数、访问控制和协变返回;
- 智能指针、工厂与多态复制;
- RTTI 和安全类型转换;
- 多重继承、虚继承与双分派;
- 设计原则、性能、ABI 和替代方案。
当这些内容能够串成一条逻辑链时,动态多态就不再是一组零散规则,而会成为一种可以主动选择、正确实现并安全维护的程序设计工具。