首页 文章

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

提问于
浏览
9

有这个代码:

#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;
}

输出:

Base: 0xbfdb81d4
Derived: 0xbfdb81d4

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

virtual void fun(){} // changed in Derived

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

Base: 0xbf93d6a4
Derived: 0xbf93d6a0

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

virtual void funOther(){} // added to Base

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

Base: 0xbfcceda0
Derived: 0xbfcceda0

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

3 回答

  • 1

    从技术上讲,this正是如此 .

    但是必须注意的是,根据语言规范,多态的实现并不一定与vtables有关:这就是规范 . 定义为“实现细节”,这超出了规范范围 .

    我们所能说的是 this 有一个类型,并指向可通过其类型访问的内容 . 再次引用成员的方式是一个实现细节 .

    通过隐式,静态或动态转换将 pointer to something 转换为 pointer to something else 时,必须更改以适应周围的事实必须被视为规则,而不是例外 .

    顺便说一下C的定义方式,问题就像答案一样毫无意义,因为他们隐含地假设实现是基于假设的布局 .

    在给定情况下,两个对象子组件共享相同原点的事实只是(非常常见的)特定情况 .

    例外是“重新解释”:当你“盲目”类型系统时,只是说“查看这一堆字节,因为它们是这种类型的实例”:这是唯一的情况,你必须期望没有地址变化(并且没有责任)从编译器关于这种转换的意义) .

  • 6

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

    +------+------+------+
    |   x  |   y  |   z  |
    +------+------+------+
    
        ^
        | this
    

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

    +------+------+------+------+
    | vptr |   x  |   y  |   z  |
    +------+------+------+------+
    
        ^
        | this
    

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

    +------+------+------+------+
    | vptr |   x  |   y  |   z  |
    +------+------+------+------+
               ^
               | this
    

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

  • 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                       |
    +------------------------------------+
    

    同时,层次结构的非多态(上层)部分中的所有类都应该对任何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
    

相关问题