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

How are base pointers able to know the memory location of base members in a derived class instance?

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

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 实例的指针。底层class成员"bMember"的检索是如何实现的?据推测,对于每个派生类型,不能保证成员位于距传递的对象地址的给定偏移量处。简而言之,对于任何给定的派生类型,"known" B 的成员切片位于何处?这是在 运行 时间通过某种与对象关联的元数据和 class 使用继承实现的吗?

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

谢谢!

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

Application Binary Interface for C++ programs, that is, the object code interfaces between user C++ code and the implementation-provided system and libraries.
This includes the memory layout for C++ data objects, including both predefined and user-defined data types, as well as internal compiler generated objects such as virtual tables. It also includes function calling interfaces, exception handling interfaces, global naming, and various object code conventions.

Here 是关于内存布局的部分。

test 必须使用 B* 类型的参数调用。编译器知道即使它看不到 test 的定义,因为 C++ 要求在引用它的任何翻译单元中声明一个函数。

C++ 允许您使用指向 CB 的指针调用 test 正是因为它知道如何 convert a CB* to a B*.

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

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

虽然虚函数的实现方式多种多样,C++标准也没有规定甚至没有推荐解决方案,但基本的策略是在具有虚函数的对象中包含一个指向"vtable"的指针。 vtable 是实际对象实现的虚函数的函数指针序列。因此,CB 对象内的 B 对象将有一个指向由 CB 定义的虚函数的 vtable 指针。这些函数必须在 this 指向实际 CB 对象的情况下调用,因此 vtable 或 B 对象还必须包含足够的信息以从 B*在运行时间。一种可能的解决方案是存储调整(将从 B* 中减去),但还有多种其他可能性,各有优缺点。

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

我认为你遗漏的一块拼图是当你传递 BA* 或 [=18= 时 test 没有传递对象本身的地址].
相反,它传递了 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不需要关心另一个类。

注意babaB子对象在同一个地方;这个特定的实现按照它们在继承列表中指定的顺序排列子对象,第一个位于派生对象中的第一个。
(这不是标准强制要求的,但它是一种非常常见的布局。)