整理多重继承的内存分布.docx

上传人:b****4 文档编号:5960522 上传时间:2023-05-09 格式:DOCX 页数:15 大小:164.75KB
下载 相关 举报
整理多重继承的内存分布.docx_第1页
第1页 / 共15页
整理多重继承的内存分布.docx_第2页
第2页 / 共15页
整理多重继承的内存分布.docx_第3页
第3页 / 共15页
整理多重继承的内存分布.docx_第4页
第4页 / 共15页
整理多重继承的内存分布.docx_第5页
第5页 / 共15页
整理多重继承的内存分布.docx_第6页
第6页 / 共15页
整理多重继承的内存分布.docx_第7页
第7页 / 共15页
整理多重继承的内存分布.docx_第8页
第8页 / 共15页
整理多重继承的内存分布.docx_第9页
第9页 / 共15页
整理多重继承的内存分布.docx_第10页
第10页 / 共15页
整理多重继承的内存分布.docx_第11页
第11页 / 共15页
整理多重继承的内存分布.docx_第12页
第12页 / 共15页
整理多重继承的内存分布.docx_第13页
第13页 / 共15页
整理多重继承的内存分布.docx_第14页
第14页 / 共15页
整理多重继承的内存分布.docx_第15页
第15页 / 共15页
亲,该文档总共15页,全部预览完了,如果喜欢就下载吧!
下载资源
资源描述

整理多重继承的内存分布.docx

《整理多重继承的内存分布.docx》由会员分享,可在线阅读,更多相关《整理多重继承的内存分布.docx(15页珍藏版)》请在冰点文库上搜索。

整理多重继承的内存分布.docx

整理多重继承的内存分布

指针的比较

  再以上面Bottom类继承关系为例讨论,下面这段代码会打印Equal吗?

1 Bottom*b= new Bottom(); 

2 Right*r=b;

3       

4 if(r==b)

