目录
前言
结构体(struct):结构体是一种自定义的数据类型,它可以将不同类型的数据组合在一起。结构体的作用是提高代码的可读性和模块化,使得代码更加清晰和易于维护。使用场景包括:存储多个相关联的数据,如学生信息、员工信息等。
联合体(union):联合体与结构体类似,但它的所有成员共享同一块内存空间。联合体的作用是在相同的内存空间中存储不同类型的数据,节省内存空间。使用场景包括:需要在不同类型之间进行切换的场景,如操作系统中的进程控制块(PCB)。
枚举(enum):枚举是一种用户自定义的数据类型,它由一组命名的整数常量组成。枚举的作用是提高代码的可读性和安全性,避免使用不明确的整数值。使用场景包括:表示状态、选项、错误码等有限取值的情况。
1. 结构体
1.1 结构体的简单回顾
结构体是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1.1 结构的声明
struct tag
{
member-list;
}variable-list;
例如描述⼀个学⽣:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
1.1.2 结构体变量的创建和初始化
#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .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;
}
1.2 结构的特殊声明
在声明结构的时候,可以不完全的声明。
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x; //注意,匿名结构体必须在这里初始化,因为结构体类型不全,后面就无法初始化
struct
{
int a;
char b;
float c;
}* ps;
上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了?
//在上⾯代码的基础上,下⾯的代码合法吗?
int main()
{
ps = &x;
return 0;
}
编译器发出了警告:
编译器会把上⾯的两个匿名结构体当成不同类型的结构体,所以是⾮法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。
1.3 结构的自引用
在结构中包含⼀个类型为该结构本⾝的成员是否可以呢?
⽐如,定义⼀个链表的节点:
struct Node
{
int data;
struct Node next;
};
上述代码正确吗?如果正确,那 sizeof(struct Node) 是多少?
仔细分析,其实是不⾏的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的大小就会⽆穷的⼤,是不合理的。
正确的⾃引⽤⽅式:
struct Node
{
int data;
struct Node* next;
};
对结构体类型进行重命名:
//对结构体类型进行重命名
typedef struct Node
{
int data;
struct Node* next;
}Node;
//此时将结构体类型重命名为了Node,以后创建结构体变量的时候就可以 Node s = ..... 等价于 struct Node s = .....
在结构体⾃引⽤使⽤的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引⼊问题,看看 下⾯的代码,可⾏吗?
typedef struct
{
int data;
Node* next;
}Node;
答案是不⾏的,因为Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使 ⽤Node类型来创建成员变量,这是不⾏的。
解决方案如下:定义结构体不要使用匿名结构体了
typedef struct Node
{
int data;
struct Node* next;
}Node;
2. 结构体内存对齐
我们已经掌握了结构体的基本使⽤了。
现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。
这也是⼀个特别热⻔的考点: 结构体内存对⻬
2.1 对齐规则
⾸先得掌握结构体的对⻬规则:
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
- VS 中默认的值为 8
- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
- 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
2.2 小练习
2.2.1 练习1
//练习1
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
2.2.2 练习2
//练习2
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
2.2.3 练习3
//练习3
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
2.2.4 练习4
//练习4-结构体嵌套问题
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
2.3 为什么存在内存对齐?
⼤部分的参考资料都是这样说的:
1. 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:
让占用空间小的成员尽量集中在⼀起
#include <stdio.h>
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%zd ", sizeof(struct S1));
printf("%zd ", sizeof(struct S2));
return 0;
}
S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的大小有了⼀些区别。
2.4 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对⻬数。
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。
3. 结构体传参
#include <stdio.h>
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函数。
原因:
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下 降。
结论:
结构体传参的时候,要传结构体的地址。
4. 结构体实现位段
结构体讲完就得讲讲结构体实现 位段 的能⼒。
4.1 什么是位段
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以 选择其他类型。
- 位段的成员名后边有⼀个冒号和⼀个数字。
⽐如:
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%zd\n", sizeof(struct A));
return 0;
}
A就是⼀个位段类型。
那位段A所占内存的⼤⼩是多少?
4.2 位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
printf("%zd\n", sizeof(struct S));
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%zd", sizeof(struct S));
return 0;
}
为什么第二个printf也可以打印出来呢?
代码虽然存在位域长度超过char类型位数的问题,但是程序仍然可以运行并打印出结果。这是因为C语言标准允许编译器对位域的处理有一定的自由度。当位域的长度超过其所依附的数据类型的长度时,超过的部分会被截断。在这个例子中,虽然位域的总长度超过了8位,但是编译器会将其截断为8位,然后进行存储和处理。因此,程序仍然可以运行并打印出结果。
需要注意的是,这种超出长度的位域可能会导致不可预期的行为,因为它依赖于具体的编译器如何处理这种情况。为了编写可移植和可靠的代码,应该避免使用超过数据类型长度的位域。
4.3 位段的跨平台问题
- int 位段被当成有符号数还是⽆符号数是不确定的。
- 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。VS中是从右向左
- 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。 VS中是舍弃
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4.4 位段的应用
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥ 使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络 的畅通是有帮助的。
4.5 位段使用的注意事项
- 位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
- 所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊ 放在⼀个变量中,然后赋值给位段的成员。
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;
}
5. 联合体
5.1 联合体类型的声明
像结构体⼀样,联合体也是由⼀个或者多个成员构成,这些成员可以不同的类型。
但是编译器只为最⼤的成员分配⾜够的内存空间。联合体的特点是所有成员共⽤同⼀块内存空间。所 以联合体也叫:共用体。
给联合体其中⼀个成员赋值,其他成员的值也跟着变化。
#include <stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = { 0 };
//计算连个变量的⼤⼩
printf("%zd\n", sizeof(un));
return 0;
}
为什么是4呢?
5.2 联合体的特点
联合的成员是共⽤同⼀块内存空间的,这样⼀个联合变量的⼤⼩,⾄少是最⼤成员的⼤⼩(因为联合 ⾄少得有能⼒保存最⼤的那个成员)。
//代码1
#include <stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = {0};
// 下⾯输出的结果是⼀样的吗?
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%p\n", &un);
return 0;
}
//代码2
#include <stdio.h>
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un = { 0 };
un.i = 0x11223344;
un.c = 0x55;
printf("%#x\n", un.i);
return 0;
}
代码1输出的三个地址⼀模⼀样,代码2的输出,我们发现将i的第4个字节的内容修改为55了。 我们仔细分析就可以画出,un的内存布局图。
5.3 相同成员的结构体和联合体对比
我们再对⽐⼀下相同成员的结构体和联合体的内存布局情况。
struct S
{
char c;
int i;
};
union Un
{
char c;
int i;
};
int main()
{
union Un un = { 0 };
struct S s = { 0 };
return 0;
}
5.4 联合体大小的计算
- 联合的⼤⼩⾄少是最⼤成员的⼤⼩。
- 当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对齐到最⼤对齐数的整数倍。
#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;
}
使⽤联合体是可以节省空间的,举例:
⽐如,我们要搞⼀个活动,要上线⼀个礼品兑换单,礼品兑换单中有三种商品:图书、杯⼦、衬衫。 每⼀种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。
- 图书:书名、作者、页数
- 杯子:设计
- 衬衫:设计、可选颜⾊、可选尺寸
那我们不耐⼼思考,直接写出⼀下结构:
struct gift_list
{
//公共属性
int stock_number; //库存
double price; //定价
int item_type; //商品类型
//其他的特殊属性
char tital[20]; //书名
char author[20]; //作者
int num_page; //页数
char design[20]; //设计
int colors; //颜色
int sizes; //尺寸
};
上述的结构其实设计的很简单,⽤起来也⽅便,但是结构的设计中包含了所有礼品的各种属性,这样 使得结构体的大小就会偏⼤,比较浪费内存。因为对于礼品兑换单中的商品来说,只有部分属性信息 是常⽤的。比如:
商品是图书,就不需要design、colors、sizes。
所以我们就可以把公共属性单独写出来,剩余属于各种商品本⾝的属性使⽤联合体起来,这样就可以 介绍所需的内存空间,⼀定程度上节省了内存。
struct Gift_List
{
//公共属性
int stock_number; //库存
double price; //定价
int item_type; //商品类型
union MyUnion
{
struct Book
{
char tital[20]; //书名
char author[20]; //作者
int num_page; //页数
}book;
struct Mug
{
char design[20]; //设计
}mug;
struct Shirt
{
char design[20]; //设计
int colors; //颜色
int sizes; //尺寸
}shirt;
};
}gift_list;
5.5 联合的⼀个练习
写⼀个程序,判断当前机器是大端?还是小端?
#include <stdio.h>
int check_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
//printf("%d\n", un.i);
//un.c = 's';
//printf("%c\n", un.c);
return un.c;
}
int main()
{
int ret = check_sys();
printf("%d", ret);
return 0;
}
6. 枚举类型
6.1 枚举类型的声明
枚举顾名思义就是⼀⼀列举。
把可能的取值⼀⼀列举。
比如我们现实⽣活中:
- 一周的星期⼀到星期⽇是有限的7天,可以⼀⼀列举
- 性别有:男、女、保密,也可以⼀⼀列举
- 月份有12个⽉,也可以⼀⼀列举
- 三原⾊,也是可以意义列举
这些数据的表⽰就可以使⽤枚举了。
enum Day //星期
{
MON,
TUES,
WED,
THUR,
FRI,
SAT,
SUN
};
enum Sex //性别
{
MALE,
FEMALE,
SECRET
};
enum Color //颜色
{
RED,
GREEN,
BLUE
};
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。
enum Color//颜⾊
{
RED=2,
GREEN=4,
BLUE=8
};
6.2 枚举类型的优点
为什么使⽤枚举?
我们可以使⽤ #define 定义常量,为什么⾮要使⽤枚举?
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符⽐较枚举有类型检查,更加严谨
- 便于调试,预处理阶段会删除 #define 定义的符号
- 使⽤⽅便,⼀次可以定义多个常量
- 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤
6.3 枚举类型的使用
6.3.1 枚举简单使用
enum Color//颜⾊
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//使⽤枚举常量给枚举变量赋值
6.3.2 之前菜单的版本
#include <stdio.h>
void Menu()
{
printf("**************************\n");
printf("*****1.add 2.sub********\n");
printf("*****3.mul 4.div********\n");
printf("*****0.exit ********\n");
printf("**************************\n");
}
int main()
{
int input = 0;
do
{
Menu();
printf("请输入你想要输入的数字>");
scanf("%d", &input);
switch (input)
{
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 0:
printf("退出程序");
break;
default:
printf("请重新输入!!!!");
}
} while(input);
}
此时,存在一个问题,就是case必须是1,2,3,4,0,写代码的时候还要往前是翻1是什么,2是什么,是不是有些麻烦了。
好好想想是否可以利用枚举的方法来优化这个功能
6.3.3 枚举菜单版本
#include <stdio.h>
void Menu()
{
printf("**************************\n");
printf("*****1.add 2.sub********\n");
printf("*****3.mul 4.div********\n");
printf("*****0.exit ********\n");
printf("**************************\n");
}
enum Calculate
{
exit,
add,
sub,
mul,
div
};
int main()
{
int input = 0;
do
{
Menu();
printf("请输入你想要输入的数字>");
scanf("%d", &input);
switch (input)
{
case add:
break;
case sub:
break;
case mul:
break;
case div:
break;
case exit:
printf("退出程序");
break;
default:
printf("请重新输入!!!!");
}
} while(input);
}