C++多重继承要慎用!

简介:

本文目的

前几天在写程序时,发现一个多重继承类,调用virtual函数会出现一个问题,该问题比较隐晦(因为不会引起程序core dump等严重的效果,我是很偶然的在单元测试中发现的),不容易定位,但是如果出现,可能对程序逻辑会带来致命的问题。

 

一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using  namespace  std;
 
class  Base1{
public :
     virtual  void  foo1() {};
};
 
class  Base2{
public :
     virtual  void  foo2() {};
};
 
class  MI : public  Base1, public  Base2{
public :
     virtual  void  foo1 () {cout << "MI::foo1"  << endl;}
     virtual  void  foo2 () {cout << "MI::foo2"  << endl;}
};
 
int  main(){
     MI oMI;
 
     Base1* pB1 =  &oMI;
     pB1->foo1();
   
     Base2* pB2 = (Base2*)(pB1); // 指针强行转换,没有偏移
     pB2->foo2();
     
     pB2 = dynamic_cast <Base2*>(pB1); // 指针动态转换,dynamic_cast帮你偏移
     pB2->foo2();
 
     return  0;
}

你会认为屏幕上会输出什么?是下面的结果吗?

MI::foo1

MI::foo2

MI::foo2

这样认为没有什么不对的,因为C++的多态性保证用父类指针可以正确的找到子类实现,并调用。所以会有上面的输出。

但是,现实却不是这样,下面是真实的输出:

clip_image002

(以上实现在VC 2005和Linux Gcc 4.1.2效果一致)

 

为什么

为什么会出现上面的情况呢,上面代码中的注释部分也许解释了,这里再来详细的来分析一下。

首先,C++使用一种称之为vtable(google “vtable” for more details)的东西实现virtual函数多态调用。vtable每个类中都有一个,该类的所有对象公用,由编译器帮你生成,只要有virtual函数的类,均会有vtable。在继承过程中,由于类Base1和类Base2都有vtable,所以类MI继承了两个vtable。简单的分析一下对象oMI内存结构,如下:

0 vtable_address_for_Base1 –> [MI::foo1, NULL]

4 vtable_address_for_Base2 –> [MI::foo2, NULL]

其实很简单,就两个vtable的指针,0和4代表相对地址,指针地址大小为4。

pB1的值为0(pB1 == 0),所以调用“pB1->foo1()”时,可以正确的找到MI::fool这个函数执行。

但是当使用强行转换,将pB1转给pB2,那么实质上pB2的值也是0(pB2 == 0),当调用“pB2->foo2()”时,无法在第一个vtalbe中找到对应的函数,但是却不报错,而是选择执行函数MI::foo1,不知道为什么会有这种行为,但是这种行为却十分恶心,导致结果无法预期的(最后调用的函数会与函数申明的循序有关),不太会引起注意,使得bug十分隐晦。

可以设想,当一个有复杂的业务逻辑的程序,而类似这种函数调用和指针强行转换分布在不同的函数或模块中,可想而知,bug定位十分困难。

当使用动态转换时,也就是“pB2 = dynamic_cast<Base2*>(pB1)”,dynamic_cast函数会根据尖括号中的类型进行指针偏移,所以pB2的值为4(pB2 == 4),这样调用“pB2->foo2()”就会按照期望的方式执行。

结论

上面的现象在单继承中是不会出现的,因为只有一个vtable(子类的virtual函数会自动追加到第一个父类的vtable的结尾)。所以不会出现上面的现象,而多重继承却出现了上面的想象,所以需要注意以下两点:

1. 多重继承需要慎用

2. 类型转换尽量采用c++内置的类型转换函数,而不要强行转换

 

参考资料

本文转自bourneli博客园博客,原文链接:http://www.cnblogs.com/bourneli/archive/2011/12/28/2305264.html ,如需转载请自行联系原作者
相关文章
|
7月前
|
C++
C++程序中的多重继承
C++程序中的多重继承
77 1
|
5月前
|
编译器 C++ 开发者
C++一分钟之-多重继承与菱形问题
【7月更文挑战第19天】C++的多重继承允许类从多个基类派生,但引入了菱形问题,即类D通过B和C(都继承自A)双重继承A,可能导致数据冗余和二义性。解决这个问题的关键是**虚继承**,通过`virtual`关键字确保基类A只被继承一次,消除冲突。理解并适当使用虚继承是处理这类问题的关键,有助于保持代码的清晰和正确性。
89 0
|
7月前
|
设计模式 编译器 数据安全/隐私保护
C++ 多级继承与多重继承:代码组织与灵活性的平衡
C++的多级和多重继承允许类从多个基类继承,促进代码重用和组织。优点包括代码效率和灵活性,但复杂性、菱形继承问题(导致命名冲突和歧义)以及对基类修改的脆弱性是潜在缺点。建议使用接口继承或组合来避免菱形继承。访问控制规则遵循公有、私有和受保护继承的原则。在使用这些继承形式时,需谨慎权衡优缺点。
156 1
|
7月前
|
C++
[C++/PTA] 日程安排(多重继承+重载)
[C++/PTA] 日程安排(多重继承+重载)
198 0
|
Java 编译器 PHP
C++的多重继承
派生类都只有一个基类,称为单继承(Single Inheritance)。除此之外,C++也支持多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。 多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java、C#、PHP 等干脆取消了多继承。 多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D: class D: public A, private B, protected C{ //类D新增加的成员 } D 是多继承形式的派生类,它以公有的方式继承 A 类,
|
编译器 C++
C++中的多重继承
🐰多重继承 🌸声明多重继承的方法 🌸多重继承派生类的构造函数与析构函数 🌸多重继承引起的二义性
|
Java 编译器 Android开发
Android C++系列:C++最佳实践4多重继承与虚继承
Java和C++在语法层面比较的时候就不得不提到C++的多继承,我们知道Android是单继承,C++是多继承。在大型项目中不可避免的会用到多继承,本文分析C++多继承的一些特征。
145 0
|
Java C++
C++继承与派生解析(继承、重载/转换运算符、多重继承、多态、虚函数/纯虚函数、抽象类)
C++继承与派生解析(继承、重载/转换运算符、多重继承、多态、虚函数/纯虚函数、抽象类)
206 0
C/C++---Person类、学生类、教师类和研究生类(多重继承)
C/C++---Person类、学生类、教师类和研究生类(多重继承)
912 0
|
C++
C++第13周项目2 - 多重继承教师类和干部类
课程首页地址:http://blog.csdn.net/sxhelijian/article/details/7910565,本周题目链接:http://blog.csdn.net/sxhelijian/article/details/8953304 【项目2】分别定义Teacher(教师)类和Cadre(干部)类,采用多重继承方式由这两个类派生出新类Teacher_Cadre(教师兼干部)
1610 0