c++内存分布(二)--虚函数和虚继承

news/2024/7/4 13:02:05
c++内存分布(二)--虚函数和虚继承
http://hi.baidu.com/kuhntoria/blog/item/5872c1fe9bfd5d0d6d22eb22.html
2011-08-31 16:20

一.多重继承

首先我们先来考虑一个很简单(non-virtual)的多重继承。看看下面这个C++类层次结构。

1 class Top
2 {
3 public:
4 int a;
5 };
6
7 class Left : public Top
8 {
9 public:
10 int b;
11 };
12
13 class Right : public Top
14 {
15 public:
16 int c;
17 };
18
19 class Bottom : public Left, public Right
20 {
21 public:
22 int d;
23 };
24
用UML表述如下:



注意到Top类实际上被继承了两次,(这种机制在Eiffel中被称作repeated inheritance),这就意味着在一个bottom对象中实际上有两个a属性(attributes,可以通过bottom.Left::a和 bottom.Right::a访问) 。

那么Left、Right、Bottom在内存中如何分布的呢?我们先来看看简单的Left和Right内存分布:



[Right 类的布局和Left是一样的,因此我这里就没再画图了。刺猬]

注意到上面类各自的第一个属性都是继承自Top类,这就意味着下面两个赋值语句:

1 Left* left = new Left();
2 Top* top = left;

left和top实际上是指向两个相同的地址,我们可以把Left对象当作一个Top对象(同样也可以把Right对象当Top对象来使用)。但是Botom对象呢?GCC是这样处理的:



但是现在如果我们upcast 一个Bottom指针将会有什么结果?

1 Bottom* bottom = new Bottom();
2 Left* left = bottom;

这段代码运行正确。这是因为GCC选择的这种内存布局使得我们可以把Bottom对象当作Left对象,它们两者(Left部分)正好相同。但是,如果我们把Bottom对象指针upcast到Right对象呢?

1 Right* right = bottom;

如果我们要使这段代码正常工作的话,我们需要调整指针指向Bottom中相应的部分。



通过调整,我们可以用right指针访问Bottom对象,这时Bottom对象表现得就如Right对象。但是bottom和right指针指向了不同的内存地址。最后,我们考虑下:

1 Top* top = bottom;

恩,什么结果也没有,这条语句实际上是有歧义(ambiguous)的,编译器会报错: error: `Top' is an ambiguous base of `Bottom'。其实这两种带有歧义的可能性可以用如下语句加以区分:

1 Top* topL = (Left*) bottom;
2 Top* topR = (Right*) bottom;

这两个赋值语句执行之后,topL和left指针将指向同一个地址,同样topR和right也将指向同一个地址。

二.虚拟继承

为了避免上述Top类的多次继承,我们必须虚拟继承类Top。

1 class Top
2 {
3 public:
4 int a;
5 };
6
7 class Left : virtual public Top
8 {
9 public:
10 int b;
11 };
12
13 class Right : virtual public Top
14 {
15 public:
16 int c;
17 };
18
19 class Bottom : public Left, public Right
20 {
21 public:
22 int d;
23 };
24

上述代码将产生如下的类层次图(其实这可能正好是你最开始想要的继承方式)。



这里我们先看看实际上内存是怎么分布的,然后再解释下为什么这么设计。



上图有两点值得大家注意。第一点就是类中成员分布顺序是完全不一样的(实际上可以说是正好相反)。第二点,类中增加了vptr指针(虚类指针),这些是被编译器在编译过程中插入到类中的(在设计类时如果使用了虚继承,虚函数都会产生相关vptr(虚表指针))。同时,在类的构造函数中会对相关指针做初始化,这些也是编译器完成的工作。Vptr指针指向了一个“virtual table”。在类中每个虚基类都会存在与之对应的一个vptr指针。为了给大家展示virtual table作用,考虑下如下代码。

1 Bottom* bottom = new Bottom();
2 Left* left = bottom;
3 int p = left->a;
第二条的赋值语句让left指针指向和bottom同样的起始地址(即它指向Bottom对象的“顶部”)。我们来考虑下第三条的赋值语句。

1 movl left, %eax # %eax = left
2 movl (%eax), %eax # %eax = left.vptr.Left
3 movl (%eax), %eax # %eax = virtual base offset
4 addl left, %eax # %eax = left + virtual base offset
5 movl (%eax), %eax # %eax = left.a
6 movl %eax, p # p = left.a

总结下,我们用left指针去索引(找到)virtual table,然后在virtual table中获取到虚基类的偏移(virtual base offset, vbase),然后在left指针上加上这个偏移量,这样我们就获取到了Bottom类中Top类的开始地址。从上图中,我们可以看到对于Left指针,它的virtual base offset是20,如果我们假设Bottom中每个成员都是4字节大小,那么Left指针加上20字节正好是成员a的地址。

我们同样可以用相同的方式访问Bottom中Right部分。

1 Bottom* bottom = new Bottom();
2 Right* right = bottom;
3 int p = right->a;

right指针就会指向在Bottom对象中相应的位置。



这里对于p的赋值语句最终会被编译成和上述left相同的方式访问a。唯一的不同是就是vptr,我们访问的vptr现在指向了virtual table另一个地址,我们得到的virtual base offset也变为12。我们画图总结下:


