C++类对象模型及内存对齐与字节序

回顾

上篇我们简单的阐述了一下面向对象与面向过程编程的基本思想以及它们之间的区别,对我们类与对象做了一个简单的了解,那么这次我们就来说一下关于类的其它相关的知识。

类对象模型

问题引入

我们知道,一个类中既可以有成员变量,还可以有成员函数,那么我们的对象中到底包含了什么?我们应该如何计算一个类的大小?

类对象模型的存储方式

我们知道对象是一个类的实例化,类是一个抽象的概念,而对象占有实际的物理内存。那么这个对象里面到底存储了什么呢?

  1. 猜测一 : 因为类中既有可能存成员变量,也有可能存成员方法,所以对象中有可能是这样存储的。

在这里插入图片描述
类中既存储了成员方法,也存储了成员函数,但是我们仔细想想,这种方式会有一定的缺陷:虽热类中的成员变量是不同的,但是它们调用了同一个函数,如果按照此种方法存储,那么每个对象中都将会存储这个函数的相同的代码,那么这样的话就有点浪费空间。

2.猜测二 : 对象中只存储成员变量,成员方法存储在公共的代码段
在这里插入图片描述
如果按照这种方式存储的话,相对来说就比较节省空间了。我们的想法是这样,但是到底是按照什么样的方法来存储的呢?接下来我们进行一个简单的验证。

class A1 {
public:
 void fun1(){}
private:
 int _a;
};
// 类中仅有成员函数
class A2 {
public:
 void f2() {}
};
// 类中什么都没有---空类
class A3
{};

结果如下:
sizeof(A1) = 4
sizeof(A2) = 1
sizeof(A3) = 1
可以看到,A1类不仅有成员方法,还有成员函数,但是它的结果是一个 int 的大小,由此例我们就知道了,对象中存储的是成员变量,成员函数存放在公共代码段。而且我们还得注意一下默认空类的大小占一个字节。
那么为什么空类的大小占一个字节呢? 这个原因我也得给大家解释一下。既然是一个类,那么它就应该能被实例化,所谓类的实例化就是在内存中给对象分配一块空间,每个实例在内存中都应该有一个独一无二的地址,为了达到这个目的,编译器往往会给空类隐含加一个字节来保证空类在实例化后也能获得独一无二的内存空间地址,因为空类也是类,它也可以支持类的实例化。

总结:一个类的大小,实际上就是该类中所有成员变量之和,当然它也要注意内存对齐,此处也要注意空类的特殊情况,它默认占一个字节。

敲黑板啦 重点来袭,此处说三遍,灰常重要灰常重要灰常重要啦~~~

内存对齐

1. 首先我们先来讨论一下什么是内存对齐。
如果你不了解内存对齐,我们可能会认为数据存储在内存中是按照连续的地址存储的,但是事实并非如此,数据存储在内存中是按照一定的规则进行存储的,这个规则就是内存对齐。

2. 为什么要进行内存对齐

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    如果还不理解什么是内存对齐,我们可以看一下如下代码
struct Test{
        int a;
        char b;
        short c;
 };
printf("size is %lu\n",sizeof(struct Test));

如果按照我们想当然的想法,在32位操作系统下,int 占 4 个字节,char 占 1 个字节, short 占 2 个字节,那么一共应该占 7 个字节,但是运行这段代码,你会发现它占 8 个字节,这就是由于内存对齐的原因导致的,假如我们做如下更改

struct Test{
       char a;
       int b;
       short c;
 };
printf("size is %lu\n",sizeof(struct Test));

与上面代码不同的是,只是将代码的位置改变,运行代码后,你会发现它占12个字节,这又是因为什么呢?不要着急,且听我细细道来,让我们一起来看看我们内存对齐的规则到底是什么样的。

3. 内存对齐的规则是什么

  • 首先第一个成员在我们结构体偏移量为0的位置处
  • 其它成员变量要对齐到对齐数的整数倍处(对齐数 = 编译器默认的对齐数与该成员变量所占字节数的较小值,不同平台下,默认的对齐数是不一样的,在VS下,默认的对齐数是8,在Linux下,默认的对齐数的4。)
