【C语言】《C语言自定义类型(结构体+联合体+枚举类型)》--详解

发布时间:2026/7/28 16:58:00
【C语言】《C语言自定义类型(结构体+联合体+枚举类型)》--详解 一.结构体1.结构体基本知识结构是⼀些值的集合这些值称为成员变量。结构的每个成员可以是不同类型的变量。2.结构体的说明结构体声明语法struct tag { member-list; // 成员列表 } variable-list; // 变量列表可选示例描述一个学生struct Stu { char name[20]; // 名字 int age; // 年龄 char sex[5]; // 性别 char id[20]; // 学号 }; // 分号不能丢3.结构体变量的创建和初始化完整示例#include stdio.h struct Stu { char name[20]; // 名字 int age; // 年龄 char sex[5]; // 性别 char id[20]; // 学号 }; int main() { // 方式1按照结构体成员的顺序初始化 struct Stu s { hjq, 20, 男, 20260818001 }; printf(name: %s\n, s.name); printf(age : %d\n, s.age); printf(sex : %s\n, s.sex); printf(id : %s\n, s.id); printf(\n); // 分隔线 // 方式2按照指定的顺序初始化使用 .成员名 struct Stu s2 { .age 18, .name hjq, .id 20260818002, .sex 女 }; printf(name: %s\n, s2.name); printf(age : %d\n, s2.age); printf(sex : %s\n, s2.sex); printf(id : %s\n, s2.id); return 0; }运行如下补充其他初始化方式// 方式3先创建后逐个赋值 struct Stu s3; strcpy(s3.name, hf); s3.age 22; strcpy(s3.sex, 男); strcpy(s3.id, 20260818003); // 方式4使用typedef简化推荐 typedef struct Stu Stu; // 或直接在声明时加 typedef Stu s4 { hf, 23, 男, 20260818004 };4.匿名结构体不完全声明// 匿名结构体类型没有标签名 tag struct { int a; char b; float c; } x; // 直接创建变量 x struct { int a; char b; float c; } a[20], *p; // 创建数组 a[20] 和指针 p上⾯的两个结构在声明的时候省略掉了结构体标签tag。那么问题来了//在上⾯代码的基础上下⾯的代码合法吗 p x;这里因为会被认为是两个不同的类型所以 *p 不能存变量 x 的地址。警告编译器会把上⾯的两个声明当成完全不同的两个类型所以是⾮法的。匿名的结构体类型如果没有对结构体类型重命名的话基本上只能使⽤⼀次。改进方法使用 typedef 解决// 如果想让匿名结构体可复用可以用typedef typedef struct { int a; char b; float c; } MyStruct; // 这样就有了类型名 MyStruct s1 { 1, a, 3.14 }; MyStruct s2 { 2, b, 2.71 }; MyStruct* p s1; // 现在可以了5.结构的⾃引⽤结构体允许包含一个指向同类型结构体的指针作为成员即自引用指针但不允许包含同类型结构体的完整变量作为成员。在结构中包含⼀个类型为该结构本⾝的成员是否可以呢⽐如定义⼀个链表的节点 struct Node { int data; struct Node next; };上述代码正确吗如果正确那 sizeof(struct Node) 是多少我们运行一下看一下结构体不能包含自身类型的完整对象编译器无法计算sizeof(struct Node)无限递归会报错C2079: next 使用未定义的 struct Node所以仔细分析其实是不⾏的因为⼀个结构体中再包含⼀个同类型的结构体变量这样结构体变量的⼤⼩就会⽆穷的⼤是不合理的。正确的⾃引⽤⽅式代码 struct Node { int data; struct Node* next; };在结构体⾃引⽤使⽤的过程中夹杂了 typedef 对匿名结构体类型重命名也容易引⼊问题看看下⾯的代码可⾏吗typedef struct { int data; Node* next; }Node;答案是不⾏的因为Node是对前⾯的匿名结构体类型的重命名产⽣的但是在匿名结构体内部提前使⽤Node类型来创建成员变量这是不⾏的。解决⽅案如下定义结构体不要使⽤匿名结构体了typedef struct Node { int data; struct Node* next; }Node;在结构体定义中使用 struct Node 来指明 next 指针的类型而不是直接使用 Node。这样编译器就能正确识别 Node 类型了6.结构体内存对⻬我们已经掌握了结构体的基本使⽤了。现在我们深⼊讨论⼀个问题计算结构体的⼤⼩。这也是⼀个特别热⻔的考点结构体内存对⻬.对⻬规则⾸先得掌握结构体的对⻬规则结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处其他成员变量要对⻬到某个数字对⻬数的整数倍的地址处。对⻬数 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。-VS 中默认的值为 8- Linux中 gcc 没有默认对⻬数对⻬数就是成员⾃⾝的⼤⼩结构体总⼤⼩为最⼤对⻬数结构体中每个成员变量都有⼀个对⻬数所有对⻬数中最⼤的的整数倍。如果嵌套了结构体的情况嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处结构体的整体⼤⼩就是所有最⼤对⻬数含嵌套结构体中成员的对⻬数的整数倍。练习如下练习1struct S1 { char c1; int i; char c2; }; printf(%d\n, sizeof(struct S1));过程如下c1是第一个成员从地址0开始放占1个字节。接下来放iint的对齐数是4所以它的起始地址必须是4的倍数。现在地址只到0下一个是1不是4的倍数所以1、2、3这三个地址都被浪费掉了从地址4开始放i占4个字节。然后放c2char的对齐数是1随便放所以直接放在地址8占1个字节。现在结构体已经占了地址0到8一共9个字节了。但是规则要求结构体总大小必须是最大对齐数这里是4的倍数9不是4的倍数所以要再补3个字节变成12。所以最后就是12。练习2struct S2 { char c1; char c2; int i; }; printf(%d\n, sizeof(struct S2));运行如下过程如下c1是第一个成员从地址0开始放占1个字节。接下来是c2它是char类型对齐数是1所以它可以挨着放直接放在地址1不用浪费空间然后到i了int的对齐数是4所以它的起始地址必须是4的倍数。现在地址到1了下一个是2不行3也不行必须到4才可以所以地址2和3被浪费掉了从地址4开始放i占4个字节这时候结构体已经占了地址0到7一共8个字节。所有成员里最大对齐数是48刚好是4的倍数不需要再补位了。所以最后就是8个字节练习3struct S3 { double d; char c; int i; }; printf(%d\n, sizeof(struct S3));运行如下过程如下d是第一个成员从地址0开始放double是8个字节所以占地址0到7。接下来是cchar类型对齐数是1可以直接挨着放放在地址8占1个字节然后到i了int的对齐数是4所以它的起始地址必须是4的倍数。现在地址到8了下一个是99不是4的倍数地址9、10、11被浪费掉了从地址12开始放i占4个字节到地址15结束这时候结构体已经占了地址0到15一共16个字节。所有成员里最大对齐数是double的816刚好是8的倍数不需要再补位。所以最后就是16个字节。练习4(结构体嵌套//练习4-结构体嵌套问题 struct S4 { char c1; struct S3 s3; double d; }; printf(%d\n, sizeof(struct S4));练习4看出这个嵌套结构体那又怎么算其实还是按之前的规则来只是多了一条被嵌套的结构体它的对齐数等于它内部所有成员中最大的那个对齐数。过程如下c1是第一个成员从地址0开始放占1个字节接下来放s3struct S3的对齐数是8因为它里面有个double所以它的起始地址必须是8的倍数。现在地址到0了下一个是1不行2、3、4、5、6、7都不行必须到8才可以所以地址1到7全被浪费了从地址8开始放s3s3自己内部占16个字节所以它占地址8到23然后放ddouble的对齐数是8起始地址必须是8的倍数。现在地址到23了下一个是2424是8的倍数8×324所以直接放占地址24到31现在整个结构体占了地址0到31一共32个字节。所有成员里最大对齐数是8s3和d都是832刚好是8的倍数不需要再补位。所以最后sizeof(struct S4)是32个字节。7.为什么存在内存对⻬?1平台原因 (移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定类型的数据否则抛出硬件异常。性能原因数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于为了访问未对⻬的内存处理器需要作两次内存访问⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数那么就可以⽤⼀个内存操作来读或者写值了。否则我们可能需要执⾏两次内存访问因为对象可能被分放在两个8字节内存块中。总体来说结构体的内存对⻬是拿空间来换取时间的做法。那在设计结构体的时候我们既要满⾜对⻬⼜要节省空间如何做到让占⽤空间⼩的成员尽量集中在⼀起代码如下struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; };S1 和 S2 类型的成员⼀模⼀样但是 S1 和 S2 所占空间的⼤⼩有了⼀些区别。8.修改默认对⻬数#pragma 这个预处理指令可以改变编译器的默认对⻬数。没写pack编译器会在成员之间插空保证每个成员都放在它对齐数的整数倍地址上访问更快但浪费内存。写了pack(1)强制1字节对齐所有成员紧挨着放中间不插空省内存但访问速度会慢一点。根据以上结果可以说明#pragma可以改变编译器的默认对⻬数。所以结构体在对⻬⽅式不合适的时候我们可以⾃⼰更改默认对⻬数。问题补充不写第二个行不行后来才知道第二个是必须写的因为#pragma pack(1)会影响它后面所有的结构体定义直到遇到#pragma pack()才会恢复。如果不恢复后面其他结构体也会按1字节对齐可能会导致性能下降或者跟其他代码不兼容。所以标准的写法就是用完之后马上恢复避免影响其他代码9.结构体传参代码如下struct S { int data[1000]; int num; }; struct S s {{1,2,3,4}, 1000}; // 方式1传结构体本身值传递 void print1(struct S s) { printf(%d\n, s.num); } // 方式2传结构体指针地址传递 void print2(struct S* ps) { printf(%d\n, ps-num); } int main() { print1(s); // 传结构体 print2(s); // 传地址 return 0; }那哪个更好首选print2传地址。原因如下在空间开销方面print1传结构体的时候会把整个结构体拷贝一份压到栈上。这个结构体里有int data[1000]4000字节int num4字节一共4004字节每次调用都要拷贝这么多数据太浪费了。print2只传一个指针8字节64位系统或4字节32位系统省很多。在时间开销方面拷贝4004字节比拷贝8字节慢得多尤其是结构体很大的时候性能差距很明显。在压栈开销方面函数传参是要压栈的数据越大压栈消耗的时间和空间就越大。结论结构体传参的时候尽量传结构体的地址指针而不是传结构体本身。除非结构体特别小比如就一两个int那传值和传指针差别不大怎么写都行。但大多数情况下传指针是更好的选择。10.结构体实现位段1. 什么是位段位段的声明和结构是类似的有两个不同位段的成员必须是 int、unsigned int 或signed int 在C99中位段成员的类型也可以选择其他类型。位段的成员名后边有⼀个冒号和⼀个数字。示例如下struct A { int _a:2; int _b:5; int _c:10; int _d:30; };A就是⼀个位段类型。那位段A所占内存的⼤⼩是多少成员是int所以一次开4个字节32个bit。_a占2个bit存进去还剩30个bit。_b占5个bit存进去还剩25个bit。_c占10个bit存进去还剩15个bit。轮到_d了它需要30个bit但当前剩下的只有15个bit不够了所以剩下的15个bit直接浪费掉再重新开4个字节32个bit来存_d。_d占30个bit放到新开的这个4字节里。四个成员占47个比特位而8个字节是64个比特位所以一共用了两个4字节8个字节。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;空间是如何开辟的3.位段的跨平台问题int 位段被当成有符号数还是⽆符号数是不确定的。位段中最⼤位的数⽬不能确定。16位机器最⼤1632位机器最⼤32写成27在16位机器会出问题。位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。当⼀个结构包含两个位段第⼆个位段成员⽐较⼤⽆法容纳于第⼀个位段剩余的位时是舍弃剩余的位还是利⽤这是不确定的。总结跟结构相⽐位段可以达到同样的效果并且可以很好的节省空间但是有跨平台的问题存在。4.位段的应⽤下图是⽹络协议中IP数据报的格式我们可以看到其中很多的属性只需要⼏个bit位就能描述这⾥使⽤位段能够实现想要的效果也节省了空间这样⽹络传输的数据报⼤⼩也会较⼩⼀些对⽹络的畅通是有帮助的。5. 位段使⽤的注意事项位段的⼏个成员共有同⼀个字节这样有些成员的起始位置并不是某个字节的起始位置那么这些位置处是没有地址的。内存中每个字节分配⼀个地址⼀个字节内部的bit位是没有地址的。所以不能对位段的成员使⽤操作符这样就不能使⽤scanf直接给位段的成员输⼊值只能是先输⼊放在⼀个变量中然后赋值给位段的成员。代码如下#includestdio.h struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; int main() { struct A sa { 0 }; scanf(%d, sa._b);//这是错误的 //正确的⽰范 int b 0; scanf(%d, b); sa._b b; return 0; }二.联合体1. 联合体类型的声明像结构体⼀样联合体也是由⼀个或者多个成员构成这些成员可以不同的类型。但是编译器只为最⼤的成员分配⾜够的内存空间。联合体的特点是所有成员共⽤同⼀块内存空间。所以联合体也叫共⽤体。给联合体其中⼀个成员赋值其他成员的值也跟着变化。代码如下#include stdio.h //联合类型的声明 union Un { char c; int i; }; int main() { //联合变量的定义 union Un un { 0 }; //计算连个变量的⼤⼩ printf(%d\n, sizeof(un)); return 0; }#include stdio.h union Un { char c; //1 int i; //4 }; int main() { union Un u; printf(%p\n, u); printf(%p\n, (u.c)); printf(%p\n, (u.i)); return 0; }为什么是 4 个字节呢这i和c的起点都是一样的对应这个红色的部分看上面输出的结果他们的内存地址也都是一样的。结论联合体的成员是共用同一块内存空间的因为联合至少要有保存最大的那个成员的能力所以一个联合变量的大小至少是最大成员的大小。注意在同一时间内你只可以使用联合体中的一个成员。2.相同成员的结构体和联合体对⽐我们再对⽐⼀下相同成员的结构体和联合体的内存布局情况。struct S { char c; int i; }; struct S s {0}; union Un { char c; int i; }; union Un un {0};3. 联合体⼤⼩的计算联合的⼤⼩⾄少是最⼤成员的⼤⼩。当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候就要对⻬到最⼤对⻬数的整数倍。#include stdio.h union Un1 { char c[5]; int i; }; union Un2 { short c[7]; int i; }; int main() { //下⾯输出的结果是什么 printf(%d\n, sizeof(union Un1)); printf(%d\n, sizeof(union Un2)); return 0; }过程如下找出成员大小char[5]5int4--最大成员 5 字节找出各成员对齐数1、4 --最大对齐数 4。判断5 是不是 4 的整数倍向上取最近的 4 的倍数8所以sizeof(union Un1) 8成员大小short[7]14int4--最大成员 14 字节;各成员对齐数2、4 --最大对齐数 4;判断14 是不是 4 的整数倍向上找最近的 4 的倍数 4×312小于 14不行4×416 所以sizeof(union Un2) 16使⽤联合体是可以节省空间的举例⽐如我们要搞⼀个活动要上线⼀个礼品兑换单礼品兑换单中有三种商品图书、杯⼦、衬衫。每⼀种商品都有库存量、价格、商品类型和商品类型相关的其他信息。图书书名、作者、⻚数杯⼦设计衬衫设计、可选颜⾊、可选尺⼨代码如下struct gift_list { //公共属性 int stock_number;//库存量 double price; //定价 int item_type;//商品类型 //特殊属性 char title[20];//书名 char author[20];//作者 int num_pages;//⻚数 char design[30];//设计 int colors;//颜⾊ int sizes;//尺⼨ };上述的结构其实设计的很简单⽤起来也⽅便但是结构的设计中包含了所有礼品的各种属性这样使得结构体的⼤⼩就会偏⼤⽐较浪费内存。因为对于礼品兑换单中的商品来说只有部分属性信息是常⽤的。⽐如商品是图书就不需要design、colors、sizes。所以我们就可以把公共属性单独写出来剩余属于各种商品本⾝的属性使⽤联合体起来这样就可以介绍所需的内存空间⼀定程度上节省了内存。struct gift_list { int stock_number;//库存量 double price; //定价 int item_type;//商品类型 union { struct { char title[20];//书名 char author[20];//作者 int num_pages;//⻚数 }book; struct { char design[30];//设计 }mug; struct { char design[30];//设计 int colors;//颜⾊ int sizes;//尺⼨ }shirt; }item; };4.联合的⼀个练习判断大小端定义大小端是数据在内存中的存储方式大端模式数据的高位字节保存在低地址低位字节保存在高地址跟我们平时写数字的习惯一样小端模式数据的低位字节保存在低地址高位字节保存在高地址举个例子int a 0x11223344地址0x000x010x020x03大端11223344小端44332211写⼀个程序判断当前机器是⼤端还是⼩端方法一用指针判断#include stdio.h int check_sys() { int a 1; // 0x00000001 return *(char*)a; // 取第一个字节 } int main() { int ret check_sys(); if (ret 1) printf(小端\n); // 第一个字节是 01说明低地址存低位 else printf(大端\n); // 第一个字节是 00说明低地址存高位 return 0; }过程如下int a 1十六进制是0x00 00 00 01强转成char*后指向的是第一个字节低地址如果第一个字节是01--小端如果第一个字节是00-- 大端方法二用联合体判断#include stdio.h int check_sys() { union U { char c; int i; } u; u.i 1; // 存整数 1 return u.c; // 取的是 int 的第一个字节 } int main() { int ret check_sys(); if (ret 1) printf(小端\n); else printf(大端\n); return 0; }过程如下联合体的成员i和c共用同一块内存u.i 1写入4个字节u.c读取的是第一个字节低地址如果第一个字节是1-- 小端如果第一个字节是0--大端三. 枚举类型1.枚举的定义枚举就是把一个集合里所有可能的值一一列举出来在C语言里用enum关键字定义枚举成员本质上是整型常量默认从0开始依次递增也可以手动指定值。枚举比#define宏更好用因为它有类型检查、支持调试、相关常量可以分组管理能让代码更可读、更易维护。实际开发中枚举常用于表示状态码、选项配置、星期月份这类取值固定的场景用枚举名代替魔术数字代码一看就懂改起来也方便。2. 枚举类型的声明enum Color//颜⾊ { RED2, GREEN4, BLUE8 };以上定义的 enum Day enum Sex enum Color 都是枚举类型。{}中的内容是枚举类型的可能取值也叫 枚举常量 。这些可能取值都是有值的默认从0开始依次递增1当然在声明枚举类型的时候也可以赋初值。enum Color//颜⾊ { RED2, GREEN4, BLUE8 };3. 枚举类型的优点我们可以使⽤ #define 定义常量为什么⾮要使⽤枚举枚举的优点增加代码的可读性和可维护性和#define定义的标识符⽐较枚举有类型检查更加严谨。便于调试预处理阶段会删除 #define 定义的符号使⽤⽅便⼀次可以定义多个常量枚举常量是遵循作⽤域规则的枚举声明在函数内只能在函数内使⽤4.枚举类型的使⽤#includestdio.h enum Color { red,//0 yellow,//1 blue//1 }; int main() { enum Color c blue; // 创建一个变量c并将blue赋给它 c yellow; // 这时将yellow赋给它完全没有问题 // 只能拿枚举常量给枚举变量赋值才不会出现类型的差异 // blue 6; // error - 枚举常量是不能改变的 printf(%d\n, c); return 0; }那是否可以拿整数给枚举变量赋值呢在C语⾔中是可以的但是在C是不⾏的C的类型检查⽐较严格。