1.89、C++中的虚继承是什么,何时应该使用它?

虚继承是啥?简单说就是“共享爷爷”

虚继承是C++里处理多重继承时的一个机制,专门用来解决“菱形继承”的麻烦。啥叫菱形继承?想象一下:有个基类A(爷爷),然后B和C(两个爹)都继承了A,再然后D(你)同时继承了B和C。结果呢?D里会有两份A的副本,一个从B那儿来,一个从C那儿来。这就好像你爷爷留下的传家宝,你爹和你叔一人拿了一份,结果你这儿存了两份一样的,占地方不说,拿的时候还得说明白是“爹给的”还是“叔给的”,不然就乱套了。

虚继承干嘛用的?它让B和C共享同一份A的实例,到D这儿就只有一份A的副本。简单说,就是把重复的“爷爷”合并成一个,既省地方,又不会让你调用的时候犯迷糊。


啥时候该用虚继承?别乱用,但也别不敢用

那虚继承是不是随便用就行呢?当然不是!它不是万能钥匙,用错了反而给自己找麻烦。我的经验是:当你设计类层次的时候,如果发现有个类会通过多条路径继承同一个基类,而且你不希望这个基类的成员在最终派生类里重复出现,那就得用虚继承。

举个例子:你在写一个游戏引擎,基类是GameObject,有个id成员表示对象的唯一标识。然后你有MovableObjectRenderableObject两个类都继承了GameObject,分别管移动和渲染逻辑。接着你又写了个Player类,需要既能动又能渲染,所以得同时继承MovableObjectRenderableObject。如果不用虚继承,Player里就会有两份GameObject::id,一个从移动那儿来,一个从渲染那儿来,你想取个id还得指明路径,烦不烦?用了虚继承,Player就只有一份id,干干净净。

但别一股脑儿啥都虚继承,它有代价:实现上会多一些开销(比如虚表指针),而且类之间的关系会稍微复杂点。所以我的主张是:虚继承是给“必须共享基类实例”的场景准备的,别把它当万金油,也别怕它不敢用,关键是设计时要想清楚。


小案例:从动物世界看虚继承的妙用

光说不练假把式,咱们写个小案例,直观感受一下虚继承的威力。我设计了一个动物分类的例子,简单但有深度,能让你看到虚继承怎么解决实际问题。

场景:蝙蝠的尴尬处境

假设我们有个基类Animal,表示所有动物,里面有个函数eat()表示吃东西:

#include <iostream>

class Animal {
public:
    void eat() {
        std::cout << "动物在吃东西。" << std::endl;
    }
};

然后有两个派生类:Mammal(哺乳动物)和Bird(鸟类),都继承自Animal

class Mammal : public Animal {
public:
    void walk() {
        std::cout << "哺乳动物在走路。" << std::endl;
    }
};

class Bird : public Animal {
public:
    void fly() {
        std::cout << "鸟儿在飞翔。" << std::endl;
    }
};

再来个Bat(蝙蝠)类,蝙蝠既是哺乳动物又会飞,所以得同时继承MammalBird

class Bat : public Mammal, public Bird {
public:
    void hang() {
        std::cout << "蝙蝠倒挂着休息。" << std::endl;
    }
};

不加虚继承:蝙蝠的“双重人格”

先试试不加虚继承,写个main函数看看:

int main() {
    Bat bat;
    // bat.eat(); // 报错!二义性:Mammal::eat() 还是 Bird::eat()?
    bat.Mammal::eat(); // 只能这么写
    bat.walk();
    bat.fly();
    bat.hang();
    return 0;
}

编译器会报错,说bat.eat()有二义性,因为Bat里有两份Animal的副本,一个从Mammal来,一个从Bird来。你得明确写bat.Mammal::eat()或者bat.Bird::eat(),麻烦不说,还容易出错。这就好比蝙蝠有两个“吃东西”的技能,一个是哺乳动物的吃法,一个是鸟类的吃法,傻傻分不清楚。

加虚继承:蝙蝠的完美统一

现在改用虚继承,在MammalBird继承Animal时加个virtual

class Mammal : virtual public Animal {
public:
    void walk() {
        std::cout << "哺乳动物在走路。" << std::endl;
    }
};

class Bird : virtual public Animal {
public:
    void fly() {
        std::cout << "鸟儿在飞翔。" << std::endl;
    }
};

Bat的定义不变,再跑一遍:

int main() {
    Bat bat;
    bat.eat(); // 完美运行,只有一份 Animal
    bat.walk();
    bat.fly();
    bat.hang();
    return 0;
}

输出:

动物在吃东西。
哺乳动物在走路。
鸟儿在飞翔。
蝙蝠倒挂着休息。

这回bat.eat()直接就行,因为MammalBird共享了同一个Animal实例,Bat里只有一份eat(),干净利落。虚继承就像给蝙蝠做了个“身份整合”,让它不再分裂。


结尾:虚继承,学好了它你就是C++高手

虚继承这东西,说白了就是C++给你的一把“整理家产”的利器。啥时候用?多重继承时基类副本重复了就用它。咋用?加个virtual,让基类共享就完事儿了。通过蝙蝠这个小案例,你应该能感觉到它的妙处了吧?


参考文献

  • • Bjarne Stroustrup. The C++ Programming Language. Addison-Wesley.
  • • Stanley B. Lippman, Josée Lajoie, Barbara E. Moo. C++ Primer. Addison-Wesley.
    本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
    (加入我的知识星球,免费获取账号,解锁所有文章。)
阅读剩余
THE END