基本指针如何能够知道派生类实例中基本成员的内存位置?

时间:2016-12-15 06:47:38

标签: c++ inheritance compiler-construction runtime

假设以下代码具有基本结构

struct A {int aMember};
struct B {bool bMember};
struct C {double cMember};
struct BA : B, A {};
struct CB : C, B {} ;

void test(B *base) {
    bool retrieved = base->bMember;
}

这里测试函数能够传递指向B,BA或CB实例的指针。如何检索基类成员" bMember"以低级别达成?据推测,对于每个派生类型,不能保证该成员位于与传递的对象收件人的给定偏移处。简而言之,它是如何知道的#34;对于任何给定的派生类型,B&C成员的切片位于何处?这是通过某种与使用继承的对象和类相关联的元数据在运行时实现的吗?

如果已经发布了一个简单的解释,我非常抱歉。我只是不知道如何用我的搜索来回复相关的答案。

三江源!

3 个答案:

答案 0 :(得分:1)

必须使用test类型的参数调用

B*。编译器知道即使它看不到test的定义,因为C ++需要在引用它的任何转换单元中声明一个函数。

C ++允许您使用指向test的指针调用CB,因为它知道如何转换 CB*B*

如果结构没有虚拟成员,则转换通常非常简单。 CB对象将在某个偏移量处包含B对象。要将CB*转换为B*,只需添加此偏移量即可。 test不需要知道参数已被转换,甚至不需要知道CB甚至存在。

如果有虚函数,事情会稍微复杂一些。原则上,编译器仍然以相同的方式调整CB*,但这不足以在运行时找到正确的虚函数。

虽然有多种方法可以实现虚函数,但C ++标准没有指定甚至推荐解决方案,但基本策略是在具有虚函数的对象中包含指向“vtable”的指针。 vtable是由实际对象实现的虚函数的函数指针序列。因此,B对象中的CB对象将具有指向CB定义的虚函数的vtable指针。必须使用指向实际this对象的CB来调用这些函数,因此vtable或B对象还必须包含足够的信息以从{{{{{}}派生CB" 1}}在运行时。一种可能的解决方案是存储调整(将从B*中减去),但是存在各种其他可能性,具有不同的优点和缺点。

答案 1 :(得分:0)

标准没有定义它。
实际的内存布局通常取决于所选的ABI 例如,其中一个是Itanium ABI 从介绍:

  

C ++程序的应用程序二进制接口,即用户C ++代码与实现提供的系统和库之间的目标代码接口。
  这包括C ++数据对象的内存布局,包括预定义和用户定义的数据类型,以及内部编译器生成的对象,如虚拟表。它还包括函数调用接口,异常处理接口,全局命名和各种目标代码约定。

Here是规则内存布局的部分。

答案 2 :(得分:0)

B的特定成员始终与base的偏移量相同,因为baseB*

我认为你错过的这个难题是当你通过test或者BA*时,CB*没有通过对象本身的地址B
相反,它传递了void test(B *base) { std::cout << "test got " << base << std::endl; } int main() { BA ba; CB cb; std::cout << "&ba: " << &ba << std::endl; std::cout << "ba's B subobject: " << static_cast<B*>(&ba) << std::endl; test(&ba); std::cout << "&cb: " << &cb << std::endl; std::cout << "cb's B subobject: " << static_cast<B*>(&cb) << std::endl; test(&cb); } 类型的各自子对象的地址。

使用您的课程的示例:

&ba: 0x28cc78
ba's B subobject: 0x28cc78
test got 0x28cc78
&cb: 0x28cc68
cb's B subobject: 0x28cc70
test got 0x28cc70

对我来说,这是印刷的

test

B的两次调用都会将B子对象传递给该函数,并且每个test对象看起来都相同,因此ba并不需要关心其他课程。

请注意,baB&#39; QElapsedTimer timer; timer.start(); slowOperation1(); qDebug() << "The slow operation took" << timer.elapsed() << "milliseconds"; qDebug() << "The slow operation took" << timer.nsecsElapsed() << "nanoseconds"; 子对象位于同一位置;这个特定的实现按照它们在继承列表中指定的顺序排列子对象,第一个对象首先位于派生对象中。
(这不是标准规定的,但它是一种非常常见的布局。)