前言
结构体在C语言中具有重要的意义。它不仅可以封装和组织数据,还可以提供抽象和封装的能力,方便数据的传递和操作,提高代码的可读性和可维护性,是C语言中常用的数据类型之一。本篇博客将详细介绍相关知识。
1 结构体的基础知识
在结构体初阶我们以及详细介绍过了有关结构体的基础。
2 结构的声明
struct tag { member-list; }variabe-list;
例如描述一个学生:
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 };
3 特殊声明
在声明结构体的时候,可以不完全声明。(匿名结构体声明)
比如:
//匿名结构体 struct { int a; char b; float c; }x; struct { int a; char b; float c; }*px;
上面的两个结构在声明的时候省略掉了结构体标签(tag)。
那问题来了?
//在上面代码的基础上,下面的代码合法吗? px = &x;
警告:
- 编译器会将上面的两个声明当成完全不同的两个类型。所以是非法的。
4 结构的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
struct Node//err,比如计算其大小sizeof(struct Node)时,大小是不确定的 { int date; struct Node next; }; //正确的自引用方法 struct Node { int date; struct Node* next ; };
注意: 上面结构体类型有时过于冗杂,我们可以采用重命名。
//我们知道结构体可以匿名。那是否可以重命名呢? //答案是否定的。 //例如下面这段代码,结构体中的Node* next变量还没有创建成功,结构体不是完整的,因此不可重命名 typedef struct//err { int date; Node* next; }Node; //正确使用 typedef struct Node { int date; struct Node* next; }Node;
5 结构体变量的定义和初始化
有了结构体类型,那如何定义和初始化呢?其实很简单。
struct Stu //声明类型 { char name[15];//名字 int age;//年龄 }; struct Stu s = { "zhangsan",20 };//初始化 struct Point { int x; int y; }; struct Node { int date; struct Point p; struct Node* next; }n1 = { 10, {4,5}, NULL };//结构体嵌套初始化
6 结构体内存对齐
接下来我们将介绍一个热门考点:结构体内存对齐
结构体内存对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的 较小值 。
。vs默认的值为8
。Linux中没有默认对齐数,对齐数就是成员自身的大小
3. 结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
6.1 计算结构体大小相关笔试题(基于VS)
笔试题一:
#include <stdio.h> struct S1 { char c1;//对齐到偏移量为0处 int i;//int占4个字节,VS默认对齐数为8;所以对齐数为最小值:4字节 //所以int对齐到偏移量为4后在向后移动4个字节即为。该位置即是int在内存中占据的空间 char c2;//同理char的对齐数为1。在int后移动一个字节即可。 }; //上述结构体struct S1共占据9个字节,而结构体的大小必须是最大对齐数4(本题中,对齐数分别为1,4,1)的整数倍 //所以结构体还需向内存申请3个字节空间大小,及总大小为12字节。 struct S2 { char c1;//第一个变量成员对齐到偏移量为0处 char c2; //该变量对齐数为1(char为1byte, VS默认为9byte,取最小值) //即在变量c1后申请1字节即可 int i;//该变量对齐数为4(int为4byte, VS默认为9byte,取最小值) //所以变量i对齐到偏移量为4处,在向后申请4个字节即可。 }; //到此结构体总共向内存申请了8byte, 为最大对齐数4(本题中三个变量对齐数分别为:1,1,4)的整数倍。 //所以该结构体的最终大小为8 int main() { printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2)); return 0; }
运行结果:
—————————————————————————————————————
笔试题二:
struct S3 { double d;//第一个成员变量对齐到偏移量为0处,在向后申请8字节(double大小为8) char c;//该成员变量对齐数为1(char为1byte,VS默认对齐数为8,取最小值) //所以变量对齐到偏移量为8处,向后申请1byte int i;//该成员变量对齐数为4(int为4byte,VS默认对齐数为8,取最小值) //所以该变量首先要对齐到偏移量为12的位置(必须对齐到4的整数倍处),在向后申请4个字节 }; //到此该结构体向内存共申请了16字节,为最大对齐数8的整数倍 //所以该结构体的最终大小为16byte struct S4 { char c1;//第一个成员变量对齐到偏移量为0处,在向后申请1字节(char大小为1) struct S3 s3;//嵌套结构体,结构体要对齐到自己最大对齐数8(上面已经求得最大对齐数为:8)的整数倍处。即该成员变量对齐到偏移量为8出,在向后申请16字节(上面已经求出其大小16byte) double d;//该成员变量对齐数为8(double为8byte,VS默认对齐数为8,取最小值) //上述两个变量总共为24个字节,所以该成员变量从偏移量为24(刚好为8得整数倍)开始,在向后申请8字节 }; //到此该结构体总共占据32个字节空间,为最大对齐数8(上述3个成员变量对齐数分别为:1,8,8)的整数倍。 //所以最终该结构体总大小为32byte int main() { printf("%d\n", sizeof(struct S3)); printf("%d\n", sizeof(struct S4)); return 0; }
运行结果:
6.2 为什么存在内存对齐?
大部分参考资料是这么说的:
1.平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处去某些特定类型的数据,否则抛出硬件异常。
2. 性能原因
结构数据(尤其是栈)应尽可能地在自然边界上对齐。
原因在于为了访问未对其的内存,处理器需要作两次内存访问;而对齐的内存仅需要一次访问。
总的来说:
结构体的内存对齐是拿空间来换时间的做法。
6.3 如何设计结构体减少空间
在设计结构体时,我们既要满足对齐,又要节省空间,如何做到呢?
我们前面已经见过这段代码:
struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; } int main() { printf("%d\n", sizeof(struct S3));//12 printf("%d\n", sizeof(struct S4));//8 return 0; }
S1和S2类型的成员一模一样,但是S1和S2所占据的空间大小有一些区别。我们可以得到:
- 在设计结构体时,尽量让占用空间小的成员尽量集中在一起,从而节省空间。
1.7 修改默然对齐数
#pragma这个预处理指令可以被用来修改我们的默认对齐数。
#include <stdio.h> #pragma pack(8)//设置默认对齐数为8 struct S1 { char c1; int i; char c2; }; #pragma pack()//取消默认对齐数 #pragma pack(1)//设置默认对齐数为1 struct S2 { char c1; int i; char c2; }; #pragma pack()//取消默认对齐数 int main() { printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2)); return 0; }
运行结果:
总结:
- 结构在对齐方式不合适说的时候,我们可以自己更改默认对齐数。
8. 结构体传参
直接上代码:
#include <stdio.h> struct S { int date[1000]; int num; }; struct S s = { {1,2,3,4}, 1000 }; //结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //结构体地址传参 void print2(struct S* s) { printf("%d\n", s->num); } int main() { print1(s); print2(&s); return 0; }
上面print1和print2函数那个更好?
答案是:首选print2函数。
原因:
函数传参的时候,参数是需要压栈的,会有时间和空间上的开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
结论:
结构体传参时,要传结构体指针。
9. 结尾
本篇博客到此就结束了。传作不易,如果对你有帮助记得三连哦!感谢您的支持!!!