在C++中,为什么当指针转换时地址会改变?

3
以下是代码示例:
#include <iostream>
using namespace std;

class B1 {
public:

  virtual void f1() {
      cout << "B1\n";
      }
};

class B2 {
public:

  virtual void f1() {
      cout << "B2\n";
      }
};        

class D : public B1, public B2 {
public:
  void f1() {
      cout << "OK\n" ;
  }
};

int main () {

D dd;    
B1 *b1d = &dd;
B2 *b2d = &dd;
D *ddd = &dd;

cout << b1d << endl;
cout << b2d << endl;
cout << ddd << endl;

b1d -> f1();
b2d -> f1();
ddd -> f1();
}

输出结果为:
0x79ffdf842ee0
0x79ffdf842ee8
0x79ffdf842ee0
OK
OK
OK

我觉得这很困惑,因为我预期 b1db2d 会相同,因为它们都指向 dd。然而,根据结果,b1db2d 的值是不同的。我认为这可能与类型转换有关,但我不确定它是如何工作的。

有人对此有想法吗?


2
这个链接在过去帮助了我很多。 - Mohamad Elghawi
这只是一个退化的例子。 - curiousguy
3个回答

12

D继承自B1B2

由于B1是从第一个继承,因此对象的B1部分将首先被构造,然后创建B2部分,最后是D

所以当你将派生类型的指针强制转换为基类型时,你看到的是这些部分在内存中的位置差异。

b1dddd具有相同的地址,因为它们都指向内存中类的开头。

b2d偏移了,因为它指向DB2部分的开头。


我明白了。这是C++标准,还是编译器特定的行为? - Hanfei Sun
@hanfeisun 这是标准行为。 - NathanOliver

5
你的理解部分正确。这个指针指向对象的地址,当然,该对象是类的一部分。更正式地说,这是指向该类vtable的指针。但是,在继承多个类的情况下,这应该指向什么?
假设你有以下代码:
class concrete : public InterfaceA, public InterfaceB

一个继承自interfaceA和interfaceB的具体对象必须能够像interfaceA和interfaceB一样运行(这就是公共继承的目的)。因此,应该有一个“this调整”,以便实现这一点。
通常,在多重继承的情况下,会选择一个基类(比如说interfaceA)。在那种情况下,...

object layout

几乎所有的编译器都有一个“惯例”来生成代码。例如,为了调用函数 funa,编译器生成的汇编代码大致如下:
call *(*objA+0)

“+0 部分是函数在 vtable 中的偏移量。”
“编译器需要在编译时知道这个方法(funa)的偏移量。”
“现在如果你想调用 funb 呢?根据我们所说的,funb 需要在 interfaceB 对象的偏移量 0 上。因此有 thunk adjustor 来进行调整,使其指向 interfaceB 的 vtable,以便正确调用 funB,再次使用:”
call *(*objB+0)

如果你声明了类似于这样的东西:
concrete *ac = new concrete();
interfaceB *ifb = ac;

你期望什么?现在混凝土扮演接口B的角色。
如果我没记错的话,你可以打印ifb和ac(它们是指针),并验证它们指向不同的地址,但如果你检查它们是否相等:
ifb == ac;

你应该得到“真”,因为它们被调整以表示它们是同一个动态生成的对象。

object layout


谢谢...讲解得非常清楚。 :) - Coder

0

C++标准规定对象的大小必须至少为1个字节。两个独立的对象不能有相同的地址

子对象可以与包含它的对象具有相同的地址。通常,没有子对象可以具有另一个子对象的相同地址,因为它们不是直接相关的。因此(通常情况下),最多只有一个子对象可以具有与容器对象相同的地址。

在这种情况下,一个D实例包含2个子对象。它们都是基类子对象。其中一个与容器对象具有相同的地址,而另一个则没有。

当您将派生类型的指针转换为基本类型时,转换后的指针将指向基类子对象。子对象具有不同地址并不令人惊讶。其中一个子对象具有与容器相同的地址也不足为奇。

实际上,顶部段落中的规则有一个例外。空基类子对象不需要具有任何大小。这被称为空基类优化。但是,您的基类不是空的,因此不适用。


它们不是空的,因为每个都有一个vtable指针。标准不要求这些vtable指针存在,但实际实现需要它们。 - JSF
@JSF 好的,这是一个不涉及标准的实现细节。在使用虚函数表的实现中,空基类优化在这里是不可能的。我需要检查标准中如何定义“空”类。 - eerorika
@JSF 我找不到“空类”的定义,但现在我认为它的意思是{};而不是一个没有子对象的类,就像我最初想的那样。因此,根据这个定义,OP的基类确实不是空的。 - eerorika

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接