5   printf("Equal!

/n");

   先明确下这两个指针实际上是指向不同地址的,r指针实际上在b指针所指地址上偏移8字节,但是,这些C++内部细节不能告诉C++程序员,所以C++编译器在比较r和b时,会把r减去8字节,然后再来比较,所以打印出的值是"Equal".

多重继承

  首先我们先来考虑一个很简单(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'isanambiguousbaseof`Bottom'。

其实这两种带有歧义的可能性可以用如下语句加以区分:

1 Top*topL=(Left*)bottom;

2 Top*topR=(Right*)bottom;  

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

虚拟继承

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

 1 classTop

 2 {

 3     public:

 4         int a;

 5 };

 6 

 7 classLeft:

virtualpublicTop

 8 {

 9     public:

10         int b;

11 };

12 

13 classRight:

virtualpublicTop

14 {

15     public:

16         int c;

17 };

18 

19 classBottom:

publicLeft,publicRight

20 {

21     public:

22         int d;

23 };

24 

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

 

     对于程序员来说,这种类层次图显得更加简单和清晰,不过对于一个编译器来说,这就复杂得多了。

我们再用Bottom的内存布局作为例子考虑,它可能是这样的:

       

      这种内存布局的优势在于它的开头部分(Left部分)和Left的布局正好相同,我们可以很轻易地通过一个Left指针访问一个Bottom对象。

不过,我们再来考虑考虑Right:

1 Right*right=bottom;

虚拟继承

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

 1 classTop

 2 {

 3     public:

 4         int a;

 5 };

 6 

 7 classLeft:

virtualpublicTop

 8 {

 9     public:

10         int b;

11 };

12 

13 classRight:

virtualpublicTop

14 {

15     public:

16         int c;

17 };

18 

19 classBottom:

publicLeft,publicRight

20 {

21     public:

22         int d;

23 };

24 

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

 

     对于程序员来说,这种类层次图显得更加简单和清晰,不过对于一个编译器来说,这就复杂得多了。

我们再用Bottom的内存布局作为例子考虑,它可能是这样的:

       

      这种内存布局的优势在于它的开头部分(Left部分)和Left的布局正好相同,我们可以很轻易地通过一个Left指针访问一个Bottom对象。

不过,我们再来考虑考虑Right:

1 Right*right=bottom;

这里我们应该把什么地址赋值给right指针呢?

理论上说,通过这个赋值语句,我们可以把这个right指针当作真正指向一个Right对象的指针(现在指向的是Bottom)来使用。

但实际上这是不现实的!

一个真正的Right对象内存布局和Bottom对象Right部分是完全不同的,所以其实我们不可能再把这个upcasted的bottom对象当作一个真正的right对象来使用了。

而且,我们这种布局的设计不可能还有改进的余地了。

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

      上图有两点值得大家注意。

第一点就是类中成员分布顺序是完全不一样的(实际上可以说是正好相反)。

第二点,类中增加了vptr指针,这些是被编译器在编译过程中插入到类中的(在设计类时如果使用了虚继承,虚函数都会产生相关vptr)。

同时,在类的构造函数中会对相关指针做初始化,这些也是编译器完成的工作。

Vptr指针指向了一个“virtual table”。

在类中每个虚基类都会存在与之对应的一个vptr指针。

为了给大家展示virtual table作用,考虑下如下代码。

1 Bottom*bottom=newBottom();

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

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对象了。

不过,需要提醒的是以上设计需要承担一个相当大的代价:

我们需要引入虚函数表,对象底层也必须扩展以支持一个或多个虚函数指针,原来一个简单的成员访问现在需要通过虚函数表两次间接寻址(编译器优化可以在一定程度上减轻性能损失)。

Downcasting

   如我们猜想,将一个指针从一个派生类到一个基类的转换(casting)会涉及到在指针上添加偏移量。

可能有朋友猜想,downcasting一个指针仅仅减去一些偏移量就行了吧。

实际上,非虚继承情况下确实是这样,但是,对于虚继承来说,又不得不引入其它的复杂问题。

这里我们在上面的例子中添加一些继承关系:

1 classAnotherBottom:

publicLeft,publicRight

2 {

3     public:

4         int e;

5         int f;

6 };

 

  这个继承关系如下图所示:

  那么现在考虑如下代码

1 Bottom*bottom1= new Bottom();

2 AnotherBottom*bottom2= new AnotherBottom();

3 Top*top1=bottom1;

4 Top*top2=bottom2;

5 Left*left= static_cast(top1);

  下面这图展示了Bottom和AnotherBottom的内存布局,同时也展示了各自top指针所指向的位置。

      现在我们来考虑考虑从top1到left的static_cast,注意这里我们并不清楚对于top1指针指向的对象是Bottom还是AnotherBottom。

这里是根本不能编译通过的!

因为根本不能确认top1运行时需要调整的偏移量(对于Bottom是20,对于AnotherBottom是24)。

所以编译器将会提出错误:

error:

cannotconvertfrombase`Top'toderivedtype`Left'viavirtualbase`Top'。

这里我们需要知道运行时信息,所以我们需要使用dynamic_cast:

1 Left*left= dynamic_cast(top1);

不过,编译器仍然会报错的 error:

cannotdynamic_cast`top'(oftype`classTop*')totype`classLeft*'(sourcetypeisnotpolymorphic)。

关键问题在于使用dynamic_cast(和使用typeid一样)需要知道指针所指对象的运行时信息。

但是,回头看看上面的结构图,我们就会发现top1指针所指的仅仅是一个整数成员a。

编译器没有在Bottom类中包含针对top的vptr,它认为这完全没有必要。

为了强制编译器在Bottom中包含top的vptr,我们可以在top类里面添加一个虚析构函数。

1 class Top

2 {

3     public:

4         virtual ~Top(){} 

5         int a;

6 };

 

    这就迫使编译器为Top类添加了一个vptr。

下面来看看Bottom新的内存布局:

 

  是的,其它派生类(Left、Right)都会添加一个vptr.top,编译器为dynamic_cast生成了一个库函数调用。

1 left=__dynamic_cast(top1,typeinfo_for_Top,typeinfo_for_Left,-1);

  __dynamic_cast定义在libstdc++(对应的头文件是cxxabi.h),有了Top、Left和Bottom的类型信息,转换得以执行。

其中,参数-1代表的是类Left和类Top之间的关系未明。

如果想详细了解,请参看tinfo.cc的实现。

二级指针

   这里的问题初看摸不着头脑,但是细细想来有些问题还是显而易见的。

这里我们考虑一个问题,还是以上节的Downcasting中的类继承结构图作为例子。

1 Bottom*b= new Bottom();

2 Right*r=b;

  (在把b指针的值赋值给指针r时,b指针将加上8字节,这样r指针才指向Bottom对象中Right部分)。

因此我们可以把Bottom*类型的值赋值给Right*对象。

但是Bottom**和Right**两种类型的指针之间赋值呢?

1 Bottom**bb=&b;

2 Right**rr=bb;

   编译器能通过这两条语句吗?

实际上编译器会报错:

error:

invalidconversionfrom`Bottom**'to`Right**'

  为什么?

 不妨反过来想想,如果能够将bb赋值给rr,如下图所示。

所以这里bb和rr两个指针都指向了b,b和r都指向了Bottom对象的相应部分。

那么现在考虑考虑如果给*rr赋值将会发生什么。

1 *rr=b;  

 注意*rr是Right*类型(一级)的指针,所以这个赋值是有效的!

二、建设项目环境影响评价   这个就和我们上面给r指针赋值一样(*rr是一级的Right*类型指针,而r同样是一级Right*指针)。

所以,编译器将采用相同的方式实现对*rr的赋值操作。

实际上,我们又要调整b的值,加上8字节,然后赋值给*rr,但是现在**rr其实是指向b的!

如下图

   呃,如果我们通过rr访问Bottom对象,那么按照上图结构我们能够完成对Bottom对象的访问,但是如果是用b来访问Bottom对象呢,所有的对象引用实际上都偏移了8字节——明显是错误的!

D.环境影响研究报告  总而言之,尽管*a和*b之间能依靠类继承关系相互转化,而**a和**b不能有这种推论。

2.环境影响报告表的内容虚基类的构造函数

安全预评价方法可分为定性评价方法和定量评价方法。

  编译器必须要保证所有的虚函数指针要被正确的初始化。

特别是要保证类中所有虚基类的构造函数都要被调用,而且还只能调用一次。

如果你写代码时自己不显示调用构造函数,编译器会自动插入一段构造函数调用代码。

这将会导致一些奇怪的结果,同样考虑下上面的类继承结构图,不过要加入构造函数。

1.建设项目环境影响评价机构的资质管理 1 class Top

 2 {

 3 public:

 4   Top(){a=-1;} 

 5   Top(int _a){a=_a;} 

 6    int a;

 7 };

 8 

 9 class Left:

 public Top

10 {

11 public:

12   Left(){b=-2;}

13   Left(int _a, int _b):

Top(_a){b=_b;}

14    int b;

15 };

16 

17 class Right:

 public Top

18 {

19 public:

20   Right(){c=-3;}

21   Right(int _a, int _c):

Top(_a){c=_c;}

22    int c;

23 };

24 

25 class Bottom:

 public Left, public Right

26 {

27 public:

28   Bottom(){d=-4;} 

29   Bottom(int _a, int _b, int _c, int _d):

Left(_a,_b),Right(_a,_c) 

30     { 

31       d=_d; 

32     }

33    int d;

34 };

35 

  先来考虑下不包含虚函数的情况,下面这段代码输出什么?

1 Bottombottom(1,2,3,4);

2 printf("%d %d %d %d %d/n",bottom.Left:

:

a,bottom.Right:

:

a,bottom.b,bottom.c,bottom.d);

   你可能猜想会有这样结果:

(3)是否符合区域、流域规划和城市总体规划。

11234

   但是,如果我们考虑下包含虚函数的情况呢,如果我们从Top虚继承派生出子类,那么我们将得到如下结果:

 

-1-1234

   如本节开头所讲,编译器在Bottom中插入了一个Top的默认构造函数,而且这个默认构造函数安排在其他的构造函数之前,当Left开始调用它的基类构造函数时,我们发现Top已经构造初始化好了,所以相应的构造函数不会被调用。

如果跟踪构造函数,我们将会看到

大纲要求Top:

:

Top()

Left:

:

Left(1,2)

Right:

:

Right(1,3)

Bottom:

:

Bottom(1,2,3,4)

  为了避免这种情况,我们应该显示地调用虚基类的构造函数

2.环境影响评价技术导则1 Bottom(int _a, int _b, int _c, int _d):

Top(_a),Left(_a,_b),Right(_a,_c) 

2 { 

3   d=_d; 

4 }

指针的比较

2.早期介入原则;  再以上面Bottom类继承关系为例讨论,下面这段代码会打印Equal吗?

2.环境影响评价工作等级的划分依据1 Bottom*b= new Bottom(); 

2 Right*r=b;

3       

4 if(r==b)

5   printf("Equal!

/n");

   先明确下这两个指针实际上是指向不同地址的,r指针实际上在b指针所指地址上偏移8字节,但是,这些C++内部细节不能告诉C++程序员,所以C++编译器在比较r和b时,会把r减去8字节,然后再来比较,所以打印出的值是"Equal".

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 求职职场 > 面试

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2