C++内存中的封装、继承、多态(上)
继我的上一篇文章:浅谈学习C++时用到的【封装继承多态】三个概念
此篇我们从C++对象内存布局和构造过程来具体分析C++中的封装、继承、多态。
一、封装模型的内存布局
常见类对象的成员可能包含以下元素:内建类型、指针、引用、组合对象、虚函数。
另一个角度的分类:
数据成员:静态、非静态
成员函数:静态、非静态、虚函数
1.仅包含内建类型的场合:
class T { int data1; char data2; double data3; };
类中的内建类型按照声明的顺序在内存中连续存储,并且分配的大小由内建类型本身的大小决定(依赖机器),布局受字节对齐影响(本篇不讨论字节对齐)。
2.包含指针和引用的场合:
class T { int data1; char data2; double data3; int& ri1;//需要构造函数 int* rp1; int (*pf)(); };
存储方式同1的场合,不同点为指针和引用通常为固定大小(32位机器4字节、64位机器8字节)。
有关引用:个人理解的引用就是懒人专用指针,取地址又间地址是很麻烦的操作,于是出现了自动取址又间址的指向常量的常指针。
在类中声明可以测出固定字节大小,所以也是占用固定的字节大小。
3.包含组合对象的场合:
class Q { int a; int b; }; class T {int data1; Q q; double data2; };
内存布局图示(本篇以及后续篇使用的环境为 32位Win7, VS2008):
再来看一下地址:
结论:(显而易见就不解释了)
类对象最终被解释成内建类型,布局依然按照声明的顺序,并且对象布局在内存中依然是连续的
4.在3的场合添加虚函数的场合
class Q { virtual void fun(){} int a; int b; }; class T { virtual void fun(){} int data1; Q q; double data2; };
内存布局图示
通过程序输出看一下
typedef void (*PF)(); int main() { T t; PF pf1, pf2; cout<<"vfptr的地址"<<(int*)&t<<endl; cout<<"vftable的地址"<<(int*)*(int*)&t<<endl; cout<<"通过vftable调用类T的fun函数: "; pf1 = (PF)*(int*)*(int*)&t; pf1(); cout<<"通过vftable调用类Q的fun函数: "; pf2 = (PF)*(int*)*(int*)&t.q; pf2(); return 0; }
输出图示
推理证明:
1.取t的地址强转成(int*)类型输出以后得到的地址 == 取t的vfptr的地址(调试窗口第一行): 虚函数指针被放在对象布局的首地址位置
2.因为(int*)&t == vfptr,那么*vfptr得到的是虚函数表的首地址。
(int*)*vfptr,把虚函数表的首地址强转成(int*)的地址 == t对象的__vftable的虚函数表的地址(调试窗口第四行行):虚函数指针指向虚函数表
3.vftable的首地址到vftable的第一个函数的地址中间相差很多空间:虚函数表还承担了虚函数以外的内容
什么内容也会放在虚函数表中呢?
虚函数表用来实现多态,多态意味着类型上的模糊,模糊以后必须有东西来记录自己的老本,否则无法实现另外一个东西——RTTI。
结论:
在包含虚函数的场合多了一个vfptr,它是一个const指针,位于类布局中的首位置,指向了虚函数表,虚函数表包含了虚函数地址,通过虚函数地址访问虚函数。
并且虚函数表的首地址存在了本类的类型信息,用于实现RTTI。
5.包含了static的场合
static的特性众所周知,从调试窗口观察变量并不能得出什么结论,我们先列出几条特性:
1.static成员为整个类共有的属性
2.static函数不包含this指针
3.static成员不能访问nonstatic成员
初步结论:
内存对象模型中对static作了隔离处理(不是所有对象具有的),static自己独霸一方。
通过以上5条现在来构建C++的封装模型:
有关普通的成员函数
所谓类,就是自己圈定了一个域名,所以在内存中的代码区也圈定了自己的域,普通的成员函数放在那里。
有关静态成员函数
在代码区中圈定的类域名中的圈定一个static区域,思路依然是独霸一方。
有关构造函数
由于构造函数的特殊性,所以在代码区拥有一个自己的构造代码区域。
现在又有了一个更完整的模型:
假定读者已经了解堆/栈/静态区和常量区/代码区
根据上图我们得到一些结论
1.类最终被解释内建类型(内建类型过了编译期以后,都不复存在,只是编译期的解读方式而已)
2.内建类型按照声明的次序顺序存储
3.存在虚函数的场合,会生成vfptr,并且vfptr->vtable->function()
4.静态成员被单独对待、数据只有一份拷贝,函数被放到static区域。
5.Type Infomation被放到vftable中
二、封装模型的构造过程
1.静态是编译期决定的,所有对象共有的数据拷贝,优先创建。
2.进入构造函数,优先创建vfptr和vftable,也就是优先构造虚函数部分
3.其次按照声明的顺序构造数据成员。
我们可以使用逗号表达式来干一些有意思的事情。
事先我们需要定义
typedef void (*PF)();
PF pf = NULL;
class Q { public: Q():b((cout<<"b constructing\n", 1)), a((cout<<"a constructing\n", 2)){}//组合对象的初始化顺序,注意初始化列表写的顺序是和声明的顺序相反的 virtual void fun(){cout<<"Q::f"<<endl;} int a; int b; }; class T { public: T():data1(((pf =(PF)*(int*)*(int*)this, pf()), cout<<"data1 constructing\n", 1)), data2((cout<<"data2 constructing\n", 2)){}//data2的构造使用了简单的逗号表达式 //data1的初始化嵌套了一层逗号表达式,结构其实是data1((为函数指针pf赋值, 调用pf), 打印data1构造中, 数值) virtual void fun(){cout<<"T::f"<<endl;} int data1; Q q; double data2; static int sdata1; }; int T::sdata1 = (cout<<"sdata1 constructing\n", 10);//用来指定静态变量的初始化顺序
以下是程序运行的结果:
静态--虚函数表--声明次序初始化。
文章不免有疏忽和不足的地方,欢迎大家批评指正。邮箱【betachen@yeah.net】
下一篇重点讲继承时和多态时的内存布局。