解锁 C++ 面向对象核心:继承与多态的深度解析与实战
一、继承:站在已有类的 “肩膀上” 编程
1. 为什么需要继承?
// 冗余的代码:每个类重复定义相同属性和方法class Student {public:
string name;
int age;
char gender;
void printBaseInfo() {
cout << "姓名:" << name << ",年龄:" << age << ",性别:" << gender << endl;
}
// 学生特有属性/方法
int studentId;
void study() { cout << "学习" << endl; }};class Teacher {public:
string name;
int age;
char gender;
void printBaseInfo() {
cout << "姓名:" << name << ",年龄:" << age << ",性别:" << gender << endl;
}
// 老师特有属性/方法
int teacherId;
void teach() { cout << "授课" << endl; }};Person,包含所有类的公共部分,再让Student、Teacher作为 “派生类(子类)” 继承Person,仅需实现特有属性和方法:// 基类:公共属性和方法class Person {public:
string name;
int age;
char gender;
void printBaseInfo() {
cout << "姓名:" << name << ",年龄:" << age << ",性别:" << gender << endl;
}};// 派生类:继承Person,仅实现特有部分class Student : public Person {public:
int studentId;
void study() { cout << name << "学习" << endl; }};class Teacher : public Person {public:
int teacherId;
void teach() { cout << name << "授课" << endl; }};2. 继承的基本语法与核心概念
语法格式:
class 派生类名 : 继承方式 基类名 {
// 派生类的成员(特有属性/方法)};继承方式(权限控制):
| 继承方式 | 基类 public 成员 | 基类 protected 成员 | 基类 private 成员 |
|---|---|---|---|
| public | 派生类 public | 派生类 protected | 不可访问 |
| protected | 派生类 protected | 派生类 protected | 不可访问 |
| private | 派生类 private | 派生类 private | 不可访问 |
关键结论:
基类 private 成员无论哪种继承方式,派生类都无法直接访问(需通过基类 public/protected 接口);
实际开发中优先用
public继承(保持基类接口的开放性,符合 “is-a” 关系);
protected继承用于 “继承但不对外暴露”,private继承几乎不用(等价于 “组合”)。
示例:继承方式的权限验证
class Base {public:
int pub;protected:
int pro;private:
int pri;};// public继承class DerivedPub : public Base {public:
void test() {
pub = 1; // 合法:public→public
pro = 2; // 合法:protected→protected
// pri = 3; // 非法:private成员不可访问
}};int main() {
DerivedPub d;
d.pub = 10; // 合法:public成员类外可访问
// d.pro = 20; // 非法:protected成员类外不可访问
return 0;}3. 派生类的构造与析构:先基后派,先派后基
构造:先调用基类构造函数 → 再调用派生类构造函数;
析构:先调用派生类析构函数 → 再调用基类析构函数。
class Base {public:
Base() { cout << "Base构造函数" << endl; }
~Base() { cout << "Base析构函数" << endl; }};class Derived : public Base {public:
Derived() { cout << "Derived构造函数" << endl; }
~Derived() { cout << "Derived析构函数" << endl; }};int main() {
Derived d;
// 输出顺序:
// Base构造函数
// Derived构造函数
// Derived析构函数
// Base析构函数
return 0;}带参构造的传递:
class Base {public:
int num;
Base(int n) : num(n) { cout << "Base带参构造:" << num << endl; }};class Derived : public Base {public:
// 派生类构造函数:初始化列表中调用基类带参构造
Derived(int n, int m) : Base(n), num2(m) {
cout << "Derived带参构造:" << num2 << endl;
}
int num2;};int main() {
Derived d(10, 20);
// 输出:
// Base带参构造:10
// Derived带参构造:20
return 0;}4. 继承的常见场景:重写与隐藏
(1)重写(Override):派生类覆盖基类的虚函数(多态基础)
virtual。(2)隐藏(Hide):派生类同名函数隐藏基类所有同名函数
class Base {public:
void func(int a) { cout << "Base::func(int):" << a << endl; }};class Derived : public Base {public:
// 隐藏基类的func(int)
void func() { cout << "Derived::func()" << endl; }};int main() {
Derived d;
d.func(); // 合法:调用派生类func()
// d.func(10); // 非法:基类func(int)被隐藏
d.Base::func(10); // 合法:显式调用基类函数
return 0;}二、多态:同一接口,不同行为
1. 多态的本质:动态绑定(运行时决策)
对比:静态绑定 vs 动态绑定
| 绑定方式 | 决策时机 | 依据 | 示例 |
|---|---|---|---|
| 静态绑定(早绑定) | 编译期 | 指针 / 引用的类型 | 普通函数、隐藏的函数 |
| 动态绑定(晚绑定) | 运行期 | 对象的实际类型 | 虚函数的重写 |
示例:无多态(静态绑定)vs 有多态(动态绑定)
// 无多态:静态绑定class Animal {public:
void speak() { cout << "动物叫" << endl; } // 普通函数};class Cat : public Animal {public:
void speak() { cout << "猫叫" << endl; } // 隐藏基类函数};// 有多态:动态绑定(基类函数加virtual)class AnimalPoly {public:
virtual void speak() { cout << "动物叫" << endl; } // 虚函数};class CatPoly : public AnimalPoly {public:
void speak() override { cout << "猫叫" << endl; } // 重写(override可选,建议加)};int main() {
// 无多态:指针类型是Animal,调用Animal::speak()
Animal* p1 = new Cat;
p1->speak(); // 输出:动物叫
// 有多态:对象实际类型是CatPoly,调用CatPoly::speak()
AnimalPoly* p2 = new CatPoly;
p2->speak(); // 输出:猫叫
delete p1;
delete p2;
return 0;}2. 多态的实现条件(缺一不可):
基类中声明虚函数(加
virtual关键字);派生类重写基类的虚函数(函数名、参数、返回值完全一致);
通过基类指针 / 引用调用虚函数。
补充:override关键字(C++11):显式声明重写,编译器会校验是否符合重写规则(避免拼写错误),建议必加。
3. 虚函数的底层:虚函数表(vtable)
每个包含虚函数的类,编译器会生成一个虚函数表(vtable) —— 存储类中所有虚函数的地址;
每个对象会包含一个虚表指针(vptr) —— 指向所属类的虚函数表;
运行时,通过 vptr 找到 vtable,再调用对应虚函数(动态绑定的核心)。
内存布局示例:
class Base {public:
virtual void func1() {}
virtual void func2() {}
int num;};class Derived : public Base {public:
void func1() override {} // 重写func1,vtable中替换为Derived::func1地址};Base对象内存:vptr(8 字节) + num(4 字节) = 12 字节(内存对齐);Derived对象内存:vptr(指向 Derived 的 vtable) + num(4 字节);Derived 的 vtable:
Derived::func1()地址 +Base::func2()地址。
4. 纯虚函数与抽象类:强制派生类实现
virtual 返回值 函数名(参数) = 0;。包含纯虚函数的类称为抽象类,核心特征:无法实例化对象(
Animal a;编译报错);派生类必须重写所有纯虚函数,否则派生类也是抽象类;
核心用途:定义接口规范(比如 “所有动物都必须实现 speak ()”)。
示例:抽象类与纯虚函数
// 抽象类:包含纯虚函数class Animal {public:
// 纯虚函数:仅定义接口,无实现
virtual void speak() = 0;
virtual void move() = 0;};// 派生类:必须实现所有纯虚函数class Dog : public Animal {public:
void speak() override { cout << "狗叫" << endl; }
void move() override { cout << "狗跑" << endl; }};class Bird : public Animal {public:
void speak() override { cout << "鸟叫" << endl; }
void move() override { cout << "鸟飞" << endl; }};// 多态函数:接收抽象类引用,适配所有派生类void animalDo(Animal& a) {
a.speak();
a.move();}int main() {
Dog d;
Bird b;
animalDo(d); // 输出:狗叫 → 狗跑
animalDo(b); // 输出:鸟叫 → 鸟飞
return 0;}5. 析构函数与多态:虚析构函数
错误示例:非虚析构导致内存泄漏
class Base {public:
Base() { cout << "Base构造" << endl; }
~Base() { cout << "Base析构" << endl; } // 非虚析构};class Derived : public Base {public:
int* arr = new int[10]; // 堆内存
Derived() { cout << "Derived构造" << endl; }
~Derived() {
delete[] arr;
cout << "Derived析构" << endl;
}};int main() {
Base* p = new Derived;
delete p; // 仅调用Base析构,Derived析构未执行 → arr内存泄漏
// 输出:
// Base构造
// Derived构造
// Base析构
return 0;}正确示例:虚析构函数
class Base {public:
virtual ~Base() { cout << "Base析构" << endl; } // 虚析构};int main() {
Base* p = new Derived;
delete p; // 先调用Derived析构,再调用Base析构
// 输出:
// Base构造
// Derived构造
// Derived析构
// Base析构
return 0;}结论:只要类作为基类被继承,且包含堆内存,就必须将析构函数设为虚函数。
三、继承与多态的避坑指南
1. 多继承的菱形问题(钻石继承)
// 菱形继承结构class A { public: int num; };class B : public A {};class C : public A {};class D : public B, public C {};int main() {
D d;
// d.num = 10; // 非法:二义性(B::num 或 C::num)
d.B::num = 10; // 合法:显式指定
d.C::num = 20;
return 0;}virtual),让共同基类只被继承一次:class B : virtual public A {};class C : virtual public A {};class D : public B, public C {};int main() {
D d;
d.num = 10; // 合法:虚继承消除二义性
return 0;}2. 虚函数的常见错误
重写时参数不一致:比如基类
virtual void func(int),派生类void func(double)→ 不是重写,是隐藏;基类函数未加
virtual:即使派生类重写,也无法触发多态(静态绑定);用对象调用虚函数:
Animal a = Cat(); a.speak();→ 对象切片,仅调用基类函数(需用指针 / 引用)。
3. 抽象类的误用
试图实例化抽象类:
Animal a;→ 编译报错;派生类未实现所有纯虚函数:派生类仍为抽象类,无法实例化。
4. 继承的 “is-a” 原则
// 错误:Car继承Engine(汽车不是引擎,而是包含引擎)class Engine { public: void run() {} };class Car : public Engine {};// 正确:组合(Car包含Engine对象)class Car {public:
Engine e;
void run() { e.run(); }};四、总结:继承与多态的核心取舍
| 特性 | 核心价值 | 适用场景 |
|---|---|---|
| 继承 | 代码复用,扩展基类功能 | 类之间存在 “is-a” 关系(学生→人) |
| 多态 | 动态绑定,统一接口适配不同实现 | 需根据对象类型动态执行不同逻辑(动物叫) |
| 抽象类 | 定义接口规范,强制派生类实现 | 框架设计(定义标准,不关心具体实现) |
| 虚析构 | 确保派生类析构函数被调用 | 基类包含堆内存,需通过基类指针释放 |
继承解决 “代码复用” 问题,让我们无需重复编写公共逻辑;
多态解决 “接口统一” 问题,让代码更灵活、易扩展(比如新增
Fish类,只需继承Animal并实现speak(),无需修改animalDo()函数)。