您的位置:首页 > 编程语言

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

2011-01-15 10:11 501 查看
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

”是一个指向这样一个对象的指针):



如果我们有虚函数:

class

A
{

public

:

int a;

virtual

void v();

};

class

B
: public

A {

public

:

int b;

};

那么我们还将有一个

vtable

指针:



也就是说,

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

的对象被布置成如下:



..

但是为什么?

为什么有两个

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

现在看起来象这样:



现在,当通过指向

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

,我们可以依据前一节推导出:



显然,我们期望

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

)。不过如果我们不能做到这一点,我们怎样可以处理普通的类型替代呢?

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



OK

。你看到

A

现在嵌入在

D

中,基本上就像其它基类那样。不过它被嵌入在

D

中,而不是在它的直接派生类中。

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

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

vtable

)上的操作是安全的?

幸运的是,这些都得到了非常小心的处理。比如说我们正在构建类型

D

的一个新对象(通过比如,

new D

)。首先,用于该对象的内存在堆上分配并且返回一个指针。

D

的构造函数被调用,但在执行任何

D

特定的构造前,在对象上调用

A

的构造函数(当然,在调整了

this

指针之后)。

A

的构造函数填充类

D

对象的

A

部分,就像它是

A

的一个实例。



控制权交还给

D

的构造函数,它调用

B

的构造函数(

在这里指针调整是不需要的)。当

B

的构造函数做完后,该对象看起来就像这样:



但等一下

... B

的构造函数修改了该对象中的

A

部分,它改变

A



vtable

指针!怎么能把这种的

B-in-D

与其他中的

B

(或者一个单独的

B

)区分开来呢?简单。虚表表告诉它这样做。这个结构,缩写为

VTT

,是一个

vtable

的表,在构造中使用。在我们的案例中,用于

D



VTT

看起来就像这样:



D

的构造函数把

D



VTT

中的一个指针传入

B

的构造函数(在这种情况下,它传入了第一个

B-in-D

项的地址)。确实,这个用于具有上面布局的对象的

vtable

是仅用于构造这个

B-in-D

的特殊

vtable



控制权返回给

D

的构造函数,接着它调用

C

的构造函数(连同一个指向

VTT

项“

C-in-D+12

”地址的参数)。当

C

的构造函数完成时,该对象看起来就像这样:



正如你所见,

C

的构造函数再一次修改了嵌入的

A



vtable

指针。这个嵌入的

C



A

对象现在使用这个

C-in-D vtable

的特殊构造,并且嵌入的

B

对象使用这个

B-in-D vtable

的特殊构造。最后,

D

的构造函数完成了这个工作,我们得到与之前相同的图:



析构函数以相同的方式但反序执行。

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();

};

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



你可以看到

C

子对象,它没有虚函数,但仍然有一个

vtable

(尽管是空的)。事实上,所有

C

的实例都有一个空的

vtable



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: