热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

C++类对象模型之内存布局

1、C类对象的内存布局在C的类对象中,有两种类的成员变量:static和非static,有三种成员函数:static、非st

1、C++类对象的内存布局 

    在C++的类对象中,有两种类的成员变量:static和非static,有三种成员函数:static、非static和virtual。那么,它们在C++的内存中是如何分布的呢?

      C++程序的内存格局通常分为四个区:全局数据区(data area),代码区(code area),栈区(stack area),堆区(heap area)(即自由存储区)。全局数据区存放全局变量,静态数据和常量。所有类成员函数和非成员函数代码存放在代码区;为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;余下的空间都被称为堆区。

     在类的定义时,

  • 类的成员函数被放在代码区。
  • 类的静态成员变量在全局数据区。
  • 非静态成员变量在类的实例内,实例在栈区或者堆区。
  • 虚函数指针、虚基类指针在类的实例内,实例在栈区或者堆区。

     类的实例如果是定义的类变量,则在栈内存区,如果是new出来的类指针,则在堆内存区,同时引用会保存在栈里。 

     为何这样设计?其实这是从c语言发展而来的。类的成员变量相当于c的结构体,类的成员函数类似于c的函数,类的静态变量类似于c的静态或全局变量,至于虚函数,函数体还是放在代码区,但虚函数的指针和成员变量一起放在数据区,这是因为虚函数的函数体有多个,不同的子类调用同一虚函数实则调用的不同函数体,因此需要在类的数据区保持真正的虚函数的指针。

     类的成员函数为什么不需要在类的数据区保持指针?因为类的成员函数是唯一的,在编译时,编译器会为每个类的成员函数改头换面,如函数名加上类名,参数加上this类指针。这样类的成员函数和c的普通函数就一样了。虚函数由于其多态的特殊性,无法这样处理,所以需要保持在类的数据区。

 

     下面就一个非常简单的类,通过逐渐向其中加入各种成员,来逐一分析上述两种成员变量及三种成员函数对类的对象的内存分布的影响。 

注:以下的代码的测试结果均是基于Ubuntu 14.04 64位系统下的G++ 4.8.2,若在其他的系统上或使用其他的编译器,可能会运行出不同的结果。 

2、含有非static成员变量及成员函数的类的对象的内存分布 


类Persion的定义如下: 
class Person
{undefined
    public:
        Person():mId(0), mAge(20){}
        void print()
        {undefined
            cout <<"id: " <                  <<", age: " <         }
    private:
        int mId;
        int mAge;
}; 

