C++内存中的封装、继承、多态(下)
上篇讲述了内存中的封装模型,下篇我们讲述一下继承和多态。
二、继承与多态情况下的内存布局
由于继承下的内存布局以及构造过程很多书籍都讲得比较详细,所以这里不细讲。重点讲多态。
继承有以下这几种情况:
1.单一继承
2.多重继承
3.重复继承
4.虚拟继承
1.单一继承的场合
假设有以下继承关系,那么大致的内存布局如下
代码
class Parent
{
public: int p;
}; class Child:public Parent
{
public: int c; }; class GrandChild:public Child
{
public: int gc;
};
对象布局:
成员变量的布局很好理解,那么在有虚函数的场合,虚函数表到底又是怎么样的呢?
为了解决这个问题我完善上面的代码。
class Parent
{
public:
Parent():p(){} virtual void fun1(){ cout<<"Parent::fun1"<<endl; }
virtual void fun2(){ cout<<"Parent::fun2"<<endl; }
virtual void fun3(){ cout<<"Parent::fun3"<<endl; } int p;
}; class Child:public Parent
{
public:
Child():c(){} virtual void fun1() { cout<<"Child::fun1"<<endl; }
virtual void c_fun2(){ cout<<"Child::c_fun2"<<endl; }
virtual void c_fun3(){ cout<<"Child::c_fun3"<<endl; } int c;
}; class GrandChild:public Child
{
public:
GrandChild():gc(){} virtual void fun1() { cout<<"GrandChild::fun1"<<endl; }
virtual void c_fun2(){ cout<<"GrandChild::c_fun2"<<endl; }
virtual void gc_fun3(){ cout<<"GrandChild::gc_fun3"<<endl; } int gc;
}; int main()
{
GrandChild grandc;
Child child;
Parent parent; return ;
}
我们先使用调试窗口查看一下虚函数表
可以看到三张表是不同的,可以看到fun1函数被改写了两次。
比较蛋疼的是grandc只能看到三个函数,君不见c_fun2和gc_fun3,还得自己动手来。
继上篇的内容,我们使用pf这个函数指针:
typedef void (*PF)();
PF pf = NULL;
在主函数里我们写下代码:
int* vtab = (int*)*(int*)&grandc; for (; *vtab != NULL; vtab++)
{
pf = (PF)*vtab;
pf();
} int* member = (int*)&grandc;
cout<<*++member<<endl;
cout<<*++member<<endl;
cout<<*++member<<endl;
成员变量输出结果与我们上篇的结论一致,咱们主要来看一下虚函数部分。
并且前三个函数同调试窗口的显示结果。
我们依据以上结果可以得到这么几个结论:
1.单一继承时,不同的类维护不同的虚函数表(only one),并且虚函数表初始情况是父类的样子。
2.当发生overwrite时,例如fun1和c_fun2都会冲刷掉父类的虚函数,代替之。
3.没有发生overwrite时,直接添加到虚函数表中。
图示:
截止到这里,结合上篇的内容,就能很容易理解为什么使用父类指针能产生多态的效果了。
2.多重继承的场合
假设有以下继承关系,那么大致的内存布局如下
由于是多继承,根据1的观点,单一继承时一个类维护一个虚函数表。多继承时怎么办呢?
那只能是继承几个类,就有几张虚函数表了。
实例代码如下:
class Base1
{
public:
Base1():b1(){} virtual void fun1(){ cout<<"Base1::fun1"<<endl; }
virtual void fun2(){ cout<<"Base1::fun2"<<endl; }
virtual void fun3(){ cout<<"Base1::fun3"<<endl; } int b1;
}; class Base2
{
public:
Base2():b2(){}
virtual void fun1(){ cout<<"Base2::fun1"<<endl; }
virtual void fun2(){ cout<<"Base2::fun2"<<endl; }
virtual void fun3(){ cout<<"Base2::fun3"<<endl; } int b2;
}; class Base3
{
public:
Base3():b3(){}
virtual void fun1(){ cout<<"Base3::fun1"<<endl; }
virtual void fun2(){ cout<<"Base3::fun2"<<endl; }
virtual void fun3(){ cout<<"Base3::fun3"<<endl; } int b3;
}; class Derived:public Base1, public Base2, public Base3
{
public:
Derived():d(){}
virtual void fun1(){ cout<<"Derived::fun1"<<endl; }
virtual void d_fun(){ cout<<"Derived::d_fun"<<endl; } int d;
};
通过调试窗口查看一下虚函数表:
可以明确的看到标注了for base,源自哪个基类的虚函数表。
并且可以看到fun1在三个表中全部被重写了,那么我们关心的d_fun到底会放在哪个表呢?
我们使用相同的办法:
typedef void (*PF)();
PF pf = NULL;
Derived dd;
/////////////Base1///////////
int* vtab1 = (int*)*(int*)ⅆ
for (; *vtab1 != NULL; vtab1++)
{
pf = (PF)*vtab1;
pf();
}
int* member1 = (int*)ⅆ
cout<<*++member1<<endl; /////////////Base2///////////
int* vtab2 = (int*)*((int*)&dd + sizeof(Base1)/);
for (; *vtab2 != NULL; vtab2++)
{
pf = (PF)*vtab2;
pf();
}
int* member2 = (int*)((int*)&dd + sizeof(Base1)/);
cout<<*++member2<<endl; /////////////Base3//////////////
int* vtab3 = (int*)*((int*)&dd + (sizeof(Base1)+sizeof(Base2))/);
for (; *vtab3 != NULL; vtab3++)
{
pf = (PF)*vtab3;
pf();
}
int* member3 = (int*)((int*)&dd + (sizeof(Base1)+sizeof(Base2))/);
cout<<*++member3<<endl;
偷了点懒,因为使用的是int型,所以没有存在字节对齐的情况,直接使用的sizeof/4,使用这种偏移量来访问不同的base区域。
以下是输出结果:
我们可以看到d_fun被放到了第一个函数表中去了(声明的次序的第一个,实例代码是base1的部分)。
结论:
1.多重继承的场合,overwirte时,父类的函数在三个表中会全部被重写。
2.子类新添加的虚函数被放到第一个虚函数表中。
图示:
3.重复继承的场合
其实重复继承只是多重继承的特例,一切的规则依然按照多重继承的规则实行。只是特殊在祖父类生成了两个拷贝镜像,形成数据重复,并且造成二义性。
无论从设计的的角度还是维护的角度,这都是一个失败的选择。
所以我们不重点讨论,直接跳到虚拟继承。
4.虚拟继承的场合
关于虚拟继承的对象模型,其实有多种方法,本文使用的的环境是vs2008,属于微软想的招儿。《深入C++对象模型》一书中明确指出了
虚拟继承的场合,对象模型的构建方式没有固定的标准,主要的思路是拆分成不变局部和共享局部。当然只有更好的方法,也都是为了达到更高的存取效率。
所以本文描述的内存布局或许只在微软编译器的场合成立,正因为如此,我们把重点放在虚拟继承的要达到的效果上。
假设有以下继承关系:
实例代码:
class Base
{
public:
Base():b(){} virtual void fun(){ cout<<"Base::fun"<<endl; }
virtual void B_fun(){ cout<<"Base::B_fun"<<endl; } int b;
}; class Base1:virtual public Base
{
public:
Base1():b1(){} virtual void fun(){ cout<<"Base1::fun"<<endl; }
virtual void fun1(){ cout<<"Base1::fun1"<<endl; }
virtual void B_fun1(){ cout<<"Base1::B_fun1"<<endl; } int b1;
}; class Base2:virtual public Base
{
public:
Base2():b2(1){}
virtual void fun(){ cout<<"Base2::fun"<<endl; }
virtual void fun2(){ cout<<"Base2::fun2"<<endl; }
virtual void B_fun2(){ cout<<"Base2::B_fun2"<<endl; } int b2;
}; class Derived:public Base1, public Base2
{
public:
Derived():d(){} virtual void fun(){ cout<<"Derived::fun"<<endl; }
virtual void fun1(){ cout<<"Derived::fun1"<<endl; }
virtual void fun2(){ cout<<"Derived::fun2"<<endl; }
virtual void D_fun(){ cout<<"Derived::D_fun"<<endl; } int d;
};
先来讨论单一虚拟继承的情况,看一下Base1的布局:
bb是Base的对象,bb1是Base1的对象。
明显可以看到与普通单一继承不同,使用了两个虚函数指针,一个指向了虚基类Base的表,以及自己再生成一个表。
而指向虚基类Base的表的虚函数fun明显被重写了。
使用代码读取:
int* vtab = (int*)*(int*)&bb1;
for (; *vtab != NULL; vtab++)
{
pf = (PF)*vtab;
pf();
}
这个循环运行会中断,原因是vtab访问了一个神奇的数字-4,这个是用来隔开的,不小心访问了。(陈皓老师的一篇博文《C++对象的内存布局》也遇到了相同的问题,而GCC却没有)
足以证明,这里的不变局部是Derived自己后来添加的函数。而共享局部fun跑到虚基类包含的虚函数表上去了。
我们使用二级指针来解决中断的问题。
Base1 bb1; int** pVtab = (int**)&bb1; //////Base1//////////
pf = (PF)pVtab[][];
pf(); //Base1::fun1 pf = (PF)pVtab[][];
pf(); //Base1::B_fun1 //cout << pVtab[0][2] << endl;//访问是一个随机值,证明越界了。
cout << pVtab[][] << endl;//-4 cout << (int)*((int*)(&bb1)+) <<endl; //b1 cout <<"0x"<<(int*)*((int*)(&bb1)+) <<endl;//NULL 父类子类分隔处 //////Base//////////
pf = (PF)pVtab[][];
pf();
pf = (PF)pVtab[][];
pf();
cout << pVtab[][] << endl;//0x00 cout << (int)*((int*)(&bb1)+) <<endl; //b
可以看出内存布局:
1.不变布局(子类)放在对象模型的前端,共享布局(虚基类)放在尾端。
2.其中子类部分,虚函数表使用了-4作为分隔结尾。接下来是子类成员变量值
3.虚基类属于共享局部,是一个正常的虚函数表布局,并且重写了fun函数。
图示:
这样是能够保证共享部分处于虚基类中(包括虚函数表),不变部分处于子类中。
接下来看完整的继承结构,解析Derived的布局。
使用代码:
Derived dd; int** pVtab = (int**)ⅆ //////Base1//////////
pf = (PF)pVtab[][];
pf(); pf = (PF)pVtab[][];
pf(); cout << pVtab[][] << endl;//-4 cout << (int)*((int*)(&dd)+) <<endl; //b1 //////Base2//////////
pf = (PF)pVtab[][];
pf();
pf = (PF)pVtab[][];
pf(); cout << pVtab[][] << endl;//-4
cout << (int)*((int*)(&dd)+) <<endl; //b2 //////Derived 成员//////////
cout << (int)*((int*)(&dd)+) <<endl; //d //////NULL虚基类分隔//////////
cout << "0x"<<(int*)*((int*)(&dd)+) <<endl; pf = (PF)pVtab[][];
pf();
pf = (PF)pVtab[][];
pf();
cout << (int)*((int*)(&dd)+) <<endl; //b
运行结果:
与单一虚拟继承类似:
1.按照声明的次序,不变布局(父类)依次放在对象模型的前端,共享布局(虚基类)放在最尾端。
2.其中不变布局部分,虚函数表使用了-4作为分隔结尾。接下来是子类成员变量值
3.虚基类属于共享局部,是一个正常的虚函数表布局,并且重写了fun函数。
图就不画了,与单一虚拟继承的情况类似。
引用《深入C++对象模型》一书的描述:
要在编译器中支持虚拟继承,困难度颇高。
难度在于,要找到一个足够有效的办法,将Base1和Base2各自维护的Base部分,折叠成为一个由Derived单一维护的Base部分,并且还可以保持base class和Derived class的指针之间的多态操作。
这也整是虚拟继承要达到的效果。
至此,全篇差不多讲完了。
主要参考书籍《深入C++对象模型》以及上文提到的陈皓老师的博文,内容稍长,难免有纰漏,望大家指正。
C++内存中的封装、继承、多态(下)的更多相关文章
- C++内存中的封装、继承、多态(上)
C++内存中的封装.继承.多态(上) 继我的上一篇文章:浅谈学习C++时用到的[封装继承多态]三个概念 此篇我们从C++对象内存布局和构造过程来具体分析C++中的封装.继承.多态. 一.封装模型的内存 ...
- java面向对象(封装-继承-多态)
框架图 理解面向对象 面向对象是相对面向过程而言 面向对象和面向过程都是一种思想 面向过程强调的是功能行为 面向对象将功能封装进对象,强调具备了功能的对象. 面向对象是基于面向过程的. 面向对象的特点 ...
- 浅谈学习C++时用到的【封装继承多态】三个概念
封装继承多态这三个概念不是C++特有的,而是所有OOP具有的特性. 由于C++语言支持这三个特性,所以学习C++时不可避免的要理解这些概念. 而在大部分C++教材中这些概念是作为铺垫,接下来就花大部分 ...
- Java三大特性(封装,继承,多态)
Java中有三大特性,分别是封装继承多态,其理念十分抽象,并且是层层深入式的. 一.封装 概念:封装,即隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别:将抽象得到的数据 ...
- php面向对象 封装继承多态 接口、重载、抽象类、最终类总结
1.面向对象 封装继承多态 接口.重载.抽象类.最终类 面向对象 封装继承多态 首先,在解释面向对象之前先解释下什么是面向对象? [面向对象]1.什么是类? 具有相同属性(特征)和方法(行为)的一 ...
- Java基础——面向对象(封装——继承——多态 )
对象 对象: 是类的实例(实现世界中 真 实存在的一切事物 可以称为对象) 类: 类是对象的抽象描述 步骤: 1.定义一个类 (用于 描述人:) ( * 人:有特征和行为) 2.根据类 创建对象 -- ...
- Python 入门 之 面向对象的三大特性(封装 / 继承 / 多态)
Python 入门 之 面向对象的三大特性(封装 / 继承 / 多态) 1.面向对象的三大特性: (1)继承 继承是一种创建新类的方式,在Python中,新建的类可以继承一个或多个父类,父类又可以 ...
- OOP三大核心封装继承多态
OOP支柱 3 个核心:封装 继承 多态 封装就是将实现细节隐藏起来,也起到了数据保护的作用. 继承就是基于已有类来创建新类可以继承基类的核心功能. 在继承中 另外一种代码重用是:包含/委托,这种重用 ...
- python面向对象(封装,继承,多态)
python面向对象(封装,继承,多态) 学习完本篇,你将会深入掌握 如何封装一个优雅的借口 python是如何实现继承 python的多态 封装 含义: 1.把对象的属性和方法结合成一个独立的单位, ...
随机推荐
- 使用UDP进行数据发送的实例一
首先如果TCP学过以后,再看UDP进行数据传输也是大同小异的,只是用到的类不同 UDP进行传输需要DataSocket和Datapacket类,Datapacket叫数据报,每一个数据报不能大于64k ...
- 用C语言实现统计一个文件夹中各种文件的比例
<UNIX环境高级编程>中的程序清单4-7就介绍了如何实现递归地统计某个目录下面的文件!我刚开始看过它的代码后,觉得照着敲太没意思了,所以就合上书自己写了一遍!为此还写了一篇博文,这是博文 ...
- ubuntu 14.04 nagios4+ndoutils2.0+centreon2.5.4配置
ubuntu 14.04 nagios4+ndoutils2.0+centreon2.5.4(原创) 开发应用centreon是开源的IT监控软件,由法国人于2003年开发,最初名为Oreon,并于2 ...
- ASP.Net MVC中JSON处理。
实体数据Model [Serializable] public class UserModel { //public UserModel(string name, string classname, ...
- DB2删除数据时的小技巧
大家对如何删除数据都不陌生,我们习惯性的这么写: 其实这么写性能并不好,尤其是删除大量数据的时候,要想获得更好的性能,可以采用如下方式: 那如果要把一个表的所有数据都删除了,该怎么办?有人可能会说,这 ...
- 借网上盛传2000w记录介绍多进程处理
2000w的数据在网上搞得沸沸扬扬,作为技术宅的我们也来凑凑热闹.据了解网上有两个版一个是数据库文件另一个是CSV文件的,前者大小有好几个G后者才几百M.对于不是土豪的我们当然下载几百M的.至于在哪下 ...
- CLR via C# 异常管理读书笔记
1. 设计异常类型层次结构应该浅而宽 2. 注意使用finally块清理资源 3. 不要什么都捕捉 4.得体地从异常中恢复 5.发生不可恢复的异常时回滚部分完成的操作-维持状态 6.隐藏实现细节来维系 ...
- MSP430F4152串口操作
/**********************************************************************/ /* 名称:串口通讯 功能:将接到的数据组后原封不 ...
- Oracle RAC LoadBalance
LoadBalance 就是把负载平均的分配到集群中的各个节点,从而提高整体的吞吐能力. Oracle 10g RAC 提供了两种不同的方法来分散负载: 通过Connection Balancing, ...
- JQuery的鼠标滚动事件
jQuery(window).height()代表了当前可见区域的大小,而jQuery(document).height()则代表了整个文档的高度,可视具体情况使用. 注意当浏览器窗口大小改变时(如最 ...