在 C++ 面向对象编程(OOP)的三大特性 ------封装、继承、多态 中,封装是基础(隐藏实现、暴露接口),而继承 与多态则是构建灵活、可扩展、可复用程序的核心支柱。继承解决了代码复用和层次化建模的问题,多态则实现了 **"一个接口,多种实现"** 的动态行为,让程序具备了运行时的弹性。
本文将从基础语法、底层原理、实战应用到常见误区,全方位深度拆解 C++ 的继承与多态,帮你彻底掌握这两个核心知识点。
一、类继承:代码复用与层次建模的基石
1.1 继承的核心概念
继承是指一个类(派生类 / 子类 )可以复用另一个类(基类 / 父类)的成员变量和成员函数,同时可以扩展自己的属性和行为。
继承的本质是描述 **is-a(是一种)** 的关系:
- 狗
is-a动物 →Dog继承Animal - 圆形
is-a图形 →Circle继承Shape
基本语法:
cpp
// 基类
class 基类名 { ... };
// 派生类:继承方式 + 基类名
class 派生类名 : 继承方式 基类名 { ... };
C++ 提供三种继承方式:public(公有继承)、protected(保护继承)、private(私有继承),实际开发中 99% 的场景使用公有继承。
1.2 继承的访问权限规则
基类的成员权限(public/protected/private)结合继承方式,决定了派生类对基类成员的访问权限,这是继承最容易踩坑的点:
| 基类成员权限 | public 继承后 | protected 继承后 | private 继承后 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
核心结论:
- 基类的私有成员 无论用哪种方式继承,派生类都无法直接访问,只能通过基类的公有 / 保护成员间接调用;
- 公有继承是唯一符合
is-a逻辑的继承方式,保护 / 私有继承多用于特殊的代码复用。
1.3 继承中的构造与析构函数
派生类不会继承基类的构造函数、析构函数、赋值运算符,但必须调用它们完成对象的初始化与销毁。
调用顺序(铁律):
- 构造 :先调用基类构造函数 → 再调用派生类构造函数
- 析构 :先调用派生类析构函数 → 再调用基类析构函数
示例代码:
cpp
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "基类构造函数执行" << endl; }
~Base() { cout << "基类析构函数执行" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "派生类构造函数执行" << endl; }
~Derived() { cout << "派生类析构函数执行" << endl; }
};
int main() {
Derived d; // 创建派生类对象
return 0;
}
输出结果:
基类构造函数执行
派生类构造函数执行
派生类析构函数执行
基类析构函数执行
1.4 成员隐藏:同名成员的处理规则
如果派生类定义了与基类同名的成员(变量 / 函数) ,基类的同名成员会被隐藏:
- 直接调用时,优先使用派生类的成员;
- 若要调用基类的同名成员,必须显式指定基类作用域。
注意:成员隐藏 ≠ 函数重写,这是新手最易混淆的概念!
二、多态:面向对象的灵魂,动态行为的实现
如果说继承是静态的代码复用 ,那多态就是动态的行为扩展。多态分为两类:
- 静态多态(编译期多态):编译时确定调用的函数,如函数重载、模板;
- 动态多态(运行期多态) :运行时才确定调用的函数,这是 C++ 多态的核心。
本文重点讲解动态多态。
2.1 动态多态的三大实现条件
动态多态不是自动生效的,必须同时满足以下 3 个条件:
- 继承关系:派生类公有继承基类;
- 虚函数重写 :基类使用
virtual关键字声明虚函数 ,派生类完全重写该虚函数(函数名、参数、返回值完全一致); - 基类指针 / 引用 :使用基类的指针或引用指向派生类对象,调用虚函数。
2.2 虚函数:多态的核心关键字
virtual是实现多态的关键,被virtual修饰的成员函数称为虚函数。
- 基类声明虚函数后,派生类重写时可省略
virtual,但建议加上override关键字(C++11),让编译器检查重写是否合法; - 虚函数的核心作用:实现运行时的函数绑定(晚绑定)。
2.3 纯虚函数与抽象类
如果一个虚函数没有实现体,仅作为接口定义,称为纯虚函数:
cpp
virtual 返回值类型 函数名(参数) = 0;
包含至少一个纯虚函数 的类称为抽象类:
- 抽象类不能实例化对象;
- 派生类必须实现所有纯虚函数,才能成为普通类(否则仍是抽象类);
- 抽象类的作用:定义统一接口,规范派生类的行为。
2.4 虚析构函数:避免内存泄漏的关键
这是继承与多态中最高频的坑 :当基类指针指向派生类对象 时,如果基类的析构函数不是虚函数,delete指针时只会调用基类析构函数,派生类析构函数不会执行,导致派生类的资源泄漏!
铁律 :只要类中存在虚函数,析构函数必须声明为虚函数。
2.5 多态的底层原理:虚表与虚指针
多态的实现依赖编译器底层的两个机制:
- 虚函数表(vtable) :每个包含虚函数的类,编译器会为其生成一张虚表,存储该类所有虚函数的地址;
- 虚指针(vptr):每个对象会包含一个隐藏的虚指针,指向所属类的虚表;
- 运行时绑定:调用虚函数时,通过虚指针找到虚表,再根据对象的实际类型调用对应的函数。
这就是动态绑定的核心:函数调用与对象的实际类型绑定,而非指针的类型。
三、实战演练:继承 + 多态完整案例
我们用图形面积计算的经典案例,完整演示继承、多态、抽象类、虚析构的用法:
cpp
#include <iostream>
#include <cmath>
using namespace std;
// 抽象基类:图形
class Shape {
protected:
string color; // 保护成员:派生类可直接访问
public:
// 构造函数
Shape(string c) : color(c) {}
// 纯虚函数:定义统一接口
virtual double getArea() const = 0;
// 虚析构函数:防止内存泄漏
virtual ~Shape() {
cout << "Shape 析构" << endl;
}
// 普通成员函数
void showColor() const {
cout << "图形颜色:" << color << endl;
}
};
// 派生类:圆形
class Circle : public Shape {
private:
double radius; // 半径
public:
Circle(string c, double r) : Shape(c), radius(r) {}
// 重写纯虚函数:override关键字检查重写合法性
double getArea() const override {
return M_PI * radius * radius;
}
~Circle() override {
cout << "Circle 析构" << endl;
}
};
// 派生类:矩形
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(string c, double w, double h) : Shape(c), width(w), height(h) {}
double getArea() const override {
return width * height;
}
~Rectangle() override {
cout << "Rectangle 析构" << endl;
}
};
int main() {
// 多态核心:基类指针指向派生类对象
Shape* shape1 = new Circle("红色", 5);
Shape* shape2 = new Rectangle("蓝色", 4, 6);
// 同一个接口getArea(),不同实现(多态)
shape1->showColor();
cout << "圆形面积:" << shape1->getArea() << endl;
shape2->showColor();
cout << "矩形面积:" << shape2->getArea() << endl;
// 虚析构:正确释放所有资源
delete shape1;
delete shape2;
return 0;
}
运行结果:
图形颜色:红色
圆形面积:78.5398
图形颜色:蓝色
矩形面积:24
Circle 析构
Shape 析构
Rectangle 析构
Shape 析构
这个案例完美体现了开闭原则 :新增图形(如三角形)时,无需修改原有代码,只需新增派生类并重写getArea即可。
四、核心误区与最佳实践
4.1 重写 vs 隐藏:一字之差,天差地别
表格
| 特性 | 虚函数重写(Override) | 成员隐藏(Hiding) |
|---|---|---|
| 关键字 | 基类必须有virtual |
无virtual |
| 函数签名 | 必须完全一致 | 同名即可,参数可不同 |
| 多态效果 | 触发动态多态 | 无多态,静态绑定 |
| 调用方式 | 基类指针自动调用派生类实现 | 需显式指定基类作用域调用 |
4.2 绝对禁止:构造 / 析构函数中调用虚函数
在基类的构造 / 析构函数中调用虚函数,不会触发多态:
- 构造时:派生类对象还未初始化,虚指针指向基类虚表;
- 析构时:派生类对象已销毁,虚指针回退到基类虚表。
4.3 多继承与菱形继承
C++ 支持多继承(一个派生类继承多个基类),但极易引发二义性 和数据冗余(菱形继承):
- 解决方案:虚继承 (
class B : virtual public A); - 最佳实践:优先使用组合(has-a),慎用多继承。
4.4 开发最佳实践
- 继承仅用于描述is-a关系,不要为了代码复用强行继承;
- 基类只要有虚函数,析构函数必为虚函数;
- 重写虚函数必须加
override,让编译器帮你检查错误; - 用抽象类定义接口,实现模块化和解耦;
- 组合优于继承,降低类之间的耦合度。
五、总结
继承与多态是 C++ 面向对象编程的灵魂:
- 继承 是静态复用 :通过
is-a关系复用基类代码,构建层次化的类结构; - 多态 是动态扩展:通过虚函数实现运行时绑定,让程序具备 "一个接口,多种实现" 的弹性;
- 两者结合,完美支撑开闭原则------ 对扩展开放,对修改关闭,是大型 C++ 项目模块化、可维护、可扩展的核心保障。