我正在阅读 Bjarne 的论文:Multiple Inheritance for C++。
在第 3 节第 370 页中,Bjarne 说“编译器将成员函数的调用转换为带有“额外”参数的“普通”函数调用;“额外”参数是指向成员函数所针对的对象的指针叫做。”
我对这个额外的论点感到困惑。请看以下两个例子:
示例 1:(第 372 页)
class A {
int a;
virtual void f(int);
virtual void g(int);
virtual void h(int);
};
class B : A {int b; void g(int); };
class C : B {int c; void h(int); };
一个 c 类对象 C 看起来像:
C:
----------- vtbl:
+0: vptr --------------> -----------
+4: a +0: A::f
+8: b +4: B::g
+12: c +8: C::h
----------- -----------
对虚函数的调用被编译器转换为间接调用。例如,
C* pc;
pc->g(2)
变成这样:
(*(pc->vptr[1]))(pc, 2)
Bjarne 的论文告诉了我上述结论。通过this
点为 C*。
在下面的例子中,Bjarne 讲述了另一个让我完全困惑的故事!
示例 2:(第 373 页)
给定两个类
class A {...};
class B {...};
class C: A, B {...};
C 类的对象可以像这样布置为连续的对象:
pc--> -----------
A part
B:bf's this--> -----------
B part
-----------
C part
-----------
在给定 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 的成员,我们只需要执行以下操作:*(this+offset)。编译器可以知道这个偏移量。这是正确的吗?
跟进示例 1 和 2 的问题:
(1)当它是一个线性链推导时(示例1),为什么可以期望C对象与B和A子对象位于同一地址?在示例 1 中使用 C* 指针访问函数 B::g 中的类 B 的成员没有问题吗?比如我们要访问成员b,运行时会发生什么?*(pc+8)?
(2)为什么我们可以对多重继承使用相同的内存布局(线性链推导)?假设在示例 2 中,类A
, B
,C
具有与示例 1 完全相同的成员。A
:int a
和f
; B
:int b
和bf
(或称它为g
);C
:int c
和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
它表明pa
和具有相同pb
的pc
地址。
class A {...};
class B {...};
class C: A, B {...};
C* pc = new C();
B* pb = NULL;
pb = (B*)pc;
A* pa = NULL;
pa = (A*)pc;
现在,pc
andpa
具有相同的地址,而与and有pb
一些偏移。pa
pc
为什么编译会产生这些差异?
示例 3:(第 377 页)
class A {virtual void f();};
class B {virtual void f(); virtual void g();};
class C: A, B {void f();};
A* pa = new C;
B* pb = new C;
C* pc = new C;
pa->f();
pb->f();
pc->f();
pc->g()
(1)第一个问题pc->g()
与示例2中的讨论有关。编译是否进行以下转换:
pc->g() ==> g__F1B((*B)((char*)pc+delta(B)))
或者我们必须等待运行时来执行此操作?
(2) Bjarne 写道: 在入口处C::f
,this
指针必须指向C
对象的开头(而不是B
部分)。但是,在编译时通常不知道B
by 指向的pb
是 a 的一部分,C
因此编译器不能减去常量delta(B)
。
为什么我们不能在编译时知道B
指向的对象pb
是 a 的一部分?C
根据我的理解,B* pb = new C
,pb
指向创建的C
对象并C
继承自B
,因此B
指针 pb 指向C
.
(3) 假设我们在编译时不知道B
指向 by的指针pb
是 a 的一部分。C
所以我们必须存储运行时的 delta(B),它实际上与 vtbl 一起存储。所以 vtbl 条目现在看起来像:
struct vtbl_entry {
void (*fct)();
int delta;
}
比亚恩写道:
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*) 不是 (C*) 在(*vt->fct)((B*)((char*)pb+vt->delta))
???? 根据我的理解和 Bjarne 在第 5.1 节第 377 页的第一句的介绍,我们应该在this
这里传递一个 C* !!!!!!
紧接着上面的代码片段,Bjarne 继续写道: 注意,在寻找指向 vtbl 的成员之前,可能需要调整对象指针以指向正确的子对象。
天啊!!!我完全不知道 Bjarne 想说什么?你能帮我解释一下吗?