C语言自定义类型详解(结构体、枚举、联合体和位段)
前言
一、结构体
1、结构体类型的声明
当我们想要描述一个复杂变量——学生,可以这样声明。
✒️代码展示:
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,这样的代码合理吗?
而且,像这样的匿名结构体类型只能使用一次,因为没有标签名。
2、结构体的自引用
众所周知,函数可以自己调用自己,叫做函数的递归,那么结构体是否也有自己引用自己呢?如果有又是如何实现的呢?
✒️代码展示:
//代码一: 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*,先后问题产生了。
代码四:
可以通过这种重定义方式实现自引用。
3、结构体变量的定义和初始化
既然已经有了结构体类型,那么对其定义和初始化就变得非常的简单
✒️代码展示:
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};//结构体嵌套初始化
4、结构体内存对齐
掌握了结构体的基本使用,还应当重点了解结构体内存对齐问题从而计算结构体的大小,这是一个关于结构体的重点考点
结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为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; }
????效果展示:
????解释说明:
5、结构体传参
✒️代码展示:
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的话,作为一个指针,占四个字节,极大提高了运行效率。
简而言之,结构体传参时,传结构体的地址更好
二、位段
1、位段的定义
位段,c语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域” 。利用位段能够用较少的位数存储数据。
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int 、signed int、char 。
- 位段的成员名后边有一个冒号和一个数字(指该成员占的比特位)。
✒️代码展示:
struct a { int _a:2; int _b:5; int _c:10; int _d:30; };
2、位段的内存分配
位段的内存分配规则:
- 位段的成员可以是 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编译器中开辟了空间以后,先使用低地址再使用高地址。并且剩余的比特位不够下一个变量存储时,那这一片空间将会被浪费。
简而言之,跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
3、位段的应用
????解释说明:
上图是网络上ip数据包的格式,当你想要在网络上发一条消息给你的好友,信息是需要进行分装的,消息作为数据只是传输的一部分,还有一部分传输的是分装中的其他信息。比如4位版本号,4位首部长度,这些信息只需要4个bit,如若不使用位段,直接每个部分一个整形的给空间,就会造成空间的大量浪费。
三、枚举
1、枚举类型的定义
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举在日常生活中很常见,例如表示星期的sunday、monday、tuesday、wednesday、thursday、friday、saturday就是一个枚举。
2、枚举的优点
枚举的优点:
- 代码的可读性变高和可维护性变强
- 和#define定义的标识符相比较枚举更加严谨,因为有类型检查。
- 防止命名污染的现象
- 方便调试,且使用方便,可以一下子定义很多常量
3、枚举的使用
枚举的说明与结构和联合相似, 其形式为:
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✔️)
四、联合体(共用体)
1、联合体的定义
需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在c语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。
2、联合体的特点
联合的成员是共用同一块内存空间的,一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
✒️代码展示:
//联合类型的声明 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类型,因此只访问了第一个字节。
3、联合体的大小计算
联合体大小计算规则:
联合的大小至少是最大成员的大小。当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
✒️代码展示:
#include <stdio.h> union un { char c; int i; }num; int main() { num.i = 1; if(num.c == 1) { printf("小端存储") } else { printf("大端存储") } return 0; }
????效果展示:
总结
到此这篇关于c语言自定义类型的文章就介绍到这了,更多相关c语言自定义类型内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!