// 父类:定义统一接口
class Shape {
public:
// 声明为虚函数,支持动态绑定
virtual void draw() const {
cout << "绘制一个通用图形" << endl;
}
// 虚析构函数:保证子类资源正确释放
virtual ~Shape() {}
};
// 子类1:重写虚函数
class Circle : public Shape {
public:
void draw() const override {
cout << "绘制一个圆形" << endl;
}
};
// 子类2:重写虚函数
class Rectangle : public Shape {
public:
void draw() const override {
cout << "绘制一个矩形" << endl;
}
};
// 统一调用接口:只依赖父类,与具体子类解耦
void renderShape(const Shape* shape) {
shape->draw(); // 运行时根据实际对象类型执行对应draw
}
int main() {
Shape* s1 = new Circle();
Shape* s2 = new Rectangle();
renderShape(s1); // 输出:绘制一个圆形
renderShape(s2); // 输出:绘制一个矩形
delete s1;
delete s2;
return 0;
}
这段代码是 C++动态多态的经典入门范例,核心是用「父类统一接口 + 子类差异化实现」,让同一句调用代码在传入不同对象时表现出完全不同的行为 —— 就像同一个 “绘制” 指令,给圆形就画圆,给矩形就画矩形,这就是 “多态(多种形态)” 的本义。
下面我们从设计初衷、逐行代码、关键字底层原理、多种实现形式、运行时流程、常见坑点六个维度,把这段代码彻底讲透。
一、先搞懂:我们为什么要写这段代码?
在没有多态的年代,如果要实现 “绘制不同图形” 的逻辑,你大概率会写成这样:
cpp
运行
void renderCircle(Circle* c) { c->draw(); }
void renderRectangle(Rectangle* r) { r->draw(); }
// 新增三角形,就要再加一个 renderTriangle 函数
每新增一种图形,就要新增一套调用逻辑,调用方的代码要反复修改,既繁琐又容易出 bug,完全违背了软件工程里「对扩展开放,对修改关闭」的开闭原则。
而多态的核心价值就是:调用方只和抽象的父类打交道,完全不用关心有多少种子类、子类怎么实现。新增图形时,只需要写一个继承父类的新子类,原有调用逻辑一行都不用改。
二、逐模块逐行代码精讲
1. 父类 Shape:定义统一的抽象规范
cpp
运行
class Shape {
public:
// 声明为虚函数,支持动态绑定
virtual void draw() const {
cout << "绘制一个通用图形" << endl;
}
// 虚析构函数:保证子类资源正确释放
virtual ~Shape() {}
};
- class Shape:定义抽象的 “图形” 基类,提取所有图形的共性 —— 都能被 “绘制”。
- virtual void draw() const:
- virtual:标记这是虚函数,是开启多态的总开关,告诉编译器这个函数要支持运行时动态绑定。
- void draw():函数名、返回值、参数列表(无参)构成了函数签名,所有子类重写时必须和它完全一致。
- const:常成员函数修饰,承诺函数内部不会修改对象的任何成员变量;子类重写时也必须保留const,否则不构成重写。
- 大括号内是默认实现:如果某个子类不重写draw,就会执行这个通用逻辑。
- virtual ~Shape() {}:虚析构函数,是多态场景下的必写项。如果没有它,通过父类指针删除子类对象时会出现内存泄漏,后面会专门讲底层原因。
2. 子类 Circle 与 Rectangle:差异化的具体实现
cpp
运行
// 子类1:重写虚函数
class Circle : public Shape {
public:
void draw() const override {
cout << "绘制一个圆形" << endl;
}
};
// 子类2:重写虚函数
class Rectangle : public Shape {
public:
void draw() const override {
cout << "绘制一个矩形" << endl;
}
};
- class Circle : public Shape:public 公有继承,表达 “圆形是一种图形” 的 is-a 关系,子类天然拥有父类的所有成员。
- void draw() const override:
- 函数签名(返回值、函数名、参数、const)和父类完全一致,这个行为叫做重写(Override),是多态的实现基础。
- override关键字:告诉编译器 “我明确要重写父类的虚函数”,让编译器帮你做语法校验,是防错神器。
- 函数体内是子类专属的业务逻辑:圆形有圆形的绘制方式,矩形有矩形的绘制方式。
3. 统一调用接口:解耦的核心
cpp
运行
// 统一调用接口:只依赖父类,与具体子类解耦
void renderShape(const Shape* shape) {
shape->draw(); // 运行时根据实际对象类型执行对应draw
}
- const Shape* shape:函数参数是父类指针,这是多态的核心载体。
- C++ 语法允许父类指针隐式指向子类对象(向上转型),这是安全的;正因为有这个特性,我们才能用一个参数接收所有子类对象。
- const修饰指针指向的对象,承诺函数内不会修改图形对象,和draw()的const属性对应。
- shape->draw():这一行就是多态真正发生的地方。
- 编译阶段:编译器只知道shape是Shape*类型,无法确定它指向什么子类,所以不会把函数调用写死。
- 运行阶段:程序会根据指针指向的真实对象类型,去执行对应子类的draw函数,这就是动态绑定(运行时绑定)。
4. main 函数:多态效果验证
cpp
运行
int main() {
Shape* s1 = new Circle();
Shape* s2 = new Rectangle();
renderShape(s1); // 输出:绘制一个圆形
renderShape(s2); // 输出:绘制一个矩形
delete s1;
delete s2;
return 0;
}
- Shape* s1 = new Circle():在堆内存上创建一个Circle对象,并用父类指针Shape*指向它。这里发生了隐式的向上转型,子类指针被安全地转换成了父类指针。
- renderShape(s1):传入父类指针,函数内部调用draw时,会自动识别出真实对象是圆形,执行Circle::draw。
- delete s1; delete s2:释放堆内存。因为父类有虚析构函数,所以会先调用子类析构、再调用父类析构,完整释放资源。
三、两个核心关键字的底层深度解析
1. virtual:多态的灵魂,动态绑定的开关
virtual是整个多态机制的根基,没有它就没有动态多态。
静态绑定 vs 动态绑定
- 静态绑定(编译期绑定):普通成员函数的调用方式。编译阶段就根据指针 / 引用的类型,写死要调用的函数地址。
- 如果draw不加virtual,哪怕Shape*指向Circle,调用draw也只会执行Shape::draw,永远不会调用子类的实现。
- 动态绑定(运行期绑定):虚函数的调用方式。编译阶段只做语法检查,真正调用哪个函数要等程序运行时,根据对象的真实类型决定。
底层原理:虚函数表(vtable)与虚指针(vptr)
虚函数的动态绑定不是魔法,是靠两张表实现的:
- Shape类的 vtable 里存着Shape::draw和Shape::~Shape的地址
- Circle类的 vtable 里存着重写后的Circle::draw和Circle::~Circle的地址
- Rectangle类的 vtable 里存着Rectangle::draw和Rectangle::~Rectangle的地址
当你用父类指针调用draw()时,程序执行三步:
这就是动态绑定的完整底层流程。
为什么析构函数必须加 virtual?
如果析构函数不是虚函数,delete s1的时候,编译器会做静态绑定 —— 只看指针类型是Shape*,就只调用Shape::~Shape(),子类的析构函数完全不会执行。
如果子类里申请了堆内存、打开了文件、持有了锁,子类析构不执行就会直接造成内存泄漏、资源泄漏。
写成虚析构后,delete会走虚函数表的动态绑定流程:先调用子类析构释放子类资源,再调用父类析构释放父类资源,完整销毁整个对象。
工程界铁律:只要一个类会被当作父类继承,并且会用父类指针操作子类对象,就必须写虚析构函数。
2. override:重写的安全卫士,编译期排雷
override是 C++11 引入的关键字,它不影响程序运行逻辑,唯一的作用是:强制编译器检查子类函数是否正确重写了父类的虚函数。
重写有非常严格的要求:函数名、参数列表、返回值、const 属性、引用限定符,必须和父类虚函数完全一致,差一个都不叫重写。
举个反面例子,如果你手滑少写了const:
cpp
运行
class Circle : public Shape {
public:
// 少了const,签名和父类不一致,这根本不是重写!
void draw() override { // 加了override → 编译直接报错
cout << "绘制一个圆形" << endl;
}
};
- 不加override:编译器不会报错,它会认为你定义了一个和父类draw同名的新函数,多态直接失效。这种隐性 bug 非常难排查。
- 加了override:编译器立刻报错,告诉你 “该函数没有重写父类的任何虚函数”,在编译阶段就把问题掐灭。
四、多态的多种实现形式与对比
1. 普通子类对象直接实例化(无多态,静态绑定)
不使用父类指针 / 引用,直接创建子类对象调用方法,属于静态绑定,没有多态效果。
cpp
运行
int main() {
Circle c;
Rectangle r;
c.draw(); // 编译期就确定调用Circle::draw
r.draw(); // 编译期就确定调用Rectangle::draw
return 0;
}
- 特点:简单直接,无虚函数表额外开销
- 缺点:无法统一管理,新增子类就要新增调用代码,扩展性差
2. 父类引用实现多态(简洁安全的写法)
除了父类指针,父类引用同样可以触发动态多态,底层原理和指针完全一致,语法更简洁,适合栈对象。
cpp
运行
// 统一接口用引用接收
void renderShape(const Shape& shape) {
shape.draw(); // 同样触发动态绑定
}
int main() {
Circle c;
Rectangle r;
renderShape(c); // 输出:绘制一个圆形
renderShape(r); // 输出:绘制一个矩形
return 0;
}
- 特点:不用手动管理内存,语法更安全,没有空指针风险
- 适用场景:局部对象、生命周期短的场景
3. 不封装统一接口的多态(不推荐)
不写renderShape统一函数,直接用父类指针调用,语法上依然是多态,但失去了解耦和可维护性。
cpp
运行
int main() {
Shape* s1 = new Circle();
Shape* s2 = new Rectangle();
// 没有统一接口,逐个手动调用
s1->draw(); // 依然是多态,输出:绘制一个圆形
s2->draw(); // 输出:绘制一个矩形
delete s1;
delete s2;
return 0;
}
- 为什么不推荐:多态的核心价值是「调用方与子类解耦」。如果每次新增子类都要手动加指针、加调用,和不用多态没有本质区别。
- 封装统一接口后,后续要加 “绘制前校验、绘制后打日志” 的逻辑,只改renderShape一个函数就行,所有子类自动生效。
五、完整运行时流程一步一步走
以Shape* s1 = new Circle(); renderShape(s1);为例,看程序运行时到底发生了什么:
- 在堆内存分配一块空间,包含 Circle 对象的隐藏成员 vptr
- 调用 Circle 构造函数,将对象的 vptr 指向 Circle 类的虚函数表
- 返回对象的内存地址
- 父类指针 s1 保存 Circle 对象的地址,静态类型是Shape*,但指向的动态类型是Circle
- 形参shape接收地址,静态类型依然是const Shape*
- 通过 shape 指针找到对象首地址,取出 vptr
- 通过 vptr 找到 Circle 类的虚函数表
- 在表中查到draw对应Circle::draw的地址
- 跳转执行函数,输出 “绘制一个圆形”
- 通过虚函数表找到析构函数地址
- 先执行Circle::~Circle()
- 再执行Shape::~Shape()
- 释放堆内存,对象销毁完成
六、常见误区与踩坑总结
误区:有继承就有多态 错。必须同时满足三个条件:有继承、有虚函数重写、父类指针 / 引用调用虚函数,才是动态多态。
误区:子类重写必须加 virtual 子类重写的函数即使不加 virtual,也会继承父类的虚函数属性。但规范写法是:父类写 virtual,子类写 override,可读性和安全性最高。
误区:构造函数也能是虚函数 错。构造对象时 vptr 还没初始化,虚函数机制无法工作,因此构造函数不能是虚函数。
对象切片陷阱 如果直接用父类对象接收子类对象:Shape s = Circle();,会发生对象切片—— 子类的专属部分会被切掉,vptr 也会变成父类的,调用draw只会执行父类版本,多态直接失效。 实现多态必须用父类指针或父类引用,不能用父类对象直接赋值。
重写 vs 重载
- 重载:同一个类中,函数名相同、参数不同,是编译期的静态多态
- 重写:父子类之间,函数签名完全一致、有 virtual 修饰,是运行期的动态多态
七、一句话核心总结
动态多态的本质就是:父类定接口规范,子类做具体实现;指针 / 引用做桥梁,虚函数表做底层支撑,运行时根据真实对象类型找对函数执行。

谢谢


