博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C++虚继承2
阅读量:4105 次
发布时间:2019-05-25

本文共 24301 字,大约阅读时间需要 81 分钟。

上一篇只是初步的写了一下虚继承,很不清楚而且有的地方自己理解也不到位。这回详细总结一下。以下内容来自vs2008 默认设置下。类的布局可以通过-d1reportSingleClassLayout查看。    
  让我们从最简单的类结构开始。
 
代码
class
A{
public
:
int
a;
void
af();
void
virtual
vaf(); };
void
A::vaf(){printf(
"
vaf\n
"
);}
void
A::af(){printf(
"
af\n
"
);}
class
B{
public
:
int
b;
void
bf();
void
virtual
vbf();};
void
B::vbf(){printf(
"
vbf\n
"
);};
void
B::bf(){printf(
"
bf\n
"
);};
class
C:
public
A,
public
B{
public
:
int
c;
void
cf();
void
virtual
vcf(); };
void
C::vcf(){printf(
"
vcf\n
"
);}
void
C::cf(){printf(
"
cf\n
"
);}
  内存中这个例子是这样的。
代码
class
A size(
8
):
+---
0
|
{vfptr}
4
|
a
+---
A::$vftable@:
|
&
A_meta
|
0
0
|
&
A::vaf
class
B size(
8
):
+---
0
|
{vfptr}
4
|
b
+---
B::$vftable@:
|
&
B_meta
|
0
0
|
&
B::vbf
class
C size(
20
):
+---
|
+---
(
base
class
A)
0
|
|
{vfptr}
4
|
|
a
|
+---
|
+---
(
base
class
B)
8
|
|
{vfptr}
12
|
|
b
|
+---
16
|
c
+---
C::$vftable@A@:
|
&
C_meta
|
0
0
|
&
A::vaf
1
|
&
C::vcfC::$vftable@B@:
|
-
8
0
|
&
B::vbf
 
  这里我们总结一下,类中有虚函数布局。 
若是类中有虚函数,那么类中第一个元素是指向虚表的指针(这个情况只有vftable)。
基类数据成员
本身类成员
最左边的基类和本类公用同一个虚函数表,从而可以简化一些操作。
  一个简单的例子,让我们看一下虚函数运行时的样子。
 
代码
C
*
pc
=
new
C;pc
->
af();pc
->
vaf();pc
->
vcf();pc
->
vbf();delete pc;
.text:
00401059
push offset aAf ;
"
af\n
"
;这里调用非虚函数,之前有一个给ecx赋值语句.text:0040105E call ds:__imp__printf.text:
00401064
mov eax, [esi].text:
00401066
mov edx, [eax].text:
00401068
add esp,
4
.text:0040106B mov ecx, esi ;这里ecx指向类A,这里因为A和C相同的开始地址.text:0040106D call edx ;这里节省了一次类的转化.text:0040106F mov eax, [esi].text:
00401071
mov edx, [eax
+
4
] ;这里调用vcf,在虚表中我们看到了他的offset
4
.text:
00401074
mov ecx, esi.text:
00401076
call edx.text:
00401078
mov eax, [esi
+
8
] ;这里调用vbf,这里需要首先调整this指针.text:0040107B mov edx, [eax] ;在找到相应的函数偏移量(这里为0).text:0040107D lea ecx, [esi
+
8
].text:
00401080
call edx
 
  有了前面的铺垫,我们步入正题,依然是一个简单的例子。
 
代码
class
D :
virtual
public
A{
int
d;
void
df();
void
virtual
vdf();};
void
D::vdf(){printf(
"
vdf\n
"
);}
void
D::df(){printf(
"
df\n
"
);}
class
E :
virtual
public
A{
public
:
int
e;
void
ef();
void
virtual
vef();};
void
E::vef(){printf(
"
vef\n
"
);}
void
E::ef(){printf(
"
ef\n
"
);}
class
F :
public
A,
public
B{
public
:
int
f;
void
ff();
void
virtual
vff();};
void
F::vff(){printf(
"
vff\n
"
);}
void
F::ff(){printf(
"
ff\n
"
);}
 
  让我们再看一下class F在内存中的布局
 
