时间:2022-04-16 09:58:35 | 栏目:C代码 | 点击:次
当我们想要描述一个复杂变量――学生,可以这样声明。
✒️代码展示:
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }s1;//分号不能丢 int main() { struct Stu s2; return 0; }
🔖解释说明:
- struct是结构体的关键字
- Stu是结构体标签名
- struct Stu是结构体的类型
- 大括号内包围的是结构体成员变量的列表
- 变量s1是类型为struct Stu的全局变量,变量s2是该类型的局部变量
在声明结构时,也有特殊的声明,比如不完全声明――匿名结构体类型,省略掉了结构体标签。
✒️代码展示:
struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[20], *p;
那么,此时,问题来了!
在上面的代码基础上,p = &x,这样的代码合理吗?
而且,像这样的匿名结构体类型只能使用一次,因为没有标签名。
众所周知,函数可以自己调用自己,叫做函数的递归,那么结构体是否也有自己引用自己呢?如果有又是如何实现的呢?
✒️代码展示:
//代码一: struct N { int data; struct N next; }; //代码二: struct Node { int data; struct Node* next; }; //代码三: typedef struct { int data; Node* next; }Node; //代码四: typedef struct Node { int data; struct Node* next; }Node;
🔖解释说明:
代码一:
这样自引用是不正确的。当想要计算struct N类型所占空间大小时,就会出现疯狂套娃现象,无法计算结果,因此是不可取的
代码二:
这才是自引用的正确打开方式。data中存放的数据,next中存放着下一个struct Node类型数据的地址
代码三:
该代码想要实现匿名结构体的自引用,但这样做是不可取的。因为需要完整的定义了该结构体才可以重新命名为Node。然而定义的成员列表中又有Node*,先后问题产生了。
代码四:
可以通过这种重定义方式实现自引用。
既然已经有了结构体类型,那么对其定义和初始化就变得非常的简单
✒️代码展示:
struct Point { int x; int y; }p1; //声明类型的同时定义变量p1 struct Point p2; //定义结构体变量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};//结构体嵌套初始化
掌握了结构体的基本使用,还应当重点了解结构体内存对齐问题从而计算结构体的大小,这是一个关于结构体的重点考点
结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量需要对齐到对齐数的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
VS中默认的值为8,Linux没有默认对齐数- 结构体总大小为最大对齐数的整数倍。
- 当嵌套结构体时,嵌套的结构体对齐需要到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍(包含嵌套结构体的对齐数)。
✒️代码展示:
//练习1 struct S1 { char c1; int i; char c2; }; printf("%d\n", sizeof(struct S1)); //练习2 struct S2 { char c1; char c2; int i; }; printf("%d\n", sizeof(struct S2)); //练习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));
👁效果展示:
🔖解释说明:
结构体类型struct S1和struct S2两者的成员组成是一样的,但是定义顺序有所差别,后者与前者相比将占用空间小的变量集中在了一起,导致两者在遵循结构体对齐条件下,所占内存大小不一样。做个对比吧!
结构体类型struct S3和struct S4是另外两个典型例子,后者嵌套前者。
简而言之,该做法就是为了拿空间换取时间
如果。。。
另外。。。
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。
这里我们将使用预处理指令#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; }
👁效果展示:
🔖解释说明:
✒️代码展示:
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->data[2]); } int main() { print1(s); //传结构体 print2(&s); //传地址 return 0; }
👁效果展示:
🔖解释说明:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,导致性能的下降。比如在这里,如果直接传值s的话,由于结构体中创建了一个很大的数组data,导致结构体过大,传参时浪费的内存空间很大,效率低下。但是如果传址&s的话,作为一个指针,占四个字节,极大提高了运行效率。
简而言之,结构体传参时,传结构体的地址更好
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域” 。利用位段能够用较少的位数存储数据。
位段的声明和结构是类似的,有两个不同:
✒️代码展示:
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
位段的内存分配规则:
- 位段的成员可以是 int、unsigned int、signed int或者char (属于整形家族)类型
- 位段的空间上是按照需要以==4个字节( int )或者1个字节( char )==的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
✒️代码展示:
struct S { char a:3; char b:4; char c:5; char d:4; } struct S s = {0}; s.a = 10; s.b = 12; s.c = 3; s.d = 4;
🔖解释说明:
在VS编译器中开辟了空间以后,先使用低地址再使用高地址。并且剩余的比特位不够下一个变量存储时,那这一片空间将会被浪费。
简而言之,跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
🔖解释说明:
上图是网络上IP数据包的格式,当你想要在网络上发一条消息给你的好友,信息是需要进行分装的,消息作为数据只是传输的一部分,还有一部分传输的是分装中的其他信息。比如4位版本号,4位首部长度,这些信息只需要4个bit,如若不使用位段,直接每个部分一个整形的给空间,就会造成空间的大量浪费。
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举在日常生活中很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就是一个枚举。
枚举的优点:
- 代码的可读性变高和可维护性变强
- 和#define定义的标识符相比较枚举更加严谨,因为有类型检查。
- 防止命名污染的现象
- 方便调试,且使用方便,可以一下子定义很多常量
枚举的说明与结构和联合相似, 其形式为:
enum 枚举名 { 标识符[=整型常数], 标识符[=整型常数], ... 标识符[=整型常数] } 枚举变量;
如果枚举没有初始化,即省掉"=整型常数"时, 则从第一个标识符开始,顺次赋给标识符0, 1, 2, …但当枚举中的某个成员赋值后,其后的成员按依次加1的规则确定其值。
✒️代码展示:
//代码1 enum Num1 { x1, x2, x3, x4 }x; //代码2 enum Num2 { y1, y2 = 0, y3 = 50, y4 }; int main() { printf("%d %d %d %d\n", x1, x2, x3, x4); printf("%d %d %d %d\n", y1, y2, y3, y4); return 0; }
👁效果展示:
注意:
- 枚举中每个成员(标识符)结束符是==","== 不是";", 最后一个成员可省略","。
- 初始化时可以赋负数, 以后的标识符仍依次加1。
- 枚举变量只能取枚举说明结构中的某个标识符常量。
- 枚举值是常量,不是变量,不能在程序中用赋值语句再对它赋值(比如上面的代码出现y3 = 3; ❎)。
- 只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量,除非进行了强制类型转换(比如上面的代码出现x = x2✔️ x = 1❎x = (enum Num1)1✔️)
需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。
联合的成员是共用同一块内存空间的,一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
✒️代码展示:
//联合类型的声明 union Un { char c; int i; }; //联合变量的定义 union Un un; int main() { //例① printf("%p\n", &(un.i)); printf("%p\n", &(un.c)); //例② un.i = 0x11223344; un.c = 0x55; printf("%x\n", un.i); return 0; }
👁效果展示:
🔖解释说明:
通过例①的结果,我们可以直观发现成员变量c和成员变量i共用地址
例②更加证实这一点,由于大小端存储,变量i是以44 33 22 11这样的顺序存储的,因为变量c与其公用地址,因此55将44覆盖,在内存中变量i为55 33 22 11,打印出来为11 22 33 55
联合体的相关应用:
在之前我们已经学会了判断计算机大小端的方法,这里可以通过共用体的特点来实现
#include <stdio.h>union Un{ char c; int i;}num;int main(){ num.i = 1; if(num.c == 1) { printf("小端存储") } else { printf("大端存储") } return 0;}
向成员变量i中存放一个1,查看成员变量c的值,由于该变量是char类型,因此只访问了第一个字节。
联合体大小计算规则:
联合的大小至少是最大成员的大小。当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
✒️代码展示:
#include <stdio.h> union Un { char c; int i; }num; int main() { num.i = 1; if(num.c == 1) { printf("小端存储") } else { printf("大端存储") } return 0; }
👁效果展示: