C++中,为什么指针转换时地址变了?
In C++, why is the address changed when the pointer is converted?
代码如下:
#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 = ⅆ
B2 *b2d = ⅆ
D *ddd = ⅆ
cout << b1d << endl;
cout << b2d << endl;
cout << ddd << endl;
b1d -> f1();
b2d -> f1();
ddd -> f1();
}
输出为:
0x79ffdf842ee0
0x79ffdf842ee8
0x79ffdf842ee0
OK
OK
OK
这让我感到困惑,因为我预计 b1d
和 b2d
会相同,因为它们都指向 dd
。但是,b1d
和 b2d
的值根据结果不同。我认为这可能与类型转换有关,但我不确定它是如何工作的。
有人对此有想法吗?
D
继承自 B1
和 B2
.
由于 B1
是从第一个对象继承的,所以首先要构造对象的 B1
部分,然后再创建对象的 B2
部分,然后 D
.
因此,当您将派生类型的指针转换为基类型时,您看到的是这些部分在内存中的不同之处。
b1d
和 ddd
具有相同的地址,因为它们都指向内存中 class 的开头。
b2d
是偏移量,因为它指向 D
.
的 B2
部分的开始
C++标准规定对象的大小必须为at least 1(字节)。两个独立的对象不能有相同的地址†.
子对象可以与包含它的对象具有相同的地址。通常†,任何子对象都不能与另一个子对象具有相同的地址,因为它们没有直接关系。因此(通常)至多一个子对象可以具有与容器对象相同的地址。
在这种情况下,D
的实例包含 2 个子对象。它们都是基础 class 子对象。其中一个与容器对象具有相同的地址,而另一个则没有。
当您将派生类型的指针转换为基类型时,转换后的指针将指向基 class 子对象。具有不同地址的子对象并不奇怪。与容器具有相同地址的子对象之一也不足为奇。
†上面一段的规则其实有一个例外。空基础 class 子对象不需要任何大小。这被称为 empty base optimization。不过,您的基础 class 并不为空,因此不适用。
您的看法部分正确。这个指针指的是对象的地址,它当然是 class 的一部分。更正式地说,这是指向 class 的 vtable 的指针。但是如果你从多个 classes 继承。那么这个应该指向什么呢?
假设你有这个:
class concrete : public InterfaceA, public InterfaceB
从 interfaceA 和 interfaceB 继承的具体对象必须能够像 bot interfaceA 和 interfaceB 一样工作(这就是 public 的要点:当您继承时)。所以应该有一个 "this- adjustment" 这样才能做到这一点。
通常,在多重继承的情况下,选择基 class(例如 interfacea)。在那种情况下,
几乎每个编译器都有一个 "convention" 来生成代码。例如,为了调用 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;
你应该是对的,因为它们被调整以描述它们是同一个动态生成的对象。
代码如下:
#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 = ⅆ
B2 *b2d = ⅆ
D *ddd = ⅆ
cout << b1d << endl;
cout << b2d << endl;
cout << ddd << endl;
b1d -> f1();
b2d -> f1();
ddd -> f1();
}
输出为:
0x79ffdf842ee0
0x79ffdf842ee8
0x79ffdf842ee0
OK
OK
OK
这让我感到困惑,因为我预计 b1d
和 b2d
会相同,因为它们都指向 dd
。但是,b1d
和 b2d
的值根据结果不同。我认为这可能与类型转换有关,但我不确定它是如何工作的。
有人对此有想法吗?
D
继承自 B1
和 B2
.
由于 B1
是从第一个对象继承的,所以首先要构造对象的 B1
部分,然后再创建对象的 B2
部分,然后 D
.
因此,当您将派生类型的指针转换为基类型时,您看到的是这些部分在内存中的不同之处。
b1d
和 ddd
具有相同的地址,因为它们都指向内存中 class 的开头。
b2d
是偏移量,因为它指向 D
.
B2
部分的开始
C++标准规定对象的大小必须为at least 1(字节)。两个独立的对象不能有相同的地址†.
子对象可以与包含它的对象具有相同的地址。通常†,任何子对象都不能与另一个子对象具有相同的地址,因为它们没有直接关系。因此(通常)至多一个子对象可以具有与容器对象相同的地址。
在这种情况下,D
的实例包含 2 个子对象。它们都是基础 class 子对象。其中一个与容器对象具有相同的地址,而另一个则没有。
当您将派生类型的指针转换为基类型时,转换后的指针将指向基 class 子对象。具有不同地址的子对象并不奇怪。与容器具有相同地址的子对象之一也不足为奇。
†上面一段的规则其实有一个例外。空基础 class 子对象不需要任何大小。这被称为 empty base optimization。不过,您的基础 class 并不为空,因此不适用。
您的看法部分正确。这个指针指的是对象的地址,它当然是 class 的一部分。更正式地说,这是指向 class 的 vtable 的指针。但是如果你从多个 classes 继承。那么这个应该指向什么呢?
假设你有这个:
class concrete : public InterfaceA, public InterfaceB
从 interfaceA 和 interfaceB 继承的具体对象必须能够像 bot interfaceA 和 interfaceB 一样工作(这就是 public 的要点:当您继承时)。所以应该有一个 "this- adjustment" 这样才能做到这一点。
通常,在多重继承的情况下,选择基 class(例如 interfacea)。在那种情况下,
几乎每个编译器都有一个 "convention" 来生成代码。例如,为了调用 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;
你应该是对的,因为它们被调整以描述它们是同一个动态生成的对象。