写点什么

【结构体内功修炼】结构体内存对齐(一)

作者:Albert Edison
  • 2022 年 10 月 01 日
    四川
  • 本文字数:5828 字

    阅读完需:约 19 分钟

【结构体内功修炼】结构体内存对齐(一)

🌟 前言

本文重点讲解结构体的大小,以及在内存中如何对齐的。

1. 结构体的声明

🍑 结构体的基础知识

结构是一些值的集合,这些值称为 成员变量


结构的每个成员可以是 不同类型 的变量。

🍑 结构的声明

struct tag{  member-list; //成员列表
}variable-list; //变量列表
复制代码


假设我现在要定义一个学生的 结构体类型 ,包括:姓名、性别、年龄、身高


struct Stu{  char name[20]; //姓名  char sex[5]; //性别  int age; //年龄  int hight; //身高};
复制代码


那么我们现在要拿刚刚定义的 学生结构体类型 去创建 结构体变量


struct Stu{  char name[20];  char sex[5];  int age;  int hight;};

int main(){ struct Stu s1;
return 0;}
复制代码


还可以用下面这种方法创建结构体变量


struct Stu{  char name[20];  char sex[5];  int age;  int hight;}s2,s3,s4;
struct Stu s5;
int main(){ struct Stu s1; return 0;}
复制代码


此时创建的 s2s3s4s5 都是创建的全局变量;


s1 为局部变量;

🍑 特殊的声明

在声明结构的时候,可以不完全的声明。


比如在定义一个结构体时,可以把它的标签去掉;


struct{  char c;  int a;  double d;}sa;
int main(){
return 0;}
复制代码


上面这种方式叫 匿名结构体类型,但是 sa 只能使用一次;


但是如果是下面这样呢?


struct{  char c;  int a;  double d;}sa;
struct{ char c; int a; double d;}*ps;
int main(){ ps = &sa; return 0;}
复制代码


sa 是一个匿名结构体,ps 是一个匿名结构体指针;


虽然它们的成员类型是一模一样的,但是编译器会认为 = 两边是不同的结构体类型,所以这种写法完全是错误的


🍑 结构体的自引用

我们思考一个问题:在结构中包含一个类型为该结构本身的成员是否可以呢?


比如这样


struct Node{  int data;  struct Node next;};
复制代码


这种情况是绝对不可以的;


如果我们在结构中包含一个类型为该结构本身的成员,那么此时我要求这个结构体类型的大小是多少?


首先在 Node 里面有个 int 类型,然后还有个 struct Node next 类型;


那么在 struct Node next 里面还有个 int 类型,和 struct Node next 类型........等等就会形成无线循环;


所以我们只需要在存上一个结构体类型的地址就好了


struct Node{  int data;  struct Node* next;};
复制代码


此时,我们创建的每个 Node 节点里面,既可以保存一个数值,又可以保存一个地址;


通过这个地址,就可以找到由 next 指向的下一个节点;


此时结构体的大小就可以确定了:int 类型 4 个字节、指针类型 4 / 8 字节;

🍑 结构体变量的定义和初始化

有了结构体类型,那如何定义变量,其实很简单👇


struct Point{  int x;  int y;}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。struct Point p3 = { 1, 2 };
复制代码


如果是更复杂的一点呢?


#include <stdio.h>
struct Node{ int data; struct Node* next;};
struct Stu{ char name[20]; char sex[5]; int age; int hight;};
int main(){ struct Node n1 = { 100, NULL }; struct Stu s1 = { "张三", "男", 20, 180 }; return 0;}
复制代码


上面可以看到,我们每次定义结构体的变量都要使用 struct Node,是不是感觉很麻烦?


那么可以使用 typedef 来对它进行 重命名,此时我们创建变量就可以使用 重命名 👇


typedef struct {  int data;  struct Node* next;}Node;
int main(){ Node n = { 0 }; return 0;}
复制代码


编译看下



那么如果结构体的成员是 嵌套 的,那么如何来初始化结构体变量呢?


#include <stdio.h>
struct Stu{ char name[20]; char sex[5]; int age; int hight;};
struct Data{ struct Stu s; char ch; double d;};
int main(){ struct Data d = { { "李四", "女", 30, 170 }, "w", 3.14 }; //结构体嵌套初始化 return 0;}
复制代码


看一下


2. 结构体传参

思考一下我们结构体传参的时候,传结构体还是传地址呢?


📝 代码示例一


#include <stdio.h>
struct S{ int data[1000]; int num;};struct S s = { {1,2,3,4}, 1000 };
//结构体传参void print1(struct S s){ printf("%d\n", s.num);}
int main(){ print1(s); //传结构体
return 0;}
复制代码


📝 代码示例二


#include <stdio.h>
struct S{ int data[1000]; int num;};struct S s = { {1,2,3,4}, 1000 };
//结构体地址传参void print2(struct S* ps){ printf("%d\n", ps->num);}
int main(){ print2(&s); //传地址
return 0;}
复制代码


上面的 print1print2 函数哪个好些呢?


答案是:首选 print2 函数。


原因:


1、函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。


2、如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。


结论:结构体传参的时候,要传结构体的地址。

3. 结构体内存对齐

关于 结构体的内存对齐 这是一个面试的高频考点,很重要;

📃 代码示例一

首先下面有一段代码,分析一下这个结构体类型的大小是多少个字节呢?


struct S1{  char c1;  int i;  char c2;};
int main(){ printf("%d\n", sizeof(struct S1)); return 0;}
复制代码


第一眼,大家会想到:c1char 类型,占 1 个字节;


iint 类型,占 4 个字节;


c2char 类型,占 1 个字节;


加起来就是 6 个字节,那么究竟是不是呢?运行看一下



咦?这里为什么是 12 呢?


这里我们就需要好好探究一下,但是再这之前,要学习一个东西,叫 offsetof


offsetof,是一个 ,它用来返回:一个成员在一个结构体起始位置的偏移量。


也就是,它可以计算一个结构体成员,相较于起始位置的一个偏移量。


用法:


size_t offsetof(structName, memberName);
复制代码


📝 代码示例


#include <stdio.h>#include <stddef.h>
struct S1{ char c1; int i; char c2;};
int main(){ printf("%d\n", offsetof(struct S1, c1)); printf("%d\n", offsetof(struct S1, i)); printf("%d\n", offsetof(struct S1, c2));
return 0;}
复制代码


🌟 运行结果



为什么是 048 呢?


首先,我们创建一个结构体类型 S1红色箭头 是结构体的起始位置,从起始位置开始,每个格子代表 1 个字节,第一个格子相较于 起始位置 偏移量是 0;第二个格子相较于 起始位置 偏移量是 1;第三个格子相较于 起始位置 偏移量是 2,以此类推....



根据上面代码的运行结果可以知道,c1 相较于 起始位置 偏移量是 0,并且 c1char 类型,占 1 个字节,所以 c1 成员所占内存空间如下👇



i 相较于 起始位置 偏移量是 4,并且 iint 类型,占 4 个字节,所以 i 成员所占内存空间如下👇



c2 相较于 起始位置 偏移量是 8,并且 c2 也是 char 类型,占 1 个字节,所以 c2 成员所占内存空间如下👇



但是我们计算出来的是 12 个字节呀,也就是说,前 3 个格子和后 3 个格子是没有用到👇



那么为什么呢?为什么要浪费掉这么多的空间来做这个事情呢?


这个时候,就引入到我们今天的主题 结构体内存对齐


首先得掌握结构体的对齐规则:


1、结构体的第一个成员,存放在结构体变量开始位置的 0 的偏移量处。



2、从第二个成员开始,都要对齐到对齐数的整数倍的地址处。


对齐数 :成员自身大小和默认对齐数的较小值。


VS 环境下,默认的对齐数是 8S1i 成员自身大小是 4 字节,而 VS 编辑器提供的默认对齐数是 8,取它俩的 最小值 为对齐数,所以 i 的对齐数就是 4


那么 i 就要对齐到 4 的整数倍的地址处,所以 i 就要放到偏移量为 4 倍数的地址处,换句话说 4 的整数倍包括 4 (这里可能有点点绕🤣)



c2 的自身大小是 1,而默认的对齐数是 8,取它俩的 最小值 为对齐数,所以 c2 的对齐数就是 1


而任何一个地址处都是 1 的倍数,所以就放到偏移量为 8 的格子处👇



3、结构体的总大小,必须是最大对齐数的整数倍


最大对齐数 :是指所有成员的对齐数中最大的那个。


c1 :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1


i :自身大小是 4VS 默认对齐数是 8,取较小值,所以对齐数是 4


c2 :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1


所以 S1 结构体的总大小必须是 4 的倍数,此时我们对齐完以后在 9 字节的格子处,9 不是 4 的倍数呀!


那么就继续往下对齐,浪费 3 个空间,此时总大小为 12 个字节了(43 倍)👇



注意:Linux 环境没有默认对齐数,对齐数就是自身的大小

📃 代码示例二

我们基本已经掌握了对齐的规则了,那么把上面的代码稍微调换一下位置


📝 代码示例


#include <stdio.h>
struct S1{ char c1; int i; char c2;};
struct S2{ char c1; char c2; int i;};
int main(){ printf("%d\n", sizeof(struct S2));
return 0;}
复制代码


🌟 运行结果



那么我们再来算一下 S2 在内存中如何对齐的?


首先结构体第一个成员永远放在 0 偏移处,那么 c1 就放在 0 偏移处👇



第二个成员 c2 自身大小是 1,默认对齐数是 8,取较小值,所以对齐数是 1


而任何一个地址处都是 1 的倍数,所以就放到偏移量为 1 的格子处👇



第三个成员 i 自身大小是 4,默认对齐数是 8,取较小值,所以对齐数是 4


那么就要往下找 4 的倍数,41 倍是 4,所以 i 对齐到偏移量为 4 的地址处



结构体的总大小,必须是最大对齐数的整数倍


c1 :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1


c2 :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1


i :自身大小是 4VS 默认对齐数是 8,取较小值,所以对齐数是 4


所以 S2 结构体最大对齐数是 4 ,那么总大小必须是 4 的倍数,此时我们对齐完以后在 8 字节的格子处,84 的倍数;


所以结构体总大小为 8👇


📃 代码示例三

我们再来看一组代码


📝 代码示例


#include <stdio.h>
struct S3{ char c1; char c2; int i;};

int main(){ struct S3 s;
printf("%d\n", sizeof(struct S3));
return 0;}
复制代码


这次我们先画图,画完再来验证


首先结构体第一个成员永远放在 0 偏移处,那么 d 就放在 0 偏移处;


ddouble 类型,占 8 个字节,所以从 0 偏移处开始,往下放 8 个字节👇



第二个成员 c2 自身大小是 1,默认对齐数是 8,取较小值,所以对齐数是 1


而任何一个地址处都是 1 的倍数,所以就放到偏移量为 8 的格子处👇



第三个成员 i 自身大小是 4,默认对齐数是 8,取较小值,所以对齐数是 4


那么就要往下找 4 的倍数,43 倍是 12,所以 i 放到到偏移量为 12 的地址处



结构体的总大小,必须是最大对齐数的整数倍


d :自身大小是 8VS 默认对齐数是 8,取较小值,所以对齐数是 8


c :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1


i :自身大小是 4VS 默认对齐数是 8,取较小值,所以对齐数是 4


所以 S3 结构体最大对齐数是 8 ,那么总大小必须是 8 的倍数,此时我们对齐完以后在 16 字节的格子处,168 的倍数;


所以结构体总大小为 16👇



🌟 运行结果


📃 代码示例四

既然我们学会了结构体内存对齐的方法,那么思考一下结构体嵌套如何对齐呢?


📝 代码示例


#include <stdio.h>
struct S3{ double d; char c; int i;};
struct S4{ char c1; struct S3 s3; double d;};
int main(){ struct S4 s;
printf("%d\n", sizeof(struct S4));
return 0;}
复制代码


我们还是先画图,画完再来验证


首先结构体第一个成员永远放在 0 偏移处,那么 c 就放在 0 偏移处;


cchar 类型,占 1 个字节,所以从 0 偏移处开始,往下放 1 个字节👇



此时第二个成员为结构体,那么引出我们的第四条规则:


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


第二个成员 s3 ,它的最大对齐数是 8,那么也就是说 s3 要对齐到 8 的整数倍处;


因为 81 倍就是 8,此时 8 地址处的格子没有被占用,所以就放到偏移量为 8 的格子处


s3 刚刚我们计算的是 16 个字节,所以从偏移量 8 的格子开始,向下放 16 个字节👇



第三个成员 d 自身大小是 8,默认对齐数是 8,所以对齐数是 4


那么就要往下找 8 的倍数,83 倍是 24,所以 d 放到到偏移量为 24 的地址处;


所以从偏移量 24 的格子开始,向下放 8 个字节👇



结构体的总大小,必须是最大对齐数的整数倍


c :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1


s3 :是嵌套结构体,它的整体大小就是所有最大对齐数是 8VS 默认对齐数是 8,所以对齐数是 8


d :自身大小是 8VS 默认对齐数是 8,所以对齐数是 8


所以 S4 结构体最大对齐数是 8 ,那么总大小必须是 8 的倍数,此时我们对齐完以后在 32 字节的格子处,328 的倍数;


所以结构体总大小为 32👇(0~31,就是 32 个字节)



🌟 运行结果


4. 为什么存在内存对齐

  • 平台原因(移植原因):


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


  • 性能原因:


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


总体来说:结构体的内存对齐是拿空间来换取时间的做法。


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


让占用空间小的成员尽量集中在一起。


📝 代码示例


#include <stdio.h>
struct S1{ char c1; int i; char c2;};
struct S2{ char c1; char c2; int i;};
int main(){ struct S1 s1; struct S2 s2;
printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2));
return 0;}
复制代码


🌟 运行结果



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


所以让占用空间小的成员尽量集中在一起。

5. 修改默认对齐数

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


📝 代码示例


#pragma pack(1) //设置默认对齐数为 1struct S1{  char c1;  int i;  char c2;};#pragma pack() //取消设置的默认对齐数,还原为默认

struct S2 //不设置,默认就是 8{ char c1; int i; char c2;};
int main(){ struct S1 s1; struct S2 s2;
printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2));
return 0;}
复制代码


🌟 运行结果



结论:结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

发布于: 刚刚阅读数: 3
用户头像

Albert Edison

关注

目前在某大厂担任后端开发,欢迎交流🤝 2022.03.08 加入

🏅️平台:InfoQ 签约作者、阿里云 专家博主、CSDN 优质创作者 🛫领域:专注于C语言、数据结构与算法、C++、Linux、MySQL、云原生的研究 ✨成就:2021年CSDN博客新星Top9,算法领域优质创作者,全网累计粉丝4W+

评论

发布
暂无评论
【结构体内功修炼】结构体内存对齐(一)_C语言_Albert Edison_InfoQ写作社区