热门标签 | 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语言
    c语言二元插值,二维线性插值c语言 ... [详细]
  • 实现系统调用
    实现系统调用一、实验环境​本次操作还是基于上次编译Linux0.11内核的实验环境进行操作。环境如下:二、实验目标​通过对上述实验原理的认识,相信 ... [详细]
  • 本文详细介绍了在单片机编程中常用的几个C库函数,包括printf、memset、memcpy、strcpy和atoi,并提供了具体的使用示例和注意事项。 ... [详细]
  • 想把一组chara[4096]的数组拷贝到shortb[6][256]中,尝试过用循环移位的方式,还用中间变量shortc[2048]的方式。得出的结论:1.移位方式效率最低2. ... [详细]
  • 编译原理中的语法分析方法探讨
    本文探讨了在编译原理课程中遇到的复杂文法问题,特别是当使用SLR(1)文法时遇到的多重规约与移进冲突。文章讨论了可能的解决策略,包括递归下降解析、运算符优先级解析等,并提供了相关示例。 ... [详细]
  • 深入浅出C语言指针
    指针是C语言中极其重要的数据类型,广泛应用于各种数据结构的表示、数组和字符串的操作以及内存地址的处理。本文将通过实例详细解析指针的基本概念及其应用。 ... [详细]
  • C语言中的指针详解
    1.什么是指针C语言中指针是一种数据类型,指针是存放数据的内存单元地址。计算机系统的内存拥有大量的存储单元,每个存储单元的大小为1字节, ... [详细]
  • Gradle 是 Android Studio 中默认的构建工具,了解其基本配置对于开发效率的提升至关重要。本文将详细介绍如何在 Gradle 中定义和使用共享变量,以确保项目的一致性和可维护性。 ... [详细]
  • 本文探讨了Linux环境下线程私有数据(Thread-Specific Data, TSD)的概念及其重要性,介绍了如何通过TSD技术避免多线程间全局变量冲突的问题,并提供了具体的实现方法和示例代码。 ... [详细]
  • C/C++ 应用程序的安装与卸载解决方案
    本文介绍了如何使用Inno Setup来创建C/C++应用程序的安装程序,包括自动检测并安装所需的运行库,确保应用能够顺利安装和卸载。 ... [详细]
  • 本报告记录了嵌入式软件设计课程中的第二次实验,主要探讨了使用KEIL V5开发环境和ST固件库进行GPIO控制及按键响应编程的方法。通过实际操作,加深了对嵌入式系统硬件接口编程的理解。 ... [详细]
  • 本文详细介绍了在Luat OS中如何实现C与Lua的混合编程,包括在C环境中运行Lua脚本、封装可被Lua调用的C语言库,以及C与Lua之间的数据交互方法。 ... [详细]
  • 本文将深入探讨C语言中的位操作符——按位与(&)、按位或(|)和按位异或(^),通过具体示例解释这些操作符如何在位级别上对数据进行操作。 ... [详细]
  • 汇编语言:编程世界的始祖,连C语言都敬畏三分!
    当C语言还在萌芽阶段时,它首次接触到了汇编语言,并对其简洁性感到震惊。尽管汇编语言的指令极其简单,但它却是所有现代编程语言的基础,其重要性不言而喻。 ... [详细]
  • 本文提供了一个使用C语言实现的顺序表区间元素删除功能的完整代码示例。该程序首先初始化一个顺序表,然后根据用户输入的数据进行插入操作,最后根据指定的区间范围删除相应的元素,并输出最终的顺序表。 ... [详细]
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社区 版权所有