GCC-3.4.6源代码学习笔记(142)

5.12.5.2.2.2.1.3.12.        完成派生类RECORD_TYPE – 生成VTT

虚表表( VTT )对于类来说不是必需的,因此下面的 build_vtt 可能生成 VTT ,有可能不产生。注意下面 5188 行的 dump_class_hierarchy ,选项“ –fdump-class-hierarchy ”会促使该函数转储我们前一个看到的内容。

 

finish_struct_1 (continue)

 

5174     /* Build the VTT for T.  */

5175     build_vtt (t);

5176  

5177     if (warn_nonvdtor && TYPE_POLYMORPHIC_P (t) && TYPE_HAS_DESTRUCTOR (t)

5178        && DECL_VINDEX (TREE_VEC_ELT (CLASSTYPE_METHOD_VEC (t), 1)) == NULL_TREE)

5179       warning ("`%#T' has virtual functions but non-virtual destructor", t);

5180  

5181     complete_vars (t);

5182  

5183     if (warn_overloaded_virtual )

5184       warn_hidden (t);

5185  

5186     maybe_suppress_debug_info (t);

5187  

5188     dump_class_hierarchy (t);

5189    

5190     /* Finish debugging output for this type.  */

5191     rest_of_type_compilation (t, ! LOCAL_CLASS_P (t));

5192   }

 

对于包含虚拟基类的类,上面构建的 vtable 还不是最终成品。它需要虚表表来代替虚表。一个 VTT 包含了:

1. 主虚指针,用于最后派生类( the most derived class )完整对象。

2. 次要 VTT ,用于每个要求 VTT 的最后派生类的直接非虚拟基类( direct non-virtual base )。

3. 次要虚指针,用于包含虚拟基类的最后派生类的直接或间接基类,或在虚拟派生路径上的基类。

4. 次要 VTT ,用于每个最后派生类的直接或间接虚拟基类。

次要 VTT 类似于完整对象的 VTT ,除了没有第四部分。

关于 VTT 及派生类布局, 这里 有一个相当好的笔记,摘录如下(原文是英文)

基础:单继承

正如我们在关于类的讨论,单继承引致一个基类数据布置在在派生类数据之前的对象布局。因此如果类 A B 有如此定义:

class A {

public :

  int a;

};

class B : public A {

public :

  int b;

};

那么类型的对象被布局成这样(其中“ b ”是一个指向这样一个对象的指针):

t1

如果我们有虚函数:

class A {

public :

  int a;

  virtual void v();

};

class B : public A {

public :

  int b;

};

那么我们还将有一个 vtable 指针:

t2                            

也就是说, top_offset typeinfo 指针位于 vtable 指针指向位置的上方。

简单的多继承

现在考虑多继承:

class A {

public :

  int a;

  virtual void v();

};

class B {

public :

  int b;

  virtual void w();

};

class C : public A, public B {

public :

  int c;

};

在这个情形下,类型 C 的对象被布置成如下:

t3

.. 但是为什么? 为什么有两个 vtable ?好吧,考虑类型替代。如果我有一个指向 C 的指针,我可以把它传给一个期望一个指向 A 的指针的函数,或一个期望指向 B 的指针的函数。如果一个函数期望一个指向 A 的指针,并且我想向它传递我的变量 c (指向 C 的类型)的值,我已经设置好了。对 A::v() 的调用可以通过(第一个) vtable 实现,并且被调用的函数可以通过我传入的指针访问成员,与通过指向 A 的指针那样。

不过,如果我向一个期望指向 B 的指针的函数传入我的指向 c 的指针变量的值,为了引用它,我们也需要在我们的 C 里有一个类型 B 的子对象。这就是为什么我们具有第二个 vtable 指针。我们可以向期望指向 B 的指针的函数传入该指针的值( c + 8 bytes ),并且它是所需的设置:它可以通过这个(第二个) vtable 指针进行调用 B::w() ,并且访问通过我们传入的指针访问成员 b ,与通过指向 B 的指针那样。

注意到被调用函数也需要这个“指针更正“( pointer-correction )。类 C 继承类 B::w() 属于这个情况。当通过指向 C 的指针调用 w() 时,这个指针(在 w() 内部它变成 this 指针)需要调整。这通常称作 this 指针调整。

在某些情况下,编译器将参数一个 thunk 来修正这个地址。考虑象上面那样的代码,不过这次 C 重载了 B 的成员函数 w()

class A {

public :

  int a;

  virtual void v();

};

class B {

public :

  int b;

  virtual void w();

};

class C : public A, public B {

public :

  int c;

  void w();

};

C 的对象布局及 vtable 现在看起来象这样:

t4

现在,当通过指向 B 的指针在一个 C 实例上调用 w() 时,这个 thunk 被调用。这个 thunk 起什么作用呢?让我们反汇编它(这里,用 gdb ):

