9.5 共用体类型
9.5.1 什么是共用体类型
有时,我们希望用同一段内存单元存放不同类型的变量。例如,将一个短整型变量、一个字符型变量和一个实型变量放在同一个内存地址开始的单元中。如下图所示:
图 9.17 显示了以上三个变量在内存中占用的字节数虽然不同,但都从同一地址开始存放。使用这种覆盖技术时,后一个数据会覆盖前面的数据。这种使几个不同的变量共享同一段内存的结构,被称为“共用体”类型的结构。
定义共用体类型变量
定义共用体类型变量的一般形式为:
例如:
union 共用体名 {
成员表列
} 变量表列;
上面的定义表示不同类型的变量 i
, ch
, f
可以存放到同一个存储单元中。在声明类型的同时也定义了变量 a
, b
, c
。
我们也可以将类型声明与变量定义分开:
union Data {
int i;
char ch;
float f;
} a, b, c;
这表示先声明一个 union Data
类型,再将 a
, b
, c
定义为 union Data
类型的变量。当然,也可以直接定义共用体变量,例如:
union Data {
int i;
char ch;
float f;
};
union Data a, b, c;
可以看到,“共用体”与“结构体”的定义形式相似,但它们的含义是不同的。结构体变量所占内存长度是各成员占用内存长度之和,每个成员分别占有其自己的内存单元。而共用体变量所占的内存长度等于最长的成员的长度。例如,上面定义的共用体变量 a
, b
, c
各占 4 个字节(因为一个 float
型变量占 4 个字节),而不是各占 4+1+4=9 个字节。
国内有些C语言书籍将 union
直译为“联合”。我认为译为“共用体”更能反映这种结构的特点,即几个变量共用一个内存区。而“联合”这个名词在一般意义上容易被理解为“将两个或若干个变量联结在一起”,难以表达这种结构的特点。然而,读者应当知道“共用体”在一些书中也被称为“联合”。在阅读其他书籍时如遇“联合”一词,应理解为“共用体”。
自己的理解
共用体的使用场景在于内存资源有限的情况下,多个变量不会同时被使用时。通过共用体,可以节省内存,尤其是在嵌入式系统或者需要严格控制内存使用的应用中。需要注意的是,使用共用体时要非常小心,因为如果不注意覆盖的顺序和类型,会导致难以调试的错误。此外,现代编译器和调试工具对于共用体的支持也越来越好,可以通过一些工具检测到共用体的使用错误。总的来说,共用体是C语言中一个强大的工具,但使用时需要慎重。
9.5.2 引用共用体变量的方式
只有先定义了共用体变量才能引用它,但应注意,不能引用共用体变量,而只能引用共用体变量中的成员。例如,前面定义了 a
, b
, c
为共用体变量,下面的引用方式是正确的:
union {
int i;
char ch;
float f;
} a, b, c;
不能只引用共用体变量,例如下面的引用是错误的:
a.i // 引用共用体变量中的整型变量 i
a.ch // 引用共用体变量中的字符变量 ch
a.f // 引用共用体变量中的实型变量 f
因为 a
的存储区可以按不同的类型存放数据,有不同的长度,仅写共用体变量名 a
,系统无法知道究竟应输出哪一个成员的值。应该写成:
printf("%d", a);
或
printf("%c", a.ch);
自己的理解
在使用共用体时,引用成员的方式至关重要。共用体变量中不同类型的成员共享相同的内存空间,因此在引用时必须明确指定具体的成员,否则会引起未定义的行为。尤其在进行输出操作时,直接引用共用体变量名是错误的,因为编译器无法确定要输出的具体类型和内容。因此,必须指定要引用的具体成员,这样才能确保程序的正确性和安全性。这也要求程序员在使用共用体时具备清晰的逻辑和严谨的代码习惯,以避免出现难以排查的错误。
9.5.3 共用体类型数据的特点
在使用共用体类型数据时要注意以下一些特点:
-
单一存储:同一个内存段可以用来存放几种不同类型的成员,但在每一瞬时只能存放其中一个成员,而不是同时存放几个。这是因为在每一个瞬时,存储单元只能有唯一的内容。在共用体变量中只能存放一个值。例如:
union Data { int i; char ch; float f; } a; a.i = 97; printf("%d", a.i); // 输出整数97 printf("%c", a.ch); // 输出字符'a' printf("%f", a.f); // 输出实数0.000000
执行以上代码时,由于97是赋给
a.i
的,因此按整数形式存储在变量单元中。如果用%d
格式符输出a.i
,会输出整数97;用%c
格式符输出a.ch
,系统会把存储单元中的信息按字符输出'a';用%f
格式符输出a.f
,系统会将存储单元中的信息按浮点数形式处理,输出0.000000。 -
初始化限制:可以对共用体变量初始化,但初始化表中只能有一个常量。例如:
union Data { int i; char ch; float f; } a = {1}; // 正确,对第一个成员初始化
-
最后赋值生效:共用体变量中起作用的成员是最后一次被赋值的成员。例如:
a.ch = 'a'; a.f = 1.5; a.i = 40; printf("%d", a.i); // 输出40 printf("%c", a.ch); // 输出'(',因为40的ASCII码对应'('
-
同一地址:共用体变量的地址和它的各成员的地址都是同一地址。例如,
&a.i
,&a.ch
,&a.f
都是同一个地址。 -
不能直接赋值:不能对共用体变量名赋值,也不能引用变量名来得到一个值。例如:
// a = 1; // 错误 int m = a; // 错误
-
C99新特性:C99允许同类型的共用体变量互相赋值,也允许把共用体变量作为函数参数。例如:
union Data b; b = a; // 合法
-
结构体与共用体嵌套:共用体类型可以出现在结构体类型定义中,也可以定义共用体数组。反之,结构体也可以出现在共用体类型定义中,数组也可以作为共用体的成员。
实例分析
在某些情况下,我们需要用同一个内存区存储不同的数据内容。例如,处理学生和教师的数据时,学生的数据包括:姓名、号码、性别、职业、班级;教师的数据包括:姓名、号码、性别、职业、职务。可以用共用体来处理这两种不同的数据类型,使程序更加灵活。
代码示例:
#include <stdio.h>
struct {
int num;
char name[10];
char sex;
char job;
union {
int class;
char position[10];
} category;
} person[2];
int main() {
int i;
for (i = 0; i < 2; i++) {
printf("Please enter the data of person:\n");
scanf("%d %s %c %c", &person[i].num, person[i].name, &person[i].sex, &person[i].job);
if (person[i].job == 's')
scanf("%d", &person[i].category.class);
else if (person[i].job == 't')
scanf("%s", person[i].category.position);
else
printf("Input error!\n");
}
printf("No.\tName\tSex\tJob\tClass/Position\n");
for (i = 0; i < 2; i++) {
if (person[i].job == 's')
printf("%d\t%s\t%c\t%c\t%d\n", person[i].num, person[i].name, person[i].sex, person[i].job, person[i].category.class);
else
printf("%d\t%s\t%c\t%c\t%s\n", person[i].num, person[i].name, person[i].sex, person[i].job, person[i].category.position);
}
return 0;
}
程序分析: 在 main
函数之前定义了结构体数组 person
,在结构体类型声明中包括了共用体类型 category
,这个共用体成员中又包括两个成员:class
和 position
。根据职业的不同,输入对应的班级号或职位,并将其存储在共用体中。
通过这种方式,在处理数据时能够灵活地利用同一段内存存储不同类型的数据,从而提高了程序的灵活性和效率。