【发布时间】:2015-06-10 05:18:38
【问题描述】:
我正在阅读 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 和 pa 具有相同的地址,而 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 部分)。但是,在编译时通常不知道pb 指向的B 是C 的一部分,因此编译器不能减去常量delta(B)。
为什么在编译时我们无法知道pb 指向的B 对象是C 的一部分?根据我的理解,B* pb = new C、pb 指向创建的C 对象,C 继承自B,所以B 指针pb 指向C 的一部分。
(3) 假设在编译时我们不知道pb 指向的B 是C 的一部分。所以我们必须存储运行时的 delta(B),它实际上与 vtbl 一起存储。所以 vtbl 条目现在看起来像:
struct vtbl_entry {
void (*fct)();
int delta;
}
Bjarne 写道:
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
我在这里完全糊涂了。为什么 (*vt->fct)((B*)((char*)pb+vt->delta)) 中的 (B*) 不是 (C*)????根据我的理解和 Bjarne 在第 5.1 节第 377 页第一句的介绍,我们应该在这里传递一个 C* 为 this !!!!!!
接着上面的代码sn-p,Bjarne继续写: 请注意,对象指针可能必须调整为 po 在寻找指向 vtbl 的成员之前 int 到正确的子对象。
哦,伙计!!!我完全不知道 Bjarne 想说什么?你能帮我解释一下吗?
【问题讨论】:
标签: c++ pointers multiple-inheritance virtual-functions this-pointer