8. 继承与多态:C++面向对象的核心战场
继承和多态,说白了就是C++里最体现「面向对象」精髓的地方。我刚开始学的时候也觉得绕,什么虚函数、纯虚函数,感觉像在变魔术。但后来在项目里写多了,才发现这些设计是真的好用——尤其是当你需要写一套框架,让别人去扩展的时候。
嗯,咱们今天就把这块彻底捋清楚。从最简单的单继承开始,一路讲到RTTI,每个点我都会结合自己的踩坑经历来讲。
8.1 单继承:最基础的复用方式
单继承就是「一个儿子只有一个爹」。这在现实里很常见,比如「汽车」继承「交通工具」。
class Vehicle {
public:
void start() { cout << "启动" << endl; }
};
class Car : public Vehicle {
public:
void honk() { cout << "鸣笛" << endl; }
};
Car 可以直接用 start(),也能用自己的 honk()。这就是复用。我个人习惯是:能用组合就别用继承,但确实需要「is-a」关系时,单继承是最干净的。
8.2 多继承:一把双刃剑
C++允许一个类继承多个父类。听起来很强大,但用不好就是灾难。
class Teacher {
public:
void teach() { cout << "教书" << endl; }
};
class Writer {
public:
void write() { cout << "写作" << endl; }
};
class Professor : public Teacher, public Writer {
// 同时拥有 teach() 和 write()
};
我在项目中遇到过一个问题:两个父类都有同一个名字的成员函数,子类调用时编译器报「ambiguous」。当时排查了半天,最后用作用域限定符解决的。所以我的建议是:多继承尽量少用,除非你很清楚自己在做什么。
8.3 虚继承:解决菱形继承的利器
菱形继承长这样:
class A { public: int x; };
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
加上 virtual 关键字后,D 里只有一份 A 的数据。我曾经在写一个图形库时用过这个——基类是「Shape」,两个中间类是「Drawable」和「Resizable」,最终类「Rectangle」同时继承它们。如果不加虚继承,Rectangle 里会有两份 Shape 的数据,内存浪费不说,逻辑也乱。
嗯,这里要注意:虚继承会影响构造顺序。虚基类的构造函数由最派生类直接调用,而不是由中间类调用。这个细节我当年面试时被问过,答不上来很尴尬。
8.4 虚函数:多态的灵魂
虚函数让C++实现了运行时多态。说白了就是:你用一个基类指针或引用调用一个函数,实际执行的是子类的版本。
class Animal {
public:
virtual void speak() { cout << "动物叫" << endl; }
};
class Dog : public Animal {
public:
void speak() override { cout << "汪汪" << endl; }
};
void makeSpeak(Animal* a) {
a->speak(); // 实际调用的是 Dog::speak()
}
为什么会这样?因为每个有虚函数的类都有一个虚函数表(vtable),里面存着函数指针。调用时通过 vptr(虚指针)找到正确的函数。这个机制我建议你理解一下,虽然平时写代码不用手动操作,但遇到性能问题或调试时很有用。
8.5 纯虚函数与抽象类
纯虚函数就是「只声明,不实现」的虚函数。包含纯虚函数的类叫抽象类,不能实例化。
class Shape {
public:
virtual double area() = 0; // 纯虚函数
};
class Circle : public Shape {
public:
double area() override { return 3.14 * r * r; }
private:
double r;
};
抽象类的作用是定义接口。我习惯把抽象类当作「契约」——所有子类必须实现 area(),否则子类也是抽象类。这在设计大型系统时特别有用,比如插件架构、策略模式等。
我曾经在一个项目里用抽象类定义了「数据源」接口,然后分别实现了「文件数据源」「网络数据源」「数据库数据源」。上层代码完全不用关心数据从哪来,只管调用接口就行。这就是多态的魅力。
8.6 运行时类型识别(RTTI)
RTTI 允许你在运行时获取对象的实际类型。主要用两个操作符:typeid 和 dynamic_cast。
Base* b = new Derived();
if (Derived* d = dynamic_cast<Derived*>(b)) {
// 转换成功,b 实际指向 Derived 对象
}
if (typeid(*b) == typeid(Derived)) {
cout << "b 是 Derived 类型" << endl;
}
嗯,这里要提醒你:RTTI 是有开销的。dynamic_cast 需要遍历继承链,typeid 需要访问类型信息。我一般只在调试或处理异常情况时用,正常业务逻辑里尽量少用。如果你发现代码里到处是 dynamic_cast,那多半是设计有问题。
知识体系总览
下面这张图把本章的核心概念串起来了,你可以对照着复习:
公众号:蓝海资料掘金营,微信deep3321