c++多继承debug经历

来源:互联网 发布:淘宝ntf鞋店靠谱吗 编辑:程序博客网 时间:2024/06/14 05:56
  • 其实这次debug实质上与多继承没有什么关系,只是在解决多继承代码bug的经历中了解到了VC++在编译代码方式。

起因是我在一次项目的过程中,实现抽象工厂模式,把本应是纯虚类的工厂父类写成了实类,结果导致了一场血案,不过也从中学习到了不少知识。
起初,代码大概如下:

...class A{...void dosome();(这里应该是纯虚函数,不然是编辑器回静态联编,你子类实现的方法父类指针没法调用,这里后续详细讲)}class B:publlic A{...void dosome();}class C:public B{...void dosome();void say(char *string);}

我首先说一点,这样多重继承要慎用,不知道哪天他会折麽的你生不如死,最好三思到底合适吗?如果合适的话,那还是用吧。其实那天的代码差不多就是这个形式,只不过代码要多一点,一不注意,认为A类是纯虚类,结果就引发了我接下来debug经历。

在类建立完成以后我就在进程中创建对象,并调用其方法,就在这里出现了问题

int  main(){...A* pmain= new C();pmain->dosome();pmain->say("hello world!!!");  //这是我预想的操作...}

发现编译器一直报错,C类里面没有say()方法,这时我就觉得很奇怪,我创建C对象写法没有错啊,检查了几遍发现应该不是语法问题,然后我觉得可能是构造函数出现了问题,于是我在C类的构造函数上打断点,发现创建对象也完美,紧接着为在他父类和间接父类上都打上断点,发现调用构造函数都很正常。这里在思考了一会儿我想先确定我A类的指针指向的到底是什么对象。
在相关类里面加入了测试函数,就是一个空函数,创建的对象能不能调用,结果发现我创建的是A类的对象,这就突破我的认知了,明明是创建的C对象,结果是A对象,难道虚基类还能通过子类创造出自己的实例来?然后我又换了一种声明方式:

{...B* pmain=new C();pmain->say("hello world!!!");...}

结果也是不能调用,然后我用相同的方法确定了创建的对象是B类。我这就发现,什么类型的指针,创建的就是什么对象。我觉得这是我没有了解到的C++的特性,便在网上查关于多重继承和VC++编译器在创建对象时的资料。

我先整理一下关于多重继承和虚继承(共享继承)的资料:
浅语博客
*假如我们有类A是父类,类B和类C继承了类A,而类D既继承类B又继承类C(这种菱形继承关系)。当我们实例化D的对象的时候,每个D的实例化对象中都有了两份完全相同的A的数据。因为保留多份数据成员的拷贝,不仅占用较多的存储空间,还增加了访问这些成员时的困难,容易出错,而实际上,我们并不需要有多份拷贝。
针对这种情况,C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员。*

class A //声明基类A{    A(int i); //申明一个带有参数的构造函数};class B: virtual public A //A是B的虚基类{    B(int n):A(n){ }  //B类构造函数,在初始化列表中对虚基类A进行初始化};class C: virtual public A //A是C的虚基类{    C(int n):A(n){ }  //C类构造函数,在初始化列表中对虚基类A进行初始化};class D: public B, public C{    D(int n):A(n),B(n),C(n){ }  //D类构造函数,在初始化列表中对所有基类进行初始化};

*【注意]:在定义类D的构造函数时,与以往使用的方法有所不同。以往,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。现在,由于虚基类在派生类中只有一份数据成员,所以这份数据成员的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
有的读者会提出:类D的构造函数通过初始化表调了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了虚基类的构造函数A,这样虚基类的构造函数岂非被调用了3次?大家不必过虑,C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C) 对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。*

我也查了许多相关资料发现我的问题并没有解决,而且发现没有相关的继承语法解释我的问题。于是我觉得并不是继承产生的问题,应该是编译器在构建子对象是有特殊的方法。于是我查资料,了解vc++在内存方面是如何构建一个对象的。

