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(虚指针)找到正确的函数。这个机制我建议你理解一下,虽然平时写代码不用手动操作,但遇到性能问题或调试时很有用。

核心要点: 虚函数必须通过指针或引用调用才能体现多态。如果直接用对象调用,即使加了 virtual,也是静态绑定。

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,那多半是设计有问题。

注意: 使用 RTTI 需要开启编译器相关选项(默认是开启的)。另外,dynamic_cast 只能用于多态类型(有虚函数的类),否则编译报错。

知识体系总览

下面这张图把本章的核心概念串起来了,你可以对照着复习:

继承与多态 继承方式 单继承 多继承 虚继承 多态机制 虚函数 纯虚函数 抽象类 运行时识别 typeid dynamic_cast 核心思想 复用接口 + 运行时多态 = 灵活可扩展的设计
本章总结: 继承解决的是代码复用问题,多态解决的是扩展性问题。虚函数是C++多态的基石,纯虚函数定义了接口契约。RTTI 是运行时最后的保险,但不要滥用。记住一句话:好的设计,继承层次不超过三层,虚函数不超过五个。

公众号:蓝海资料掘金营,微信deep3321