Fih*_*hop 5 c++ pointers virtual-functions multiple-inheritance this-pointer
我正在阅读Bjarne的论文:C++的多重继承.
在第370页的第3节中,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); };
Run Code Online (Sandbox Code Playgroud)
类c对象C看起来像:
C:
----------- vtbl:
+0: vptr --------------> -----------
+4: a +0: A::f
+8: b +4: B::g
+12: c +8: C::h
----------- -----------
Run Code Online (Sandbox Code Playgroud)
对虚函数的调用由编译器转换为间接调用.例如,
C* pc;
pc->g(2)
Run Code Online (Sandbox Code Playgroud)
变成这样的东西:
(*(pc->vptr[1]))(pc, 2)
Run Code Online (Sandbox Code Playgroud)
Bjarne的论文告诉我上述结论.传球this点是C*.
在下面的例子中,Bjarne讲了另一个让我困惑的故事!
示例2 :(第373页)
鉴于两个班级
class A {...};
class B {...};
class C: A, B {...};
Run Code Online (Sandbox Code Playgroud)
C类的对象可以像这样的连续对象进行布局:
pc--> -----------
A part
B:bf's this--> -----------
B part
-----------
C part
-----------
Run Code Online (Sandbox Code Playgroud)
在给出C*的情况下调用B的成员函数:
C* pc;
pc->bf(2); //assume that bf is a member of B and that C has no member named bf.
Run Code Online (Sandbox Code Playgroud)
Bjarne写道:"当然,B :: bf()期望B*(成为它的指针)." 编译器将调用转换为:
bf__F1B((B*)((char*)pc+delta(B)), 2);
Run Code Online (Sandbox Code Playgroud)
为什么在这里我们需要一个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
-----------
Run Code Online (Sandbox Code Playgroud)
(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
Run Code Online (Sandbox Code Playgroud)
它显示了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;
Run Code Online (Sandbox Code Playgroud)
现在,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()
Run Code Online (Sandbox Code Playgroud)
(1)第一个问题pc->g()与示例2中的讨论有关.编译是否进行以下转换:
pc->g() ==> g__F1B((*B)((char*)pc+delta(B)))
Run Code Online (Sandbox Code Playgroud)
或者我们必须等待运行时才能执行此操作?
(2)Bjarne写道:在进入时C::f,this指针必须指向C对象的开头(而不是指向B部分).但是,在编译时通常不知道B指向的pb是a的一部分,C因此编译器不能减去常量delta(B).
为什么我们不能知道B指向的对象pb是C编译时的一部分?根据我的理解,B* pb = new C,pb指向创建的C对象,并C从中继承B,所以B指针PB指向的一部分C.
(3)假设我们不知道B指向的指针pb是C编译时的一部分.因此,我们必须为运行时存储delta(B),该运行时实际存储在vtbl中.所以vtbl条目现在看起来像:
struct vtbl_entry {
void (*fct)();
int delta;
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
我在这里完全糊涂了.为什么(B*)不是(C*)(*vt->fct)((B*)((char*)pb+vt->delta))?基于我的理解和Bjarne在5.1节第377页的第一句中的介绍,我们应该传递一个C*,就像this这里!!!!!!
接下来是上面的代码片段,Bjarne继续写道:请注意,在查找指向vtbl的成员之前,可能必须将对象指针调整为po int到正确的子对象.
天啊!!!我完全不知道Bjarne试图说什么?你能帮我解释一下吗?
Bjarne 写道:“自然地,B::bf() 期望 B*(成为它的 this 指针)。” 编译器将调用转换为:
bf__F1B((B*)((char*)pc+delta(B)), 2);
Run Code Online (Sandbox Code Playgroud)
为什么这里我们需要一个 B* 指针作为 this ?
单独考虑B:编译器需要能够编译代码 ala B::bf(B* this)。它不知道可以进一步派生哪些类B(并且派生代码的引入可能要在B::bf编译后很长时间才会发生)。的代码B::bf不会神奇地知道如何将指针从其他类型(例如C*)转换为B*可用于访问数据成员和运行时类型信息(RTTI /虚拟调度表,typeinfo)的指针。
相反,调用者有责任在涉及的任何实际运行时类型(例如)中提取对子对象B*的有效值。在这种情况下,保存整个对象的起始地址,该地址可能与子对象的地址匹配,并且子对象是进一步进入内存的某个固定但非0的偏移量:它就是该偏移量(以字节为单位)必须将其添加到 中才能获得有效的调用对象- 当指针从一个类型转换为另一个类型时,该调整就会完成。BCC*CABC*B*B::bfC*B*
(1) 当它是线性链派生时(示例 1),为什么可以预期 C 对象与 B 以及 A 子对象位于同一地址?在示例1的
C*函数内部使用指针访问B类的成员没有问题吗?B::g比如我们要访问成员b,运行时会发生什么?*(pc+8)?
线性推导 B : A 和 C : B 可以被认为是连续将 B 特定字段添加到 A 末尾,然后将 C 特定字段添加到 B 末尾(这仍然是 B 特定字段添加到 A 末尾) )。所以整个事情看起来像:
[[[A fields...]B-specific-fields....]C-specific-fields...]
^
|--- A, B & C all start at the same address
Run Code Online (Sandbox Code Playgroud)
然后,当我们谈论“B”时,我们谈论的是所有嵌入的 A 字段以及添加的字段,而对于“C”,仍然存在所有 A 和 B 字段:它们都从同一地址开始。
关于*(pc+8)- 这是正确的(考虑到我们要向地址添加 8 个字节,而不是通常的 C++ 行为添加受指点大小的倍数)。
(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
-----------
Run Code Online (Sandbox Code Playgroud)
没有原因 - 这正是发生的事情......相同的内存布局。不同之处在于 B 子对象不认为A是其自身的一部分。现在是这样的:
[[A fields...][B fields....]C-specific-fields...]
^ ^
\ A&C start \ B starts
Run Code Online (Sandbox Code Playgroud)
因此,当您调用B::bf它时,它想知道B对象从哪里开始 -this您提供的指针应该位于上面列表中的“+4”;如果您B::bf使用 a进行调用C*,则编译器生成的调用代码将需要添加 4 以形成 的隐式this参数B::bf()。 B::bf()不能简单地知道从哪里开始A或C从 +0 开始:B::bf()对这些类一无所知,并且如果您给它一个指向除其自己的 +4 地址之外的任何内容的指针,则不知道如何到达b或其 RTTI。