以下是代码:
#include <iostream>
using namespace std;
class B1 {
public:
virtual void f1() {
cout << "B1\n";
}
};
class B2 {
public:
virtual void f1() {
cout << "B2\n";
}
};
class D : public B1, public B2 {
public:
void f1() {
cout << "OK\n" ;
}
};
int main () {
D dd;
B1 *b1d = ⅆ
B2 *b2d = ⅆ
D *ddd = ⅆ
cout << b1d << endl;
cout << b2d << endl;
cout << ddd << endl;
b1d -> f1();
b2d -> f1();
ddd -> f1();
}
输出结果为:
0x79ffdf842ee0
0x79ffdf842ee8
0x79ffdf842ee0
OK
OK
OK
这让我感到困惑,因为我预计b1d
和b2d
会相同,因为它们都指向dd
。但是,b1d
和b2d
的值根据结果而有所不同。我认为它可能与打字有关,但我不确定它是如何工作的。
有没有人有这方面的想法?
答案 0 :(得分:12)
D
继承自B1
和B2
。
由于B1
是从第一个继承的,因此首先构建对象的B1
部分,然后创建对象的B2
部分,然后D
。
所以你看到的是当你将派生类型的指针强制转换为基类型时,这些部分在内存中的区别。
b1d
和ddd
具有相同的地址,因为它们都指向内存中类的开头。
b2d
偏移,因为它指向B2
的{{1}}部分的开头。
答案 1 :(得分:5)
您对此的看法部分属实。该指针指的是对象的地址,当然这是一个类的一部分。为了更正式,这是指向该类的vtable的指针。但是在你从多个类继承的情况下。那么这应该指向什么呢?
说你有这个:
class concrete : public InterfaceA, public InterfaceB
从interfaceA和interfaceB继承的具体对象必须能够像bot interfaceA和interfaceB一样行动(这是公共的重点:当你继承时)。所以应该有一个“这个调整”,这样就可以做到。
通常,在多重继承的情况下,选择基类(例如interfacea)。在那种情况下,
几乎每个编译器都有一个“约定”来生成代码。例如,为了调用funa,编译器生成的程序集类似于:
call *(*objA+0)
其中+0部分是vtable内函数的偏移量。
编译器需要在编译时知道此方法的(funa)偏移量。
如果你想打电话,会发生什么?基于我们所说的,funb需要位于interfaceB对象的偏移0处。因此有thunk adjustor来调整它以便它指向interfaceB的vtable,以便可以正确地调用funB,再次使用:
call *(*objB+0)
如果你声明这样的话:
concrete *ac = new concrete();
interfaceB *ifb = ac;
你期待什么? concrete现在扮演interfaceB的角色:
如果我没记错的话,你可以打印ifb和ac(它们是指针),并验证它们是否指向不同的地址,但如果你检查它们是否相等:
ifb == ac;
你应该变得正确,因为它们被调整以便描述它们是相同的动态生成的对象。
答案 2 :(得分:0)
C ++标准指定对象的大小必须为at least 1(字节)。两个单独的对象不能具有相同的地址†。
子对象可以与包含它的对象具有相同的地址。通常†,没有子对象可以与另一个子对象具有相同的地址,因为它们不是直接相关的。因此(通常)最多一个子对象可以与容器对象具有相同的地址。
在这种情况下,D
的实例包含2个子对象。它们都是基类子对象。其中一个地址与容器对象的地址相同,另一个则没有。
当您将派生类型的指针强制转换为基类型时,转换指针将指向基类子对象。具有不同地址的子对象没有什么令人惊讶的。其中一个子对象与容器具有相同的地址也没有任何意外。
†顶部段落中的规则实际上有例外。空基类子对象不需要任何大小。这称为empty base optimization。您的基类不是空的,因此不适用。