我们知道,C语言是允许我们自己来创造类型的,这些类型就叫做——自定义类型。
自定义类型又包括结构体类型,联合体类型还有枚举类型。
今天的文章,我们就着重讲解这其中的结构体类型。
目录
结构体的声明
1.1结构的基础知识
1.2结构的声明
1.3 匿名结构体的情况
1.4结构的自引用
1.5重命名匿名结构体的情况
1.6结构体变量的定义和初始化
1.7结构体内存对齐
1.8为什么存在内存对齐” />1.9我们可以耍些小聪明达到节省空间的效果。
2.1修改默认对齐数
2.2 结构体传参
3.1位段
3.2 位段的内存分配
3.3 位段的跨平台问题
结构体的声明
1.1结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.2结构的声明
struct tag { member–list; }variable–list;
我们以这种方式来描述一个结构体。下面是简单的示范,我们来描述一个学生:
struct Stu{ char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号}; //分号不能丢
定义局部变量和全局变量的关系:
#define _CRT_SECURE_NO_WARNINGSstruct Stu{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号}s1,s2,s3; //全局变量int main(){struct Stu s4;struct Stu s5;//局部变量return 0;}
1.3 匿名结构体的情况
也可以省略不写结构体标签,不过这样会导致一个结果,结构体只能定义一次类型。
#define _CRT_SECURE_NO_WARNINGS#includestruct {char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号}s1; //全局变量struct {char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号}*ps; //全局变量int main(){s1.age = 1;printf("%d", s1. age);return 0;}
在上述的代码中,体现为定义结构体变量s1之后,无法再次定义诸如s2,s3等结构体类型。
不过要是你本来就准备只用一次结构体的话,定义一个匿名结构体也不错就是了。
上面的两个结构在声明的时候省略掉了结构体标签, 那么问题来了?
//在上面代码的基础上,下面的代码合法吗?
ps=&s1;
答案是否定的,及时两个结构体里面的元素都相同,编译器也会他们当成两个完全不同的类型,所以是非法的。
1.4结构的自引用
我们想要使用结构体实现类似于链表的功能。
在结构中包含一个类型为该结构本身的成员是否可以呢?
#includestruct Node{int data;struct Node n;};int main(){return 0;}
我们开动小脑筋,立马就发现了错误。
struct Node这个节点它所占用的空间有多大呢?
它不仅要存放一个整形,还要存放一个n。
这就无限循环下去了,struct Node里面还有一个struct Node。
大小是无法得出的,这是一个错误示范。
我们转变战略,用指针来实现。
#define _CRT_SECURE_NO_WARNINGS#includestruct Node{int data;//4struct Node *next;//4/8};int main(){struct Node n1;struct Node n2;n1.next = &n2;return 0;}
创建两个节点n1,n2,把它们像链条一样串起来。
编译器没有报错,这样的写法是正确的,同时我们发现,struct Node的大小可以轻而易举地算出,我们得出一个结论:
不是在自己的类型里面包含一个自己类型的变量,而是在自己的类型里面包含一个自己类型的指针。这样的实现方式才是可行的。
1.5重命名匿名结构体的情况
下面的代码是否可行呢?
#includetypedef struct {int data;}S;int main(){return 0;}
可行,不过S不再是匿名结构体的变量,而是变成了匿名结构体类型。
怎么用呢?这么用:
#includetypedef struct {int data;}S;int main(){S s;s.data = 1;printf("%d", s.data);return 0;}
能用这种方式模拟实现上面的链表呢?
这样写行吗?
typedef struct {int data;Node* next;}Node;
不行,在没有重命名出Node时就调用了Node。
在这种情况下,我们只能老老实实地写出类型名了!
typedef struct Node{int data; struct Node* next;}Node;
1.6结构体变量的定义和初始化
有了结构体类型,那如何定义变量,其实很简单。
int x; int y;}p1; //声明类型的同时定义变量p1struct Point p2; //定义结构体变量struct Point{p2//初始化:定义变量的同时赋初值。struct Point p3 = {x, y};struct Stu //类型声明{ char name[15];//名字 int age; //年龄};struct Stu s = {"zhangsan", 20};//初始化struct Node{ int data; struct Point p; struct Node* next; }n1 = {10, {4,5}, NULL}; //结构体嵌套初始化struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
1.7结构体内存对齐
我们已经掌握了结构体的基本使用了。 现在我们深入讨论一个问题:计算结构体的大小。 这就到了本文的重中之重: 结构体内存对齐。
计算以下的结构体大小。
#includeint main(){struct S1{char c1;int i;char c2;};printf("%d\n", sizeof(struct S1));//练习2struct S2{char c1;char c2;int i;};printf("%d\n", sizeof(struct S2));//练习3struct 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));}
运行结果如下:
是不是跟想的完全不一样?
没错,结构体的大小并不是成员大小的简单相加,而是有自己的一套规则的。
- 结构体的第一个成员永远是放在零偏移处。
- 从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。
- 这个对齐数是成员自身大小和默认对齐数的较小值。
- VS中默认的值为8
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。如果不够,则浪费空间来对齐。
我们以s1为例子来试验一下上述规则,如图所示。
因为从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。
所以1,2,3三个字节被浪费,int类型的存储从4开始到7,char类型存到8处。
最后结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
S1中最大对齐数为4,8正好是4的整数倍,所以结构体S1的总大小为8。
再看S4的情况:
白色为浪费部分。
1.8为什么存在内存对齐” />1.9我们可以耍些小聪明达到节省空间的效果。
让占用空间小的成员尽量集中在一起。
//例如:struct S1{ char c1; int i; char c2;};struct S2{ char c1; char c2; int i;};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
2.1修改默认对齐数
我们可以通过#pragma pack()指令来修改默认对齐数。
#include #pragma pack(1)//设置默认对齐数为1struct S1{char c1;int i;char c2;};int main(){//输出的结果是什么?printf("%d\n", sizeof(struct S1));return 0;}
可以看到,答案不再是12,默认对齐数确实被修改了。
想要取消的话就引入一个空指令。
#include #pragma pack(1)//设置默认对齐数为1#pragma pack()//取消设置的默认对齐数,还原为默认struct S1{char c1;int i;char c2;};int main(){//输出的结果是什么?printf("%d\n", sizeof(struct S1));return 0;}
2.2 结构体传参
下面print1和print2那个比较好?
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);}//结构体地址传参void print2(struct S* ps){ printf("%d\n", ps->num);}int main(){ print1(s); //传结构体 print2(&s); //传地址 return 0;}
上面的 print1 和 print2 函数哪个好些? 答案是:首选print2函数。 原因:
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
所以结构体传参数时,要传结构体的地址。
3.1位段
结构体讲完就得讲讲结构体实现 位段 的能力。
struct A{ int _a:2; int _b:5; int _c:10; int _d:30;};
A就是一个位段的类型,位段可以控制所给的空间大小,达到节省空间的目的。
它所占空间是多大?
#include struct A{int _a : 2;int _b : 5;int _c : 10;int _d : 30;};int main(){printf("%d\n", sizeof(struct A));return 0;}
它占了8*8=64个比特位。
从16个字节优化到8个字节,位段的功能可以说是十分强大。
3.2 位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型 2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。 3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
#include //一个例子struct S{char a : 3;char b : 4;char c : 5;char d : 4;};int main(){struct S s = { 0 };s.a = 10;s.b = 12;s.c = 3;s.d = 4;//空间是如何开辟的?return 0;}
- 首先做一个假设,假设内存中的比特位是由右向左使用的。
- 一个字节内部,剩余的比特位不够使用时,直接浪费掉。
我们猜想是这个样子。
转换成16进制为:
62 03 04
我们来调试看看:
我们的猜想是正确的!
3.3 位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。 2. 位段中最大位的数目不能确定。( 16 位机器最大 16 , 32 位机器最大 32 ,写成 27 ,在 16 位机 器会出问题。 3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。 4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。 总结: 跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
这篇博客旨在总结我自己阶段性的学习,要是能帮助到大家,那可真是三生有幸!如果觉得我写的不错的话还请点个赞和关注哦~我会持续输出编程的知识的!