0x0804860c <_ZThn8_N1C1wEv+0>:  addl   $0xfffffff8,0x4(%esp)

0x08048611 <_ZThn8_N1C1wEv+5>:  jmp    0x804853c <_ZN1C1wEv>

那么它仅调整这个 this 指针并跳到 C::w() 。一切都没问题。

但上面不是意味着 B vtable 总是指向这个 C::w() thunk 吗?我是说,如果我们有一个 B 的指针指向 B (而不是 C ),我们不想调用这个 thunk ,对吧?

对的。上面 C 中嵌入的用于 B vtable 是特定于这个 C 中的 B 的情况。 B 的正常 vtable 是通常形式的,并且直接指向 B::w()

菱形层次:基类的多个拷贝(非虚拟继承)

OK 。现在要解决真正麻烦的东西。回忆当形成继承菱形时,基类多个拷贝通常遇到的问题:

class A {

public :

  int a;

  virtual void v();

};

class B : public A {

public :

  int b;

  virtual void w();

};

class C : public A {

public :

  int c;

  virtual void x();

};

class D : public B, public C {

public :

  int d;

  virtual void y();

};


注意到 D B C 继承。而 B C 都从 A 继承。这意味着 D 具有 A 两个 拷贝。对象的布局及嵌入的 vtable ,我们可以依据前一节推导出:

t5

显然,我们期望 A 的数据(成员 a )在 D 的对象布局中出现两次(它就是如此),并且我们期望 A 的虚拟成员函数在 vtable 表示两次( A::v() 确实如此)。 OK ,这里没有什么新玩意。

菱形层次:虚拟基类的单个拷贝

但是如果我们虚拟继承会怎样呢? C++ 的虚拟继承允许我们指定一个菱形的层次,但要保证虚拟继承的基类仅有一份拷贝。因此让我们按这样的方式写代码:

class A {

public :

  int a;

  virtual void v();

};

class B : public virtual A {

public :

  int b;

  virtual void w();

};

class C : public virtual A {

public :

  int c;

  virtual void x();

};

class D : public B, public C {

public :

  int d;

  virtual void y();

};

一下子事情变得复杂多了。如果我们可以在我们的 D 的表达中仅拥有 A 的一份拷贝,那么我们可以不再依赖我们的“小技巧”把 C 嵌入到一个 D 中(并且在 D vtable 中嵌入一个用于 D C 部分的 vtable )。不过如果我们不能做到这一点,我们怎样可以处理普通的类型替代呢?

让我们尝试把布局画出来:

t6

OK 。你看到 A 现在嵌入在 D 中,基本上就像其它基类那样。不过它被嵌入在 D 中,而不是在它的直接派生类中。

多继承情况下的构造与析构

当上面的对象被构造时,对象如何在内存中被构造?并且我们如何确保构造函数在一个部分构造的对象(及它的 vtable )上的操作是安全的?

幸运的是,这些都得到了非常小心的处理。比如说我们正在构建类型 D 的一个新对象(通过比如, new D )。首先,用于该对象的内存在堆上分配并且返回一个指针。 D 的构造函数被调用,但在执行任何 D 特定的构造前,在对象上调用 A 的构造函数(当然,在调整了 this 指针之后)。 A 的构造函数填充类 D 对象的 A 部分,就像它是 A 的一个实例。

t7

控制权交还给 D 的构造函数,它调用 B 的构造函数( 在这里指针调整是不需要的)。当 B 的构造函数做完后,该对象看起来就像这样:

t8

但等一下 ... B 的构造函数修改了该对象中的 A 部分,它改变 A vtable 指针!怎么能把这种的 B-in-D 与其他中的 B (或者一个单独的 B )区分开来呢?简单。虚表表告诉它这样做。这个结构,缩写为 VTT ,是一个 vtable 的表,在构造中使用。在我们的案例中,用于 D VTT 看起来就像这样:

t10

D 的构造函数把 D VTT 中的一个指针传入 B 的构造函数(在这种情况下,它传入了第一个 B-in-D 项的地址)。确实,这个用于具有上面布局的对象的 vtable 是仅用于构造这个 B-in-D 的特殊 vtable

控制权返回给 D 的构造函数,接着它调用 C 的构造函数(连同一个指向 VTT 项“ C-in-D+12 ”地址的参数)。当 C 的构造函数完成时,该对象看起来就像这样:

t10

正如你所见, C 的构造函数再一次修改了嵌入的 A vtable 指针。这个嵌入的 C A 对象现在使用这个 C-in-D vtable 的特殊构造,并且嵌入的 B 对象使用这个 B-in-D vtable 的特殊构造。最后, D 的构造函数完成了这个工作,我们得到与之前相同的图:

t11

