孔乙己之五----虚函数(下)

本文深入探讨了C++中多继承情况下虚函数的处理方法,包括虚函数表、构造函数及虚函数调用代码的变化,并解释了为何需要这样做。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文作者:sodme 本文出处:http://blog.youkuaiyun.com/sodme 声明: 本文可以不经作者同意, 任意复制, 转载, 但任何对本文的引用都请保留文章开始前三行的作者, 出处以及声明信息. 谢谢. 本文所需代码可从以下地址获得( 此地址含有多继承c++和asm代码 ): http://docs.google.com/Doc?id=dcb4rbgm_51cqf52xff 前文中, 我们知道了单继承时的虚函数处理方法, 那么, 多继承时, 又是如何处理的呢? 我们仍然重点考察以下几个方面: 1. 虚函数表的变化; 2. 构造函数的变化; 3. 虚函数调用代码的变化. 三个类的继承关系如图: |----> Base1Class MyClass | |----> Base2Class 类MyClass的虚函数表:
.long0 .long_ZTI7MyClass .long_ZN7MyClass14virtual_test_1Ev .long_ZN10Base1Class14virtual_test_3Ev .long_ZN7MyClass15virtual_test_myEv .long-8 .long_ZTI7MyClass .long_ZN10Base2Class14virtual_test_2Ev .long_ZN10Base2Class14virtual_test_3Ev
咦? 从形式上看, 与单继承时确实有所不同, 整个虚函数表似乎被分成了两个部分: 第一部分是Base1Class的, 第二部分是Base2Class的. 那个中幺机何在呢? 别急, 慢慢看. 类MyClass的构造函数:
movl8(%ebp),%eax movl%eax,(%esp) call_ZN10Base1ClassC2Ev;调用Base1Class的构造函数 movl8(%ebp),%eax addl$8,%eax movl%eax,(%esp) call_ZN10Base2ClassC2Ev;调用Base2Class的构造函数,注意其传递的this指针 movl$_ZTV7MyClass+8,%edx movl8(%ebp),%eax movl%edx,(%eax);将MyClass的虚函数表地址放在this处 movl$_ZTV7MyClass+28,%edx movl8(%ebp),%eax movl%edx,8(%eax);将MyClass虚函数表中属于Base2Class的那部分虚函数放在了this+8处 movl8(%ebp),%eax movl$1,16(%eax);对数据data1的访问是:this+16 movl8(%ebp),%eax movl$2,20(%eax);对数据data2的访问是:this+20
通过以上的语句和注释, 我们可以发现: 类MyClass的构造函数中, 分别调用了Base1Class和Base2Class的构造函数, 这并不奇怪, 但奇怪的是传递给Base2Class构造函数的this指针变成了MyClass::this+8. 另外, 类MyClass的虚函数表初始时, 分别初始化了两个地方, 一处是this, 一处是this+8. 而类MyClass的两个数据成员data1 和 data2的访问, 也不再是前文单继承情况下的 this+12 和 this+16, 而是多了4个字节. 种种迹象表明, 多继承情况下, 对象结构似乎变成了这样: | | |------------------------------| this -> | Base1Class虚函数表地址 | 0 |------------------------------| | Base1Class::base_1_data | +4 |------------------------------| | Base2Class虚函数表地址 | +8 |------------------------------| | Base2Class::base_2_data | +12 |------------------------------| | MyClass::data1 | +16 |------------------------------| | MyClass::data2 | +20 |------------------------------| | | 下面, 我们再看调用方式的变化. pMyClass->virtual_test_1():
movl-16(%ebp),%eax ;取this指针 movl(%eax),%eax;取虚函数表地址 movl(%eax),%edx;取virtual_test_1()函数地址 movl-16(%ebp),%eax movl%eax,(%esp) call*%edx ;调用virtual_test_1()
pMyClass->virtual_test_2():
movl-16(%ebp),%eax;取this指针 movl8(%eax),%eax;this=this+8 movl(%eax),%edx;取MyClass中属于Base2Class的虚函数表地址,即virtual_test_2()首址 movl-16(%ebp),%eax ;取this指针 addl$8,%eax;this=this+8 movl%eax,(%esp) call*%edx;调用virtual_test_2()
pMyClass->virtual_test_my():
movl-16(%ebp),%eax ;取this指针 movl(%eax),%eax;取虚函数表地址 addl$8,%eax;取virtual_test_my()存放的地址 movl(%eax),%edx;取virtual_test_my()函数首址 movl-16(%ebp),%eax movl%eax,(%esp) call*%edx ;调用virtual_test_my()
由此, 可以看出, 在多继承情况下, 在对象结构模型中, 会分开多处存放多个虚函数表的不同起始地址(当然, 虚函数表仍然只有一份, 只是在各处存放的针对于这同一个虚函数表的起始地址不同而已). 那么, 为什么这样作呢? 换个角度想一下, 自然也就明白了. 类MyClass分别继承于两个互不相干的类: Base1Class 和 Base2Class. 由于Base1Class和Base2Class互相没有继承关系, 那么, Base1Class的虚函数表中就不会有Base2Class的虚函数信息, 更重要的, 他们的类成员数据不会被另一个类包含. 反之亦然. 这样, 也就导致同时继承于这二者的类MyClass无法通过唯一的一个this指针来访问分属于两个类的不同的数据, 所以, 把它们分开管理几乎是必然的. 但是, 形如以下的语句:
pMyClass=newMyClass; Base2Class*pBase2Class; pBase2Class=pMyClass; pBase2Class->virtual_test_2();
如果pBase2Class的值仍然是pMyClass的this指针, 那么pBase2Class->virtual_test_2()这样的调用, 岂不是有问题了吗? 因为pBase2Class是Base2Class类型, 按Base2Class的类定义, 它的虚函数表是:
.long0 .long_ZTI10Base2Class .long_ZN10Base2Class14virtual_test_2Ev .long_ZN10Base2Class14virtual_test_3Ev
那么, pBase2Class->virtual_test_2()将会被转化成以下形式:
movl-12(%ebp),%eax ;取this指针 movl(%eax),%eax;取Base2Class虚表地址 movl(%eax),%edx;取virtual_test_2()地址 movl-12(%ebp),%eax movl%eax,(%esp) call*%edx
但是, 我们知道, pMyClass的虚表明明是:
.long0 .long_ZTI7MyClass .long_ZN7MyClass14virtual_test_1Ev .long_ZN10Base1Class14virtual_test_3Ev .long_ZN7MyClass15virtual_test_myEv .long-8 .long_ZTI7MyClass .long_ZN10Base2Class14virtual_test_2Ev .long_ZN10Base2Class14virtual_test_3Ev
从此虚表中可以看到, virtual_test_2()地址, 应该是+28呀?! 呵呵. 一切玄妙皆在这条赋值语句"pBase2Class = pMyClass;", 这条语句, 偷偷干了这些事:
movl-16(%ebp),%eax;-16(%ebp)是pMyClass addl$8,%eax;eax=pMyClass->this+8 movl%eax,-32(%ebp) jmp.L31 .L29: movl$0,-32(%ebp) .L31: movl-32(%ebp),%eax movl%eax,-12(%ebp);将this+8存入了pBase2Class变量中
this+8! 又是this+8! 没错. 当我们执行 pBase2Class = pMyClass 这条向下兼容的赋值语句时, 编译器会检查他们的继承派生关系, 并将正确的this指针赋给pBase2Class, 而并不是把this指针直接赋值左边的变量. 有人说, C++难, 可能也就是难在这些不容易为人听知的地方吧. 隐藏的东西越多, 学习的开销越大. 在我的测试代码中, 大家可以发现, 我注释掉了一条语句:
//pMyClass->virtual_test_3();
之所以把它注释掉, 是为了说明这样一个问题: 1. Base1Class和Base2Class可以拥有同名的虚函数, 无引用他们的情况下, 可以编译通过; 2. 但是, 如果有对同名虚函数的引用, 编译器则会报"未决的或容易引起歧义的调用"之类的错误. 但是, 如果换一种方式调用, 则是可以过关的:
pMyClass=newMyClass; pBase2Class=pMyClass; pBase2Class->virtual_test_3();
原因很显然, pBase2Class本身的类型, 已经消除了virtual_test_3()的调用歧义.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值