环境:CLion2021.3;64位macOS Big Sur
文章目录
「地表最强」C语言(一)基本数据类型
「地表最强」C语言(二)变量和常量
「地表最强」C语言(三)字符串+转义字符+注释
「地表最强」C语言(四)分支语句
「地表最强」C语言(五)循环语句
「地表最强」C语言(六)函数
「地表最强」C语言(七)数组
「地表最强」C语言(八)操作符
「地表最强」C语言(九)关键字
「地表最强」C语言(十)#define定义常量和宏
「地表最强」C语言(十一)指针
「地表最强」C语言(十二)结构体、枚举和联合体
「地表最强」C语言(十三)动态内存管理,含柔性数组
「地表最强」C语言(十四)文件
「地表最强」C语言(十五)程序的环境和预处理
「地表最强」C语言(十六)一些自定义函数和宏
「地表最强」C语言(十七)阅读程序
十二、结构体、枚举和联合体
12.1 结构体
结构体可以让C语言创造出新的类型,结构体是一些值的集合,这些值可以是不同类型的。
12.1.1 结构体声明及初始化
(1)完全声明
struct B
{
char c;
short s;
double d;
};
struct Stu
{
struct B b;//成员可以是另外一个结构体变量,但不能是自己本身
//Struct stu s;//err,结构体内部不能有自己
char name[20];
int age;
char id[20];
}s1, s2={ {'a', 2, 3.14}, "fudan", 20, "12345" };//s1和s2是结构体变量,是全局变量,在此处也可以初始化。
int main()
{
struct Stu s = { {'w',20,3.14},"张三",20,"001" };//当然也可以在主函数内定义并初始化对象,这个对象是局部变量
}
(2)匿名结构体类型(不完全声明)
struct {
char c;
int i;
char ch;
double;
} s;//只能在此处定义变量,且只能使用一次,因为在其他地方,不知道类型的名字,没办法定义
struct {
char c;
int i;
char ch;
double;
} *ps;//同上
int main()
{
ps = &s;//err,即使这两个结构体内部成员类型相同,但是仍然会被解析成两个不同的结构体类型,类型不同当然不能赋值。
//上述代码在c中可以通过,但是cpp中则会报错,因为cpp语法更为严格,所以不要使用,即使在c中
}
(3)结构体成员的访问
struct Stu s
{
struct B b;
char name[20];
int age;
};
struct B
{
char c;
int s;
double d;
};
void print1(struct Stu stu)
{
//普通结构体变量访问其成员使用 .
printf("%c %d %lf %s %d %s\n", stu.b.c, stu.b.s, stu.b.d, stu.name, stu.age, stu.id);
}
void print2(struct Stu* pstu)
{
//指针访问成员变量时使用 ->
printf("%c %d %lf %s %d %s\n", pstu->b.c, pstu->b.s, pstu->b.d, pstu->name, pstu->age, pstu->id);
}
int main()
{
struct Stu s = {{'c', 2, 3.14}, "zhangsan", 20};
struct Stu* ps = &s;
printf("%c\n", s.b.c);
printf("%s\n", s.id);
//写一个函数打印s的内容
print1(s);//传值调用,需要开辟一块和当前结构体大小一样的空间,效率较低
print2(ps);//传址调用,效率较高
}
12.1.2 结构体的自引用
结构体的自引用可以找到和他同类型的下一个结构体,链表就是这么实现的
struct Node
{
int data;
struct Node* next;//结构体自引用:结构体中包含同类型结构体的指针,注意不是同类型的结构体
};
typedef struct Node {
int data;
struct Node *next;//不能直接使用Node* next,因为执行到这里还没有给这个结构体重命名为Node
} Node;//这样定义可以,因为已经有了struct Node类型,可以重命名
//上述两种方法都是结构体自引用的实现方式,而下面的是错误的
//typedef struct
//{
// int data;
// Node* next;//此处有错误,会陷入死递归
//}Node;//err
12.1.3 结构体的大小计算:内存对齐
对齐规则:
1.结构体的第一个成员永远存放在结构体变量在内存中存储位置的0偏移量处
2.从第二个成员往后的所有成员,都存放在一个对齐数的整数倍的地址处。 对齐数:成员的大小和默认对齐数的较小值 vs默认对齐数8,linux以自身为对齐数
3.结构体的总大小是结构体所有成员的最大对齐数的整数倍。
4.嵌套的结构体对齐到自己成员变量的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
//#pragma pack(2)//更改默认对齐数为2
struct S {
char c1;
int i;
char c2;
};
//#pragma pack()//改回默认对齐数
struct S2 {
char c;
int i;
double d;
};
struct S3 {
char c1;
char c2;
int i;
};
struct S4 {
double d;
char c;
int i;
};
struct S5 {
char c1;
struct S4 s4;
double d;
};
struct S s = {0};
struct S2 s2={0};
struct S3 s3={0};
struct S4 s4={0};
struct S5 s5={0};
printf("%d\n",sizeof(s));// 1 + 3 + 4 + 1 + (3*4 - 9) = 12
printf("%d\n",sizeof(s2));// 1 + 3 + 4 + 8 = 16
printf("%d\n",sizeof(s3));// 1 + 1 + 2 + 4 = 8
printf("%d\n",sizeof(s4));// 8 + 1 + 3 + 4 = 16
printf("%d\n",sizeof(s5));// 1 + 7 + 16 + 8 = 32
为什么存在内存对齐:
1.平台原因(可移植性):某些硬件平台只能在某些地址处读取某些特定的数据类型,否则抛出异常。
2.性能原因:数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要两次内存访问;而对齐的内存仅需要一次内存访问。
结构体的内存对齐实际上是用空间换取时间,但应尽量设计的节省空间:
1.让占用空间小的成员尽量集中在一起 2.修改默认对齐数
#pragma pack(2)//更改默认对齐数为2
运行结果:
计算结构体中某变量相对于首地址的偏移量,即offsetof宏的实现:
printf("%d\n",offsetof(struct S ,c1));
printf("%d\n",offsetof(struct S ,i));
printf("%d\n",offsetof(struct S ,c2));
结果与上述图片分析一致:
模拟实现offsetof():见地表最强C语言汇总(十三)一些自定义函数(持续更新)
13.8
12.1.4 结构体传参
struct S6
{
int data[1000];
int num;
};
void print1(struct S6 s)
{
printf("%d\n",s.num);
}
void print2(struct S6* ps)
{
printf("%d\n",ps->num);
}
int main()
{
struct S6 s6 = {{1, 2, 3, 4}, 1000};
//函数传参时,参数需要压栈,会有时间和空间上的开销;如果传递的结构体对象过大,参数压栈的系统开销比较大,会导致性能的下降。
print1(s6); //需要开辟一块和当前结构体大小一样的空间,效率较低,浪费空间
print2(&s6);//只需要开辟一块指针空间,节省了很多空间,效率高
return 0;
}
函数调用的参数压栈:
栈,先进后出,后进先出。
每一个函数调用都会在内存的栈区开辟一块空间,通常情况下,传参是从右向左传递的。
一个小问题:
//int i = 0;//在此定义i和在括号内定义结果不同
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//i和arr是局部变量,放在栈区上的。栈区内存的使用习惯是先使用高地址空间,再使用低地址空间。
//而数组随着下标的增长,地址是由低到高变化的,因此可能导致死循环。这种情况发生于i的地址与arr[12]地址恰好相同时。
//导致死循环时,无法停下,所以不报错,运行时报错得在运行结束以后才能报错
//不同编译器空的整形个数不一样,由编译器决定。
for (int i = 0; i <= 12; i++)//i定义在arr的后边,不会发生死循环
{
arr[i] = 0;
printf("hello\n");
}
12.1.5 结构体实现位段(位段的填充&可移植性)
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是int、unsigned int、signed int、char
2.位段的成员后边有一个冒号和一个数字
位段的空间上是按照需要以4个字节(int)或1个字节(char)的方式来开辟的
位段涉及很多不确定因素,是不跨平台的,注重可移植性的程序应该避免使用位段,不跨平台的原因:
1. int位段被当成有符号数还是无符号数是不确定的;
2. 位段中最大位的数目不能确定(32位机器int4个字节,可以设置30个bit,而16位机器int2个字节,无法设置30个bit)
3. 位段中的成员的内存是从左向右分配还是从右向左分配是没有规定的
不要和大小端混淆,大小段讨论的是字节序的存储顺序,位段是一个字节内部的存储顺序
5. 当一个结构包含两个位段,第二个位段成员比较大,第一个位段剩余的位无法容纳它,剩余的位舍弃还是利用也是不确定的
和结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
位段应用场景:网络包的传输等
struct C {
//先开辟4个字节,即32个bit
int _a: 2;// _a需要2个bit位 32 - 2 = 30
int _b: 5;// _b需要5个bit位 30 - 5 = 25
int _c: 10;// _c需要10个bit位 25 - 10 = 15
//不够了,再开辟4个字节,即32bit,前边剩余的空间,C语言没有规定要不要使用
int _d: 30;// _d需要30个bit位
//至此,struct C共开辟了8个字节的大小
//注意,不能超过其类型的最大限度,如int后边的数字不能超过32
};
struct D {
//先开辟1个字节,即8个bit
char a: 3;
char b: 4;
char c: 5;
char d: 4;
};
int main()
{
//以struct D为例,分析位段的大小
struct D d = {0};
d.a = 10;
d.b = 12;
d.c = 3;
d.d = 4;
3 4 5 3
printf("%d\n", sizeof(struct D));//3
return 0;
}
看一下内存,验证了猜想正确:
CLion:一个字节的空间由地位向高位使用,即从右向左;一个字节剩余的空间不够下一个成员使用时,舍弃这块儿空间。
12.2 枚举
如果something是可以一一列举的,那么把这所有的情况全部罗列出来就是枚举。
比如,三原色:红绿蓝;月份:1-12月等等。
和#define定义的常量一样,枚举也是常量。
//#define RED 3
//#define BLUE 5
//#define GREEN 7
enum Color {
RED = 3,//枚举类型的可能取值,是常量
BLUE,
GREEN
};
int main()
{
enum Color color = 2;//CPP语法更严格,不通过,因为类型不同,2为int型,color为枚举型
printf("%d\n", RED);//默认为0,递增一,可以赋初值,但是不可以更改
printf("%d\n", BLUE);
printf("%d\n", GREEN);
RED = 6;// err,不可更改
return 0;
}
当一种情况既能又枚举解决,又能用#define解决时,使用枚举:
1.增加了代码的可读性和维护性
2.和#define定义的标识符比较,枚举有类型检查,更严谨
3.防止了命名污染(枚举是封装在内部的,而#define是全局的,容易命名冲突等)
4.便于调试
5.使用方便,一次可以定义多个常量
//test.c -> 编译(预编译 -> 编译 -> 汇编) -> 链接 -> test.exe
12.3 联合体(共用体)
- 什么是联合体
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间,所以联合也叫共用体。
union Un {
char c;//1
int i;//4
};
int main()
{
union Un u = {10};
u.i = 1000;
u.c = 100;//改变c的同时,i也改变了,因此联合体在同一时间只能使用一个成员
// printf("%d\n",sizeof(u));//4
printf("%p\n", &u);
printf("%p\n", &(u.c));
printf("%p\n", &(u.i));
return 0;
}
u、u.c、u.i的地址都是一样的,说明联合体的变量共用一块空间:
在来看看内存的变化:
u.i = 1000;//00000000 00000000 00000011 11101000
00000000 00000000 00000011 11101000
对应16进制为
00 00 03 e8
u.c = 100;//01100100
由于联合体成员共用同一快空间,因此直接在原来的基础上赋值,后8位发生了变化:
00000000 00000000 00000011 01100100
对应16进制为
00 00 03 64
- 联合体大小的计算
联合体的大小,至少是最大成员的大小。
同时,联合体也存在内存对齐,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un2 {
char a[5];//5
int i;//4
};
union Un2 u2;
printf("%d\n",sizeof(u2));//8