c++ 非虚拟继承可以正常使用,虚拟继承后执行的问题
2009-10-28 11:31
351 查看
本文源于C++对话系列
类层次结构相当简单:
class parent { public: virtual void f(); // etc... }; class child : public virtual parent { public: void f(); };
我所做的修改之一就是把child虚拟地继承于parent,使它在这个类层次结构的任何地方都能使用。我尽了最大努力,但看上去我别无选择——我打算深入挖掘鲍勃的代码了。“又要搞破坏了,亲爱的朋友”,我嘀咕着,设置了一个断点,准备单步跟踪这个函数。经过半小时的严格调试,我从一团杂乱中找到了问题的根源:
void parentPtrPtr(parent **b) { (*b)->f(); } void childPtrPtr(child **d) { parentPtrPtr(reinterpret_cast<parent **>(d)); }
对于最后一个函数,我自有想法。我知道要谨慎对待cast,特别是reinterpret_cast。我去掉reinterpret_cast,不出所料,编译器卡在这行代码上,抱怨paraent**和child**是不相关的类型。
“你的怀疑是对的,我的学徒工。”当Guru柔和的声音在我身后响起时,我记得只有一次没被她吓一跳。这次也是如此,但没有太吃惊。“指向child的指针的指针,”她接着说:“不能被隐式转换为指向base的指针的指针。
我向后踢了踢腿,伸了伸懒腰,“我也这样想。鲍勃所做的一切好象就是为了避免编译器报错。我不理解为什么它能在原先的类上工作,但现在却会导致崩溃。唉,我甚至不明白为什么编译器不能做隐式类型转换。”
“你现在所体验的崩溃正是它不允许这么做的十足理由,”Guru理了理耳后一络银色的头发,“你的派生类虚拟地继承于基类。”我象一只在车灯前不知所措的鹿一样紧紧盯着她,让她知道我没搞明白。“考虑一种简单继承的情况,”她说,拿起一支笔,开始在我的白色书写板上写起来:
class parent { /* 不管怎样都行,但至少要有一个虚拟函数*/ }; class child : public parent { /* 随便怎样都行 */ };
“虚拟函数的一种典型实现,”她接着说,“在内存布局中,编译器首先会给类附加一个指向虚拟函数表(vptr)的指针,然后是parent类的数据,再接下来是child类的数据。”
parent::vptr |
parent data |
child data |
“是这样,我的的孩子。神圣的标准并没有规定必须要有虚拟函数表,事实上,它甚至没提到它。然而,庄严的标准委员会的成员们的目标之一就是规范化现有编译器的实际情况。允许child** 隐式转换为parent**将在许多现有的编译器上造成问题。其中一个特别的问题就是我接下来要说的,如果你允许我说下去的话。”她严肃地看着我,我没敢说什么。
“这种布局方式意味着this所指向的地址可以既是基类又是派生类。使用我们所假设的编译器,this始终指向vptr。派生类的成员函数可以简单地通过this取得其数据成员的地址,只要加上一个包括基类大小和vptr大小的偏移量即可。
“然而,当你在这个混合体里再加上虚拟继承时,情况就变得更加复杂,考虑一下:”
class parent { /* whatever */ }; class child1 : public virtual parent { /* whatever */ }; class child2 : public virtual parent { /* whatever */ }; class multi : public child1, public child2 { /* whatever */ };
“显然,由于上面parent类的数据紧随child类的数据之后,编译器无法对中间类child1和child2采取和child一样的布局形式,因为虚拟继承规定只能出现基类数据的一份拷贝。解决方案——确切地说,解决方案之一就是在每个子对象的开始处附加一个vptr,如下所示:”
parent::vptr |
parent data |
child1::vptr |
child1 data |
child2::vptr |
child2 data |
multi::vptr |
multi data |
“哦!我明白你的意思了,”我说,“当你有一个multi对象时,指向它的指针的值有时会稍有不同,这取决于它是指向parent子对象还是child1子对象。这也就解释了为什么中间类必须使用virtual关键字——我以前一直认为这是个错误,而multi应该是使用virtual的类。所以,不管怎样,让我们回到原来的那个问题。这就是说……嗯……不,我还是不太明白,这跟指向指针的指针又有什么关系呢?”
她慢条斯理地说:“让我们给这些值分配任意的内存地址,假定每个类恰好包含一个整型数,这样整型数和指针的大小都是4个字节,不存在填充和对齐问题。让我们再加上一些代码,举几个multi类的实例:”
void f() { multi m; multi *pm = &m; multi **ppm = ± //...
Address | Description | Value |
0x1000 | parent::vptr | ? |
0x1004 | parent data | ? |
0x1008 | child1::vptr | ? |
0x100c | child1 data | ? |
0x1010 | child2::vptr | ? |
0x1014 | child2 data | ? |
0x1018 | multi vptr | ? |
0x101c | multi data | ? |
0x1020 | Pm | 0x1000 |
0x1024 | Ppm | 0x1020 |
//... child1 * pc1 = &m; child1 ** ppc1 = &pc1; //...
“编译器简单地取m的地址,将它调整为正确的子对象(在这里,也就是child1子对象的起始处),把它赋值给pcl。ppcl只是简单地包含pc1的地址:”
Address | Description | Value |
0x1028 | Pc1 | 0x1008 |
0x102c | ppc1 | 0x1028 |
//... “但现在假定你想把ppm赋值给ppcl:” //… ppc1 = ppm; //该怎么处理呢? //…
“假定编译器允许你把装在ppm内的值直接赋值给ppc1,ppc1的值就会被设为0x1020,也就是pm的地址,pm指向的是multi的基地址0x1000。当你对ppc1进行“提领”(dereference)操作时,你希望得到一个指向child1指针,但事实上你得到的却是指向parent的指针(或指向multi的指针,它们在我们假定的编译器里有相同的基地址。)”
“我明白了,”我惊叹道,“但等一下,为什么它在原来的类里没问题呢?”
“在非虚拟的继承里,这个特定的编译器把相同的基地址赋值给child和parent子对象。本质上,这两个地址的比特模式(bit-pattern)恰好相同,所以reinterpret_cast碰巧能正确工作。”
“太好了,”我说,“我明白这个道理了。现在我该怎么解决这个问题呢?”
“记住这句话‘每个问题都可以通过附加一个间接层来解决’。现在这种情况下,我的孩子,我们的间接层已经太多了。”
“太多的间接层,”我陷入沉思,突然,灵光一闪,“去掉一个间接层!”我迅速地在白色写字板写了一些代码。
void childPtrPtr(child **d) { parent *pp = *d; parentPtrPtr(&pp); }
“很好,我的孩子。当你将child*赋值给parent*时,就会对this参数进行调整。现在,parentPtrPtr函数得到的是正确的指向parent的指针的指针。”
相关文章推荐
- 虚拟IP实验,遇到场景启用使用虚拟IP就报错,不启用可以正常运行的问题,解决方法
- 关于Gson.jar导入正常使用,代码无错,但程序执行解析时崩溃的一些问题
- VS下面运行release版本可以正常运行而直接执行exe文件会出现问题
- C++中虚拟继承问题
- delphi 使用工控机控件 iThreadTimes 出现问题, 导致主程序创建页面的时候, 阻塞消息, 不能正常执行。
- Controls 属性与继承 TShape 类的小练习(使用TShape可以解决很多图形问题)
- 在使用win 7 无线承载网络时,启动该服务时,有时会提示:组或资源的状态不是执行请求操作的正确状态。 网上有文章指出,解决这个问题的方法是在设备管理器中启动“Microsoft托管网络虚拟适配
- centos ssh服务可以使用(能远程访问),SSH命令无法执行的问题
- 父类的正常成员函数子类可以使用,并不是继承下来 的,子类是怎么调用父类的函数 : 可能是利用了一种叫函数名联编的方法.没有隐藏的情况下用函数名字来决定调用的函数.
- 关于为什么某些C/C++环境下浮点数可以“正常”比较的问题
- C++设计一个无法被继承且能正常使用的类
- C++ 虚基类问题、继承体系中的构造函数执行过程。(
- C++中遇到同名函数问题,可以使用命名空间解决
- 使用crontab不能正常执行的问题
- 如何实现不能被继承的C++的类,且能正常使用
- 使用crontab不能正常执行的问题
- 我创建了一个托盘图标,可以正常使用,点击右键打开菜单。问题是如果点击右键后不选择其中一个菜单项进行操作的话,它就总不消失。
- 偶遇问题 - - 程序图标显示异常,程序可以正常使用
- C++虚拟继承问题
- 在继承自 UITableViewController 重构时使用 xib 无法显示问题,