C++对象的内存分析(4)
前言
本章节是4个课题的最后一个,我们将讨论多重继承情况下,对象内存的布局。阅读本文,请思考下面的问题:当子类从多个基类继承,虚函数指针和成员变量将如何布局?编译器如何进行子类和基类之间类型转换?如果多个基类具有同样的虚函数,子类选择哪个实现来调用?如果子类重写该虚函数,那么它覆盖的是哪个基类的实现呢?
多重继承
我们将分析这样的例子:CFinal类继承自CBasic类和CBasic1类;CBasic类和CBasic1类都定义有虚函数add和minus;CBasic类和CBasic1类都定义有成员变量int i;子类CFinal重写了虚函数add;子类CFinal增加了新的虚函数AVG。类图如下:
代码:
class CBasic
{
public :
CBasic()
{
Array= new int [2];
}
int i;
int *Array;
virtual int add( int a, int b)
{
return a+b;
}
virtual int minus( int a, int b)
{
return a-b;
}
void HelloWorld()
{
cout<< "hello world" <<endl;
}
};
class CBasic1
{
public :
virtual int add( int a, int b)
{
cout<< "CBasic1::Add" <<endl;
return a+b;
}
virtual int minus( int a, int b)
{
cout<< "CBasic1::Minus" <<endl;
return a-b;
}
int i;
int iBasic1;
};
class CFinal: public CBasic, public CBasic1
{
int add( int a, int b)
{
cout<< "CFinal::Add" <<endl;
return a+b;
}
virtual int AVG( int a, int b)
{
cout<< "CFinal::AVG" <<endl;
return (a+b)/2;
}
int iFinal;
};
|
构造CFinal类对象:
CFinal *f= new CFinal;
|
我们还是用Watch窗口来观察对象的布局:
我们发现,在Watch窗口中打印f->__vfptr是不允许的,这是因为f中有2个虚函数指针,编译器不知道你想引用的是哪一个,因此我们需要把f转换为基类类型才能打印__vfptr,对于成员变量int i也是同样的。通过对内存布局的观察,我们得到这样的CFinal类内存结构图:
我们发现:
1)CBasic类对象位于CFinal类对象的前端,相应的,CBasic类的虚函数指针位于CFinal类对象的最前端。这是由于在定义CFinal类时,我们把CBasic类写在前面,编译器把它作为主基类。因此,编译器将在CBasic类的虚函数表表尾增加一个元素,来储存子类新增加的虚函数AVG的地址(请参考分析(2)中关于虚函数AVG在虚函数表中的位置的分析)。
2)CBasic1对象开始于紧接着CBasic对象结束的位置,CFinal类新增的成员变量存储在CFinal对象的尾端。
3)对于子类CFinal重写的add方法,在CBasic的虚函数表和CBasic1的虚函数表中,对应的元素都重定向为指向CFinal类的实现。对于CBasic1类来说,这种重定向是通过Thunk技术来实现的(特指VC++。本文将不讨论Thunk技术)。
下面我们讨论章节开始提出的问题,如果我们用f对象调用minus方法,哪个基类的实现会被调用?同样我们也尝试调用成员变量i,因为2个基类都定义了它。运行下面的代码:
int _tmain( int argc, _TCHAR* argv[])
{
CFinal *f= new CFinal;
CBasic *b=(CBasic*)f;
f->minus(4,3); //编译错误,错误码C2385
int x=f->i; //编译错误,错误码C2385
b->minus(4,3); //成功
int y=b->i; //成功 return 0;
}
|
我们发现,直接调用minus函数或者i是不被允许的,因为编译器不知道你想调用的是哪个基类的实现!然而,通过类型转换来指定特定的基类再进行调用,则可以成功。
下一个问题,编译器如何进行子类和基类间的类型转换。对于主基类CBasic来说,这不成问题,因为它位于CFinal对象的最前端,不需要进行指针调整。那么转换为CBasic1类型呢?编译器会在CFinal对象指针的基础上加上12字节,跳过CBasic类对象从而指向CBasic1对象。你可能有这样的问题,为什么编译器知道要加上12字节,而不是13,14字节呢?这是因为编译器知道CFinal对象的布局,它清楚的知道CBasic1对象在CFinal对象中的偏移地址。如果CBasic对象的长度改变了,比如长度增加到16,需要重新编译整个程序,这样使用了CFinal对象的部分在分配地址和类型转换时,也将做出相应的改变。
关于多态。对于子类中重写(override)的虚函数,在子类所有的虚函数表中对应的元素都被重定向为指向子类的新实现(如果基类有此虚函数的话),因此,无论是转换为哪一个基类,多态都能被实现。
结论
让我们试着为多重继承的情况做出结论(如果你对上面的内容重复一遍没有兴趣,跳过这段):
当子类从2个(或多个)带虚函数的基类继承时
1)子类中,主基类的对象内存位于子类对象内存的最前端,相应地,主基类的虚函数指针地址等于子类对象地址。
2)子类新增加的虚函数,将在主基类的虚函数表尾增加新的元素元素,来指向其实现。
3)子类中其他基类的对象位于紧接着主基类结束后的地址。
4)子类新增的成员变量位于子类对象内存的尾端。
5)在子类中重写(override)的基类虚函数,在其所有基类的虚函数表中,对应的元素都覆盖为指向子类新实现的地址(通过THUNK技术实现)。
6)对于多个基类中都定义过的虚函数,如果子类没有重写它,子类对象是不能直接调用的,因为编译器不知道你希望调用的是具体哪个基类的实现。同样,在多个子类中定义的成员变量,也不能被子类对象直接调用。
5)子类对象转换为基类类型时,除非是主基类,需要进行指针调整,指针加上若干字节,跳过位于其前端的其他基类占用的地址,从而指向需要转换的基类。编译器之所以精确的知道需要偏移的地址,因为它通过类定义清楚地知道子类对象的内存布局。
6)关于多态。对于子类中重写(override)的虚函数,在子类所有的虚函数表中对应的元素都被重定向为指向子类的新实现(如果基类有此虚函数的话),因此,无论是转换为哪一个基类,多态都能被实现。
其他情况
至此,本文已经把所有经典常见的对象布局情况进行了研究,下面我们再简要的看几个更复杂的情形。
1,菱形多重继承。CFinal类的基类CBasic类和CBasic1类有共同的基类CInitial。
类图:
分析:这种情况和Subject4中多重继承的情况没有不同。对于编译器来说,CFinal类对2个基类的处理方式没有因为他们有共同的基类而有什么不同,只是在CFinal类对象中的CBasic类和CBasic类的内部又都分别包含了一个CInitial类。我们在Subject4种得到的所有结论在此都是适用的。
2,主基类没有虚函数的多重继承。考虑在Subject4的情况中,把CBasic类中的2个虚函数去掉,使其没有虚函数,在定义CFinal时仍把CBasic写在前面作为主虚函数。
类图:
分析:这种情况下,虽然我们把CBasic写在前面,但是CFinal类事实上把CBasic1类,即带虚函数指针和虚函数表的基类作为主基类,把它布局在对象的最前端。内存结构图:
授权声明
本文为Binhua Liu原创作品。本文允许复制,修改,传递。转载请注明出处。本文发表于2010年6月25日。
http://www.cnblogs.com/Binhua-Liu/archive/2010/06/25/1765059.html
C++对象的内存分析(4)相关推荐
- C++对象的内存分析(2)
C++对象的内存分析(2) Binhua Liu 前言 本章节讨论单继承情况下类对象的内存特性.阅读时请思考这几个问题:从子类到基类的类型转换,编译器做了什么?多态是怎么实现的?类的成员函数(包括虚函 ...
- C++对象的内存分析(5)
前言 前面4节我们已经完成了对4种C++对象布局的分析,本文试图覆盖更多的,常见的C++面向对象的概念.所以,最后2节将继续阐述2个主题:接口和抽象类以及构造函数.虚构函数和虚析构函数. 接口 这里我 ...
- java转型 内存_java 对象转型内存分析
对象转型: 一个基类的引用类型变量可以指向其子类的对象(要求传个动物,传给狗是可以的,狗是动物) 一个基类的引用不可以访问其子类对象的新增成员(狗会游泳不代表所有的动物都会游泳,把狗当作动物来看时不可 ...
- 由static_cast和dynamic_cast到C++对象占用内存的分析
static_cast和dynamic_cast是C++的类型转换操作符.编译器隐式执行的任何类型转换都可以由static_cast显式完成,即父类和子类之间也可以利用static_cast进行转换. ...
- Java中的内存分析
分析内存是深入了解编程的第一步,以下来演示一下编程中常见的内存分析,文章脉络: 数据类型 数据类型不同,内存分配位置和大小也不同,用一张图表示Java中的数据类型.除了基本数据类型,其它全部是引用类型 ...
- JVM学习笔记之-堆,年轻代与老年代,对象分配过程,Minor GC、Major GC、Full GC,堆内存大小与OOM,堆空间分代,内存分配策略,对象分配内存,小结堆空间,逃逸分析,常用调优工具
堆的核心概述 概述 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域.Java堆区在JVM 启动的时候即被创建,其空间大小也就确定了.是JVM管理的最大一块内存空间. 堆内存的大小是可 ...
- 源码分析:Java对象的内存分配
Java对象的分配,根据其过程,将其分为快速分配和慢速分配两种形式,其中快速分配使用无锁的指针碰撞技术在新生代的Eden区上进行分配,而慢速分配根据堆的实现方式.GC的实现方式.代的实现方式不同而具有 ...
- 6.面向对象,构造器,递归以及对象创建时内存分析(内含代码与练习)
面向对象的概念以及特征 概念 实质上将 "数据" 与 "行为" 的过程, 以类的形式封装起来, 一切以 对象 为中心的 面向对象的程序设计过程中有两个重要概念: ...
- (转)c++对象内存分析4
前言 本章节是4个课题的最后一个,我们将讨论多重继承情况下,对象内存的布局.阅读本文,请思考下面的问题:当子类从多个基类继承,虚函数指针和成员变量将如何布局?编译器如何进行子类和基类之间类型转换 ...
最新文章
- 翻译:WebApi 认证--用户认证Oauth解析
- 科普| 什么是图数据库?
- 成功解决 ValueError: fill value must be in categories
- C++:构造函数2——拷贝构造函数
- Android.View.InflateException: Binary XML File Line #异常的解决
- List和Set集合使用
- php阅读心得,PHP学习路上的一点心得
- HMM:隐马尔可夫模型 - 表示
- 中国石油大学c语言程序设计答案,中国石油大学《C语言程序设计》期末复习题和答案.doc...
- c语言窗体关机程序代码,c语言 关机程序代码
- MATLAB创建数组方法
- byte 16进制 2进制理解
- 产业科技创新杂志产业科技创新杂志社产业科技创新编辑部2022年第3期目录
- Android常用设置
- ES6(ES2015)
- 计算机vb基础知识,计算机VB基础知识---知识导学.doc
- “因遭勒索软件攻击,我被认定工作失职开除,并被老东家索赔 21.5 万元”
- http client的英文文档 牛逼
- 开源地图服务器 网站,开源WebGIS:地图发布与地图服务
- 精华阅读第 13 期 |常见的八种导致 APP 内存泄漏的问题 1