C语言结构体
结构体的基本概念
C 语言结构体(Struct)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、 char、 float等基本类型组成的。你可以认为结构体是一种聚合类型。
结构体的定义形式为:
1 | struct 结构体名{ |
结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member) 。例如:
1 | struct stu{ |
stu 为结构体名,它包含了 5 个成员,分别是 name、 num、 age、 group、 score。结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。
1 | 注意大括号后面的分号;不能少,这是一条完整的语句。 |
结构体也是一种数据类型,它由程序员自己定义,可以包含多个其他类型的数据。
像 int、 float、 char 等是由 C 语言本身提供的数据类型,不能再进行分拆,我们称之为基本数据类型;而结构体可
以包含多个基本类型的数据,也可以包含其他的结构体,我们将它称为复杂数据类型或构造数据类型。
结构体变量
既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
1 | struct stu stu1, stu2; |
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字 struct 不能少。
也可以在定义结构体的同时定义结构体变量:
1 | struct stu{ |
将变量放在结构体定义的最后即可。
如果只需要 stu1、 stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:
1 | struct{ //没有写 stu |
理论上讲结构体的各个成员在内存中是连续存储的,和数组非常类似,例如上面的结构体变量 stu1、 stu2 的内存
分布如下图所示,共占用 4+4+4+1+4 = 17 个字节。

但是在编译器的具体实现中,各个成员之间可能会存在缝隙,对于 stu1、 stu2,成员变量 group 和 score 之间就存在 3 个字节的空白填充(见下图)。这样算来, stu1、 stu2 其实占用了 17 + 3 = 20 个字节

成员的获取和赋值
结构体使用点号.获取单个成员。获取结构体成员的一般格式为 :
1 | 结构体变量名.成员名; |
例如:
1 |
|
运行结果
Tom 的学号是 12,年龄是 18,在 A 组,今年的成绩是 136.5!
除了可以对成员进行逐一赋值,也可以在定义时整体赋值,例如:
1 | struct{ |
不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值,这和数组的赋值非常类似。
需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储 。
结构体数组
所谓结构体数组,是指数组中的每个元素都是一个结构体。在实际应用中, C 语言结构体数组常被用来表示一个拥有相同数据结构的群体,比如一个班的学生、一个车间的职工等。
定义结构体数组和定义结构体变量的方式类似,例如:
1 | struct stu{ |
表示一个班级有 5 个学生。
结构体数组在定义的同时也可以初始化,例如:
1 | struct stu{ |
当对数组中全部元素赋值时,也可不给出数组长度,例如:
1 | struct stu{ |
结构体数组的使用也很简单,例如,获取 Wang ming 的成绩:
1 | class[4].score; |
修改 Li ping 的学习小组:
1 | class[0].group = 'B'; |
结构体指针(指向结构体的指针)
当一个指针变量指向结构体时,我们就称它为结构体指针。 C 语言结构体指针的定义形式一般为:
1 | struct 结构体名 *变量名; |
例如:
1 | //结构体 |
也可以在定义结构体的同时定义结构体指针:
1 | struct stu{ |
注意,结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表
达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&,所以给 pstu 赋值只能写作:
1 | struct stu *pstu = &stu1; |
而不能写作:
1 | struct stu *pstu = stu1; |
还应该注意,结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、 float、 char 这些关键字本身不占用内存一样;结构体变量才包含实实在在的数据,才需要内存来存储。
下面的写法是错误的,不可能去取一个结构体名的地址,也不能将它赋值给其他变量:
1 | struct stu *pstu = &stu; |
获取结构体成员
通过结构体指针可以获取结构体成员,一般形式为:
1 | (*pointer).memberName |
或者:
1 | pointer->memberName |
第一种写法中, .的优先级高于* , ( * pointer)两边的括号不能少。如果去掉括号写作* pointer.memberName,那么就
等效于*(pointer.memberName),这样意义就完全不对了。
第二种写法中,->是一个新的运算符,有了它,可以通过结构体指针直接取得结构体成员;
例如:
1 |
|
运行结果:
Tom的学号是12,年龄是18,在A组,今年的成绩是136.5!
Tom的学号是12,年龄是18,在A组,今年的成绩是136.5!
结构体指针作为函数参数
结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编
译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运
行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。
【示例】计算全班学生的总成绩、平均成绩和以及 140 分以下的人数。
1 |
|
运行结果:
sum=707.50
average=141.50
num_140=2
枚举类型(enum 关键字)
在实际编程中,有些数据的取值往往是有限的,只能是非常少量的整数,并且最好为每个值都取一个名字,以方便在后续代码中使用,比如一个星期只有七天,一年只有十二个月,一个班每周有六门课程等。
以每周七天为例,我们可以使用#define 命令来给每天指定一个名字:
1 |
|
运行结果:
5↙
Friday
#define 命令虽然能解决问题,但也带来了不小的副作用,导致宏名过多,代码松散,看起来总有点不舒服。 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; |
有了枚举变量,就可以把列表中的值赋给它:
1 | enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun }; |
或者:
1 | enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat; |
【示例】判断用户输入的是星期几。
1 |
|
运行结果:
4↙
Thursday
需要注意的两点是:
(1) 枚举列表中的 Mon、 Tues、 Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。
(2) Mon、 Tues、 Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。
枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可
以将枚举理解为编译阶段的宏。
共用体(union 关键字)
共用体(Union)和结构体的语法类似 ,它的定义格式为:
1 | union 共用体名{ |
共用体有时也被称为联合或者联合体,这也是 Union 这个单词的本意。
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
共用体也是一种自定义类型,可以通过它来创建变量,例如:
1 | union data{ |
上面是先定义共用体,再创建变量,也可以在定义共用体的同时创建变量:
1 | union data{ |
如果不再定义新的变量,也可以将共用体的名字省略:
1 | union{ |
共用体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、 b、 c)也占用 8 个字节的内存,例如:
1 |
|
运行结果:
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 单片机,情况就会有所不同:

这是和机器的存储模式,大端小端有关。
共用体的应用
共用体在一般的编程中应用较少,在单片机中应用较多。对于 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 |
|
运行结果:
Input info: HanXiaoXiao 501 f s 89.5↙
Input info: YanWeiMin 1011 m t math↙
Input info: LiuZhenTao 109 f t English↙
Input info: ZhaoFeiYan 982 m s 95.0↙
Name Num Sex Profession Score / Course
HanXiaoXiao 501 f s 89.500000
YanWeiMin 1011 m t math
LiuZhenTao 109 f t English
ZhaoFeiYan 982 m s 95.000000
大端小端以及判别方式
大端和小端是指数据在内存中的存储模式,它由 CPU 决定:
(1) 大端模式( Big-endian) 是指将数据的低位(比如 1234 中的 34 就是低位)放在内存的高地址上,而数据的高位(比如 1234 中的 12 就是高位)放在内存的低地址上。这种存储模式有点儿类似于把数据当作字符串顺序处理,地址由小到大增加,而数据从高位往低位存放。
(2) 小端模式( 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 处理器还可以由硬件来选择是大端模式还是小端模式)。
位域(位段)
有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑, C 语言又提供了一种叫做位域的数据结构。
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。 例如:
1 | struct bs{ |
后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存。成员 n、 ch 被:后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4、 6 位(Bit)的内存。
C 语言标准规定,位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度, :后面的数字不能超过这个长度。
C 语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、 signed int 和unsigned int(int 默认就是 signed int);到了 C99, _Bool 也被支持了。
无名位域
位域成员可以没有名称,只给出数据类型和位宽,如下所示:
1 | struct bs{ |
无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。
位运算
这个在数字电路和单片机学过,C语言的也一样,仅记录。
C 语言提供了六种位运算符:
运算符 | & | | | ^ | ~ | << | >> |
---|---|---|---|---|---|---|
说明 | 按位与 | 按位或 | 按位异或 | 取反 | 左移 | 右移 |