C++内存模型2——类对象

        本篇主要给大家分享类对象在内存中是如何进行内存分配的。咱们都知道类包含属性方法,那么,类属性和方法在内存中是如何进行分配的呢?

一、对象大小

        以下代码声明了一个Base类,类中有成员方法、成员属性 、静态方法、静态属性,那么,根据这个类的定义创建一个类对象,所占的内存空间为多少呢?

#include <iostream>
using namespace std;

class Base{
public:
    void f(){};//成员方法
    void g(){};//成员方法
    static void s_f(){};//静态方法

public:
    int x,y;成员属性
    static int z;//静态属性
};

int main(int argc, char const *argv[])
{
	Base base;
	cout << "size of base = " << sizeof(base) << endl;
	cout << "address of base = " << &base << endl;
	cout << "address of x = " << &base.x << endl;
	cout << "address of y = " << &base.y << endl;
    return 0;
}

程序输出

类对象大小为8, 这说明类对象的大小和成员属性相关! 大家可以自己修改成员属性的类型或者新增成员属性进行测试!并且base对象的地址和属性x的地址是相同的! 

为什么会这样呢?

        类定义其实是一个模子,编译器根据这个模子的定义来创建一个类对象,那么每个类对象就应该有自己的独立内存空间!咱们前面一节讲到内存中有一块叫代码区的地方,那里应该是来存放咱们类定义的地方(类的模子)。

        大家再仔细想想,如果将类的所有信息(成员和属性)都复制一份到类对象中,是不是需要更多的内存,如果类对象非常多,这个开销是非常大!所以,能将更多信息和对象进行分离是C++编译器充分抽象和设计的结果。

        类成员属性肯定是需要跟类对象走的,这个应该很好理解,所以刚才的程序输出的大小就是 int x, y; 的大小 8 ;而成员方法、静态方法、静态属性是不跟类对象走的,所以这些信息是不占类对象的空间大小的。

这就是单个对象的内存模型,即,在没有虚函数的情况下,只存储非静态成员变量

如果有虚函数,那么又会不一样!下面将成员函数 g() 定义为虚函数:

#include <iostream>
using namespace std;

class Base{
public:
    void f(){};//成员方法
    virtual void g(){};//虚函数
    static void s_f(){};//静态方法

public:
    int x,y;成员属性
    static int z;//静态属性
};

int main(int argc, char const *argv[])
{
	Base base;
	cout << "size of base = " << sizeof(base) << endl;
	cout << "address of base = " << &base << endl;
	cout << "address of x = " << &base.x << endl;
	cout << "address of y = " << &base.y << endl;
    
    return 0;
}

程序输出:

此时,类对象大小为 16 , 这说明类对象的大小还和虚函数有关系!

这里要注意,如果有虚函数则会在内存中开辟一个虚函数表,虚函数表是一个数组结构;类对象中会新增一个指针来指向该虚函数表(虚函数表是类实现多态的重要手段),在我们这个示例中可以看到,base的地址和成员x的地址相差了8个字节,这新增的8字节就是用来保存虚函数表地址的!

二、成员函数和静态函数的区别

        成员函数与静态函数都是不占对象的内存,那么,对象是如何能够调用这些函数的呢?或者说成员函数与静态函数有什么区别呢?

        下面代码演示如何通过函数对象/函数指针的方式去调用成员函数和静态函数:

#include <iostream>
#include <functional>
using namespace std;

class Base{
public:
    void f(){
    	cout << "member function f() \n";
    };//成员方法
    virtual void g(){};//虚函数
    static void s_f(){
    	cout << "static function f() \n";
    };//静态方法

public:
    int x,y;成员属性
    static int z;//静态属性
};

int main(int argc, char const *argv[])
{
	Base base;
	base.f();
	base.s_f();

	function<void(Base*)> fun1 = &Base::f;//通过函数对象调用成员方法
	function<void()> fun2 = &Base::s_f;//通过函数对象调用静态方法
	fun1(&base);
	fun2();
    
    return 0;
}

        上面的代码能够正确编译并且正确执行!通过function对象的定义,我们能够看出成员函数和静态函数在底层的实现上是有区别的,成员函数要比静态函数多一个Base*的参数,这个参数在cpp代码中是没有的,是编译之后自动加上的!

        这个自动加上的参数也就是咱们this指针的由来!当你要将成员函数声明为const类型,实际编译器会将自动加上的参数添加上const修饰符!形如:void f(const Base* this)

 

