当基类不是多态而是派生时,'this'地址不匹配

scd*_*dmb 9 c++ polymorphism inheritance memory-layout

有这个代码:

#include <iostream>

class Base
{
public:
    Base() {
        std::cout << "Base: " << this << std::endl;
    }
    int x;
    int y;
    int z;
};

class Derived : Base
{
public:
    Derived() {
        std::cout << "Derived: " << this << std::endl;
    }

    void fun(){}
};

int main() {
   Derived d;
   return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出:

Base: 0xbfdb81d4
Derived: 0xbfdb81d4
Run Code Online (Sandbox Code Playgroud)

但是,当Derived类中的函数'fun'更改为virtual时:

virtual void fun(){} // changed in Derived
Run Code Online (Sandbox Code Playgroud)

然后,'this'的地址在两个构造函数中都不相同:

Base: 0xbf93d6a4
Derived: 0xbf93d6a0
Run Code Online (Sandbox Code Playgroud)

另一件事是如果类Base是多态的,例如我添加了一些其他虚函数:

virtual void funOther(){} // added to Base
Run Code Online (Sandbox Code Playgroud)

那么'this'匹配的地址再次:

Base: 0xbfcceda0
Derived: 0xbfcceda0
Run Code Online (Sandbox Code Playgroud)

问题是 - 当Base类不是多态的并且Derived类是?时,为什么'this'地址在Base和Derived类中是不同的?

AnT*_*AnT 14

当您具有类的多态单继承层次结构时,大多数(如果不是全部)编译器遵循的典型约定是该层次结构中的每个对象必须以VMT指针(指向虚方法表的指针)开头.在这种情况下,VMT指针很早就被引入到对象内存布局中:通过多态层次结构的根类,而所有下层类只是继承它并将其设置为指向它们正确的VMT.在这种情况下,任何派生对象中的所有嵌套子对象都具有相同的this值.通过读取*this编译器的内存位置,这种方式可以立即访问VMT指针,而不管实际的子对象类型如何.这正是您上次实验中发生的情况.当您使根类具有多态性时,所有this值都匹配.

但是,当层次结构中的基类不是多态时,它不会引入VMT指针.VMT指针将由层次结构中较低位置的第一个多态类引入.在这种情况下,流行的实现方法是由层次结构的非多态(上部)部分引入的数据之前插入VMT指针.这是您在第二个实验中看到的内容.内存布局Derived如下

+------------------------------------+ <---- `this` value for `Derived` and below
| VMT pointer introduced by Derived  |
+------------------------------------+ <---- `this` value for `Base` and above
| Base data                          |
+------------------------------------+
| Derived data                       |
+------------------------------------+
Run Code Online (Sandbox Code Playgroud)

同时,层次结构的非多态(上层)部分中的所有类都不应该知道任何VMT指针.Base类型对象必须以数据字段开头Base::x.同时,层次结构的多态(较低)部分中的所有类必须以VMT指针开头.为了满足这两个要求,编译器被迫调整对象指针值,因为它在层次结构中从一个嵌套的基础子对象上下转换为另一个.这立即意味着跨多态/非多态边界的指针转换不再是概念性的:编译器必须添加或减去一些偏移量.

来自层次结构的非多态部分的子对象将共享它们的this值,而来自层次结构的多态部分的子对象将共享它们自己的不同this值.

在沿层次结构转换指针值时必须添加或减去一些偏移量并不罕见:编译器必须在处理多继承层次结构时始终执行此操作.但是,您的示例显示了如何在单继承层次结构中实现它.

加法/减法效果也将在指针转换中显示

Derived *pd = new Derived;
Base *pb = pd; 
// Numerical values of `pb` and `pd` are different if `Base` is non-polymorphic
// and `Derived` is polymorphic

Derived *pd2 = static_cast<Derived *>(pb);
// Numerical values of `pd` and `pd2` are the same
Run Code Online (Sandbox Code Playgroud)

  • @Agnel Kurian:当你进行`pb == pd`比较时,编译器会发现指针类型不同.该语言称`Base*`必须用作"常用"类型进行比较,即在这种情况下`pd`必须转换为`Base*`类型.换句话说,你的'pb == pd`被解释为`pb ==(Base*)pd`.转换是在比较之前调整右侧指针的内容.你并不是真的用'pb == pd`来比较指针的"数字"值. (2认同)

Han*_*ant 6

这看起来像是对象中具有v表指针的多态的典型实现的行为.Base类不需要这样的指针,因为它没有任何虚方法.这样可以在32位计算机上保存对象大小的4个字节.典型的布局是:

+------+------+------+
|   x  |   y  |   z  |
+------+------+------+

    ^
    | this
Run Code Online (Sandbox Code Playgroud)

但是Derived类确实需要v表指针.通常存储在对象布局中的偏移0处.

+------+------+------+------+
| vptr |   x  |   y  |   z  |
+------+------+------+------+

    ^
    | this
Run Code Online (Sandbox Code Playgroud)

因此,为了使Base类方法看到对象的相同布局,代码生成器在调用Base类的方法之前将4添加到this指针.构造函数看到:

+------+------+------+------+
| vptr |   x  |   y  |   z  |
+------+------+------+------+
           ^
           | this
Run Code Online (Sandbox Code Playgroud)

这解释了为什么你看到4添加到Base构造函数中的this指针值.