第10章 结构体总结
在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。结构体的定义形式为:
1 2 3 | struct 结构体名{ 结构体所包含的变量或数组 }; |
结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member)。请看下面的一个例子:
1 2 3 4 5 6 7 | struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在学习小组 float score; //成绩 }; |
结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。
结构体也是一种数据类型,它由程序员自己定义,可以包含多个其他类型的数据。 既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
1 | struct stu stu1, stu2; |
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct不能少。
1 2 3 4 5 6 7 | struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在学习小组 float score; //成绩 } stu1, stu2 |
如果只需要 stu1、stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。如下所示:
1 2 3 4 5 6 7 | struct { //没有写 stu char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在学习小组 float score; //成绩 } stu1, stu2; |
理论上讲结构体的各个成员在内存中是连续存储的,和数组非常类似,例如上面的结构体变量 stu1、stu2 的内存分布如下图所示,共占用 4+4+4+1+4 = 17 个字节。

但是在编译器的具体实现中,各个成员之间可能会存在缝隙(个人:字节对齐),对于 stu1、stu2,成员变量 group 和 score 之间就存在 3 个字节的空白填充(见下图)。这样算来,stu1、stu2 其实占用了 17 + 3 = 20 个字节。
关于成员变量之间存在“裂缝”的原因,我们将在《C语言和内存》专题中的《C语言内存对齐,提高寻址效率》一节中详细讲解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <stdio.h> int main(){ struct { char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } stu1; //给结构体成员赋值 stu1.name = "Tom" ; stu1.num = 12; stu1.age = 18; stu1.group = 'A' ; stu1.score = 136.5; //读取结构体成员的值 printf ( "%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n" , stu1.name, stu1.num, stu1.age, stu1.group, stu1.score); return 0; } |
上面的代码在vstudio2022上运行会报错,
修改程序后,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <stdio.h> int main() { struct { const char * name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } stu1; //给结构体成员赋值 stu1.name = "Tom" ; stu1.num = 12; stu1.age = 18; stu1.group = 'A' ; stu1.score = 136.5; //读取结构体成员的值 printf ( "%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n" , stu1.name, stu1.num, stu1.age, stu1.group, stu1.score); return 0; } |
除了可以对成员进行逐一赋值,也可以在定义时整体赋值,(个人:前面那种是先定义,后初始化,这里是定义时直接初始化)例如:
1 2 3 4 5 6 7 | struct { char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } stu1, stu2 = { "Tom" , 12, 18, 'A' , 136.5 }; |
不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值,这和数组的赋值非常类似。(个人:这里结构体定义时的直接初始化采用的是列表初始化,和数组定义时直接初始化采用的是列表初始化一样的方式)
需要注意的是,
- 结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;
- 结构体变量才包含了实实在在的数据,需要内存空间来存储。
定义结构体数组和定义结构体变量的方式类似,请看下面的例子:
1 2 3 4 5 6 7 | struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } class [5]; |
结构体数组在定义的同时也可以初始化,(个人:采用列表初始化的形式),例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } class [5] = { { "Li ping" , 5, 18, 'C' , 145.0}, { "Zhang ping" , 4, 19, 'A' , 130.5}, { "He fang" , 1, 18, 'A' , 148.5}, { "Cheng ling" , 2, 17, 'F' , 139.0}, { "Wang ming" , 3, 17, 'B' , 144.5} }; |
当对数组中全部元素赋值时,也可不给出数组长度,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } class [] = { { "Li ping" , 5, 18, 'C' , 145.0}, { "Zhang ping" , 4, 19, 'A' , 130.5}, { "He fang" , 1, 18, 'A' , 148.5}, { "Cheng ling" , 2, 17, 'F' , 139.0}, { "Wang ming" , 3, 17, 'B' , 144.5} }; |
结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&。
还应该注意,结构体和结构体变量是两个不同的概念:
- 结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;
- 结构体变量才包含实实在在的数据,才需要内存来存储。
下面的写法是错误的,不可能去取一个结构体名的地址,也不能将它赋值给其他变量:
1 2 | struct stu *pstu = &stu; struct stu *pstu = stu; |
通过结构体指针可以获取结构体成员,
-
1
(*pointer).memberName
-
1
pointer->memberName
在实际编程中,有些数据的取值往往是有限的,只能是非常少量的整数,并且最好为每个值都取一个名字,以方便在后续代码中使用,C语言提供了一种枚举(Enum)类型,能够列出所有可能的取值,并给它们取一个名字。
枚举类型的定义形式为:
1 | enum typeName{ valueName1, valueName2, valueName3, ...... }; |
enum是一个新的关键字,专门用来定义枚举类型,这也是它在C语言中的唯一用途;typeName是枚举类型的名字;valueName1, valueName2, valueName3, ......是每个值对应的名字的列表。(个人:也就是所能取的每个值都有一个对应的名字)
注意最后的;不能少。例如,列出一个星期有几天:
1 | enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun }; |
可以看到,我们仅仅给出了名字,却没有给出名字对应的值,这是因为枚举值默认从 0 开始,往后逐个加 1(递增);也就是说,week 中的 Mon、Tues ...... Sun 对应的值分别为 0、1 ...... 6。
我们也可以给每个名字都指定一个值:
1 | enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 }; |
更为简单的方法是只给第一个名字指定值:
1 | enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun }; |
这样枚举值就从 1 开始递增,跟上面的写法是等效的。
枚举是一种类型,通过它可以定义枚举变量:
1 | enum week a, b, c; |
也可以在定义枚举类型的同时定义变量:
1 | enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a, b, c; |
有了枚举变量,就可以把列表中的值赋给它:(个人:给枚举变量赋的值是这个枚举类型列表中的能够取的值的名字,但是不能直接给枚举类型变量初始化一个int类型的值)
1 2 | enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun }; enum week a = Mon, b = Wed, c = Sat; |
或者:
1 | enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat; |
【示例】判断用户输入的是星期几。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <stdio.h> int main(){ enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day; scanf ( "%d" , &day); switch (day){ case Mon: puts ( "Monday" ); break ; case Tues: puts ( "Tuesday" ); break ; case Wed: puts ( "Wednesday" ); break ; case Thurs: puts ( "Thursday" ); break ; case Fri: puts ( "Friday" ); break ; case Sat: puts ( "Saturday" ); break ; case Sun: puts ( "Sunday" ); break ; default : puts ( "Error!" ); } return 0; } |
注意,代码scanf
(
"%d"
, &day);这里使用的是%d,说明要求输入的是一个整数,
枚举和宏其实非常类似:
- 宏在预处理阶段将名字替换成对应的值,
- 枚举在编译阶段将名字替换成对应的值。
我们可以将枚举理解为编译阶段的宏。
对于上面的代码,在编译的某个时刻会变成类似下面的样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <stdio.h> int main(){ enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day; scanf ( "%d" , &day); switch (day){ case 1: puts ( "Monday" ); break ; case 2: puts ( "Tuesday" ); break ; case 3: puts ( "Wednesday" ); break ; case 4: puts ( "Thursday" ); break ; case 5: puts ( "Friday" ); break ; case 6: puts ( "Saturday" ); break ; case 7: puts ( "Sunday" ); break ; default : puts ( "Error!" ); } return 0; } |
Mon、Tues、Wed 这些名字都被替换成了对应的数字。
这意味着,Mon、Tues、Wed 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&取得它们的地址。这就是枚举的本质。
case 关键字后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量,正是由于 Mon、Tues、Wed 这些名字最终会被替换成一个整数,所以它们才能放在 case 后面。
枚举类型变量需要存放的是一个整数,我猜测它的长度和int应该相同,下面来验证一下:
1 2 3 4 5 6 7 | #include <stdio.h> int main(){ enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day = Mon; printf ( "sizeof(enum week):%d,\nsizeof(day):%d,\nsizeof(1):%d,\nsizeof(Mon)%d,\nsizeof(int)%d,\n" , sizeof ( enum week), sizeof (day), sizeof (1), sizeof (Mon), sizeof ( int ) ); return 0; } |
结构体和共用体的区别在于:
- 结构体的各个成员会占用不同的内存,互相之间没有影响;
- 而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
- 结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),
- 共用体占用的内存等于最长的成员占用的内存。
- 共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
共用体也是一种自定义类型,可以通过它来创建变量,例如:
1 2 3 4 5 6 | union data{ int n; char ch; double f; }; union data a, b, c; |
上面是先定义共用体,再创建变量,也可以在定义共用体的同时创建变量:
1 2 3 4 5 | union data{ int n; char ch; double f; } a, b, c; |
如果不再定义新的变量,也可以将共用体的名字省略:
1 2 3 4 5 | union { int n; char ch; double f; } a, b, c; |
共用体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、b、c)也占用 8 个字节的内存,请看下面的演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <stdio.h> union data{ int n; char ch; short m; }; int main(){ union data a; printf ( "%d, %d\n" , sizeof (a), sizeof ( union data) ); a.n = 0x40; printf ( "%X, %c, %hX\n" , a.n, a.ch, a.m); a.ch = '9' ; printf ( "%X, %c, %hX\n" , a.n, a.ch, a.m); a.m = 0x2059; printf ( "%X, %c, %hX\n" , a.n, a.ch, a.m); a.n = 0x3E25AD54; printf ( "%X, %c, %hX\n" , a.n, a.ch, a.m); return 0; } |
运行结果:
1 2 3 4 5 | 4, 4 40, @, 40 39, 9, 39 2059, Y, 2059 3E25AD54, T, AD54 |
这段代码不但验证了共用体的长度,还说明共用体成员之间会相互影响,修改一个成员的值会影响其他成员。
要想理解上面的输出结果,弄清成员之间究竟是如何相互影响的,就得了解各个成员在内存中的分布。以上面的 data 为例,各个成员在内存中的分布如下:
成员 n、ch、m 在内存中“对齐”到一头,(个人:这里的对齐是指的成员的起点位置对齐,上图中上面为低内存位置,下面为高内存位置)
- 对 ch 赋值修改的是前一个字节,
- 对 m 赋值修改的是前两个字节,
- 对 n 赋值修改的是全部字节。
也就是说,ch、m 会影响到 n 的一部分数据,而 n 会影响到 ch、m 的全部数据。
上图是在绝大多数 PC 机上的内存分布情况,如果是 51 单片机,情况就会有所不同:
(个人:这里字符'9'的ASCII码为57,对应的16进制表示为0x39,可以看到,不管是大端还是小端,成员在内存中都是对齐到起点位置)
为什么不同的机器会有不同的分布情况呢?这跟机器的存储模式有关,我们将在教程《大端小端以及判别方式》一节中展开探讨。
共用体在一般的编程中应用较少,在单片机中应用较多。对于 PC 机,经常使用到的一个实例是: 现有一张关于学生信息和教师信息的表格。学生信息包括姓名、编号、性别、职业、分数,教师的信息包括姓名、编号、性别、职业、教学科目。请看下面的表格:
Name
|
Num
|
Sex
|
Profession
|
Score / Course
|
---|---|---|---|---|
HanXiaoXiao |
501
|
f
|
s
|
89.5
|
YanWeiMin |
1011
|
m
|
t
|
math
|
LiuZhenTao |
109
|
f
|
t
|
English
|
ZhaoFeiYan |
982
|
m
|
s
|
95.0
|
f 和 m 分别表示女性和男性,s 表示学生,t 表示教师。可以看出,学生和教师所包含的数据是不同的。现在要求把这些信息放在同一个表格中,并设计程序输入人员信息然后输出。
如果把每个人的信息都看作一个结构体变量的话,那么教师和学生的前 4 个成员变量是一样的,第 5 个成员变量可能是 score 或者 course。当第 4 个成员变量的值是 s 的时候,第 5 个成员变量就是 score;当第 4 个成员变量的值是 t 的时候,第 5 个成员变量就是 course。
经过上面的分析,我们可以设计一个包含共用体的结构体,请看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <stdio.h> #include <stdlib.h> #define TOTAL 4 //人员总数 struct { char name[20]; int num; char sex; char profession; union { float score; char course[20]; } sc; } bodys[TOTAL]; int main(){ int i; //输入人员信息 for (i=0; i<TOTAL; i++){ printf ( "Input info: " ); scanf ( "%s %d %c %c" , bodys[i].name, &(bodys[i].num), &(bodys[i].sex), &(bodys[i].profession)); if (bodys[i].profession == 's' ){ //如果是学生 scanf ( "%f" , &bodys[i].sc.score); } else { //如果是老师 scanf ( "%s" , bodys[i].sc.course); } fflush (stdin); } //输出人员信息 printf ( "\nName\t\tNum\tSex\tProfession\tScore / Course\n" ); for (i=0; i<TOTAL; i++){ if (bodys[i].profession == 's' ){ //如果是学生 printf ( "%s\t%d\t%c\t%c\t\t%f\n" , bodys[i].name, bodys[i].num, bodys[i].sex, bodys[i].profession, bodys[i].sc.score); } else { //如果是老师 printf ( "%s\t%d\t%c\t%c\t\t%s\n" , bodys[i].name, bodys[i].num, bodys[i].sex, bodys[i].profession, bodys[i].sc.course); } } return 0; } |
大端和小端是指数据在内存中的存储模式,它由 CPU 决定:(个人:endian,字节存储顺序)
- 大端模式(Big-endian)是指将数据的低位(比如 1234 中的 34 就是低位)放在内存的高地址上,而数据的高位(比如 1234 中的 12 就是高位)放在内存的低地址上。这种存储模式有点儿类似于把数据当作字符串顺序处理,地址由小到大增加,而数据从高位往低位存放。
- 小端模式(Little-endian)是指将数据的低位放在内存的低地址上,而数据的高位放在内存的高地址上。这种存储模式将地址的高低和数据的大小结合起来,高地址存放数值较大的部分,低地址存放数值较小的部分,这和我们的思维习惯是一致,比较容易理解。
为什么有大小端模式之分
计算机中的数据是以字节(Byte)为单位存储的,每个字节都有不同的地址。
现代 CPU 的位数(可以理解为一次能处理的数据的位数)都超过了 8 位(一个字节),PC机、服务器的 CPU 基本都是 64 位的,嵌入式系统或单片机系统仍然在使用 32 位和 16 位的 CPU。
对于一次能处理多个字节的CPU,必然存在着如何安排多个字节的问题,也就是大端和小端模式。以 int 类型的 0x12345678 为例,它占用 4 个字节,
- 如果是小端模式(Little-endian),那么在内存中的分布情况为(假设从地址 0x 4000 开始存放):
内存地址 | 0x4000 | 0x4001 | 0x4002 | 0x4003 |
---|---|---|---|---|
存放内容 | 0x78 | 0x56 | 0x34 | 0x12 |
- 如果是大端模式(Big-endian),那么分布情况正好相反:
内存地址 | 0x4000 | 0x4001 | 0x4002 | 0x4003 |
---|---|---|---|---|
存放内容 | 0x12 | 0x34 | 0x56 | 0x78 |
- 我们的 PC 机上使用的是 X86 结构的 CPU,它是小端模式;(个人:低位放在低内存位置)
- 51 单片机是大端模式;(个人:低位放在高内存位置)
- 很多 ARM、DSP 也是小端模式(部分 ARM 处理器还可以由硬件来选择是大端模式还是小端模式)。
借助共用体,我们可以检测 CPU 是大端模式还是小端模式,请看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <stdio.h> int main(){ union { int n; char ch; } data; data.n = 0x00000001; //也可以直接写作 data.n = 1; if (data.ch == 1){ printf ( "Little-endian\n" ); } else { printf ( "Big-endian\n" ); } return 0; } |
共用体的各个成员是共用一段内存的。1 是数据的低位,如果 1 被存储在 data 的低字节,就是小端模式,这个时候 data.ch 的值也是 1。如果 1 被存储在 data 的高字节,就是大端模式,这个时候 data.ch 的值就是 0。
C语言位域
有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。例如,开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。。
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。请看下面的例子:
1 2 3 4 5 | struct bs{ unsigned m; unsigned n: 4; unsigned char ch: 6; } |
:后面的数字用来限定成员变量占用的位数。
- 成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存。
- 成员n、ch 被:后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4、6 位(Bit)的内存。
n、ch 的取值范围非常有限,数据稍微大些就会发生溢出,请看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <stdio.h> int main(){ struct bs{ unsigned m; unsigned n: 4; unsigned char ch: 6; } a = { 0xad, 0xE, '$' }; //第一次输出 printf ( "%#x, %#x, %c\n" , a.m, a.n, a.ch); //更改值后再次输出 a.m = 0xb8901c; a.n = 0x2d; a.ch = 'z' ; printf ( "%#x, %#x, %c\n" , a.m, a.n, a.ch); return 0; } |
对于 n 和 ch,第一次输出的数据是完整的,第二次输出的数据是残缺的。
- 第一次输出时,n、ch 的值分别是 0xE、0x24('$' 对应的 ASCII 码为 0x24),换算成二进制是 1110、10 0100,都没有超出限定的位数,能够正常输出。
- 第二次输出时,n、ch 的值变为 0x2d、0x7a('z' 对应的 ASCII 码为 0x7a),换算成二进制分别是 10 1101、111 1010,都超出了限定的位数。超出部分被直接截去,剩下 1101、11 1010,换算成十六进制为 0xd、0x3a(0x3a 对应的字符是 :)。
C语言标准规定,位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度,:后面的数字不能超过这个长度。
例如上面的 bs,
- n 的类型是 unsigned int,长度为 4 个字节,共计 32 位,那么 n 后面的数字就不能超过 32;
- ch 的类型是 unsigned char,长度为 1 个字节,共计 8 位,那么 ch 后面的数字就不能超过 8。
我们可以这样认为,位域技术就是在成员变量所占用的内存中选出一部分位宽来存储数据。
C语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int(int 默认就是 signed int);到了 C99,_Bool 也被支持了。但编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型,所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持。
位域的存储
C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间。
位域的具体存储规则如下:
1) 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。(个人:也就是新的存储位置地址符合结构体的成员变量的内存对齐原则,这里表达的应该是这个意思,但说的太随意)
以下面的位域 bs 为例:
1 2 3 4 5 6 7 8 9 10 | #include <stdio.h> int main(){ struct bs{ unsigned m: 6; unsigned n: 12; unsigned p: 4; }; printf ( "%d\n" , sizeof ( struct bs)); return 0; } |
- m、n、p的类型都是unsigned int,sizeof的结果为4个字节(Byte),也即32个位(Bit)。m、n、p 的位宽之和为6+12+4 = 22,小于32,所以它们会挨着存储,中间没有缝隙。sizeof(struct bs) 的大小之所以为 4,而不是 3,是因为要将内存对齐到 4 个字节,以便提高存取效率,
- 如果将成员 m 的位宽改为 22,那么输出结果将会是 8,因为 22+12 = 34,大于 32,n 会从新的位置开始存储,相对 m 的偏移量是 sizeof(unsigned int),也即 4 个字节。(个人:也就是新的存储位置地址符合结构体的成员变量的内存对齐原则)
- 如果再将成员 p 的位宽也改为 22,那么输出结果将会是 12,三个成员都不会挨着存储。
2) 当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会。(个人:我实验的结果,clion+mingw64和vstudio2022都不会压缩)
请看下面的位域 bs:
1 2 3 4 5 6 7 8 9 10 | #include <stdio.h> int main(){ struct bs{ unsigned m: 12; unsigned char ch: 4; unsigned p: 4; }; printf ( "%d\n" , sizeof ( struct bs)); return 0; } |
在 VC/VS 下的运行结果为 12,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)。
m 、ch、p 的长度分别是 4、1、4 个字节,共计占用 9 个字节内存,为什么在 VC/VS 下的输出结果却是 12 呢?这个疑问将在《C语言和内存》专题的《C语言内存对齐,提高寻址效率》一节中为您解开。
3) 如果成员之间穿插着非位域成员,那么不会进行压缩。例如对于下面的 bs:
1 2 3 4 5 | struct bs{ unsigned m: 12; unsigned ch; unsigned p: 4; }; |
在各个编译器下 sizeof 的结果都是 12。
1 2 3 4 5 6 7 8 9 10 | #include <stdio.h> int main(){ struct bs{ unsigned m: 12; unsigned ch; unsigned p: 4; }; printf ( "%d\n" , sizeof ( struct bs)); return 0; } |
通过上面的分析,我们发现,位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号。
无名位域
位域成员可以没有名称,只给出数据类型和位宽,如下所示:
1 2 3 4 5 | struct bs{ int m: 12; int : 20; //该位域成员不能使用 int n: 4; }; |
无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。
上面的例子中,如果没有位宽为 20 的无名成员,m、n 将会挨着存储,sizeof(struct bs) 的结果为 4;有了这 20 位作为填充,m、n 将分开存储,sizeof(struct bs) 的结果为 8。
C语言中的位运算是根据内存中的二进制位进行运算的,而不是数据的二进制形式;
以-9&5为例,-9 的在内存中的存储和 -9 的二进制形式截然不同:
-9 & 5可以转换成如下的运算:
-9 & 5的结果是 5。
对上面的分析进行检验,
1 2 3 4 5 6 | #include <stdio.h> int main(){ int n = 0X8FA6002D; printf ( "-9 & 5结果为:%d\n" , -9 & 5); return 0; } |
右移运算符>>用来把操作数的各个二进制位全部右移若干位,低位丢弃,高位补 0 或 1。如果数据的最高位是 0,那么就补 0;如果最高位是 1,那么就补 1。
对上面的结果进行校验,
1 2 3 4 5 | #include <stdio.h> int main(){ printf ( "%d, %d\n" , 9>>3, (-9)>>3 ); return 0; } |
用C语言对数据或文件内容进行加密
数据加密解密是一个常用的功能,如果你不希望让别人看到文件中的内容,可以通过密钥(也称”密码“)将文件的内容加密。比如文本文件(.txt),加密前的内容是能够读懂的,加密后的内容是”乱码“,都是一些奇怪的字符,根本无法阅读。
数据加密解密的原理也很简单,就是使用异或运算。请先看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <stdio.h> #include <stdlib.h> int main(){ char plaintext = 'a' ; // 明文 char secretkey = '!' ; // 密钥 char ciphertext = plaintext ^ secretkey; // 密文 char decodetext = ciphertext ^ secretkey; // 解密后的字符 char buffer[9]; printf ( " char ASCII\n" ); // itoa()用来将数字转换为字符串,可以设定转换时的进制(基数) // 这里将字符对应的ascii码转换为二进制 printf ( " plaintext %c %7s\n" , plaintext, itoa(plaintext, buffer, 2)); printf ( " secretkey %c %7s\n" , secretkey, itoa(secretkey, buffer, 2)); printf ( "ciphertext %c %7s\n" , ciphertext, itoa(ciphertext, buffer, 2)); printf ( "decodetext %c %7s\n" , decodetext, itoa(decodetext, buffer, 2)); return 0; } |
运行结果:
看到了吗,plaintext 与 decodetext相同,也就是说,两次异或运算后还是原来的结果。(个人:异或运算满足结合律,自己和自己异或的结果为0,任何数和0异或的结果为它本身)
这就是加密的关键技术:
- 通过一次异或运算,生成密文,密文没有可读性,与原文风马牛不相及,这就是加密;
- 密文再经过一次异或运算,就会还原成原文,这就是解密的过程;
- 加密和解密需要相同的密钥,如果密钥不对,是无法成功解密的。
上面的加密算法称为对称加密算法,加密和解密使用同一个密钥。
如果加密和解密的密钥不同,则称为非对称加密算法。在非对称算法中,加密的密钥称为公钥,解密的密钥称为私钥,只知道公钥是无法解密的,还必须知道私钥。
注意:程序中的 itoa() 位于 stdlib.h 头文件,它并不是一个标准的C函数,只有Windows下有。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
2021-05-08 文本分类算法之Fasttext 模型