//我们可以通过语句修改编译器默认的对齐数
#pragma pack(4) //修改默认的对齐数为4
#pragma pack()  //还原默认的对齐数
  • 结构体的总大小要对齐到最大对齐数的整数倍上。
  • 如果有嵌套的结构体,嵌套的结构体的大小要对齐到最大对齐数的整数倍上,结构体的总大小要对齐到最大对齐数的整数倍上(包含嵌套的结构体)

注:总的来说,结构体的内存对齐规则就是一种以空间换时间的方法。

4. 那我们应该如何设计结构体让其满足内存对齐,还要节省空间呢?

  • 其实仔细想想,其实很简单,我们根据它的规则,很容易就能想到,让尽可能占字节数小的成员变量在一起就可以实现节省空间的原理了。例如:
struct A1{
	char c1;
	int a;
	char c2;
};

根据内存对齐的规则,我们可以知道sizeof(A1) = 12 ,假如我们做如下变换

struct A1{
	char c1;
	char c2;
	int a;
};

sizeof(A1) = 8,这样就节省了空间,所以当我们以后定义结构体时,可以尽量的将所占字节数小的变量尽量集中在一起就可以了。

5. 如何知道结构体中某个成员相对于结构体起始位置的偏移量

  • 废话不多说,咱们直接上代码
#include <iostream>

struct Test {
	int i;
	char a;
	short b;
	double l;
};


int main() {
	
	size_t offset1 = (size_t)(&((Test*)0)->i);
	size_t offset2 = (size_t)(&((Test*)0)->a);
	size_t offset3 = (size_t)(&((Test*)0)->b);
	size_t offset4 = (size_t)(&((Test*)0)->l);


	std::cout << "offset i = " << offset1 << std::endl;
	std::cout << "offset a = " << offset2 << std::endl;
	std::cout << "offset b = " << offset3 << std::endl;
	std::cout << "offset l = " << offset4 << std::endl;


	std::cout << "sizeof(Test) = " << sizeof(Test) << std::endl;
	
	system("pause");
	return 0;
}

首先将0强制类型转化为结构体指针类型s*,此时的零的类型就是 s*,那么其当然可以访问其成员m(通过(s*)0 -> m 的方式访问),那么此时再取这个变量的地址,即 & ((s*)0->m),这个地址呢减去结构体的基地址就是我们要求的偏移量。结构体的基地址是什么呢,不就是(s*)0吗?那么(s*)0等于多少?不就是0嘛,所以这样就能取到每个成员的偏移地址了。结果如下:
在这里插入图片描述
当然我们也可以将这个定义成为一个宏,然后计算成员变量的偏移量

#define GETOFFSET(Test,m) (size_t) &( ( (Test*)0 )->m)

字节序

在计算机中,有两种存储数据的方式,一种是大端字节序,另一种是小端字节序,我们也称它为大小端数据存储模式。

什么是大小端?如何测试某台机器是大端还是小端

  • 大端字节序:高地址存低位,低地址存高位
  • 小端字节序:低地址存低位,高地址存高位

例如:0x1122在内存中的存储方式
在这里插入图片描述

为什么会有大小端之分

  • 计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读取字节,先读第一个字节,再读第二个字节。所以,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

如何检验我们的机器是大端字节序还是小端字节序

此处我以两种方式测试大小端,这个都比较容易理解,咱们直接上代码

#include <iostream>

//定义一个联合体,因为联合体成员共用一块地址空间
int check1() {
	typedef union Test {
		int a;
		char c;
	}Test;
	Test t;
	t.a = 1;
	return t.c == 1;
}

//直接利用指针比较
int check2() {
	int a = 0x11223344;
	char* p = (char*)&a;
	if (*p == 0x11) {
		std::cout << "大端字节序" << std::endl;
	}
	else {
		std::cout << "小端字节序" << std::endl;
	}
	return 0;
}

int main(void) {
	
	std::cout << "利用联合体方式判断结果如下:" << std::endl;
	if (check1() == 1) {
		std::cout << "小端字节序" << std::endl;
	}
	else {
		std::cout << "大端字节序" << std::endl;
	}
	std::cout << "利用指针方式判断结果如下:" << std::endl;
	check2();
	system("pause");
	return 0;
}

在我的电脑胜结果如下采用小端存储模式。
在这里插入图片描述
ALL OVER 你懂了吗~~~~~

猜你喜欢

转载自blog.csdn.net/qq_43503315/article/details/90037077