【C语言】内存对齐和大小端问题

什么是字节对齐?

在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐”. 比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除.

字节对齐有什么作用?

字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间。

对于32位机来说,4字节对齐能够使cpu访问速度提高,比如说一个long类型的变量,如果跨越了4字节边界存储,那么cpu要读取两次,这样效率就低了。但是在32位机中使用1字节或者2字节对齐,反而会使变量访问速度降低。所以这要考虑处理器类型,另外还得考虑编译器的类型。在vc中默认是4字节对齐的,GNU gcc 也是默认4字节对齐。

首先我们先举个例子,试着算一下结构体的大小

class S1
{
    
    
	 char _c1;
	 int _i;
	 char _c2;
};
class S2
{
    
    
	 char c1;
	 char c2;
	 int i;
};
class S3
{
    
    
	 double d;
	 char c;
	 int i;
};
class S4
{
    
    
	 char c1;
	 struct S3 s3;
	 double d;
};

在这里插入图片描述

我们先来看一下结构体内存对齐的规则:

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8,gcc中的对齐数为4
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
    在这里插入图片描述

为什么要进行内存对齐

· 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
· 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

我们普通程序员心中的内存印象,由一个个字节组成,但是CPU却不是这么看待的cpu把内存当成是一块一块的,块的大小可以是2,4,8,16 个字节,因此CPU在读取内存的时候是一块一块进行读取的,块的大小称为内存读取粒度。

我们再来看看为什么内存不对齐会影响读取速度?

假设CPU要读取一个4字节大小的数据到寄存器中(假设内存读取粒度是4),分两种情况讨论:

1.数据从0字节开始

2.数据从1字节开始

解析:当数据从0字节开始的时候,直接将0-3四个字节完全读取到寄存器,结算完成了。

  当数据从1字节开始的时候,问题很复杂,首先先将前4个字节读到寄存器,并再次读取4-7字节的数据进寄存器,接着把0字节,5,6,7字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器,对一个内存未对齐的寄存器进行了这么多额外操作,大大降低了CPU的性能。

但是这还属于乐观情况,上文提到内存对齐的作用之一是平台的移植原因,因为只有部分CPU肯干,其他部分CPU遇到未对齐边界就直接罢工了。

如何让结构体按照指定的对齐参数进行对齐?

· 使用**(# pragma pack())**

# pragma pack(2)   struct d
{
    
    
	char a; // 1  
	long  long b; // 8    
	short c; // 2 
};

还有一种方法是在编译器中设置默认参数
在这里插入图片描述

求结构体中某个成员相对于结构体起始位置的偏移量

offsetof宏。
offsetof(结构体类型,成员名称) offsetof(s,m)
模拟实现
offsetof (size_t)(((s*)0)->m)

什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景

大小端概念

小端:低位字节序的内容放在低地址处,高位字节序内容放在高地址处

大端:低位字节序的内容放在高地址处,高位字节序内容放在低地址处
在这里插入图片描述
看了这个例子,一定会有这样的疑问:应该是00 00 00 01,为什么会是这样的?
因为这是是小端存储在这里插入图片描述

如何测试大小端?

方法一:利用公用体(联合union)(常用)

int Check_sys()
{
    
    	
union Un
{
    
    		
	char a;	
	int b;
}
	un;	
	un.b = 1;	
return un.a;
}

int main()
{
    
    	
	int ret = Check_sys();	
if (1 == ret)	
{
    
    		
	printf("当前模式为小端存储\n");	
}
   
else
{
    
    		
	printf("当前模式为大端存储\n");
}	
system("pause");	
return 0;}

在这里插入图片描述
共用体:共用体是多种数据的覆盖存储,几个不同的成员 变量共占同一段内存空间,而且都是从同一地址开始存储的,只是在任意时刻只存储一种数据,因此分配给共用体的存储区域大小至少要有存储最大一个成员数据类型所占用的存储空间。

用共用体来检测大小端的原理:由于共用体类型采取的是存储覆盖的机制,准许不同的类型数据相互覆盖,是一种同一存储区域由不同类型成员共享的数据类型。
在这里插入图片描述
所以我们只要给b赋值为1,如果a的值也为1,那么就是小端,反之就是大端。

方法二:

int main()
{
    
    
    int a = 0x12345678;//十六进制值为0x12345678
    char*p=(char*)&a;//强转地址,使得指针p仅可以解引用到一个字节大小的值
    if(p == 0x78)
    {
    
    
        printf("小端模式!\n");
    }
    else
    {
    
    
        printf("大端模式!\n");  
    }
    return 0;
}

大小端的考虑场景

我们在网络编程中,经常用到两台主机之间进行互发消息,这时候我们就需要进行考虑大小端的问题,因为两台主机的大小端不确定,有可能出现一个大端一个小端的情况,这样子就会产生歧义。

注意:大小端和操作系统是没有任何关系的,决定他们是什么端是由cpu的架构来决定的。

猜你喜欢

转载自blog.csdn.net/Vcrossover/article/details/112604363