析构函数以相同的方式但反序执行。 D 的析构函数被调用。用户的析构代码运行后,该析构函数调用 C 的析构函数,并且指导它使用 D VTT 的相关部分。 C 的析构函数,以在构造过程中相同的方式,操纵这个 vtable 指针;就是说,这个 vtable 指针现在指向 C-in-D 的构造 vtable construction vtable )。然后运行用户的用于 C 的析构代码,并且把控制权返回给 D 的析构函数,它接着调用 B 的析构函数连同 D 中的 VTT 的一个引用。 B 的析构函数设置对象的相关部分来引用 B-in-D 的构造 vtable construction vtable )。运行用户用于 B 的析构代码,并且把控制权返回给 D 的析构函数,它最后调用 A 的析构函数。 A 的析构函数改变用于对象 A 部分的 vtable 来引用用于 A vtable 。最后,控制权返回给 D 的析构函数,对象的析构完成。曾用于该对象的内存返回给系统。

现在,事实上,事情还要更复杂些。你是否曾经看到那些“ in-charge ”及“ not-in-charge ”规格的构造函数及析构函数,在 GCC 产生的警告及错误消息或 GCC 生成的 2 进制文件中?是的,事实是可以有 2 个构造函数的实现,以及多达 3 个的析构函数的实现。

一个“ in-charge ”(或者完整对象)构造函数是会构造虚拟基类的,而一个“ not-in-charge ”(或基类对象)构造函数则不会。考虑我们上面的例子。如果一个 B 被构造,它的构造函数需要调用 A 的构造函数来。类似的, C 的构造函数需要构造 A 。然而,如果 B C 作为 D 的一个构造的一部分来构造,它们的构造函数不应该构造 A ,因为 A 是一个虚拟基类,并且 D 的构造函数将担负起在 D 的实例中仅构造它一次的责任。考虑这些情况:

·         如果你执行“ new A ”, A 的“ in-charge ”构造函数被调用来构造 A

·         当你执行“ new B ”, B 的“ in-charge ”构造函数被调用。它将调用 A 的“ not-in-charge ”构造函数。

·         new C ”类似于“ new B ”。

·         new D ”调用 D 的“ in-charge ”构造函数。我们浏览这个例子。 D 的“ in-charge ”构造函数调用 A B C not-in-charge ”版本的构造函数(以这个次序)。

一个“ in-charge ”析构函数类似于一个“ in-charge ”构造函数——它负责析构虚拟基类。类似的,有“ not-in-charge ”析构函数产生。但这里还有第三个。一个“ in-charge deleting ”析构函数除了析构对象之外,还负责回收内存。那么什么时候其中一个会被选中使用呢?

首先,有两类对象可以被析构——在栈上分配的,及在堆上分配的。考虑这个代码(假设使用之前我们具有虚拟继承的菱形派生结构):

D d;            // 在栈上分配一个 D 并构造它

D *pd = new D;  // 在堆上分配一个 D 并构造它

/* ... */

delete pd;      // D 调用“ in-charge deleting ”析构函数

return ;         // 为栈分配的 D 调用“ in-charge ”析构函数

我们看到实际的 delete 操作符没有由执行删除的代码来调用,而是由用于要被删除对象的“ in-charge deleting ”析构函数来调用。为什么要这样做?为什么不让这个调用者调用“ in-charge ”析构函数,然后删除这个对象呢?那样你只需要析构函数的 2 个实现,而不是 3 ...

是的,编译器可以这样做,不过出于其他原因事情会更复杂。考虑这个代码(假设一个虚析构函数,你总是这样用,对吧? ... 对?!?):

D *pd = new D;  // 在堆上分配一个 D 并构造它

C *pc = d;      // 我们有一个 C 的指针指向我们堆上分配的 D

/* ... */

delete pc;      // 通过 vtable 调用析构函数的 thunk ,但对于删除?

如果你没有 D 的析构函数的一个“ in-charge deleting ”形式,那么删除操作将需要调整指针,就像这个析构函数 thunk 做的那样。记住, C 对象嵌入在一个 D 里,因此我们上面的 C 指针被调整指向我们 D 对象的内部。我们不能就这样删除这个指针,因为它不是那个当我们构造 D 时由 malloc() 返回的指针。

因此,如果我们没有一个“ in-charge deleting ”析构函数,我们不得不对删除操作符使用 thunk (并把它们保存在我们的 vtable 中),或其他类似的东西。

多继承,一边具有虚函数

OK 。最后一个练习。如果我们有一个具有虚拟继承的菱形继承层次,就像之前那样,但仅在一边有虚函数,会怎样呢?这样:

class A {

public :

  int a;

};

class B : public virtual A {

public :

  int b;

  virtual void w();

};

class C : public virtual A {

public :

  int c;

};

class D : public B, public C {

public :

  int d;

  virtual void y();

};

在这个情形下对象的布局如下:

t12

你可以看到 C 子对象,它没有虚函数,但仍然有一个 vtable (尽管是空的)。事实上,所有 C 的实例都有一个空的 vtable

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值