代码
class
F size(
36
):
+---
|
+---
(
base
class
D)
0
|
|
{vfptr}
4
|
|
{vbptr}
8
|
|
d
|
+---
|
+---
(
base
class
E)
12
|
|
{vfptr}
16
|
|
{vbptr}
20
|
|
e
|
+---
24
|
f
+---
+---
(
virtual
base
A)
28
|
{vfptr}
32
|
a
+---
F::$vftable@D@:
|
&
F_meta
|
0
0
|
&
D::vdf
1
|
&
F::vffF::$vftable@E@:
|
-
12
0
|
&
E::vefF::$vbtable@D@:
0
|
-
4
1
|
24
(Fd(D
+
4
)A)F::$vbtable@E@:
0
|
-
4
1
|
12
(Fd(E
+
4
)A)F::$vftable@A@:
|
-
28
0
|
&
A::vaf
 
  这里又增加了一个指向虚基表的指针vbptr,我们可以看出这个指针的目的在于计算包含虚继承的类的位置(有直接虚继承和间接虚继承)。让我们总结下有虚继承下的布局。
将类中非虚继承的基类放置最前面。这样访问非虚继承函数不需再计算偏移量。
在派生类中若是没有vbtable则增加一个,除非能从原来的非虚继承类继承到了vbtable。
派生类数据成员
虚基类
  可见,虚基类始终在类的尾部,那么当类生长的时候,也就是继续被继承时,则很有可能使虚基的偏移量变大。
比如在class D的虚基表中,D与A偏移量为0,而在class F中D与A偏移量变为了24,所以只能加入一个vbptr指向虚基表。
有了前面的知识,那么运行时的情况就好分析了。
 