C++对象继承内存分布struct Employee { … };
*struct Manager : Employee { … };
struct Worker : Employee { … };
struct MiddleManager : Manager, Worker { … };
如果经理类和工人类都继承自雇员类,很自然地,它们每个类都会从雇员类获得一份数据拷贝。如果不作特殊处理,一线经理类的实例将含有两个雇员类实例,它们分别来自两个雇员基类。如果雇员类成员变量不多,问题不严重;如果成员变量众多,则那份多余的拷贝将造成实例生成时的严重开销。更糟的是,这两份不同的雇员实例可能分别被修改,造成数据的不一致。因此,我们需要让经理类和工人类进行特殊的声明,说明它们愿意共享一份雇员基类实例数据。
很不幸,在C++中,这种“共享继承”被称为“虚继承”,把问题搞得似乎很抽象。虚继承的语法很简单,在指定基类时加上virtual关键字即可。
struct Employee { … };
struct Manager : virtual Employee { … };
struct Worker : virtual Employee { … };
struct MiddleManager : Manager, Worker { … };
使用虚继承,比起单继承和多重继承有更大的实现开销、调用开销。回忆一下,在单继承和多重继承的情况下,内嵌的基类实例地址比起派生类实例地址来,要么地址相同(单继承,以及多重继承的最靠左基类),要么地址相差一个固定偏移量(多重继承的非最靠左基类)。然而,当虚继承时,一般说来,派生类地址和其虚基类地址之间的偏移量是不固定的,因为如果这个派生类又被进一步继承的话,最终派生类会把共享的虚基类实例数据放到一个与上一层派生类不同的偏移量处。请看下例:*
这里写图片描述
*struct G : virtual C {
int g1;
void gf();
};
译者注:GdGvbptrG(In G, the displacement of G’s virtual base pointer to G)意思是:在G中,G对象的指针与G的虚基类表指针之间的偏移量,在此可见为0,因为G对象内存布局第一项就是虚基类表指针; GdGvbptrC(In G, the displacement of G’s virtual base pointer to C)意思是:在G中,C对象的指针与G的虚基类表指针之间的偏移量,在此可见为8。*
这里写图片描述
struct H : virtual C {
int h1;
void hf();
};
这里写图片描述
*struct I : G, H {
int i1;
void _if();
};
暂时不追究vbptr成员变量从何而来。从上面这些图可以直观地看到,在G对象中,内嵌的C基类对象的数据紧跟在G的数据之后,在H对象中,内嵌的C基类对象的数据也紧跟在H的数据之后。但是,在I对象中,内存布局就并非如此了。VC++实现的内存布局中,G对象实例中G对象和C对象之间的偏移,不同于I对象实例中G对象和C对象之间的偏移。当使用指针访问虚基类成员变量时,由于指针可以是指向派生类实例的基类指针,所以,编译器不能根据声明的指针类型计算偏移,而必须找到另一种间接的方法,从派生类指针计算虚基类的位置。
在VC++中,对每个继承自虚基类的类实例,将增加一个隐藏的“虚基类表指针”(vbptr)成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量。
其它的实现方式中,有一种是在派生类中使用指针成员变量。这些指针成员变量指向派生类的虚基类,每个虚基类一个指针。这种方式的优点是:获取虚基类地址时,所用代码比较少。然而,编译器优化代码时通常都可以采取措施避免重复计算虚基类地址。况且,这种实现方式还有一个大弊端:从多个虚基类派生时,类实例将占用更多的内存空间;获取虚基类的虚基类的地址时,需要多次使用指针,从而效率较低等等。
在VC++中,G拥有一个隐藏的“虚基类表指针”成员,指向一个虚基类表,该表的第二项是GdGvbptrC。(在G中,虚基类对象C的地址与G的“虚基类表指针”之间的偏移量(当对于所有的派生类来说偏移量不变时,省略“d”前的前缀))。比如,在32位平台上,GdGvptrC是8个字节。同样,在I实例中的G对象实例也有“虚基类表指针”,不过该指针指向一个适用于“G处于I之中”的虚基类表,表中一项为IdGvbptrC,值为20。
观察前面的G、H和I,我们可以得到如下关于VC++虚继承下内存布局的结论:
 首先排列非虚继承的基类实例;
 有虚基类时,为每个基类增加一个隐藏的vbptr,除非已经从非虚继承的类那里继承了一个vbptr;
 排列派生类的新数据成员;
 在实例最后,排列每个虚基类的一个实例。*

学习了这么多,快要写一个C++编译器了(开玩笑),但是最后发现只是没写纯虚类,导致编译器静态联编函数,用什么指针在编译阶段编译器就已经确定了创建对象,当然不会有子类的方法,虽然没浪费时间,但是也想把自己打一顿,太蠢了。让我明白看来杀不死你的bug才能让你更强大啊。