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

问题是 - 当基类不是多态而派生类是多态时,为什么基类和派生类中的“这个”地址不同?

4

3 回答 3

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
于 2012-07-21T16:25:21.950 回答
6

这看起来像一个典型的多态实现的行为,对象中有一个 v-table 指针。Base 类不需要这样的指针,因为它没有任何虚拟方法。这在 32 位机器上的对象大小中节省了 4 个字节。一个典型的布局是:

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

    ^
    | this

然而,Derived 类确实需要 v-table 指针。通常存储在对象布局中的偏移量 0 处。

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

    ^
    | this

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

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

这就解释了为什么您会在 Base 构造函数中看到将 4 添加到 this 指针值。

于 2012-07-21T16:35:51.510 回答
1

从技术上讲,正是发生的事情。

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

我们所能说的就是它this有一个类型,并指向可以通过它的类型访问的内容。再次,对成员的解除引用是如何发生的,这是一个实现细节。

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

通过定义 C++ 的方式,这个问题和答案一样毫无意义,因为它们隐含地假设实现是基于假定的布局。

事实上,在给定的情况下,两个对象子组件共享相同的来源,这只是一个(非常常见的)特殊情况。

例外是“重新解释”:当您“蒙蔽”类型系统时,只说“看看这堆字节,因为它们是这种类型的实例”:这是唯一你必须期望没有地址更改(并且没有责任)的情况来自编译器关于这种转换的意义)。

于 2012-07-21T17:24:59.367 回答