当然,关键点在于我们希望能够让访问一个真正单独的Right对象也如同访问一个经过upcasted(到Right对象)的Bottom对象一样。这里我们也在Right对象中引入vptrs。



OK,现在这样的设计终于让我们可以通过一个Right指针访问Bottom对象了。不过,需要提醒的是以上设计需要承担一个相当大的代价:我们需要引入虚函数表,对象底层也必须扩展以支持一个或多个虚函数指针,原来一个简单的成员访问现在需要通过虚函数表两次间接寻址(编译器优化可以在一定程度上减轻性能损失)。

三.实验

#include <string>

//#include <iostream.h>

#include <stdio.h>

#include <iostream>

#include <assert.h>

#include <memory.h>

using namespace std;

class A

{

char k[3];

public:

virtual void aa(){};

};

class B:public virtual A

{

char j[3];

public:

virtual void bb(){};

};

class C:public virtual B

{

char i[3];

public:

virtual void cc(){};

};

int main()

{

cout<<sizeof(A)<<endl;

cout<<sizeof(B)<<endl;

cout<<sizeof(C)<<endl;

return 0;

}

结果:8 20 32

分析:

1.在虚继承时,子类虚函数不是直接加在父类虚函数表下面,而是自己生成虚函数表,也就是不同类生成自己的虚函数表,而不是叠加在父类后面。

2.虚继承内存情况:

虚类指针—子类虚表指针—子类成员变量—父类虚表指针—父类成员变量

总结:

1. 虚继承和非虚继承的区别:

(1)非虚继承:子类虚函数加在父类虚函数的下面,总共是需要一个虚表指针。

虚继承:子类虚函数不能加在父类虚函数下面,不同类的虚函数自己建立虚函数表,子类父类都有自己的虚函数表,有两个虚表指针。

(2)非虚继承:成员变量是按显示父类,后是子类的顺序存储的。

虚继承:非虚继承把父类和子类的成员变量放在一起,父类和子类的虚函数放在一起这样存储的。

虚继承是按类存储的,先是子类,后是父类,父类放在最后面。


http://www.niftyadmin.cn/n/4645238.html

相关文章

关于内核符号表

关于内核符号表 http://soft-app.iteye.com/blog/920312 在编写驱动的过程中&#xff0c;常会使用到EXPORT_SYMBOL宏来将定义的函数名导出到内核符号表。以前只是简单的知道如果一个模块中定义的函数要提供给其他模块调用&#xff0c;就必须进行导出。这段时间在编译单个模块…

内核符号表详解

内核符号表详解 http://hi.baidu.com/apollon2010/blog/item/ecbe1d5a3bd2ec8a800a18dc.html 关键词&#xff1a; Kernel Symbol Table、/proc/ksyms、system.map、Oops、LKM 这应该是一个很基本的内核概念&#xff0c;和模块、系统调用等一样基础&#xff0c;但牵涉的东…

如何读取被禁用的网卡信息

http://topic.csdn.net/u/20080310/19/5fa9b49d-c7f5-42be-986f-4cb46fb4e0b0.html?2079547956 比如&#xff0c;有两张网卡&#xff0c;一张启用&#xff0c;一张禁用&#xff0c;如何获取被禁用的网卡的信息&#xff0c;如网卡MAC&#xff0c;网卡名称等。GetAdaptersInfo…

如何在IDA中找到MFC程序的消息处理函数

比起用Win32SDK写的程序&#xff0c;要分析MFC应用程序要麻烦不少。在前者&#xff0c;只要找到注册窗口类的地方就知道其WinProc的位置。那里是程序的控制中心&#xff0c;只要顺藤摸瓜就可以找到你感兴趣的地方。对于用MFC写的程序&#xff0c;这一切都变得复杂起来了。这时&…

距离矢量路由协议通用属性

一、路由分类大多的路由协议都属于两个类别1、距离矢量&#xff08;Distance Ventor&#xff09;、2、链路状态&#xff08;Link State&#xff09;二、距离矢量路由协议因为路由是以矢量&#xff08;距离&#xff0c;方向&#xff09;的方式被通告出去的&#xff0c;其中距离是…

JSR 299 建议草案第二版已提交

昨天 Gavin King 提交了 JSR 299 &#xff08;Contexts and Dependency Injection for the Java EE platform&#xff09;的第二个建议草案 给 JCP。相比上一个建议草案&#xff0c;主要有如下四个大的修订&#xff1a; 在依赖注射注解&#xff08;annotation&#xff09;上全面…

csdn博客集

逆向 http://blog.csdn.net/pll621/article/category/138401 http://blog.csdn.net/cattom/article/category/220275

锂电池知识

一&#xff0e;锂电池保护 一般用户接触到手机锂离子电池,在外面看到的除了电池外壳,还有就是几个五金触片了,如图中"电池正极,电池负极"就是的电池正负极输出. ┏━━Fuse━━━━━┳━━━━━━━━━┫电池正极 ┃ R1 ┃ ┃ ┇ ┏┻━━┓ ┏┻┓ ┃保护IC┃…