1.89、C++中的虚继承是什么,何时应该使用它?
虚继承是啥?简单说就是“共享爷爷”
虚继承是C++里处理多重继承时的一个机制,专门用来解决“菱形继承”的麻烦。啥叫菱形继承?想象一下:有个基类A(爷爷),然后B和C(两个爹)都继承了A,再然后D(你)同时继承了B和C。结果呢?D里会有两份A的副本,一个从B那儿来,一个从C那儿来。这就好像你爷爷留下的传家宝,你爹和你叔一人拿了一份,结果你这儿存了两份一样的,占地方不说,拿的时候还得说明白是“爹给的”还是“叔给的”,不然就乱套了。
虚继承干嘛用的?它让B和C共享同一份A的实例,到D这儿就只有一份A的副本。简单说,就是把重复的“爷爷”合并成一个,既省地方,又不会让你调用的时候犯迷糊。
啥时候该用虚继承?别乱用,但也别不敢用
那虚继承是不是随便用就行呢?当然不是!它不是万能钥匙,用错了反而给自己找麻烦。我的经验是:当你设计类层次的时候,如果发现有个类会通过多条路径继承同一个基类,而且你不希望这个基类的成员在最终派生类里重复出现,那就得用虚继承。
举个例子:你在写一个游戏引擎,基类是GameObject
,有个id
成员表示对象的唯一标识。然后你有MovableObject
和RenderableObject
两个类都继承了GameObject
,分别管移动和渲染逻辑。接着你又写了个Player
类,需要既能动又能渲染,所以得同时继承MovableObject
和RenderableObject
。如果不用虚继承,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
(蝙蝠)类,蝙蝠既是哺乳动物又会飞,所以得同时继承Mammal
和Bird
:
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()
,麻烦不说,还容易出错。这就好比蝙蝠有两个“吃东西”的技能,一个是哺乳动物的吃法,一个是鸟类的吃法,傻傻分不清楚。
加虚继承:蝙蝠的完美统一
现在改用虚继承,在Mammal
和Bird
继承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()
直接就行,因为Mammal
和Bird
共享了同一个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】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)