一.结构体
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 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
- 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
练习如下:
练习1:
struct S1 { char c1; int i; char c2; }; printf("%d\n", sizeof(struct S1));过程如下:
c1是第一个成员,从地址0开始放,占1个字节。接下来放
i,int的对齐数是4,所以它的起始地址必须是4的倍数。现在地址只到0,下一个是1,不是4的倍数,所以1、2、3这三个地址都被浪费掉了,从地址4开始放i,占4个字节。然后放c2,char的对齐数是1,随便放,所以直接放在地址8,占1个字节。现在结构体已经占了地址0到8,一共9个字节了。但是规则要求结构体总大小必须是最大对齐数(这里是4)的倍数,9不是4的倍数,所以要再补3个字节,变成12。
所以最后就是12。
练习2:
struct 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个字节。所有成员里最大对齐数是4,8刚好是4的倍数,不需要再补位了。
所以最后就是8个字节
练习3:
struct S3 { double d; char c; int i; }; printf("%d\n", sizeof(struct S3));运行如下:
过程如下:
d是第一个成员,从地址0开始放,double是8个字节,所以占地址0到7。接下来是
c,char类型,对齐数是1,可以直接挨着放,放在地址8,占1个字节;然后到
i了,int的对齐数是4,所以它的起始地址必须是4的倍数。现在地址到8了,下一个是9,9不是4的倍数,地址9、10、11被浪费掉了,从地址12开始放i,占4个字节,到地址15结束;这时候结构体已经占了地址0到15,一共16个字节。所有成员里最大对齐数是
double的8,16刚好是8的倍数,不需要再补位。所以最后就是16个字节。
练习4:(结构体嵌套)
//练习4-结构体嵌套问题 struct S4 { char c1; struct S3 s3; double d; }; printf("%d\n", sizeof(struct S4));练习4,看出这个嵌套结构体,那又怎么算?
其实还是按之前的规则来,只是多了一条:被嵌套的结构体,它的对齐数等于它内部所有成员中最大的那个对齐数。
过程如下:
c1是第一个成员,从地址0开始放,占1个字节;接下来放
s3,struct S3的对齐数是8(因为它里面有个double),所以它的起始地址必须是8的倍数。现在地址到0了,下一个是1,不行,2、3、4、5、6、7都不行,必须到8才可以,所以地址1到7全被浪费了,从地址8开始放s3,s3自己内部占16个字节,所以它占地址8到23;然后放
d,double的对齐数是8,起始地址必须是8的倍数。现在地址到23了,下一个是24,24是8的倍数(8×3=24),所以直接放,占地址24到31;现在整个结构体占了地址0到31,一共32个字节。所有成员里最大对齐数是8(
s3和d都是8),32刚好是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 num(4字节),一共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位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
- 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4.位段的应⽤
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。
5. 位段使⽤的注意事项
位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。
代码如下:
#include<stdio.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]=5,int=4-->最大成员 = 5 字节;找出各成员对齐数:1、4 -->最大对齐数 = 4。
判断:5 是不是 4 的整数倍?向上取最近的 4 的倍数:
8所以sizeof(union Un1) = 8
成员大小:
short[7]=14,int=4-->最大成员 = 14 字节;各成员对齐数:2、4 -->最大对齐数 = 4;
判断:14 是不是 4 的整数倍?向上找最近的 4 的倍数: 4×3=12(小于 14,不行),4×4=16 所以
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:
| 地址 | 0x00 | 0x01 | 0x02 | 0x03 |
|---|---|---|---|---|
| 大端 | 11 | 22 | 33 | 44 |
| 小端 | 44 | 33 | 22 | 11 |
写⼀个程序,判断当前机器是⼤端?还是⼩端?
方法一:用指针判断
#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//颜⾊ { RED=2, GREEN=4, BLUE=8 };以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。
enum Color//颜⾊ { RED=2, GREEN=4, BLUE=8 };3. 枚举类型的优点
我们可以使⽤ #define 定义常量,为什么⾮要使⽤枚举?
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符⽐较枚举有类型检查,更加严谨。
- 便于调试,预处理阶段会删除 #define 定义的符号
- 使⽤⽅便,⼀次可以定义多个常量
- 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤
4.枚举类型的使⽤
#include<stdio.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++的类型检查⽐较严格。