本篇文章重点介绍结构体相关知识以及深入介绍的结构体的内存对齐与位段的实现 ———————————— 内存对齐+位段——————————————————
一.结构体
1.结构体类型的声明
1.1基础知识
结构体是一些值的集合,这些值成为成员变量。结构的每个成员可以是不同类型的变量。
1.2声明
struct tag//标签,可以随便写,但最好有意义 { 结构体成员; //member - list; }结构体变量;
比如描述一个学生:
struct Stu { char name[10];//姓名 int age;//年龄 char sex[10];//性别 char id[10];//学号 }s;//创建一个结构体变量s
1.3特殊声明
在结构体声明的时候,有时可以不完全声明,也叫做匿名声明
比如:
struct //不写标签,这种属于匿名结构体类型 { int a; int b; char c; }x;
struct { char arr[10]; double f; }*p;
1.注意上面这两种结构体都是属于匿名结构体类型,不告诉你名字,这种结构体类型如果要使用必须在声明的时候就在后面定义变量,不能再到主函数里面引用,因为你不知道这个结构体的名字是什么,所以必须在声明的时候就定义变量。
2.如果在上面的代码的基础上这样写有问题吗?
p=&x;
这两个都是结构体类型能这样写吗?
答案是不能:
警告:
编译器会把上面的两个声明当成完全不同的两个类型,所以是不可以的。
1.4结构体的自引用
1.什么叫结构体的自引用呢?
2.在结构体中包含一个类型为该结构本身的成员是否可以呢?
//代码1 struct Node { int data; struct Node next; }; //这样可以吗? 如果可以那想想sizeof(struct Node)的大小是多少呢?
正确的自引用方式:
struct Node { int data; struct Node* next; }; //应该里面放的是指向该类型的指针,而不是直接将该类型的变量直接放进去
还有一个注意点:
typedef重命名可以让结构体声明简单一些:
typedef struct Node { int data; struct Node*next }Node;//将结构体 struct Node类型重命名为Node
1.5结构体变量的定义和初始化与访问
结构体类型知道如何声明后,那怎么定义变量呢?
struct Point { int x; int y; }p1;//结构体声明时定义变量 struct Point p2;//或者声明完后,再进行定义变量 struct Point p3 = { 10,20 };//定义变量的时候,初始化变量 struct Stu//类型声明 { char name[10]; int age; char sex[20]; }s1;//声明的时候定义变量 s1 = { "zhangsan",18,"nan" };//初始化 struct Stu s2 = { "xiao tao",20,"nan" };//定义变量的时候初始化 struct Node { int data; struct Point p1; }n1 = { 1,{2,3} };//结构体嵌套初始化 struct Node n2 = { 10,{15,20} };//结构体嵌套初始化
2.结构体内存对齐
我们已经基本了解结构体的基本知识了
现在我们开始深入探讨一个问题:结构体大小怎么计算
这涉及一个关键知识点:内存对齐
我们先来看一个问题:
struct S1 { char c1;//1个字节 int i;//4字节 char c2;//1字节 }; struct S2 { char c1;//1字节 char c2;//1字节 int i;//4字节 }; int main() { //结构体S1 和结构体S2成员都一样只不过位置不同,那大小一样吗?按理讲结构体成员大小相加就是结构体大小了吧,但真的是这样吗? printf("%d\n", sizeof(struct S2)); printf("%d\n", sizeof(struct S1)); return 0; }
结果:
这个结果告诉我们结构体成员在内存中并不是简单的按照大小进行连续的排列的而是按照某种规则进行排列的。那这个规则是怎样的呢?
2.1如何计算?
首先要掌握结构体内存对齐规则
1.第一个成员在偏移为0的地址处(结构体变量偏移量)
2.从第二个成员开始,每个成员都要对齐到一个对齐数的整数倍数处
对齐数:结构体成员自身大小和默认对齐数中较小值
VS中默认对齐数是8
Linux gcc中没有对齐数,对齐数默认是结构体成员大小
3.结构体最终的大小必须是结构体成员中对齐数最大的整数倍
4.如果结构体中嵌套了结构体成员,嵌套的结构体对齐数就是该结构体中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体中的对齐数)的整数倍。
我们来做几个题目来熟悉熟悉吧
就拿上面的问题来解答吧:
1.
2.
//练习3 struct S3 { double d; char c; int i; }; printf("%d\n", sizeof(struct S3)) //练习4-结构体嵌套问题 struct S4 { char c1; struct S3 s3; double d; }; printf("%d\n", sizeof(struct S4))
练习3图解:
练习4图解:
2.3为什么存在内存对齐?
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问就只需要一次访问。
总体来说:
结构体内存对齐是拿空间换取时间的做法
那在设计结构体的时候,我们既要满足对齐,又要节省空间,该如何做到呢?
答:让占用空间小的成员尽量集中到一起。
//例如: struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; };
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别
二.位段
结构体实现位段的能力:
位段和结构体相似,只是对结构体进行了相应的限制,需要多少内存就申请多少内存。
2.1什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int,unsigned int 或者signed int 或者整形家族的char类型
2.位段成员后面要有一个冒号和一个数字
例如:
struct A { int _a : 3; int _b : 4; int _c : 15; int _d : 20; };
A就是一个位段类型
那位段A的大小是多少呢?
/
int main() { printf(“%d”, sizeof(struct A)); return 0; }
2.2位段的内存分配
- 位段的成员可以是 int unsigned int ,signed int或者是char类型的(整形家族里的)
- 位段的空间每次开辟是按照需要以4个字节(int类型)或者1个字节(char类型)的方式来开辟的
- 位段涉及很多不确定的因素,位段是步跨平台的,如果注重可移植性应该避免使用位段。
举一个例子:
struct S { char a : 4; char b : 5; char c : 3; char d : 4; }; int main() { struct S s = { 0 }; s.a = 12; s.b = 10; s.c = 4; s.d = 3; return 0; } //空间是如何开辟的呢?
1.位段都是限制好每个变量所需要的空间大小。
2.在VS2022环境上每个字节(8个比特位)分配内存是从低位到高位的
3.(char类型)一个字节所提供的空间大小不足变量所需的,要舍弃剩余空间,重新开辟一个字节。
2.3位段的跨平台问题
1.int 位段被当成有符号数还是无符号数是不确定的
2.位段中最大位的数目是不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。)
3.位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构体相比,位段只要计算好也可以达到相同的效果,但可以很好的节省空间,但存在跨平台问题。
2.4位段的应用