编译器的这个指针的详细信息,虚函数和多重继承

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 af; B:int bbf(或称之为g); C:int ch.为什么不直接使用内存布局:

 -----------               
+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,pbpc具有相同的地址.

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)

现在,pcpa具有相同的地址,而pb一些偏移papc.

为什么编译会产生这些差异?


示例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指向的对象pbC编译时的一部分?根据我的理解,B* pb = new C,pb指向创建的C对象,并C从中继承B,所以B指针PB指向的一部分C.

(3)假设我们不知道B指向的指针pbC编译时的一部分.因此,我们必须为运行时存储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试图说什么?你能帮我解释一下吗?

Ton*_*roy 3

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()不能简单地知道从哪里开始AC从 +0 开始:B::bf()对这些类一无所知,并且如果您给它一个指向除其自己的 +4 地址之外的任何内容的指针,则不知道如何到达b或其 RTTI。