Person类包含两个非static的int型的成员变量&#xff0c;一个构造函数和一个非static成员函数。为弄清楚该类的对象的内存分布&#xff0c;对该类的对象进行一些操作如下&#xff1a; 
int main()
{undefined
    Person p1;
    cout <<"sizeof(p1) &#61;&#61; " <     int *p &#61; (int*)&p1;
    cout <<"p.id &#61;&#61; " <<*p <<", address: "  <

    &#43;&#43;p;
    cout <<"p.age &#61;&#61; " <<*p <<", address: " <

    cout <     
    Person p2;
    cout <<"sizeof(p2) &#61;&#61; " <     p &#61; (int*)&p2;
    cout <<"p.id &#61;&#61; " <<*p <<", address: " <

    &#43;&#43;p;
    cout <<"p.age &#61;&#61; " <<*p <<", address: " <

    return 0;

其运行结果如下&#xff1a; 

      从上图可以看到类的对象的占用的内存均为8字节&#xff0c;使用普通的int&#xff0a;指针可以遍历输出对象内的非static成员变量的值&#xff0c;且两个对象中的相同的非static成员变量的地址各不相同。 

      据此&#xff0c;可以得出结论&#xff0c;在C&#43;&#43;中&#xff0c;非static成员变量被放置于每一个类对象中&#xff0c;非static成员函数放在类的对象之外&#xff0c;且非static成员变量在内存中的存放顺序与其在类内的声明顺序一致。即person对象的内存分布如下图所示&#xff1a; 

3、含有static和非static成员变量和成员函数的类的对象的内存分布

向Person类中加入一个static成员变量和一个static成员函数&#xff0c;如下&#xff1a;
class Person
{undefined
     public:
         Person():mId(0), mAge(20){ &#43;&#43;sCount; }
         ~Person(){ --sCount; }
         void print()
         {undefined
             cout <<"id: " <                   <<", age: " <          }
         static int personCount()
         {undefined
             return sCount;
         }
     private:
         static int sCount;
         int mId;
         int mAge;
}; 

      测试代码不变&#xff0c;与第1节中的代码相同。其运行结果不变&#xff0c;与第1节中的运行结果相同。 据此&#xff0c;可以得出&#xff1a;static成员变量存放在类的对象之外&#xff0c;static成员函数也放在类的对象之外。

其内存分布如下图所示&#xff1a;


4、加入virtual成员函数的类的对象的内存分布

在Person类中加入一个virtual函数&#xff0c;并把前面的print函数修改为虚函数&#xff0c;如下&#xff1a; 
class Person
{undefined
    public:
        Person():mId(0), mAge(20){ &#43;&#43;sCount; }
        static int personCount()
        {undefined
            return sCount;
        }
 
        virtual void print()
        {undefined
            cout <<"id: " <                  <<", age: " <         }
        virtual void job()
        {undefined
            cout <<"Person" <         }
        virtual ~Person()
        {undefined
            --sCount;
            cout <<"~Person" <         }
 
    protected:
        static int sCount;
        int mId;
        int mAge;
};

为了查看类的对象的内存分布&#xff0c;对类的对象执行如下的操作代码&#xff0c;如下&#xff1a; 
int main()
{undefined
    Person person;
    cout <     int *p &#61; (int*)&person;
    for (int i &#61; 0; i     {undefined
        cout <<*p <     }
    return 0;

其运行结果如下&#xff1a; 


从上图可以看出&#xff0c;加virtual成员函数后&#xff0c;类的对象的大小为16字节&#xff0c;增加了8。通过int&#xff0a;指针遍历该对象的内存&#xff0c;可以看到&#xff0c;最后两行显示的是成员数据的值。

      C&#43;&#43;中的虚函数是通过虚函数表&#xff08;vtbl&#xff09;来实现&#xff0c;每一个类为每一个virtual函数产生一个指针&#xff0c;放在表格中&#xff0c;这个表格就是虚函数表。每一个类对象会被安插一个指针&#xff08;vptr&#xff09;&#xff0c;指向该类的虚函数表。vptr的设定和重置都由每一个类的构造函数、析构函数和复制赋值运算符自动完成。

      由于本人的系统是64位的系统&#xff0c;一个指针的大小为8字节&#xff0c;所以可以推出&#xff0c;在本人的环境中&#xff0c;类的对象的安插的vptr放在该对象所占内存的最前面。其内存分布图如下&#xff1a;


注&#xff1a;虚函数的顺序是按虚函数定义顺序定义的&#xff0c;但是它还包含其他的一些字段&#xff0c;本人还未明白它是什么&#xff0c;在下一节会详细说明虚函数表的内容。


5、虚函数表&#xff08;vtbl&#xff09;的内容及函数指针存放顺序


      在第3节中&#xff0c;我们可以知道了指向虚函数表的指针&#xff08;vptr&#xff09;在类中的位置了&#xff0c;而函数表中的数据都是函数指针&#xff0c;于是便可利用这点来遍历虚函数表&#xff0c;并测试出虚函数表中的内容。

测试代码如下&#xff1a;
typedef void (*FuncPtr)();
int main()
{undefined
    Person person;
    int **vtbl &#61; (int**)*(int**)&person;
    for (int i &#61; 0; i <3 && *vtbl !&#61; NULL; &#43;&#43;i)
    {undefined
        FuncPtr func &#61; (FuncPtr)*vtbl;
        func();
        &#43;&#43;vtbl;
    }
 
    while (*vtbl)
    {undefined
        cout <<"*vtbl &#61;&#61; " <<*vtbl <         &#43;&#43;vtbl;
    }
    return 0;
}

代码解释&#xff1a;
由于虚函数表位于对象的首位置上&#xff0c;且虚函数表保存的是函数的指针&#xff0c;若把虚函数表当作一个数组&#xff0c;则要指向该数组需要一个双指针。我们可以通过如下方式获取Person类的对象的地址&#xff0c;并转化成int**指针&#xff1a;
Person person;
int **p &#61; (int**)&person;

再通过如下的表达式&#xff0c;获取虚函数表的地址&#xff1a;
 
int **vtbl &#61; (int**)*p;

然后&#xff0c;通过如下语句获得虚函数表中函数的地址&#xff0c;并调用函数。
FuncPtr func &#61; (FuncPtr)*vtbl;
func();

最后&#xff0c;通过&#43;&#43;vtbl可以得到函数表中下一项地址&#xff0c;从而遍历整个虚函数表。

其运行结果如下图所示&#xff1a;


从上图可以看出&#xff0c;遍历虚函数表&#xff0c;并根据虚函数表中的函数地址调用函数&#xff0c;它先调用print函数&#xff0c;再调用job函数&#xff0c;最后调用析构函数。函数的调用顺序与Person类中的虚函数的定义顺序一致&#xff0c;其内存分布与第3节中的对象内存分布图相一致。从代码和运行结果&#xff0c;可以看出&#xff0c;虚函数表以NULL标志表的结束。但是虚函数表中还含有其他的数据&#xff0c;本人还没有清楚其作用。

6、继承对于类的对象的内存分布的影响


本文并不打算详细地介绍继承对对象的内存分布的影响&#xff0c;也不介绍虚函数的实现机制。这里主要给出一个经过本人测试的大概的对象内存模型&#xff0c;由于代码较多&#xff0c;不一一贴出。假设所有的类都有非static的成员变量和成员函数、static的成员变量及成员函数和virtual函数。
1&#xff09;单继承&#xff08;只有一个父类&#xff09;
类的继承关系为&#xff1a;class Derived : public Base

Derived类的对象的内存布局为&#xff1a;虚函数表指针、Base类的非static成员变量、Derived类的非static成员变量。

2&#xff09;多重继承&#xff08;多个父类&#xff09;
类的继承关系如下&#xff1a;class Derived : public Base1, public Base2

Derived类的对象的内存布局为&#xff1a;基类Base1子对象和基类Base2子对象及Derived类的非static成员变量组成。基类子对象包括其虚函数表指针和其非static的成员变量。

3&#xff09;重复继承&#xff08;继承的多个父类中其父类有相同的超类&#xff09;
类的继承关系如下&#xff1a;
class Base1 : public Base
class Base2:  public Base
class Derived : public Base1, public Base2

Derived类的对象的内存布局与多继承相似&#xff0c;但是可以看到基类Base的子对象在Derived类的对象的内存中存在一份拷贝。这样直接使用Derived中基类Base的相关成员时&#xff0c;就会引发歧义&#xff0c;可使用多重虚拟继承消除之。

4&#xff09;多重虚拟继承&#xff08;使用virtual方式继承&#xff0c;为了保证继承后父类的内存布局只会存在一份&#xff09;
类的继承关系如下&#xff1a;
class Base1 : virtual public Base
class Base2:  virtual public Base
class Derived : public Base1, public Base2

Derived类的对象的内存布局与重复继承的类的对象的内存分布类似&#xff0c;但是基类Base的子对象没有拷贝一份&#xff0c;在对象的内存中仅存在在一个Base类的子对象。但是它的非static成员变量放置在对象的末尾处。
 



推荐阅读
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • JVM:33 如何查看JVM的Full GC日志
    1.示例代码packagecom.webcode;publicclassDemo4{publicstaticvoidmain(String[]args){byte[]arr ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • PriorityQueue源码分析
     publicbooleanhasNext(){returncursor&amp;amp;lt;size||(forgetMeNot!null&amp;amp;am ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • OO第一单元自白:简单多项式导函数的设计与bug分析
    本文介绍了作者在学习OO的第一次作业中所遇到的问题及其解决方案。作者通过建立Multinomial和Monomial两个类来实现多项式和单项式,并通过append方法将单项式组合为多项式,并在此过程中合并同类项。作者还介绍了单项式和多项式的求导方法,并解释了如何利用正则表达式提取各个单项式并进行求导。同时,作者还对自己在输入合法性判断上的不足进行了bug分析,指出了自己在处理指数情况时出现的问题,并总结了被hack的原因。 ... [详细]
  • 31.项目部署
    目录1一些概念1.1项目部署1.2WSGI1.3uWSGI1.4Nginx2安装环境与迁移项目2.1项目内容2.2项目配置2.2.1DEBUG2.2.2STAT ... [详细]
  • 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件
    本文旨在全面介绍Windows内存管理机制及C++内存分配实例中的内存映射文件。通过对内存映射文件的使用场合和与虚拟内存的区别进行解析,帮助读者更好地理解操作系统的内存管理机制。同时,本文还提供了相关章节的链接,方便读者深入学习Windows内存管理及C++内存分配实例的其他内容。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • 《2017年3月全国计算机等级考试二级C语言上机题库完全版》由会员分享,可在线阅读,更多相关《2017年3月全国计算机等级考试二级C语言上机题库完全版( ... [详细]
  • c语言基础编写,c语言 基础
    本文目录一览:1、C语言如何编写?2、如何编写 ... [详细]
  • 介绍平常在多线程开发中,总避免不了线程同步。本篇就对net多线程中的锁系统做个简单描述。目录一:lock、Monitor1:基础 ... [详细]
author-avatar
麦芽糖的-寂寞
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有