目录
- 0 简介
- 1 结构基础知识
- 1.1 结构声明
- 1.2 结构成员
- 1.3 结构成员的直接访问
- 1.4 结构成员的间接访问
- 1.5 结构的自引用
- 1.6 不完整的声明
- 1.7 结构的初始化
- 2 结构、指针和成员
- 2.1 访问指针
- 2.2 访问结构
- 2.3 访问结构成员
- 2.4 访问嵌套的结构
- 2.5 访问指针成员
- 3 结构的存储分配
- 4 作为函数参数的结构
- 5 位段
- 6 联合
- 6.1 变体记录
- 6.2 联合的初始化
- 7 总结
0 简介
在C语言中,相同类型的数据元素若想放在一起,可以采用数组,那不同类型的元素呢?
有的计算机语言是不会care
这个问题的,比方说python
,毕竟python
本来就是所谓的弱数据类型的语言;C
语言虽然出生较早,但历尽千帆,仍能常年在计算机语言排行榜中名列前茅,其优势之一便是对数据类型和内存的精准把控。而结构和联合,正是这种思想的完美诠释与实践。
在部分的教材和资料中,结构也称为结构体
,联合也称为联合体
,共同体
,共用体
。
正所谓“一花独放不是春,百花齐放春满园”,因其可以容纳多种类型的数据,在实际开发中,结构和联合经常合作发力,为项目开发提供了很大的便利。
本篇内容概览:
1 结构基础知识
聚合数据类型能够同时存储超过一个的单独数据。C提供了两种类型聚合数据类型,数组
和结构
。结构是一些值的集合,这些值称为它的成员。
数组可以元素可以通过下标访问,这只是因为数组的元素长度相同。但是在结构中情况并非如此。由于一个结构的成员可能长度不同,所以不能使用下标来访问他们。
1.1 结构声明
结构体的声明形式如下:
struct tag {member-list} variable-list;
除了结构标签的不完整声明,所有可选部分不能全部省略,至少要出现两个。这句话有点费解 ,只需要看几个例子便可以明白,在实际开发中,结构体常见的声明方式只有两种,将在下面进行说明。
struct {
int a;
char b;
float c;
}x;
这里声明了一个名叫x的变量,包含三个成员,声明没有什么错误,但是因为拓展性不强,在实际开发中并不常用,较为常用的是以下两种。
struct SIMPLE{
int a;
char b;
float c;
};
这种有了标签,就可以避免重复造轮子,有点像C++中创建的对象。对于具有相同属性的事物,只需要创建一次即可,在具体的使用中,可根据自己的需要进行变量的创建。
还有一种更加方便的方法,也就是采用typedef
创建一种新的类型。
typedef struct {
int a;
char b;
float c;
}Simple;
这种方法看起来在定义的时候稍微复杂了一点点,但是在使用的时候方便了一些,举个例子:
#include <stdio.h>
struct SIMPLE{
int a;
char b;
float c;
};
typedef struct {
int a;
char b;
float c;
}Simple;
int main()
{
//定义结构体变量
struct SIMPLE s1;
Simple s2;
//给结构体变量赋值
s1.a = 10;
s1.b = '?';
s1.c = 3.1415;
s2.a = 20;
s2.b = '!';
s2.c = 3.1415;
//打印输出
printf("s1.a = %d\n",s1.a);
printf("s1.b = %c\n",s1.b);
printf("s1.c = %f\n",s1.c);
printf("s2.a = %d\n",s2.a);
printf("s2.b = %c\n",s2.b);
printf("s2.c = %f\n",s2.c);
return 0;
}
运行,打印输出:
可以看到如果采用了typedef
关键字来声明结构体,则在定义结构体变量的时候就可以少写一个struct
关键字,看似不会简洁太多,但是在大型项目开发中,会省去更多的时间。
1.2 结构成员
到目前为止的例子里,我只使用了简单类型的结构成员。但可以在一个结构外部声明的任何变看都可以作为结构的成员。尤其是,结构成员可以是标量、数组、指针甚至是其他结构(标量一般指的是整型或者浮点型的数据)。
比如:
struct COMPLEX(
float f;
int a[20];
long *lp;
struct SIMPLE s;
struct SIMPLE sa[10];
struct SIMPLE *sp;
};
1.3 结构成员的直接访问
结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数,左操作数就是结构变量的名字,右操作数就是需要访问的成员的名字。这个表达式的结果就是指定的成员。例如,考虑下面这个声明:
struct COMPLEX comp;
比方说,表达式
((comp.sa)[4]).c
下标引用和点操作符具有相同的优先级,他们的结合性都是从左向右,所以可以省略所有的括号。变成了下面的表达式
comp.sa[4].c
此二者等效。
所以在访问嵌套的数据类型的时候,与普通的数据元素访问没有什么区别。只需要一层一层访问即可。
1.4 结构成员的间接访问
当我们拥有一个指向结构的指针时,该如何访问这个结构的成员呢?首先就是对指针执行间接访问操作。然后使用点操作符来访问它的成员。但是点操作符的优先级高于间接访问操作符,所以在访问结构的成员的时候就出现了下面的情况。
先定义一个指向结构体的指针:
struct COMPLEX *CP;
然后用访问其元素:
(*cp).f
这样的操作符显然过于繁琐,于是在C语言中就出现了->
操作符,结构体指针可用来访问结构体成员,如下:
cp->f
cp->a
cp->s
表达式1
访问一个浮点数成员,表达式2
访问一个数组名,表达式3
访问一个结构。
1.5 结构的自引用
结构体中包含一个同类型的结构体是否合法呢?答案是否定的。因为这样可以无穷无尽地包含下去,导致程序无法执行。比如:
struct SELF_REF1 {
int a;
struct SELF_REF1 b;
int c;
};
但是结构体中包含指向同类型结构体的指针是合法的,比如:
struct SELF_REF2 {
int a;
struct SELF_REF2 *b;
int c;
};
因为我们仅仅是多了一个指向同类型结构体的指针,不会出现层层包含,无穷无尽的情况。在实际开发中,经常用来实现一些数据结构,比方说链表和树,每个节点都可能指向相同类型的下一个节点(有时不止一个)。
1.6 不完整的声明
通常情况下,我们的声明都是完整的,但若是有两个相互包含的结构体,究竟应该先定义哪一个呢?
这个时候,我们的不完整声明就派上用场了,如下所示:
struct B;
struct A {
struct B *partner;
};
struct B {
struct A *partner;
};
虽然声明了结构体B,但是并未完全声明,因为结构体成员未给出,主要是没法给出,若是给出A
,但A
也尚未定义。于是就有了这样的处理方法。
如此一来,就形成了“你中有我,我中有你”的两个结构体。如下图所示:
1.7 结构的初始化
结构体的初始化方式和数组的初始化很类似。在一对花括号内用逗号分隔,然后再分别对各个成员进行初始化(赋值)即可。如果初始列表的值不够,剩余的结构成员将使用缺省值进行初始化。举个例子:
struct INIT_EX {
int a;
short b[10];
Simple c;
}x = { 10,{1,2,3,4,5},{10,'x', 3.14} };
可以看到成员中有个数组b
,我们仅初始化了起始的5
个元素,其余元素则会采用缺省值进行初始化,一般情况下初始化为0
。如下图所示:
2 结构、指针和成员
直接或者通过指针访问结构体是相当简单的,因为这样的做法和数组非常类似,但是在稍微复杂一点的情形下,我们又该如何访问该结构体成员呢?
为了更好地阐述结构体,结构体指针,结构体成员之间的关系,先定义相关的结构体。
typedef struct {
int a;
short b[2];
} Ex2;
typedef struct {
int a;
char b[3];
Ex2 c;
struct EX *d;
}EX;
再定义相关的结构体变量。
EX x = { 10, "Hi", {5, { -1, 25}}, 0};
EX *px = &x;
2.1 访问指针
来看看px
的含义。
表达式px的右值是整个结构体的内容。如下图所示:
左值很好理解,就是可以接受一个新的值。
2.2 访问结构
要想访问结构,也很简单,直接用间接访问操作符即可,所以表达式*px
的右值就是px所指向的整个结构。
表达式的左值,同样也可以接受新值。
2.3 访问结构成员
访问结构成员也是一样,我们先来访问结构体变量x
中的变量a
和变量b
。
访问a
很简单,可以直接使用表达式px->a
b是个数组,所以px->b
表示的是b首元素的地址,表达式*(px->b)
和px->b[0]
访问该数组第一个元素的值,访问后续元素和数组的访问方式类似。如下图所示:
2.4 访问嵌套的结构
C也是个结构体,要想访问C中的元素,要先通过px->c
访问到c结构体,然后再访问其中的元素即可。
比如,px->c.a
是访问结构体c的a元素,px->c.b
与上述说法一样,是一个指针常量,指向结构体c
中b
数组的首地址,访问b中的元素同样有两种方式,下标访问(px->c.b[0]
)和间接访问(*(px->c.b)
)。如下图所示:
2.5 访问指针成员
现在我们的结构体成员d尚未指向任何结构体,所以先建一个结构体,并把x.d指向它。
EX y = { 20, "mm", {12, { 5, 7}}, 0 };
x.d = &y;
现在y也指向了一个结构,整体变成了这样的结构:
那么要想访问结构体y中的元素,则先要通过px->y
访问到y
结构体。结构体y
中一些元素的访问方法如下:
px->d->a;
px->d->b[0];
px->d->c.a;
px->d->c.b[0];
3 结构的存储分配
结构的存储分配也是一个非常有意思的话题。毕竟C语言偏向底层的语言,很多数据的定义直接关系到分配内存的大小。
例如:
#include<stdio.h>
struct ALIGN1 {
char a;
int b;
char c;
};
struct ALIGN2 {
int b;
char a;
char c;
};
int main()
{
printf("struct ALIGN1占%d个字节内存\n",sizeof(struct ALIGN1));
printf("struct ALIGN2占%d个字节内存\n",sizeof(struct ALIGN2));
return 0;
}
打印输出:
可以看到,两个基本相同的结构体,仅仅因为数据存储顺序的不同,会导致其占不同的内存。具体的内存分配如下图所示:
绿色部分表示没有具体含义的空间。
在声明中对结构的成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现。这种做法可以最大限度地减少因边界对齐而带来的空间损失。
4 作为函数参数的结构
结构体也可以作为函数的参数进行传递。直接传递结构体是合法的,但这种操作并不是很“优雅”。同数组一样,往往采用指针的方式进行传递,不过数组是默认以指针的方式进行传递,而结构体却不是这样。如果直接将结构体变量名称当做实参传入,会直接将整个结构体传入该函数,比较浪费栈空间。
所以我们在定义自定义函数的时候,形参就定义为结构体指针,到时候传入结构体指针即可。比如:
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
typedef struct
{
char name[10];
short age;
}people_info;
void get_info(people_info *info)
{
strcpy(info->name, "Mystic");
info->age = 22;
}
int main()
{
//定义结构体变量并初始化
people_info p1 = {"No Name",25};
//定义结构体指针
people_info *p = &p1;
//重新给结构体定义新值
get_info(p);
//打印输出
printf("%s\n", p->name);
printf("%d\n", p->age);
system("pause");
return 0;
}
我们定义了一个结构体,然后调用get_info
函数来给结构体录入个人信息。再返回主函数,验证我们录入的信息是否正确。
打印输出:
可以看出,我们的函数运行是没有问题的。
5 位段
位段是一个神奇的存在,仅仅从这一点上,就可以看出C语言的设计师为了紧密联系内存,到底花了多少心思。这种设计,就相当于将一个完整的数据分成了若干个部分,每个单独的部分可以表示不同的含义。
有两点需要注意:首先,位段成员必须声明为int、signed int或unsigned int类型。其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占的数目。
举个例子:
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
typedef struct
{
unsigned int class_1 : 5;
unsigned int class_2 : 5;
unsigned int class_3 : 5;
unsigned int class_4 : 5;
unsigned int class_5 : 5;
}class_name;
int main()
{
class_name normal;
normal.class_1 = 26;
normal.class_2 = 23;
normal.class_3 = 30;
normal.class_4 = 31;
normal.class_5 = 29;
printf("%d\n", normal.class_1);
printf("%d\n", normal.class_2);
printf("%d\n", normal.class_3);
printf("%d\n", normal.class_4);
printf("%d\n", normal.class_5);
printf("结构体占内存大小为%d个字节\n", sizeof(normal));
system("pause");
return 0;
}
比方说我们想存储5
个班级的人数,每个班级最多为31
人,我们就可以定义个结构体来实现位段,如果不使用位段,即使每个班级的人数定义为char
类型的变量,总共也需要5
个字节,当我们定义了位段,就可以省去一个字节。
打印输出:
当然,不仅仅是节省存储空间这么简单,在实际中还有其他的妙用。比方说我们需要两个设备之间的通信,发送PWM波,那么我们就需要变量来保存该PWM
波的基本属性,包括每组的脉冲数num
、占空比duty
和总共发送组的数量group
。然后需要先将该信息发送给另一个设备,方便该设备做好接收准备,每个数据帧有两个字节,构成如下:
如果没有位段我们就需要一系列的移位运算(和其他运算),才能得到最终想要发送的数据,但是有了位段,就简单多了,而且不易出错,程序如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
union
{
struct signal_info
{
unsigned int num : 4; //每组发送的PWM波数量
unsigned int duty : 7; //占空比(%)
unsigned int group : 5; //总共发送几组
};
unsigned int signal_information;
}signal_info_union;
int main()
{
signal_info_union.num = 1;
signal_info_union.duty = 50;
signal_info_union.group = 10;
printf("%d\n", signal_info_union.signal_information);
system("pause");
return 0;
}
注:上述例子用到了联合体,如果对联合体不熟悉,可以先看本文后面的章节。
打印输出:
我们手动的计算结果也是21281
,二者相吻合。
6 联合
在C语言中,变量的定义是分配存储空间的过程。一般每个变量都具有其独有的存储空间,那么可不可以在同一个内存空间中存储不同的数据类型呢?
答案是可以的,使用联合体就可以达到这样的目的。较之于结构体,联合体在实际的开发中出现的频率并不是很高,但这并不是说联合体不重要。
可见,在内存方面,C语言的设计者可谓是下足了功夫,这也是C语言虽饱经风霜,却从未在计算机语言的发展长河中销声匿迹的原因之一。
6.1 变体记录
变体记录可以看作联合的升级版, 变体记录中联合成员是比int
和float
更为复杂的结构。
那么,究竟什么是变体记录呢?在网上我找到了这样的一番描述。
若记录是由一部分固定不变和另一部分变化部分是随固定部分中的某个数据项的具体取值而定的数据项所组成的称为记录变体。
大概就可以知道是什么意思了,所以这个所谓的变体记录并不是联合体独有的概念。只是一种数据的记录形式。
考虑下面的情况:
仓库储存两种货物, 一种是零件(part), 一种是装配件(subassembly), 装配件由一些零件组成。一个零件信息包括零件成本,零件供应商编号;一个装配件信息包括组成装配件的零件数, 以及零件信息。显然, 仓库的一条存货记录(inventory)可能是零件, 也可能是装配件, 并且包含入库日期和操作员编号, 可以用变体记录实现。
// 零件
struct PARTINFO {
int cost; // 零件成本
int supplier; // 供应商编号
};
// 装配件
struct SUBASSYINFO {
int n_parts; // 零件数
PARTINFO parts[MAXPARTS]; // 每个零件信息
};
// 存货记录
struct INVREC {
char date[9]; // 入库日期
int oper; // 操作员编号
enum (PART, SUBASSY} type;
union {
struct PARTINFO part;
struct SUBASSYINFO subassy;
} info;
} record;
我们可以通过以下方式访问存货记录。
record.oper 获取存货操作员编号
if(record.type == PART)
{
record.info.part.cost获取存货零件成本
record.info.part.supplier 获取存货零件供应商编号
}
else if(record.type == SUBASSY)
{
record.info.subassy.n_parts 获取存货装配件包含零件数
record.info.subassy.parts[0].cost 获取存货装配件第一个零件的成本
}
6.2 联合的初始化
联合变量可以被初始化,但这个初始值必须是联合第一个成员的类型,而且它必须位于一对花括号里面。例如:
union {
int a;
float b;
char c[4];
}x = { 5 };
我们不能将其初始化为一个浮点值或者字符值。如果给出的初始值是任何其他类型,它就会转换(如果可能的话)为一个整数并赋值给x.a
。
7 总结
结构的成员可以是标量、数组或指针。结构也可以包含本身也是结构的成员(除了自己,但它的成员可以是指向这个结构的指针)。
一个联合的所有成员都存储于同一个内存位置。通过访问不同类型的联合成员,内存中相同的位组合可以被解释位不同的东西。联合变量也可以进行初始化,但初始化值必须与联合第1
个变量的类型匹配。
在大型项目的开发中,往往会将结构体,联合体,数组等数据类型联合起来使用,还有各种各样让人眼花缭乱的指针,这对我们C
语言的技术功底提出了较高的要求。
—END—