c语言结构体的内存对齐

简介: c语言结构体的内存对齐

前言:

c语言中结构体的空间大小怎么算呢?就是把里面所有变量的字节大小全部加起来吗?

如果你这么想,说明你对结构体还不够了解,而要想知道结构体开辟空间的规则,那我们就必须先要知道内存对齐是什么。

接下来我就说说内存对齐是个啥玩意吧!

1.结构体内存对齐

举例:

struct s1 {
  char c1;
  int a;
  char c2;
}s1;
 
struct s2 {
  char c1;
  char c2;
  int a;
}s2;
int main() {
  printf("%d %d\n", sizeof s1, sizeof s2);
 
  return 0;
}

思考:为什么s1结构体和s2结构体只是声明的成员变量顺序不一样,但是求得的空间大小却有差异呢?

首先我们要明白结构体内存对齐的规则。

1.1结构体对齐规则:

1. 第一个成员在与结构体变量偏移量为0的地址处。

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

我们首先来观察结构体s1:

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

根据对齐规则,我们第一个成员变量在与结构体变量偏移量为0的地址处,也就是在0这个位置开始。

注意,图中的编号仅代表地址的相对位置。

然后再看第二个成员变量 int a,a的字节大小为4,4比8小,所以a的对齐数是4,那么我们就在上一个成员变量的位置往下找到a对齐数整数倍的位置,于是我们把a放在4这个位置上。

再然后呢,根剧对齐规则,最后一个成员变量的相对位置放在8这个位置上(char c2的对齐数为1,8是1的整数倍数)。

所以最终结构体s1的成员变量存放的相对位置为:

那么这个时候的整个结构体的空间大小是什么呢?是不是最后一个成员变量的位置呢?

显然不是看最后一个成员变量所占最后一个字节的位置,因为这样一来sizeof (s1)=8,而这是错误的。

结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

根据这一规则,我们先算出来结构体的最大对齐数,也就是a的对齐数,即4。

从第一个变量所占字节的第一个位置,到最后一个成员变量所占字节的最后一个位置,我们算出来至少需要9个字节才能把装下这三个成员变量(0-8),而大于或者等于9又是4的整数倍的第一个数是12,所以我们得到的结构体s1的空间大小是12个字节。同理观察结构体s2:

根据以上的对齐规则,我们很容易得到s2成员变量的内存结构位置,

算出来结构体的最大对齐数,也就是a的对齐数,即4。

从第一个变量所占字节的第一个位置,到最后一个成员变量所占字节的最后一个位置,我们算出来至少需要8个字节才能把装下这三个成员变量(0-7),而大于或者等于8又是4的整数倍的第一个数是8,所以我们得到的结构体s1的空间大小是8个字节。

根据内存规则我们很快就知道了为什么sizeof(s1)和sizeof(s2)的大小,那内存对齐的意义是什么呢?

1.2为什么会存在内存对齐?

1. 平台原因(移植原因):

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。

2. 性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访 问。 

这里我们来举一个例子:

struct s1 {
  char c1;
  int a;
}s1;

如果不遵循内存对齐原则:

第一次读取我们可以顺利读取到变量c1.

第二次读取我们只能再找到整型a的前三个字节

第三次读取我们才能找到a的最后一个字节

所以,如果没有内存对齐,我们要想完整的找到这两个成员变量需要读取三次。

如果遵循内存对齐原则:

第一次读取我们就能找到变量c1.

而第二次读取我们就能找到变量a了

1.3总结:

结构体的内存对齐是拿空间来换取时间的做法。虽然根据内存对齐可能会开辟不必要的空间,但是为了保障读取数据的速度,拿空间换时间是值得的!

其实,在设计结构体的时候,我们的目的是既要满足对齐,又要节省空间,这又如何做到呢?

我们可以通过调整成员变量声明的顺序来减少结构体因为内存对齐的空间亏损。

struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。

1.4 修改默认对齐数

vs的默认对齐数是8,那么我们可不可以修改默认对齐数呢?

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。\

#include<stdio.h>
#pragma pack(2)//修改默认对齐数为2
struct s1 {
  char c1;
  int a;
  char c2;
}s1;
#pragma pack()取消设置的默认对齐数,还原为默认
struct s2 {
  char c1;
  char c2;
  int a;
}s2;
int main() {
  printf("%d %d\n", sizeof s1, sizeof s2);
 
  return 0;
}

当我们修改默认对齐数,并且只影响结构体s1,那此时的sizeof(s1)又是多少呢?

答案是8,过程自己去算。

相关文章
|
9天前
|
存储 C语言
C语言学习记录——动态内存函数介绍(malloc、free、calloc、realloc)
C语言学习记录——动态内存函数介绍(malloc、free、calloc、realloc)
17 1
|
9天前
|
编译器 C语言 C++
C语言学习记录——位段(内存分配、位段的跨平台、位段的应用)
C语言学习记录——位段(内存分配、位段的跨平台、位段的应用)
11 0
|
1天前
|
算法 Java 程序员
面向对象编程(OOP)通过对象组合构建软件,C语言虽是过程式语言,但可通过结构体、函数指针模拟OOP特性
【6月更文挑战第15天】面向对象编程(OOP)通过对象组合构建软件,C语言虽是过程式语言,但可通过结构体、函数指针模拟OOP特性。封装可使用结构体封装数据和方法,如模拟矩形对象。继承则通过结构体嵌套实现静态继承。多态可通过函数指针模拟,但C不支持虚函数表,实现复杂。C语言能体现OOP思想,但不如C++、Java等语言原生支持。
12 7
|
1天前
|
程序员 C语言 C++
【C语言基础】:动态内存管理(含经典笔试题分析)-2
【C语言基础】:动态内存管理(含经典笔试题分析)
|
1天前
|
编译器 C语言
【C语言基础】:自定义类型(一)--> 结构体-2
【C语言基础】:自定义类型(一)--> 结构体
|
1天前
|
编译器 Linux C语言
【C语言基础】:自定义类型(一)--> 结构体-1
【C语言基础】:自定义类型(一)--> 结构体
|
1天前
|
存储 小程序 编译器
【C语言基础】:数据在内存中的存储
【C语言基础】:数据在内存中的存储
|
1天前
|
安全 C语言
【C语言基础】:内存操作函数
【C语言基础】:内存操作函数
TU^
|
1天前
|
程序员 编译器 C语言
C语言之动态内存管理
C语言之动态内存管理
TU^
5 1
|
1天前
|
C语言
C语言——结构体
C语言——结构体