三、虚函数表

        在没有继承的情况下,虚函数会让类对象的大小多8个字节用来存储虚函数表的地址,并且,这个过程是在编译阶段自动加上的,该地址存储在对象的起始地址,这个结论在前面已经得到验证!

        下面就看看虚函数表到底是长什么样的:

#include <iostream>
using namespace std;

class Base
{
private:
    int x, y;
public:
    Base(){}
    
    virtual ~Base(){ 
        cout << "Base Destructor" << endl;
    }

    virtual void test(){
        cout << "call member function" << endl;
    }

    virtual void testInt(int n){
        cout << "call member function int n = " << n << endl;
    }

    static void staticFunc(){
        cout << "static call function " << endl;
    }
};

int main(){
    typedef void (*pClassFunc1)(void*);//类成员函数指针
    typedef void (*pClassFunc2)(void*, int);//类成员函数指针

    Base base;
    printf("base %p \n", ((pClassFunc1**)&base)[0][0]);//base 0x4012f8 
    printf("base %p \n", ((pClassFunc1**)&base)[0][1]);//base 0x401330 
    printf("base %p \n", ((pClassFunc1**)&base)[0][2]);//base 0x40135c 
    printf("base %p \n", ((pClassFunc2**)&base)[0][3]);//base 0x401388 
    ((pClassFunc1**)&base)[0][0](&base);//调用complete析构函数
    ((pClassFunc1**)&base)[0][1](&base);//调用deleting析构函数
    ((pClassFunc1**)&base)[0][2](&base);//调用test函数
    ((pClassFunc2**)&base)[0][3](&base, 1);//调用testInt函数

    return 0;
}

        通过上面的代码,我们直接将对象头部8字节地址取出来,然后根据该地址存储的信息去找到虚函数表,再像数组一样一个个去取到虚函数的地址。这里会生成4个虚函数,每个虚函数的地址不一样并且不连续! 虚函数信息如下:

         这里虚函数表中为什么会有0和typeinfo信息,请查看另一篇typeid 和虚函数_master-计算机科学专栏-CSDN博客上一篇C++类型预断——RTTI_master-计算机科学专栏-CSDN博客讲到了typeid的动态类型推断需要借助虚函数表,本篇专门来讲一下这个原理,让大家理解的更透彻! 前面咱们讲到 typeid 的操作返回值是 type_info 对象的引用,然后输出返回值的地址是相同的,测试代码如下:#include <iostream>#include <functional>using namespace std;class Base{...https://blog.csdn.net/zhaxun/article/details/120166140

        Base类中虚函数表信息结构如下图:

        通过上图,我们可以看到虚函数表中的函数的顺序是和声明的顺序一致,谁先声明为virtual,那么谁就在虚函数表中的顺序排前面。

 四、内存对齐

        类对象的大小还受到内存对齐的影响。也就是类成员属性在内存中存储并不一定是连续的。例如上例中的Base类的大小为16,因为刚好内存是对齐的,如果新增一个属性z:


class Base
{
private:
    int x, y, z;
public:
    Base(){}
    
    virtual ~Base(){ 
        cout << "Base Destructor" << endl;
    }

    virtual void test(){
        cout << "call member function" << endl;
    }

    virtual void testInt(int n){
        cout << "call member function int n = " << n << endl;
    }

    static void staticFunc(){
        cout << "static call function " << endl;
    }
};

        此时,Base类的大小就变成了24,因为要满足8字节对齐(以单位最大为参考)。

如果不希望使用内存对齐(比如通讯协议就不应该使用内存对齐),可以使用#pragma pack(1)来做预处理,这样编译器就按照1字节对齐来进行处理。

#pragma pack(n)作为一个预编译指令用来设置多少个字节对齐的。值得注意的是,n的缺省数值是按照编译器自身设置,一般为8,合法的数值分别是1、2、4、8、16。

即编译器只会按照1、2、4、8、16的方式分割内存。若n为其他值,是无效的。

 

五、为什么要进行内存对齐

        尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度.

        现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。

        假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作。

        现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。

有了内存对齐的知识,那咱们在定义struct和class时就应当注意,不同的定义顺序会导致类的大小不一样。并且,在某些情况下,我们不应该使用内存对齐!(例如制定通讯协议)

        

        至此,咱们对单个类的内存结构有了深刻的认识,后面一篇,我再对继承类和多继承的情况做详细解析!

 

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chls

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值