我知道的只是 “ 肉随便加 ”和 “ 要加多少加多少 ” 这些词。 ———— 路飞
阶段2目标:
此阶段开始大量刷题,多多参加编程类竞赛,在实战中锻炼编程思维和本领,并且要在不断复习夯实初阶的基础上,刻意地进行编程思维的训练。学无止境!为了精进编程,可以去学习一切为他服务的课程!
写在前面:
在C语言中有许多自带的内置类型,如:char、 int 、 float 、double、 long int、 long long .....
当然,我们也可以自定义一些数据类型,就是我们今天所谈到的:结构体、枚举、联合
目录
本章重点
结构体
- 结构体类型的声明
- 结构的自引用
- 结构体变量的定义和初始化
- 结构体内存对齐
- 结构体传参
- 结构体实现位段(位段的填充&可移植性)
枚举
- 枚举类型的定义
- 枚举的优点
- 枚举的使用
联合
- 联合类型的定义
- 联合的特点
- 联合大小的计算
一、结构体
1.什么是结构体?
结构体是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。
注释:生活中往往一件东西是由多个复杂类型构成的,例如:
人👨💻——>名字+年龄+性别+身高+体重+身份证号码+电话......
书📕——>书名+作者+出版社+定价+书号......
2.结构体的声明
struct tag
{
member - list;
}variable - list;
比较抽象,看个例子~ 一本书
struct Book
{
char name[20];
char author[20];
float price;
};
当然,也可以在最后加上结构体定义的变量
struct Book
{
char name[20];
char author[20];
float price;
}b1, b2;
对于结构体定义变量,注意有以下几种写法,对比一下了解即可:
struct Book
{
char name[20];
char author[20];
float price;
}b1, b2;//全局
struct Book b3;//全局
int main()
{
struct Book b4;//局部
return 0;
}
3.特殊的声明形式
在声明结构的时候,可以不完全的声明。(匿名结构体)
//匿名结构体类型
struct
{
char name[20];
char author[20];
float price;
}b1, b2;
可见,在声明的时候省略掉了结构体标签(tag)也是可以的,
但是变量创建就限制了只能在结构体末尾创建,你还想在main()函数中写struct b3;是错误的。
思考一下,同样是用一个匿名结构体来声明,这样写是否可行??
p = &b1;
//匿名结构体类型
struct
{
char name[20];
char author[20];
float price;
}b1, b2;
//匿名结构体类型
struct
{
char name[20];
char author[20];
float price;
}* p;
int main()
{
p = &b1;
return 0;
}
我们看出,会出现警告,那是因为——>
警告: 编译器会把上面的两个声明当成完全不同的两个类型。 所以是非法的
4.结构的自引用
自己能够找到自己类型的下一个节点。
拿数据结构中的链表举例:
struct Node
{
int data;//数据域
struct Node* next;//指针域
};
对比以下写法,看是否可行?
//代码1
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码,可行否?
//代码2
//解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;
代码1:这种写法是错误的,因为我们是先定义,后使用。而Node是在结构体后命名的,结构体中又有Node*,则是不允许的。
5.结构体变量的定义与初始化
结构体变量定义
struct Point
{
int x;
int y;
}p1 = { 1,1 }, p2 = {2,2};
struct Point p3 = { 3,3 };
int main()
{
struct Point p4 = { 4,4 };
return 0;
}
结构体嵌套初始化
//结构体嵌套初始化
struct S
{
double d;
struct Point p;
char name[20];
};
int main()
{
struct S s = { 3.14,{5,9},"QBJ" };
return 0;
}
6.※结构体内存对齐 (计算结构体的大小)
为啥会输出12? 8?? 什么jb玩意这是?? 如何计算结构体的大小?? 不要着急,我们来讲一下结构体内存对齐的知识你就懂了~~
//结构体内存对齐
//12
struct S1
{
char c1;
int a;
char c2;
};
//8
struct S2
{
char c1;
char c2;
int a;
};
int main()
{
struct S1 s1 = { 'x',100,'y' };
struct S2 s2 = { 'A','Q' ,6};
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
结构体对齐数的3个规则:
- 结构体的第一个成员永远放在结构体起始位置偏移量为0的位置
- 结构体成员从第二个成员开始,总是放在偏移量为一个对齐数的整数倍处 对齐数 = 编译器默认的对齐数和变量自身大小的较小值 Linux没有默认对齐数 VS 默认对齐数是8
- 结构体的总大小必须是各个成员的对齐数中,最大那个对齐数的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
以S1为例:
以S2为例:
练习一个:计算结构体S3的大小
struct S3
{
double d;
char c;
int i;
};
图解解释:
结构体中嵌套结构体怎么计算?
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
图解解释:
7.为什么存在结构体内存对齐?
大部分的参考资料都是如是说的:
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
通过对比S1、S2我们发现结构体中同样的成员变量,排列顺序不一样,空间占用也不一样,既然同样都会浪费消耗空间,那怎么设计可以让其浪费空间比较少呢?
//12
struct S1
{
char c1;
int i;
char c2;
};
//8
struct S2
{
char c1;
char c2;
int i;
};
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?
让占用空间小的成员尽量集中在一起。
8.修改默认对齐数
之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为8
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));//12
printf("%d\n", sizeof(struct S2));//6
return 0;
}
你可能会说,那我把默认对齐数都是1,那不就节省了空间了吗?———— 还是那个问题,这样的确节省了空间,但是执行效率不高,我们采用以空间换取时间的思想。
结论:
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。( 一般不会瞎更改,都是采用2的几次方形式 )
百度笔试题
写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
考察: offsetof 宏的实现
size_t offsetof( structName, memberName );
offsetof是宏,因为其参数传递的是结构体的类型.....具体,以后学了宏之后再深究,今天先看看计算结构体某变量对于首地址的偏移量的计算方法。
#include<stdio.h>
#include<stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S1,c1));//0
printf("%d\n", offsetof(struct S1, i));//4
printf("%d\n", offsetof(struct S1, c2));//8
return 0;
}
9.结构体传参
值传递
#include<stdio.h>
struct S
{
int data[100];
int num;
};
void Print1(struct S tmp)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", tmp.data[i]);
}
printf("\nnum=%d\n", tmp.num);
}
int main()
{
struct S s = { {1,2,3,4,5,6,7,8,9,10},100 };
Print1(s);
return 0;
}
但是,值传递有明显缺点。就该题而言,我们是拷贝了一份值,打印的是拷贝的那块值,而这样拷贝,空间会占用很多,有没有一种节省空间的传参方式呢?————当然有,那就是,传递地址。
#include<stdio.h>
void Print2(struct S* ps)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", ps->data[i]);
}
printf("\nnum=%d\n", ps->num);
}
int main()
{
struct S s = { {1,2,3,4,5,6,7,8,9,10},100 };
Print2(&s);
return 0;
}
上面的 Print1 和 Print2 函数哪个好些?
答案是:首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:
结构体传参的时候,要传结构体的地址。
二、位段
结构体讲完就得讲讲结构体实现 位段 的能力。
1.什么是位段?
位段的声明和结构体的声明类似,有两个不同点:
- 位段的成员必须是 int、unsigned int 或signed int 。
- 位段的成员名后边有一个冒号和一个数字。
比如:如下程序A就是一个位段
//位段是可以节省空间的!
//位段 - 二进制位
//
//性别
//男 女 保密 ...
//01 10 00 ...
struct A
{
int _a : 2;//_a 2个bite位
int _b : 5;//_b 5个bite位
int _c : 10;//_c 10个bite位
int _d : 30;//_d 30个bite位
};
//共47bite - 6byte就够了?
//而实际上,是8byte
int main()
{
printf("%d\n", sizeof(struct A));//8byte
return 0;
}
2.位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
比如:
举例说明位段存储过程( 基于VS2019编译器,因为各个编译器可能位段是不一样的 )
3.位段的跨平台问题
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
对于位段跨平台问题,主要有以下影响因素:
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
解释:
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
//在早期,16位平台下,int 是2byte - 16个bite ,
//那么_d :30超过最大范围16bite是不被允许的
//
//而在,32位平台下,int _d:30就是可以的,
//因为32位平台下,int 是4个字节,32bite
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
例如刚刚所假设的
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
那么,就目前大内存的电脑CPU而言,谁还会为了几个比特位而去考虑可以移植性差的位段呢?? 别急,马上介绍位段的应用价值————
4.位段的应用
网络协议方面知识:
所以,位段存在是非常有必要的。在网络协议底层知识会有很多应用,后续学得多了,自然而然就会明白其重要性。
三、枚举
1.什么是枚举类型
枚举顾名思义就是一一列举。把可能的取值一一列举。比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举
性别有:男、女、保密,也可以一一列举
月份有12个月,也可以一一列举
颜色也可以一一列举
2.枚举类型的定义
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枚举类型中,初始化枚举常量。在main()函数中不能更改枚举常量的值:
//会报错
RED = 8;
在main()函数中,可以用枚举类型定义变量。
int main()
{
enum Color c = GREEN;//建议写法
//不建议写法:
//enum Color c = 7;
//将常数赋值给c,在.c文件可以执行,但是.cpp就不可以,不建议这样写
return 0;
}
3.枚举类型的优点 为什么使用枚举类型?
用#define就可以同样达到枚举的效果,代码如下:请思考我们可以使用 #define 定义常量,为什么非要使用枚举?
#define RED 4
#define GREEN 7
#define BLUE 666
#include<stdio.h>
//enum Color
//{
// RED = 4,
// GREEN = 7,
// BLUE = 666
//};
int main()
{
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
return 0;
}
我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较,枚举有类型检查,更加严谨
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
解释:
1.增加代码的可读性和可维护性
因为用#define来直接定义未尝不可,只是把RED、GERRN、BLUE赋值成的值未免显得唐突些;而用enum就增强了代码可读性,只是将RED、GERRN、BLUE用一些值来代替。
2.和#define定义的标识符比较,枚举有类型检查,更加严谨。
在enum枚举类型定义的枚举常量,他们是有类型的,类型就是枚举类型;而#define定义的标识符没有类型,是无所谓的。所以枚举类型更加严谨。
3.防止了命名污染(封装)
在enum枚举类型中,枚举常量可能重名,但是他们处在枚举类型内部,对应的枚举类型不同;而#define中定义的变量名字位于全局,如果重复,就会产生bug。
4.便于调试
5.使用方便,一次可以定义多个常量
4.枚举的使用
#include<stdio.h>
enum Color
{
RED = 1,
GREEN = 2,
BLUE = 4
};
int main()
{
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //err
//不可更改
return 0;
}
应用:增加可读性的举例——>
可读性不高的代码:
#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\n", &input);
switch (input)
{
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 0:
break;
}
} while ();
return 0;
}
enum增加可读性的代码:
#include<stdio.h>
enum Option
{
EXIT, //0
ADD, //1
SUB, //2
MUL, //3
DIV //4
};
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\n", &input);
switch (input)
{
case ADD:
break;
case SUB:
break;
case MUL:
break;
case DIV:
break;
case EXIT:
break;
}
} while ();
return 0;
}
四、联合(共用体)
1.联合类型的定义
联合也是一种特殊的自定义类型 。这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)。
比如:
结果是多少?? 5?
//联合类型的声明
union Un
{
char c;//1
int i;//4
};
int main()
{
//联合变量的定义
union Un u;
//计算两个变量的大小
printf("%d\n", sizeof(u));
return 0;
}
结果是4,因为是联合体(共用)。我们可以再试一下~( 见联合体的特点 ):
2.联合体的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
//联合类型的声明
union Un
{
char c;//1
int i;//4
};
int main()
{
//联合变量的定义
union Un u;
计算两个变量的大小
//printf("%d\n", sizeof(u));
printf("%p\n", &u);
printf("%p\n", &(u.c));
printf("%p\n", &(u.i));
return 0;
}
结果:我们发现结果是一模一样的。这说明了联合体(共用体)是共用一块大空间的。
就是图示这样:
面试题
判断当前计算机的大小端
我们以前会这样写代码去判断大小端:
int main()
{
int a = 1;
//0x 00 00 00 01
//判断计算机大小端
//低 -----------------> 高
//01 00 00 00 - 小端存储
//00 00 00 01 - 大端存储
char* p = (char*)&a;
if (*p == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
学了联合体,我们就可以利用它的特点这样去判断计算机的大小端:
int main()
{
int a = 1;
//0x 00 00 00 01
//判断计算机大小端
//低 -----------------> 高
//01 00 00 00 - 小端存储
//00 00 00 01 - 大端存储
//联合类型的声明
union Un
{
char c;//1
int i;//4
}u;
u.i = 1;
if (u.c == 1)//因为是联合体,所以拿出第一个字节去判断即可
{
printf("小端\n");
}
else
printf("大端\n");
return 0;
}
当然,为了美观我们也可以将其封装成一个函数:
#include<stdio.h>
int check_sys()
{
//联合类型的声明
union Un
{
char c;//1
int i;//4
}u;
u.i = 1;
return u.c;
}
int main()
{
int a = 1;
//0x 00 00 00 01
//判断计算机大小端
//低 -----------------> 高
//01 00 00 00 - 小端存储
//00 00 00 01 - 大端存储
if (check_sys() == 1)//因为是联合体,所以拿出第一个字节去判断即可
{
printf("小端\n");
}
else
printf("大端\n");
return 0;
}
3.联合体的应用场景
联合体应用于成员间 不同时出现(即: 用你了,就不用我 )。
就比如:学校教务系统~,老师、学生都可以登录。那么大概率情况下,老师就是老师,学生就是学生,不可能有既是老师,又是学生的情况。
这个时候,就可以应用联合体。
以后具体工程中,再慢慢领会即可~~~~~~~~~~~~~~~
4.联合体大小的计算
同时满足这个两个条件:
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
举个例子,比如思考如下结果:
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;
}
注释解释:
#include<stdio.h>
union Un1
{
char c[5];//共5个字节,而char类型对齐数是1,所以最终对齐数还是 1
int i;//共4个字节,VS编译器默认对齐数是8,取较小对齐数,所以最终对齐数是 4
};
//想取联合体中的最大成员大小作为联合体大小?
//err
//最大成员大小是5,不满足 最大成员大小(5)是最大对齐数(4)的整数倍
//所以,对齐到最大对齐数(4)的整数倍
//联合体大小是 8
union Un2
{
short c[7];//共14个字节,而short类型对齐数是2,所以最终对齐数还是 2
int i;//共4个字节,VS编译器默认对齐数是8,取较小对齐数,所以最终对齐数是 4
};
//想取联合体中的最大成员大小作为联合体大小?
//err
//最大成员大小是14,不满足 最大成员大小(14)是最大对齐数(4)的整数倍
//所以,对齐到最大对齐数(4)的整数倍
//联合体大小是 16
int main()
{
printf("%d\n", sizeof(union Un1));//8
printf("%d\n", sizeof(union Un2));//16
return 0;
}