C++ 编译器';s此指针、虚拟函数和多重继承的详细信息
我正在读比亚恩的论文: 在第370页第3节中,比亚恩说,“编译器将成员函数的调用转换为带有“额外”参数的“普通”函数调用;“额外”参数是指向调用成员函数的对象的指针。” 我被这场争论中的多余部分弄糊涂了。请参见以下两个示例: 示例1:(第372页) c类对象c看起来像:C++ 编译器';s此指针、虚拟函数和多重继承的详细信息,c++,pointers,multiple-inheritance,virtual-functions,this-pointer,C++,Pointers,Multiple Inheritance,Virtual Functions,This Pointer,我正在读比亚恩的论文: 在第370页第3节中,比亚恩说,“编译器将成员函数的调用转换为带有“额外”参数的“普通”函数调用;“额外”参数是指向调用成员函数的对象的指针。” 我被这场争论中的多余部分弄糊涂了。请参见以下两个示例: 示例1:(第372页) c类对象c看起来像: struct vtbl_entry { void (*fct)(); int delta; } [[[A fields...]B-specific-fields....]C-specific-fields..
struct vtbl_entry {
void (*fct)();
int delta;
}
[[[A fields...]B-specific-fields....]C-specific-fields...]
^
|--- A, B & C all start at the same address
C:
编译器将对虚拟函数的调用转换为间接调用。比如说,
C* pc;
pc->g(2)
变得像:
(*(pc->vptr[1]))(pc, 2)
比亚恩的论文告诉了我上述结论。通过此点为C*
在下面的例子中,比亚恩讲了另一个让我完全困惑的故事
示例2:(第373页)
给两个班
class A {...};
class B {...};
class C: A, B {...};
C类对象可以作为连续对象进行布局,如下所示:
pc--> -----------
A part
B:bf's this--> -----------
B part
-----------
C part
-----------
[[A fields...][B fields....]C-specific-fields...]
^ ^
\ A&C start \ B starts
调用给定C*的B的成员函数:
C* pc;
pc->bf(2); //assume that bf is a member of B and that C has no member named bf.
Bjarne写道:“很自然,B::bf()期望一个B*(成为它的this指针)。”编译器将调用转换为:
bf__F1B((B*)((char*)pc+delta(B)), 2);
为什么这里需要一个B*指针作为this
?
如果我们只传递一个*C指针作为this
,我想我们仍然可以正确地访问B的成员。例如,要在B::bf()中获取类B的成员,我们只需要执行如下操作:*(此+偏移量)。编译器可以知道该偏移量。是这样吗
例1和例2的后续问题:
(1) 当它是一个线性链派生(示例1)时,为什么C对象可以预期与B和a子对象位于相同的地址?在示例1中,使用C*指针访问函数B::g中类B的成员没有问题吗?例如,我们想要访问成员b,在运行时会发生什么*(pc+8)
(2) 为什么我们可以对多重继承使用相同的内存布局(线性链派生)?假设在示例2中,类A
,B
,C
的成员与示例1完全相同<代码>A
:内部A
和f
B
:intb
和bf
(或称之为g
)C
:intc
和h
。为什么不直接使用内存布局,如:
-----------
+0: a
+4: b
+8: c
-----------
(3) 我已经编写了一些简单的代码来测试线性链派生和多重继承之间的差异
class A {...};
class B : A {...};
class C: B {...};
C* pc = new C();
B* pb = NULL;
pb = (B*)pc;
A* pa = NULL;
pa = (A*)pc;
cout << pc << pb << pa
现在,pc
和pa
具有相同的地址,而pb
是pa
和pc
的偏移量
为什么编译会产生这些差异
示例3:(第377页)
(1) 第一个问题是关于pc->g()
,它与示例2中的讨论有关。编译器是否执行以下转换:
pc->g() ==> g__F1B((*B)((char*)pc+delta(B)))
或者我们必须等待运行时执行此操作
(2) 比亚恩写道:在进入C::f
时,该指针必须指向C
对象的开头(而不是B
部分)。但是,在编译时通常不知道pb
指向的B
是C
的一部分,因此编译器不能减去常量delta(B)
为什么我们不能知道pb
指向的B
对象在编译时是C
的一部分?根据我的理解,B*pb=newc
,pb
指向已创建的C
对象,C
继承自B
,因此B
指针pb指向C
的一部分
(3) 假设我们不知道在编译时,通过pb
指向的B
指针是C
的一部分。所以我们必须为运行时存储delta(B),它实际上与vtbl一起存储。因此,vtbl条目现在看起来像:
struct vtbl_entry {
void (*fct)();
int delta;
}
[[[A fields...]B-specific-fields....]C-specific-fields...]
^
|--- A, B & C all start at the same address
比亚恩写道:
pb->f() // call of C::f:
register vtbl_entry* vt = &pb->vtbl[index(f)];
(*vt->fct)((B*)((char*)pb+vt->delta)) //vt->delta is a negative number I guess
我完全搞糊涂了。为什么(B*)不是(*vt->fct)((B*)((char*)pb+vt->delta))????根据我的理解和比亚恩在377页5.1节第一句的介绍,我们应该在这里传递一个C*asthis
在上面的代码片段之后,比亚恩继续写:
请注意,对象指针可能必须调整为po
在查找指向vtbl的成员之前,将int设置为正确的子对象
哦,老兄!!!我完全不知道比亚恩想说什么?你能帮我解释一下吗 理论上,编译器将获取代码中的任何this
,如果引用指针,则编译器将知道this
引用的是什么
Bjarne写道:“很自然,B::bf()期望一个B*(成为它的this指针)。”编译器将调用转换为:
bf__F1B((B*)((char*)pc+delta(B)), 2);
为什么在这里我们需要一个B*指针作为这个
单独考虑B
:编译器需要能够编译代码alaB::bf(B*this)
。它不知道可以从B
进一步派生出哪些类(并且在编译B::bf
很久之后才可能引入派生代码)。B::bf
的代码不会神奇地知道如何将指针从其他类型(例如C*
)转换为B*
,它可以用来访问数据成员和运行时类型信息(RTTI/虚拟调度表,typeinfo)
相反,调用方负责将有效的B*
提取到所涉及的任何实际运行时类型(例如C
)中的B
子对象。在这种情况下,C*
保存整个C
对象的起始地址,该地址可能与A
子对象的地址相匹配,而B
子对象是某个固定但非0的内存偏移量:必须将该偏移量(以字节为单位)添加到中
[[[A fields...]B-specific-fields....]C-specific-fields...]
^
|--- A, B & C all start at the same address
-----------
+0: a
+4: b
+8: c
-----------
[[A fields...][B fields....]C-specific-fields...]
^ ^
\ A&C start \ B starts
class A {
virtual void fa();
int a;
};
class B {
virtual void fb();
int b;
};
----------- ---vtbl---
+0: vptr --------------> +0: A::fa
+4: a ----------
-----------
----------- ---vtbl---
+0: vptr --------------> +0: B::fb
+4: b ----------
-----------
class C: A, B {
int c;
virtual void fa();
};
----------- ---vtbl---
+0: vptr1 -------------> +0: A::fa
+4: a
+8: vptr2 -------------> +4: B::fb
+12: b +8: C::fc
+16: c ----------
-----------