代码
.text:0040104F mov dword ptr [eax
+
4
], offset
??
_8F@@7BD@@@ ;
const
F::`vbtable
'
{for `D
'
}.text:
00401056
mov dword ptr [eax
+
10h], offset
??
_8F@@7BE@@@ ;
const
F::`vbtable
'
{for `E
'
} ;首先将虚基表初始化 eax
=
this
.text:0040105D mov dword ptr [eax
+
1Ch], offset
??
_7A@@6B@ ;
const
A::`vftable
'
.text:
00401064
mov ecx, [eax
+
4
] ;
*
ecx
=
vbtableFD.text:
00401067
mov dword ptr [eax], offset
??
_7D@@6B0@@ ;
const
D::`vftable
'
{for `D
'
}.text:0040106D mov edx, [ecx
+
4
] ;获得vbtableFD表中第2项,也就是D和A虚函数表的offset.text:
00401070
mov dword ptr [edx
+
eax
+
4
], offset
??
_7D@@6BA@@@ ;
const
D::`vftable
'
{for `A
'
} ;根据和虚基表的offset
+
虚基表中和虚函数的offset
+
this找到虚函数位置以下类推.text:
00401078
mov ecx, [eax
+
10h].text:0040107B mov dword ptr [eax
+
0Ch], offset
??
_7E@@6B0@@ ;
const
E::`vftable
'
{for `E
'
}.text:
00401082
mov edx, [ecx
+
4
].text:
00401085
mov dword ptr [edx
+
eax
+
10h], offset
??
_7E@@6BA@@@ ;
const
E::`vftable
'
{for `A
'
}.text:0040108D mov ecx, [eax
+
4
].text:
00401090
mov dword ptr [eax], offset
??
_7F@@6BD@@@ ;
const
F::`vftable
'
{for `D
'
}.text:
00401096
mov dword ptr [eax
+
0Ch], offset
??
_7F@@6BE@@@ ;
const
F::`vftable
'
{for `E
'
}.text:0040109D mov edx, [ecx
+
4
].text:004010A0 mov dword ptr [edx
+
eax
+
4
], offset
??
_7F@@6BA@@@ ;
const
F::`vftable
'
{for `A
'
}.text:004010A8 mov esi, eax.text:004010AE mov eax, [esi
+
4
] ;eax
=*
vbtableFD.text:004010B1 mov ecx, [eax
+
4
] ;ecx
=
虚基表中和虚函数的offset.text:004010B4 mov edx, [ecx
+
esi
+
4
] ;
*
edx
=
vftable.text:004010B8 mov eax, [edx] .text:004010BA lea ecx, [ecx
+
esi
+
4
] ;
this
=
class
A的开始.text:004010BE call eax ;pf
->
vaf();.text:004010C0 mov edx, [esi].text:004010C2 mov eax, [edx].text:004010C4 mov ecx, esi ;classD和classF公用虚表.text:004010C6 call eax.text:004010C8 mov edx, [esi
+
0Ch].text:004010CB mov eax, [edx].text:004010CD lea ecx, [esi
+
0Ch] ;修正this,指向class E.text:004010D0 call eax.text:004010D2 mov edx, [esi].text:004010D4 mov eax, [edx
+
4
].text:004010D7 mov ecx, esi.text:004010D9 call eax
 
  再看下虚函数覆盖的问题。
 
代码
class
G{
public
:
int
g;
void
gf();
void
virtual
vgf();
void
virtual
vaf();};
void
G::gf(){printf(
"
gf\n
"
);}
void
G::vgf(){printf(
"
vgf\n
"
);}
void
G::vaf(){printf(
"
vaf_g\n
"
);}
class
H:
public
A,
public
G{
public
:
int
h;
void
hf();
void
vaf();
void
vgf();
void
virtual
vhf();};
void
H::hf(){printf(
"
hf\n
"
);}
void
H::vaf(){printf(
"
vaf_H\n
"
);}
void
H::vgf(){printf(
"
vgf_h\n
"
);}
void
H::vhf(){printf(
"
vhf\n
"
);}
class
H size(
20
):
+---
|
+---
(
base
class
A)
0
|
|
{vfptr}
4
|
|
a
|
+---
|
+---
(
base
class
G)
8
|
|
{vfptr}
12
|
|
g
|
+---
16
|
h
+---
H::$vftable@A@:
|
&
H_meta
|
0
0
|
&
H::vaf
1
|
&
H::vhfH::$vftable@G@:
|
-
8
0
|
&
H::vgf
1
|
&
thunk:
this
-=
8
;
goto
H::vaf
 
  由于A类和G类的函数vaf都被子类H覆盖,由于A和H共用虚函数表,那么如果在G类中依然保留被覆盖的函数则浪费空间。实际是通过以下代码实现的。
 
代码
.text:004010B0 ; [thunk]:
public
:
virtual
void
__thiscall H::vaf`adjustor{
8
}
'
(void)
.text:004010B0
?
vaf@H@@W7AEXXZ proc near ; DATA XREF: .rdata:
00402158
o.text:004010B0 sub ecx,
8
;这里调整this指针,指向class G
=
class
A.text:004010B3 jmp
?
vaf@H@@UAEXXZ ; H::vaf(
void
);转向到G表中的vaf().text:004010B3
?
vaf@H@@W7AEXXZ endp
 
  可见要是要使用thunk,根本上是处理以达到节省函数表大小,通过修改this指针去调用子类表项,那么也就是当子类覆盖父类多个方法时,只保留一份,其他的则跳转执行。
 
代码
mov ecx, esicall edx ;调用vafmov eax, [esi
+
8
] ;
*
eax
=
vftable_Gmov edx, [eax]lea ecx, [esi
+
8
]call edx ;vgfmov eax, [esi]mov edx, [eax
+
4
] mov ecx, esicall edx ;vhf
 
  虚函数中还有2个非常重要的部分一个纯虚函数,一个虚析构函数。由于析构函数和构造函数结合的实在是太紧密了。下一篇先总结下虚析构函数当然也包括构造函数的部分。

转载地址:http://rejsi.baihongyu.com/

你可能感兴趣的文章
Maven手动上传第三方jar包的命令
查看>>
libmemcached编译 出错
查看>>
Scribe在CentOS 5.8和6.4上编译不过的解决办法
查看>>
thrift使用过程中的问题
查看>>
scribed 安装
查看>>
MSVC 2012动态编译及静态编译 QT 5.0.2
查看>>
Qt Creator 配置Msvc 2012的调试器
查看>>
QT5静态编译教程,主要针对vs2012
查看>>
使用mysqldump,从mysql中导出数据库结构
查看>>
phantomjs在liunx中报 Error: EACCES: permission denied syscall: 'link'
查看>>
CentOS7 安装 Composer
查看>>
解决Composer项目update三方组件失败的方法
查看>>
PHP 7 编译安装时出现 undefined reference to `libiconv' 错误的解决方法
查看>>
一步一步教你做ios推送
查看>>
使用openssl将私钥转换为无密码私钥
查看>>
Iperf 带宽性能测试使用方法与参数说明
查看>>
Linux 修改MAC地址的四种方法介绍
查看>>
Ubuntu:永久修改MAC地址
查看>>
UDID解决办法
查看>>
”A valid provisioning profile for this executable was not found“解决方案
查看>>