C++内存中的封装、继承、多态(上)

时间:2021-09-23 19:28:07

C++内存中的封装、继承、多态(上)

继我的上一篇文章:浅谈学习C++时用到的【封装继承多态】三个概念

此篇我们从C++对象内存布局和构造过程来具体分析C++中的封装、继承、多态。

 

一、封装模型的内存布局

常见类对象的成员可能包含以下元素:内建类型、指针、引用、组合对象、虚函数。

另一个角度的分类:

数据成员:静态、非静态

成员函数:静态、非静态、虚函数

 

1.仅包含内建类型的场合:

class T
{
    int     data1;
    char    data2;
    double  data3;
};

类中的内建类型按照声明的顺序在内存中连续存储,并且分配的大小由内建类型本身的大小决定(依赖机器),布局受字节对齐影响(本篇不讨论字节对齐)

 

2.包含指针和引用的场合:

C++内存中的封装、继承、多态(上)
class T
{
    int     data1;
    char    data2;
    double  data3;
    int&    ri1;//需要构造函数 int*    rp1;
    int     (*pf)();
};
C++内存中的封装、继承、多态(上)

存储方式同1的场合,不同点为指针和引用通常为固定大小(32位机器4字节、64位机器8字节)。

有关引用:个人理解的引用就是懒人专用指针,取地址又间地址是很麻烦的操作,于是出现了自动取址又间址的指向常量的常指针

在类中声明可以测出固定字节大小,所以也是占用固定的字节大小。

 

3.包含组合对象的场合:

C++内存中的封装、继承、多态(上)
class Q
{
    int a;
    int b;
};

class T
{int      data1;
    Q        q;
    double  data2;
};
C++内存中的封装、继承、多态(上)

内存布局图示(本篇以及后续篇使用的环境为 32位Win7, VS2008):

C++内存中的封装、继承、多态(上)

再来看一下地址:

C++内存中的封装、继承、多态(上)

结论:(显而易见就不解释了)

类对象最终被解释成内建类型,布局依然按照声明的顺序,并且对象布局在内存中依然是连续的

 

4.在3的场合添加虚函数的场合

C++内存中的封装、继承、多态(上)
class Q
{
    virtual void fun(){}
    int a;
    int b;
};

class T
{
    virtual void fun(){}
    int      data1;
    Q        q;
    double  data2;
};
C++内存中的封装、继承、多态(上)

内存布局图示

C++内存中的封装、继承、多态(上)

 

通过程序输出看一下

C++内存中的封装、继承、多态(上)
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;
}
C++内存中的封装、继承、多态(上)

输出图示

C++内存中的封装、继承、多态(上)

推理证明:

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++的封装模型:

C++内存中的封装、继承、多态(上)

 

有关普通的成员函数

所谓类,就是自己圈定了一个域名,所以在内存中的代码区也圈定了自己的域,普通的成员函数放在那里。

有关静态成员函数

在代码区中圈定的类域名中的圈定一个static区域,思路依然是独霸一方。

有关构造函数

由于构造函数的特殊性,所以在代码区拥有一个自己的构造代码区域。

现在又有了一个更完整的模型:

假定读者已经了解堆/栈/静态区和常量区/代码区

C++内存中的封装、继承、多态(上)

 

 

根据上图我们得到一些结论

1.类最终被解释内建类型(内建类型过了编译期以后,都不复存在,只是编译期的解读方式而已)

2.内建类型按照声明的次序顺序存储

3.存在虚函数的场合,会生成vfptr,并且vfptr->vtable->function()

4.静态成员被单独对待、数据只有一份拷贝,函数被放到static区域。

5.Type Infomation被放到vftable中

 

二、封装模型的构造过程

1.静态是编译期决定的,所有对象共有的数据拷贝,优先创建。

2.进入构造函数,优先创建vfptr和vftable,也就是优先构造虚函数部分

3.其次按照声明的顺序构造数据成员。

我们可以使用逗号表达式来干一些有意思的事情。

事先我们需要定义

typedef void (*PF)();
PF pf = NULL;

C++内存中的封装、继承、多态(上)
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);//用来指定静态变量的初始化顺序
C++内存中的封装、继承、多态(上)

 

以下是程序运行的结果:

C++内存中的封装、继承、多态(上)

 静态--虚函数表--声明次序初始化。

 

文章不免有疏忽和不足的地方,欢迎大家批评指正。邮箱【betachen@yeah.net】

下一篇重点讲继承时和多态时的内存布局。