1.结构体
1.1结构体类型的基础知识
结构体类型是一些值的集合,这些值被称为成员变量,成员变量可以是不同类型的变量。
1.2结构体类型的声明
结构体的声明格式如下:
struct tag//tag表示标签名
{
member-list;//成员列表
//由1或者多个成员组成
}variable-list;//变量列表
//定义1或者多个结构体变量
结构体声明举例如下
struct Stu
{
char name[20];
int ID;
}s;//分号不能丢
1.3结构体特殊声明
匿名结构体
struct 匿名结构体声明省略了tag(标签名)
{
int i;
char ch;
}x;
struct //匿名结构体声明省略了tag(标签名)
{
int i;
char ch
}*p;
上述代码中,声明了两个匿名结构体,分别定义了两个匿名结构体类型变量,struct x 和 struct* p。这两个匿名结构体变量只能使用一次。
//在上面代码的基础上,解释下面的代码的合法性
p=&x
[外链图片转存失败,源站可能有防盗在这里插入!链机制,建描述]议将图片上https://传(imbe.csdnimg.cn/bc9gYGW1502aa2cf4edea95e978d4bbb.png302)(img src=“https://img-blog.csdnimg.cn/260f8cd5c4e244369fb0fb358217b574.png” width=“60%”>)]< img src=“https://img-blog.csdnimg.cn/bc91502aa2cf4edea985e97dd2842bbb.png” width=“60%”>
p=&x这段代码是非法的,通过编译代码可知,p = &x这段代码,在编译器看来是不兼容的。虽然代码可以正常的运行,但是这并不代码代码是正确的。我们应该避免的去写这类隐形问题的代码。
1.4结构体的自引用
代码1
struct Node
{
int data;
struct Node next;
};
//这样自引用是否可行?
结构体中是否能够自己调用自己呢?
答案是不可以的。这样自引用是不行的,因为这样声明的结构体我们无从得知它的具体大小。
代码2
struct Node
{
int data;
struct Node* next;
};
正确的结构体自引用必须是在结构体中,通过同类型指针的形式来进行。因为这样结构体的大小才能够确定。
代码3
typedef struct Node
{
int data;
Node* next;
}Node;
//这样是否可行呢?
这样显然是不行的,这里对struct Node类型结构体进行重命名,但是代码3在这份声明里就直接使用了重命名后的Node。这样是非法的。正确代码如代码4.
代码4
typedef struct Node
{
int data;
struct Node* next;
}Node;
在此声明后便可以使用Node来表示struct Node这个结构体。
1.5结构体变量的定义和初始化
struct stu
{
char ch;
int i;
}s1;//定义是全局的结构体变量。
struct stu s2={'c',10};//定义并初始化。
struct Node
{
struct Node* nex;
struct stu;
}p = {NULL,{'g', 23}};//嵌套定义并初始化
int main()
{
//定义的是巨变的结构体变量
struct stu s4 = {.i = 20, .ch = 'x'};//乱序初始化
return 0;
}
1.6结构体内存对齐
什么是结构体内存对齐?
结构体内存对齐指的是通过消耗空间换取时间效率的一种内存访问方式。
下面将介绍结构体内存对齐的规则:
1、第一个成员的地址与结构体变量的起始地址偏移量为0。
2、其他成员要对齐到对应的对齐数处,取决于该成员的自身的大小所能够整除的倍数处。如int类型成员的对齐数就是基于0偏移量后4的倍数。
对齐数也就是:编译器默认对齐数与成员自身大小,两者中的较小值。 VS2019:默认对齐数是8。 Linux gcc:无默认对齐数。
3、结构体的大小必须是最大对齐数(成员对齐数中的最大值)的整数倍。
4、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面通过例题来对此知识点进行巩固。
//练习1
struct s1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct s1));
return 0;
}
//练习2
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S2));
return 0;
}
//练习3
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S3));
return 0;
}
//练习4-结构体嵌套问题
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
补充:从练习1和练习2中不难发现在没有特殊情况下,可以尽量将对其数较小的成员变量往前放,这样可以节省一定的空间。
1.7修改默认对齐数
当需要设计对结构体对齐数有特殊要求时,可以使用#pargma指令修改对齐数。
这里介绍修改默认对齐数指令:#pragma pack(要修改的对齐数)。若为#pragma pack()表示恢复默认对齐数。
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
1.8结构体传参
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;
}
结论:在结构体传参传结构体名,当结构体过大时,传参压栈时,效率会下降。而传结构体的地址,可以有效的避免,因为结构体过大而导致的性能开销。所以,结构体传参建议使用传指针的方式。
2.位段
2.1什么是位段
位段就是结构体成员以二进制位来进行对数据的存储和使用。位段是结构体可以实现的一种能力。位段用来节省空间的。
位段的声明和结构体类似,但是有两点不同:
1、位段的成员必须是整型类型数据。如int 、unsigned int、char、unsigned char。char类型在内存中是以ASCII码存储的,所以本质上也是整型类型。
2、位段成员名后有一个冒号和一个数字。
2.2位段的声明
struct A
{
int _a : 2;//2表示占用2个二进制位
int _b : 5;
int _c : 10;
int _d : 30;
};
2.3位段在内存中的分配
上面我通过代码举例了一个位段A,它所占内存空间的大小是多少呢?
这里我就简单介绍位段在内存中的分配,便可以一探究竟。
位段的内存分配规则:
1、位段的成员必须是整型类型
2、位段在空间开辟上,是根据实际需求以4个字节(int)或者1个字节(char)的形式来进行申请空间。
3、位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
//通过下面代码举例
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;
//空间是如何开辟的?
注:上述代码在不同的环境下的结果可能会有所不同。当前作者使用的环境为VS2019。
2.4位段的跨平台问题
1、 int 位段被当成有符号数还是无符号数是不确定的。这是因为C语言标准没有明确规定。
2、不同的机器环境下,位段的最大数是不确定的,如32位机器的最大为段数就是32,而在16位机器下,位段的最大数为16,若在16位机器上运行位段数为27的程序,这将必然导致程序产生不可预知的错误。
3、位段中的成员在内存中申请空间是从低地址处向高地址处申请,还是从高地址处向低地址处申请。这是取决于编译器的。C语言标准中是没有相关规定的。
4、当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。在C语言的标准中也没有具体地规定。
2.5位段的应用
上图是一个网络网络协议的封装,这是位段的一个应用场景。为什么需要用位段来封装呢?
当数据在网络中进行传输时,使用位段进行封装可以有效地节省带宽的压力了,提升数据在网络中传输的效率。
3.枚举
3.1枚举的定义
枚举顾名思义就是值分别列举出来的一种自定义类型。枚举类型默认不初始化的情况下是从0开始依次加1。
enum Day//星期
{
Mon,//0
Tues,//1
Wed,//2
Thur,//3
Fri,//4
Sat,//5
Sun//6
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
这里我举几个特殊的例子
//例1,默认不给定值进行初始化
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
int main()
{
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
return 0;
}
//例2,给定值进行初始化
enum Sex//性别
{
MALE = 2,
FEMALE = 6,
SECRET = 8
};
int main()
{
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
return 0;
}
//例3,不完全给定值进行初始化
enum Sex//性别
{
MALE ,
FEMALE = 6,
SECRET
};
int main()
{
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
return 0;
}
枚举常量可以指定值进行初始化,若未指定值,起始位置为0,然后其他位置的值为上一个枚举常量的值+1。
3.2枚举常量的优点
在前面的学习中,我们还简单接触了#define定义的常量。那么枚举常量的优点是什么呢?
1、枚举常量可以增加代码的可读性和可维护性。
2、枚举常量相比于#define定义的常量来说,有类型检查,增加了代码的安全性。
3、使用方便,一次可以定义多个常量。还可以对枚举类型进行调试。
3.3枚举类型的使用
enum Color//颜色
{
RED=5,
GREEN=6,
BLUE=7
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //ok??
初始化枚举类型变量必须使用枚举常量,这样左右两边的类型值才能够配对。若直接通过值大小一样的整数来初始化,虽然在部分C语言的编译器下可以跑得过去,但这绝不是一个好的习惯。
4联合体
4.1联合体的定义
联合体是一种特殊的自定义类型。联合体成员特征是该自定义类型的变量是共用同一块空间的。联合体又称为共用体。
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un;
return 0;
}
4.2联合体的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
union Un
{
int i;
char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
4.3联合体大小的计算
1、联合体的大小至少是最大的成员体的大小。
2、当最大成员体的大小不是最大对齐数的整数倍时,则要对齐到对齐数的最大整数倍处。
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));
}