当前位置:网站首页>【结构体内功修炼】结构体内存对齐(一)
【结构体内功修炼】结构体内存对齐(一)
2022-08-05 07:39:00 【Albert Edison】

文章目录

前言
本文重点讲解结构体的大小,以及在内存中如何对齐的。
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;
}
此时创建的 s2、s3、s4、s5 都是创建的全局变量;
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;
}
上面的 print1 和 print2 函数哪个好些呢?
答案是:首选 print2 函数。
原因:
1、函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
2、如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
3. 结构体内存对齐
关于 结构体的内存对齐 这是一个面试的高频考点,很重要;
代码示例一
首先下面有一段代码,分析一下这个结构体类型的大小是多少个字节呢?
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
第一眼,大家会想到:c1 是 char 类型,占 1 个字节;
i 是 int 类型,占 4 个字节;
c2 是 char 类型,占 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;
}
运行结果
为什么是 0、4、8 呢?
首先,我们创建一个结构体类型 S1,红色箭头 是结构体的起始位置,从起始位置开始,每个格子代表 1 个字节,第一个格子相较于 起始位置 偏移量是 0;第二个格子相较于 起始位置 偏移量是 1;第三个格子相较于 起始位置 偏移量是 2,以此类推…
根据上面代码的运行结果可以知道,c1 相较于 起始位置 偏移量是 0,并且 c1 是 char 类型,占 1 个字节,所以 c1 成员所占内存空间如下
i 相较于 起始位置 偏移量是 4,并且 i 是 int 类型,占 4 个字节,所以 i 成员所占内存空间如下
c2 相较于 起始位置 偏移量是 8,并且 c2 也是 char 类型,占 1 个字节,所以 c2 成员所占内存空间如下
但是我们计算出来的是 12 个字节呀,也就是说,前 3 个格子和后 3 个格子是没有用到
那么为什么呢?为什么要浪费掉这么多的空间来做这个事情呢?
这个时候,就引入到我们今天的主题 结构体内存对齐 !
首先得掌握结构体的对齐规则:
1、结构体的第一个成员,存放在结构体变量开始位置的 0 的偏移量处。
2、从第二个成员开始,都要对齐到对齐数的整数倍的地址处。
对齐数 :成员自身大小和默认对齐数的较小值。
在 VS 环境下,默认的对齐数是 8,S1 中 i 成员自身大小是 4 字节,而 VS 编辑器提供的默认对齐数是 8,取它俩的 最小值 为对齐数,所以 i 的对齐数就是 4;
那么 i 就要对齐到 4 的整数倍的地址处,所以 i 就要放到偏移量为 4 倍数的地址处,换句话说 4 的整数倍包括 4 (这里可能有点点绕)
c2 的自身大小是 1,而默认的对齐数是 8,取它俩的 最小值 为对齐数,所以 c2 的对齐数就是 1;
而任何一个地址处都是 1 的倍数,所以就放到偏移量为 8 的格子处
3、结构体的总大小,必须是最大对齐数的整数倍
最大对齐数 :是指所有成员的对齐数中最大的那个。
c1 :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
i :自身大小是 4,VS 默认对齐数是 8,取较小值,所以对齐数是 4;
c2 :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
所以 S1 结构体的总大小必须是 4 的倍数,此时我们对齐完以后在 9 字节的格子处,9 不是 4 的倍数呀!
那么就继续往下对齐,浪费 3 个空间,此时总大小为 12 个字节了(4 的 3 倍)
注意: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 的倍数,4 的 1 倍是 4,所以 i 对齐到偏移量为 4 的地址处
结构体的总大小,必须是最大对齐数的整数倍
c1 :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
c2 :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
i :自身大小是 4,VS 默认对齐数是 8,取较小值,所以对齐数是 4;
所以 S2 结构体最大对齐数是 4 ,那么总大小必须是 4 的倍数,此时我们对齐完以后在 8 字节的格子处,8 是 4 的倍数;
所以结构体总大小为 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 偏移处;
d 是 double 类型,占 8 个字节,所以从 0 偏移处开始,往下放 8 个字节
第二个成员 c2 自身大小是 1,默认对齐数是 8,取较小值,所以对齐数是 1;
而任何一个地址处都是 1 的倍数,所以就放到偏移量为 8 的格子处
第三个成员 i 自身大小是 4,默认对齐数是 8,取较小值,所以对齐数是 4;
那么就要往下找 4 的倍数,4 的 3 倍是 12,所以 i 放到到偏移量为 12 的地址处
结构体的总大小,必须是最大对齐数的整数倍
d :自身大小是 8,VS 默认对齐数是 8,取较小值,所以对齐数是 8;
c :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
i :自身大小是 4,VS 默认对齐数是 8,取较小值,所以对齐数是 4;
所以 S3 结构体最大对齐数是 8 ,那么总大小必须是 8 的倍数,此时我们对齐完以后在 16 字节的格子处,16 是 8 的倍数;
所以结构体总大小为 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 偏移处;
c 是 char 类型,占 1 个字节,所以从 0 偏移处开始,往下放 1 个字节
此时第二个成员为结构体,那么引出我们的第四条规则:
4、 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
第二个成员 s3 ,它的最大对齐数是 8,那么也就是说 s3 要对齐到 8 的整数倍处;
因为 8 的 1 倍就是 8,此时 8 地址处的格子没有被占用,所以就放到偏移量为 8 的格子处
s3 刚刚我们计算的是 16 个字节,所以从偏移量 8 的格子开始,向下放 16 个字节
第三个成员 d 自身大小是 8,默认对齐数是 8,所以对齐数是 4;
那么就要往下找 8 的倍数,8 的 3 倍是 24,所以 d 放到到偏移量为 24 的地址处;
所以从偏移量 24 的格子开始,向下放 8 个字节
结构体的总大小,必须是最大对齐数的整数倍
c :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
s3 :是嵌套结构体,它的整体大小就是所有最大对齐数是 8,VS 默认对齐数是 8,所以对齐数是 8;
d :自身大小是 8,VS 默认对齐数是 8,所以对齐数是 8;
所以 S4 结构体最大对齐数是 8 ,那么总大小必须是 8 的倍数,此时我们对齐完以后在 32 字节的格子处,32 是 8 的倍数;
所以结构体总大小为 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;
}
运行结果
S1 和 S2 类型的成员一模一样,但是 S1 和 S2 所占空间的大小有了一些区别。
所以让占用空间小的成员尽量集中在一起。
5. 修改默认对齐数
之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
代码示例
#pragma pack(1) //设置默认对齐数为 1
struct 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;
}
运行结果
结论:结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。
边栏推荐
猜你喜欢
随机推荐
文本特征化方法总结
VXE-Table融合多语言
支持触屏slider轮播插件
Qt writes custom controls: one of the text spotlight effects
The magic weapon for small entrepreneurs!
Redis数据库学习
别把你的天使弄丢了
Redis常用命令
微信 小程序 之PC端 不支持 wx.previewMedia 方法 故用自定义轮播图进行 模拟照片视频的播放
Liunx教程超详细(完整)
Support touch screen slider carousel plugin
Bluetooth gap protocol
唤醒手腕 - 微信小程序、QQ小程序、抖音小程序学习笔记(更新中)
专用机终端安装软件后报IP冲突
存储过程编写经验和优化措施
RNote108---Display the running progress of the R program
Hash 这些知识你也应该知道
[Shanghai] Hiring .Net Senior Software Engineer & BI Data Warehouse Engineer (Urgent)
学习机赛道加速:请“卷”产品,不要“卷”营销
GAN generates anime avatar Pytorch









