C++ 面向对象进阶:继承深化与多态详解
上一篇博客我们介绍了 C++ 面向对象的基础概念,包括类与对象、封装、构造 / 析构函数及继承的基本用法。本文将深入探讨继承的高级特性,并详细讲解面向对象的另一核心特性 —— 多态,帮助你理解复杂类层次设计和灵活的接口实现。
一、继承的深入探讨
继承作为代码复用的核心机制,在实际开发中存在诸多细节需要掌握。除了基础的继承语法,我们还需关注成员访问控制、构造函数传递及菱形继承等问题。
1.1 继承中的构造与析构顺序
当子类继承父类时,对象的创建和销毁会涉及父子类构造函数与析构函数的调用顺序,这是内存管理的关键:
- 构造顺序:先调用父类构造函数,再调用子类构造函数(先有父再有子)
- 析构顺序:与构造相反,先调用子类析构函数,再调用父类析构函数(先销毁子再销毁父)
#include <iostream>
using namespace std;class Parent {
public:Parent() { cout << "Parent构造函数" << endl; }~Parent() { cout << "Parent析构函数" << endl; }
};class Child : public Parent {
public:Child() { cout << "Child构造函数" << endl; }~Child() { cout << "Child析构函数" << endl; }
};int main() {Child c;// 输出顺序:// Parent构造函数 → Child构造函数// 程序结束时:Child析构函数 → Parent析构函数return 0;
}
注意:若父类没有默认构造函数,子类必须在初始化列表中显式调用父类的有参构造:
class Parent {
public:Parent(int a) { cout << "Parent有参构造:" << a << endl; }
};class Child : public Parent {
public:// 必须通过初始化列表调用父类有参构造Child() : Parent(10) { cout << "Child构造函数" << endl; }
};
1.2 继承中的同名成员处理
当子类与父类存在同名成员时,需要通过作用域分辨符区分:
- 同名成员变量:
子类对象.父类名::成员
访问父类成员 - 同名成员函数:若子类重写了父类函数,直接调用会执行子类版本;需加作用域访问父类版本
class Parent {
public:int num = 100;void show() { cout << "Parent show: " << num << endl; }
};class Child : public Parent {
public:int num = 200; // 同名成员变量void show() { cout << "Child show: " << num << endl; } // 同名成员函数
};int main() {Child c;cout << c.num << endl; // 200(子类成员)cout << c.Parent::num << endl; // 100(父类成员)c.show(); // Child show: 200(子类函数)c.Parent::show(); // Parent show: 100(父类函数)return 0;
}
1.3 菱形继承问题与虚继承
当一个子类同时继承两个父类,而这两个父类又继承自同一个基类时,会产生菱形继承问题:
- 数据冗余:子类会保存两份基类成员
- 二义性:访问基类成员时无法确定来自哪个父类
// 菱形继承结构
class Base { public: int a; };
class Parent1 : public Base {};
class Parent2 : public Base {};
class Child : public Parent1, public Parent2 {};int main() {Child c;// c.a = 10; // 错误:二义性(Parent1::a 还是 Parent2::a?)c.Parent1::a = 10; // 需显式指定,仍存在数据冗余return 0;
}
解决方法:虚继承(virtual)通过virtual
关键字修饰父类的继承方式,使基类成员在子类中只保留一份:
class Base { public: int a; };
// 虚继承:Parent1和Parent2共享Base成员
class Parent1 : virtual public Base {};
class Parent2 : virtual public Base {};
class Child : public Parent1, public Parent2 {};int main() {Child c;c.a = 10; // 正确:仅一份Base::areturn 0;
}
二、多态:面向对象的灵活性核心
多态是指同一接口在不同场景下表现出不同行为,分为静态多态和动态多态:
- 静态多态:编译期确定(函数重载、运算符重载)
- 动态多态:运行期确定(基于虚函数的继承体系)
2.1 动态多态的实现条件
- 存在继承关系
- 子类重写父类的虚函数(函数名、参数、返回值完全一致)
- 父类指针或引用指向子类对象
#include <iostream>
using namespace std;// 父类:定义虚函数
class Animal {
public:// 虚函数:用virtual修饰virtual void speak() {cout << "动物叫" << endl;}
};// 子类:重写虚函数
class Cat : public Animal {
public:// 重写:函数签名与父类虚函数一致void speak() override { // override关键字可显式标识重写(C++11)cout << "喵喵叫" << endl;}
};class Dog : public Animal {
public:void speak() override {cout << "汪汪叫" << endl;}
};// 统一接口:接收父类指针
void doSpeak(Animal* animal) {animal->speak(); // 运行时根据实际对象类型调用对应函数
}int main() {Cat cat;Dog dog;doSpeak(&cat); // 输出:喵喵叫doSpeak(&dog); // 输出:汪汪叫return 0;
}
2.2 虚函数表与多态原理
C++ 通过虚函数表(vtable) 实现动态多态:
- 含有虚函数的类会生成一个虚函数表,存储虚函数地址
- 类的每个对象会包含一个虚表指针(vptr),指向该类的虚函数表
- 子类重写虚函数时,会替换虚表中对应函数的地址
- 调用虚函数时,通过 vptr 找到虚表,再调用实际函数地址(运行期确定)
2.3 纯虚函数与抽象类
当父类的虚函数无需实现(仅作为接口)时,可声明为纯虚函数,包含纯虚函数的类称为抽象类:
- 纯虚函数语法:
virtual 返回类型 函数名(参数) = 0;
- 抽象类特点:
- 不能实例化对象
- 子类必须重写所有纯虚函数才能实例化,否则仍是抽象类
// 抽象类(接口)
class Shape {
public:// 纯虚函数:仅声明,无实现virtual double calculateArea() = 0;virtual double calculatePerimeter() = 0;
};// 子类实现接口
class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}// 必须重写所有纯虚函数double calculateArea() override {return 3.14 * radius * radius;}double calculatePerimeter() override {return 2 * 3.14 * radius;}
};int main() {// Shape s; // 错误:抽象类不能实例化Shape* circle = new Circle(5);cout << "面积:" << circle->calculateArea() << endl; // 78.5delete circle;return 0;
}
2.4 多态的应用场景
多态是框架设计的核心思想,典型应用包括:
- 接口统一:用父类指针 / 引用接收不同子类对象,简化调用
- 扩展方便:新增子类无需修改原有接口代码(开闭原则)
- 回调机制:通过虚函数实现事件响应的灵活绑定
例如图形绘制框架:
// 框架代码(无需修改)
void drawShapes(Shape* shapes[], int count) {for (int i = 0; i < count; i++) {cout << "面积:" << shapes[i]->calculateArea() << endl;}
}// 扩展新图形(只需新增子类)
class Rectangle : public Shape {// ...实现纯虚函数
};int main() {Shape* shapes[] = {new Circle(5), new Rectangle(3,4)};drawShapes(shapes, 2); // 统一调用,自动适配不同图形return 0;
}
三、继承与多态的常见问题
3.1 虚析构函数
当父类指针指向子类对象时,若父类析构函数不是虚函数,删除指针只会调用父类析构,导致子类资源泄漏:
class Parent {
public:// 虚析构:确保子类析构被调用virtual ~Parent() { cout << "Parent析构" << endl; }
};class Child : public Parent {
private:int* data;
public:Child() { data = new int; }~Child() { delete data; cout << "Child析构" << endl; }
};int main() {Parent* p = new Child;delete p; // 若Parent析构非虚函数,仅调用Parent析构// 虚析构时输出:Child析构 → Parent析构return 0;
}
结论:基类析构函数应声明为虚函数。
3.2 不能被重写的函数
- 静态成员函数:属于类,无 this 指针,无法放入虚函数表
- 构造函数:对象未完全创建,无法多态调用
- 友元函数:不是类成员函数,不存在重写概念
四、总结
本文深入讲解了 C++ 继承的高级特性(构造顺序、同名成员、菱形继承)和多态的核心机制(虚函数、纯虚函数、抽象类),主要知识点包括:
- 继承中构造与析构的调用顺序及参数传递
- 虚继承解决菱形继承的数据冗余和二义性
- 动态多态的实现条件与虚函数表原理
- 纯虚函数与抽象类在接口设计中的应用
- 虚析构函数的重要性
多态是面向对象编程灵活性的核心,掌握它能让你设计出更具扩展性和复用性的代码。下一篇将介绍运算符重载、模板等 C++ 高级特性,敬请关注!
如果本文对你有帮助,欢迎点赞收藏,有任何疑问或补充欢迎在评论区交流~