• 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
    ", 1)), a((cout<<"a constructing
    ", 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
    ", 1)), data2((cout<<"data2 constructing
    ", 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
    ", 10);//用来指定静态变量的初始化顺序

    以下是程序运行的结果:

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

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

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

  • 相关阅读:
    Javascript、CSS和IMG之网页执行探索
    从零开始学习Node.js例子九 设置HTTP头
    从零开始学习Node.js例子八 使用SQLite3和MongoDB
    如何做到 jQuery-free?
    jQuery的deferred对象详解
    使用openxml读取xml数据
    Drupal commerce 性能优化
    DataTable数据进行排序、检索、合并、分页、统计
    jquery实现替代iframe的功能
    9_Jvn框架之实现ORM持久层save操作(第九讲)
  • 原文地址:https://www.cnblogs.com/clor001/p/3313589.html
Copyright © 2020-2023  润新知