文章目录
- [第16章 结构、联合和枚举](#第16章 结构、联合和枚举)
-
- [16.1 结构变量](#16.1 结构变量)
-
- [16.1.1 结构变量的声明](#16.1.1 结构变量的声明)
- [16.1.2 结构变量的初始化](#16.1.2 结构变量的初始化)
- [16.1.3 指示器(C99)](#16.1.3 指示器(C99))
- [16.1.4 对结构的操作](#16.1.4 对结构的操作)
- [16.2 结构类型](#16.2 结构类型)
-
- [16.2.1 结构标记的声明](#16.2.1 结构标记的声明)
- [16.2.2 结构类型的定义](#16.2.2 结构类型的定义)
- [16.2.3 结构作为参数和返回值](#16.2.3 结构作为参数和返回值)
- [16.2.4 复合字面量(C99)](#16.2.4 复合字面量(C99))
- [16.2.5 匿名结构(C1X)](#16.2.5 匿名结构(C1X))
- [16.3 嵌套的数组和结构](#16.3 嵌套的数组和结构)
-
- [16.3.1 嵌套的结构](#16.3.1 嵌套的结构)
- [16.3.2 结构数组](#16.3.2 结构数组)
- [16.3.3 结构数组的初始化](#16.3.3 结构数组的初始化)
- [16.3.4 程序------维护零件数据库](#16.3.4 程序——维护零件数据库)
- [16.4 联合](#16.4 联合)
-
- [16.4.1 用联合来节省空间](#16.4.1 用联合来节省空间)
- [16.4.2 用联合来构造混合的数据结构](#16.4.2 用联合来构造混合的数据结构)
- [16.4.3 为联合添加"标记字段"](#16.4.3 为联合添加“标记字段”)
- [16.4.4 匿名联合(C1X)](#16.4.4 匿名联合(C1X))
- [16.5 枚举](#16.5 枚举)
-
- [16.5.1 枚举标记和类型名](#16.5.1 枚举标记和类型名)
- [16.5.2 枚举作为整数](#16.5.2 枚举作为整数)
- [16.5.3 枚举声明"标记字段"](#16.5.3 枚举声明“标记字段”)
- 问与答
- 写在最后
第16章 结构、联合和枚举
------函数延迟绑定:数据结构导致绑定。记住:在编程过程后期再结构化数据。
本章介绍3
种新的类型:结构、联合和枚举 。结构
是可能具有不同类型的值(成员)的集合。联合
和结构
很类似,不同之处在于联合的成员共享同一存储空间 。这样的结果是,联合可以每次存储一个成员,但是无法同时存储全部成员。枚举
是一种整数类型,它的值由程序员来命名。
在这3
种类型中,结构是到目前为止最重要的一种,所以本章的大部分内容是关于结构的。16.1节
说明了如何声明结构变量,以及如何对其进行基本操作。随后,16.2节
解释了定义结构类型的方法,借助结构类型,我们就可以编写接受结构类型参数或返回结构的函数。16.3节
探讨如何实现数组和结构的嵌套。本章的最后两节分别讨论了联合(16.4节)
和枚举(16.5节)
。
16.1 结构变量
到目前为止介绍的唯一一种数据结构就是数组。数组
有两个重要特性:
- 首先,数组的所有元素具有相同的类型;
- 其次,为了选择数组元素需要指明元素的位置(作为整数下标)。
结构
所具有的特性与数组有很大不同 。结构的元素(在C
语言中的说法是结构的成员)可能具有不同的类型。而且,每个结构成员都有名字,因此为了选择特定的结构成员需要指明结构成员的名字而不是它的位置。
由于大多数编程语言提供类似的特性,因此结构可能听起来很熟悉。在其他一些语言中,经常把结构称为记录(record)
,把结构的成员称为字段(field)
。
16.1.1 结构变量的声明
当需要存储相关数据项的集合时,结构是一种合乎逻辑的选择 。例如,假设需要记录存储在仓库中的零件。每种零件需要存储的信息可能包括零件的编号(整数)、零件的名称(字符串)以及现有零件的数量(整数)。为了产生一个可以存储全部
3
种数据项的变量,可以使用类似下面这样的声明:
c
struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} part1, part2;
//结构的成员在内存中是按照声明的顺序存储的
每个结构变量都有3
个成员:number(零件的编号)
、name(零件的名称)
和on_hand(现有数量)
。注意 !!这里的声明格式和C
语言中其他变量的声明格式一样。struct{...}
指明了类型,part1
和part2
是具有这种类型的变量。
每个结构代表一种新的作用域 。任何声明在此作用域内的名字都不会和程序中的其他名字冲突。[用C
语言的术语可表述为,每个结构都为它的成员设置了独立的名字空间(name space)
。]例如,下列声明可以出现在同一程序中:
c
struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} part1, part2;
struct {
char name[NAME_LEN+1];
int number;
char sex;
} employee1, employee2;
结构part1
和part2
中的成员number
和成员name
不会与结构employee1
和employee2
中的成员number
和成员name
冲突。
16.1.2 结构变量的初始化
和数组一样,结构变量也可以在声明的同时进行初始化 。为了对结构进行初始化,要把待存储到结构中的值的列表准备好,并用花括号把它括起来:
c
struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} part1 = {528, "Disk drive", 10},
part2 = {914, "Printer cable", 5};
初始化器中的值必须按照结构成员的顺序来显示 。在此例中,结构part1
的成员number
值为528
,成员name
则是"Disk drive"
,以此类推。
结构初始化器遵循的原则类似于数组初始化器的原则。用于结构初始化器的表达式必须是常量 。例如,不能用变量来初始化结构
part1
的成员on_hand
。(这一限制从C99
开始放宽了,见18.5节
。)初始化器中的成员数可以少于它所初始化的结构 ,就像数组那样,任何"剩余的"成员都用0
作为它的初始值。特别地,剩余的字符数组中的字节数为0
,表示空字符串。
16.1.3 指示器(C99)
在8.1节
学习数组时讨论过C99
中的指示器,它在结构中也可以使用。考虑前面这个例子中part1
的初始化器:
c
{528, "Disk drive", 10}
指示器与之类似,但是在初始化时需要按成员的名字
来指定初值:
c
{.number = 528, .name = "Disk drive", .on_hand = 10}
点号和成员名称的组合也是指示器(数组元素的指示器在形式上有所不同)。
指示器有几个优点:
- 其一,易读且容易进行验证,这是因为读者可以清楚地看出结构中的成员和初始化器中的值之间的对应关系。
- 其二,初始化器中的值的顺序不需要与结构中成员的顺序一致。以上这个例子可以写为
c
{.on_hand = 10, .name = "Disk drive", .number = 528}
因为顺序不是问题,所以程序员不必记住原始声明时成员的顺序。而且成员的顺序在之后还可以改变,不会影响指示器。
指示器中列出来的值前面不一定要有指示器 (数组也是如此,见
8.1节
)。考虑下面的例子:
c
{.number = 528, "Disk drive", .on_hand = 10}
值"Disk drive"
的前面并没有指示器,所以编译器会认为它用于初始化结构中位于number
之后的成员。初始化器中没有涉及的成员都设为0
。
16.1.4 对结构的操作
既然最常见的数组操作是取下标(根据位置选择数组元素),那么也就无须惊讶结构最常用的操作是选择成员了。但是,结构成员是通过名字而不是通过位置访问的。
为了访问结构内的成员,首先写出结构的名字,然后写一个句点,再写出成员的名字。例如,下列语句将显示结构
part1
的成员的值:
c
printf("Part number: %d\n", part1.number);
printf("Part name: %s\n", part1.name);
printf("Quantity on hand: %d\n", part1.on_hand);
结构的成员是左值(4.2节)
,所以它们可以出现在赋值运算的左侧,也可以作为自增或自减表达式的操作数:
c
part1.number = 258; /* changes part1's part number */
part1.on_hand++; /* increments part1's quantity on hand */
用于访问结构成员的句点
实际上就是一个C
语言的运算符 。它的运算优先级与后缀++
和后缀--
运算符一样,所以句点运算符的优先级几乎高于所有其他运算符。考虑下面的例子:
c
scanf("%d", &part1.on_hand);
表达式&part1.on_hand
包含两个运算符(即&
和.
)。.
运算符的优先级高于&
运算符,所以就像希望的那样,&
计算的是part1.on_hand
的地址。
结构的另一种主要操作是赋值运算:
c
part2 = part1;
这一语句的效果是把part1.number
复制到part2.number
,把part1.name
复制到part2.name
,以此类推。
因为数组不能用=
运算符进行复制,所以结构可以用=
运算符复制应该是一个惊喜 。更大的惊喜是,对结构进行复制时,嵌在结构内的数组也被复制。一些程序员利用这种性质来产生"空"结构,以封装稍后将进行复制的数组:
c
struct { int a[10]; } a1, a2;
a1 = a2; /* legal, since a1 and a2 are structures */
运算符=
仅仅用于类型兼容的结构 。两个同时声明 的结构(比如part1
和part2
)是兼容的。正如下一节你会看到的那样,使用同样的"结构标记
"或同样的类型名声明
的结构也是兼容的。
请注意 !!除了赋值运算,
C
语言没有提供其他用于整个结构的操作。特别是不能使用运算符==
和!=
来判定两个结构相等还是不等。
16.2 结构类型
16.1节
虽然说明了声明结构变量的方法,但是没有讨论一个重要的问题:命名结构类型。假设程序需要声明几个具有相同成员的结构变量。如果一次可以声明全部变量,那么没有什么问题。但是,如果需要在程序中的不同位置声明变量,那么问题就复杂了。如果在某处编写了:
c
struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} part1;
并且在另一处编写了:
c
struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} part2;
那么立刻就会出现问题。重复的结构信息会使程序膨胀。因为难以确保这些声明会保持一致,所以将来修改程序会有风险。
但是这些还不是最大的问题。根据C语言的规则,part1
和part2
不具有兼容的类型(因为没有同时声明),因此不能把part1
赋值给part2
,反之亦然。而且,因为part1
和part2
的类型都没有名字,所以也就不能把它们用作函数调用的参数。
为了克服这些困难,需要定义表示结构类型(而不是特定的结构变量)的名字。C
语言提供了两种命名结构的方法:可以声明"结构标记
",也可以使用typedef
来定义类型名[类型定义(7.5节)
]。
16.2.1 结构标记的声明
结构标记(structure tag)
是用于标识某种特定结构的名字。下面的例子声明了名为part
的结构标记:
c
struct part {
int number;
char name[NAME_LEN+1];
int on_hand;
};
//注意,右花括号后的分号是必不可少的,它表示声明结束。
请注意!!如果无意间忽略了结构声明结尾的分号,可能会导致奇怪的错误。考虑下面的例子:
cstruct part { int number; char name[NAME_LEN+1]; int on_hand; } /*** WRONG: semicolon missing ***/ f(void) { ... return 0; /* error detected at this line */ }
程序员没有指定函数
f
的返回类型(编程有点儿随意)。因为前面的结构声明没有正常终止,所以编译器会假设函数f
的返回值是struct part
类型的。编译器直到执行函数中第一条return
语句时才会发现错误,结果得到含义模糊的出错消息。
一旦创建了标记part
,就可以用它来声明变量了:
c
struct part part1, part2;
但是,不能通过省略单词struct
来缩写这个声明:
c
part part1, part2; /*** WRONG ***/
part
不是类型名。如果没有单词struct
的话,它就没有任何意义。
因为结构标记只有在前面放置了单词struct
时才会有意义 ,所以它们不会和程序中用到的其他名字发生冲突。程序拥有名为part
的变量是完全合法的(虽然有点儿容易混淆)。
顺便说一句,结构标记的声明可以和结构变量的声明合并在一起:
c
struct part {
int number;
char name[NAME_LEN+1];
int on_hand;
} part1, part2;
在这里不仅声明了结构标记part
(可能稍后会用part
声明更多的变量),而且声明了变量part1
和part2
。
所有声明为struct part
类型的结构彼此之间是兼容的:
c
struct part part1 = {528, "Disk drive", 10};
struct part part2;
part2 = part1; /* legal; both parts have the same type */
16.2.2 结构类型的定义
除了声明结构标记,还可以用typedef
来定义真实的类型名。例如,可以按照如下方式定义名为Part
的类型:
c
typedef struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} Part;
注意,类型Part
的名字必须出现在定义的末尾,而不是在单词struct
的后边。
可以像内置类型那样使用Part
。例如,可以用它声明变量:
c
Part part1, part2;
因为类型Part
是typedef
的名字,所以不允许书写struct Part
。无论在哪里声明,所有的Part
类型的变量都是兼容的。
需要命名结构时,通常既可以选择声明
结构标记
,也可以使用typedef
。但是,正如稍后将看到的,结构用于链表(17.5节)
时,强制使用声明结构标记。在本书的大多数例子中,我使用的是结构标记而不是typedef
名。
16.2.3 结构作为参数和返回值
函数可以有结构类型的实际参数和返回值 。下面来看两个例子。当把part
结构用作实际参数时,第一个函数显示出结构的成员:
c
void print_part(struct part p)
{
printf("Part number: %d\n", p.number);
printf("Part name: %s\n", p.name);
printf("Quantity on hand: %d\n", p.on_hand);
}
下面是print_part
可能的调用方法:
c
print_part(part1);
第二个函数返回part
结构,此结构由函数的实际参数构成:
c
struct part build_part(int number, const char * name, int on_hand)
{
struct part p;
p.number = number;
strcpy (p.name, name);
p.on_hand = on_hand;
return p;
}
注意,函数build_part
的形式参数名和结构part
的成员名相同是合法的,因为结构拥有自己的名字空间。下面是build_part
可能的调用方法:
c
part1 = build_part(528, "Disk drive", 10);
给函数传递结构和从函数返回结构都要求生成结构中所有成员的副本。这样的结果是,这些操作对程序强加了一定数量的系统开销,特别是结构很大的时候。为了避免这类系统开销,有时用传递指向结构的指针来代替传递结构本身是很明智的做法。类似地,可以使函数返回指向结构的指针来代替返回实际的结构。在17.5节
的例子中,可以看到用指向结构的指针作为参数或者作为返回值的函数。
除了效率方面的考虑之外,避免创建结构的副本还有其他原因。例如,
<stdio.h>
定义了一个名为FILE
的类型,它通常是结构。每个FILE
结构存储的都是已打开文件的状态信息,因此在程序中必须是唯一的。<stdio.h>
中每个用于打开文件的函数都返回一个指向FILE
结构的指针,每个对已打开文件执行操作的函数都需要用FILE
指针作为参数。
有时,可能希望在函数内部初始化结构变量来匹配其他结构(可能作为函数的形式参数)。在下面的例子中,part2
的初始化器是传递给函数f
的形式参数:
c
void f(struct part part1)
{
struct part part2 = part1;
...
}
C
语言允许这类初始化器,因为初始化的结构(此例中的part2
)具有自动存储期(10.1节)
,也就是说它局部于函数并且没有声明为static
。初始化器可以是适当类型的任意表达式,包括返回结构的函数调用。
16.2.4 复合字面量(C99)
9.3节
介绍过从C99
开始引入的新特性复合字面量。在那一节中,复合字面量被用于创建没有名字的数组,这样做的目的通常是将数组作为参数传递给函数。复合字面量同样也可以用于"实时"创建一个结构,而不需要先将其存储在变量中。生成的结构可以像参数一样传递,可以被函数返回,也可以赋值给变量。接下来看两个例子。
首先,使用复合字面量创建一个结构,这个结构将传递给函数。例如,可以按如下方式调用print_part
函数:
c
print_part((struct part) {528, "Disk drive", 10});
上面的复合字面量创建了一个part
结构,依次包括成员528
、"Disk drive"
和10
。这个结构之后被传递到print_part
显示。
下面的语句把复合字面量赋值给变量:
c
part1 = (struct part) {528, "Disk drive", 10};
//可以赋值是因为结构本身的原因,不适用于数组
int a[3];
a = (int[]){1, 2, 3}; //错误语句
这一语句类似于包含初始化器的声明,但不完全一样------初始化器只能出现在声明中,不能出现在这样的赋值语句中。
一般来说,复合字面量包括用圆括号括住的类型名和后续的初始化器 。如果复合字面量代表一个结构,类型名可以是结构标签的前面加上struct
(如本例所示)或者typedef
名。一个复合字面量的初始化器部分可以包含指示器:
c
print_part((struct part) {.on_hand = 10,
.name = "Disk drive",
.number = 528});
复合字面量不会提供完全的初始化,所以任何未初始化的成员默认值为0
。
16.2.5 匿名结构(C1X)
从
C11
开始,结构
或者联合(16.4节)
的成员也可以是另一个没有名字的结构。如果一个结构或者联合包含了这样的成员:
- 没有名称
- 被声明为结构类型,但是只有成员列表而没有标记。
则这个成员就是一个匿名结构(anonymous structure)
。
在下例中,struct t
和union u
的第二个成员都是匿名结构:
c
struct t {int i; struct {char c; float f;};};
union u {int i; struct {char c; float f;};};
现在的问题是,如何才能访问匿名结构的成员 ?若某个匿名结构S
是结构或者联合X
的成员,那么S
的成员就被当作X
的成员。进一步,对于多层嵌套的情况,如果符合以上条件,则可以递归地应用这种关系。
在下面的例子中,struct t
包含了一个没有标记、没有名称的结构成员,这个结构成员的成员c
和f
被认为属于struct t
。
c
struct t
{
int i;
struct s {int j, k:3;}; // 有标记的成员
struct {char c; float f;}; // 无标记且未命名的成员(匿名结构)
struct {double d;} s; // 命名的成员
} t;
t.i = 2006;
t.j = 5; // 非法
t.k = 6; // 非法
t.c = 'x'; // 正确
t.f = 2.0; // 正确
t.s.d = 22.2;
出于同样的原因,下面的类型声明将在转换期间得到一个表示错误的诊断信息。因为struct tag
的第二个成员是匿名结构,而匿名结构的成员中又有一个是匿名结构,所以,匿名结构的成员i
和f
被当作struct tag
的成员,这意味着struct tag
有两个成员的名称相同,都是i
。
c
struct tag
{
struct {int i;};
struct {struct {int i; float f;}; double d;};
char c;
};
尽管匿名结构的成员被当作隶属于包含该结构的上层结构的成员,但它的初始化器依然必须采用被花括号包围的形式。
在下例中,尽管匿名结构的成员x
被认为属于包含它的那个结构struct t
,但它的初始化器仍然需要使用一对花括号。
c
struct t {char c; struct {int x;};};
struct t t = {'x', 1}; // 非法
struct t t = {'x', {1}}; // 合法
16.3 嵌套的数组和结构
结构和数组的组合没有限制 。数组可以将结构作为元素,结构也可以包含数组和结构作为成员。我们已经看过数组嵌套在结构内部的示例(结构part
的成员name
)。下面探讨其他的可能性:成员是结构的结构和元素是结构的数组。
16.3.1 嵌套的结构
把一种结构嵌套在另一种结构中经常是非常有用的。例如,假设声明了如下的结构,此结构用来存储一个人的名、中间名和姓:
c
struct person_name {
char first[FIRST_NAME_LEN+1];
char middle_initial;
char last[LAST_NAME_LEN+1];
};
//可以用`结构person_name`作为更大结构的一部分内容:
struct student {
struct person_name name;
int id, age;
char sex;
} student1, student2;
//访问student1的名、中间名或姓需要应用两次.运算符
strcpy(student1.name.first, "Fred");
使name
成为结构(而不是把first
、middle_initial
和last
作为student
结构的成员)的好处之一就是可以把名字作为数据单元来处理,这样操作起来更容易。例如,如果打算编写函数来显示名字,那么只需要传递一个实际参数(person_name
结构)而不是三个实际参数:
c
display_name(student1.name);
同样,把信息从结构person_name
复制给结构student
的成员name
将只需要一次而不是三次赋值:
c
struct person_name new_name;
...
student1.name = new_name;
16.3.2 结构数组
数组和结构最常见的组合之一就是其元素为结构的数组 。这类数组可以用作简单的数据库 。例如,下列结构part
的数组能够存储100
种零件的信息:
c
struct part inventory[100];
为了访问数组中的某种零件,可以使用取下标的方式。例如,为了显示存储在位置i
的零件,可以写成
c
print_part(inventory[i]);
访问结构part
内的成员要求结合使用取下标和成员选择。为了给inventory[i]
中的成员number
赋值883
,可以写成
c
inventory[i].number = 883;
访问零件名中的单个字符要求先取下标(选择特定的零件),然后选择成员(选择成员name
),再取下标(选择零件名称中的字符)。为了使存储在inventory[i]
中的名字变为空字符串,可以写成
c
inventory[i].name[0] = '\0';
16.3.3 结构数组的初始化
初始化结构数组与初始化多维数组的方法非常相似。每个结构都拥有自己的带有花括号的初始化器,数组的初始化器简单地在结构初始化器的外围括上另一对花括号。
初始化结构数组的原因之一是,我们打算把它作为程序执行期间不改变的信息的数据库。例如,假设程序在打国际长途电话时需要访问国家(地区)代码。首先,设置结构用来存储国家(地区)名和相应代码:
c
struct dialing_code {
char *country;
int code;
};
注意,country
是指针而不是字符数组。如果计划用dialing_code
结构作为变量,则可能有问题,但是这里没这样做。当初始化dialing_code
结构时,country
会指向字面串。
接下来,声明这类结构的数组并对其进行初始化,从而使此数组包含一些世界上人口最多的国家(地区)的代码:
c
const struct dialing_code country_codes[] =
{{"Argentina", 54}, {"Bangladesh", 880},
{"Brazil", 55}, {"Burma (Myanmar)", 95},
{"China", 86}, {"Colombia", 57},
{"Congo, Dem. Rep. of", 243}, {"Egypt", 20},
{"Ethiopia", 251}, {"France", 33},
{"Germany", 49}, {"India ", 91},
{"Indonesia" 62}, {"Iran", 98},
{"Italy", 39}, {"Japan", 81},
{"Mexico", 52}, {"Nigeria", 234},
{"Pakistan", 92}, {"Philippines", 63},
{"Poland", 48}, {"Russia", 7},
{"South Africa", 27}, {"Korea", 82},
{"Spain", 34}, {"Sudan", 249},
{"Thailand", 66}, {"Turkey", 90},
{"Ukraine", 380}, {"United Kingdom", 44},
{"United States", 1}, {"Vietnam", 84}};
//每个结构值两边的内层花括号是可选的。然而,基于书写风格的考虑,最好不要省略它们。
由于结构数组(以及包含数组的结构)很常见,因此从C99
开始的初始化器允许指示器的组合 。假定我们想初始化inventory
数组使其只包含一个零件,零件编号为528
,现货数量为10
,名字暂时为空:
c
struct part inventory[100] =
{[0].number = 528, [0].on_hand = 10, [0].name[0] = '\0'};
列表中的前两项使用了两个指示器(一个用于选择数组元素0
,即part
结构,另一个用于选择结构中的成员)。最后一项使用了3
个指示器:一个用于选择数组元素,一个用于选择该元素的name
成员,还有一个用于选择name
的元素0
。
16.3.4 程序------维护零件数据库
为了说明实际应用中数组和结构是如何嵌套的,现在开发一个相对大一点的程序,此程序用来维护仓库存储的零件信息数据库。程序围绕一个结构数组构建,且每个结构包含以下信息:零件的编号、名称以及数量。程序将支持下列操作。
- 添加新零件编号、名称和初始的现货数量。如果零件已经在数据库中,或者数据库已满,那么程序必须显示出错消息。
- 给定零件编号,显示出零件的名称和当前的现货数量。如果零件编号不在数据库中,那么程序必须显示出错消息。
- 给定零件编号,改变现有的零件数量。如果零件编号不在数据库中,那么程序必须显示出错消息。
- 显示列出数据库中全部信息的表格。零件必须按照输入的顺序显示出来。
- 终止程序的执行。
使用i(插入)
、s(搜索)
、u(更新)
、p(显示)
和q(退出)
分别表示这些操作。与程序的会话可能如下所示:
bash
Enter operation code: i
Enter part number: 528
Enter part name: Disk drive
Enter quantity on hand: 10
Enter operation code: s
Enter part number: 528
Part name: Disk drive
Quantity on hand: 10
Enter operation code: s
Enter part number: 914
Part not found.
Enter operation code: i
Enter part number: 914
Enter part name: Printer cable
Enter quantity on hand: 5
Enter operation code: u
Enter part number: 528
Enter change in quantity on hand: -2
Enter operation code: s
Enter part number: 528
Part name: Disk drive
Quantity on hand: 8
Enter operation code: p
Part Number Part Name Quantity on Hand
528 Disk drive 8
914 Printer cable 5
Enter operation code: q
程序将在结构中存储每种零件的信息。这里将数据库的大小限制为100
种零件,这使得用数组来存储结构成为可能,这里称此数组为inventory
。(如果这里的限制值太小,可以在将来修改。)为了记录当前存储在数组中的零件数,使用名为num_parts
的变量。
因为这个程序是以菜单方式驱动的,所以十分容易勾勒出主循环结构:
c
for (;;) {
提示用户输入操作码
读操作码
switch(操作码){
case 'i': 执行插入操作; break;
case 's': 执行搜索操作; break;
case 'u': 执行更新操作; break;
case 'p': 执行显示操作; break;
case 'q': 终止程序;
default: 显示出错消息;
}
}
为了方便起见,接下来将分别设置不同的函数执行插入、搜索、更新和显示操作。因为这些函数都需要访问inventory
和num_parts
,所以可以把这些变量设置为外部变量 。或者把变量声明在main
函数内,然后把它们作为实际参数传递给函数。从设计角度来说,使变量局部于函数通常比把它们外部化更好(如果忘记了原因,见10.2节
)。然而,在此程序中,把inventory
和num_parts
放在main
函数中只会使程序复杂化。
由于稍后会解释的一些原因,这里决定把程序分割为三个文件:inventory.c
文件,它包含程序的大部分内容;readline.h
文件,它包含read_line
函数的原型;readline.c
文件,它包含read_line
函数的定义。本节的后面将讨论后两个文件,现在先集中讨论inventory.c
文件。
c
/*
inventory.c
--Maintains a parts database (array version)
*/
#include <stdio.h>
#include "readline.h"
#define NAME_LEN 25
#define MAX_PARTS 100
struct part {
int number;
char name[NAME_LEN+1];
int on_hand;
} inventory[MAX_PARTS];
int num_parts = 0; /* number of parts currently stored */
int find_part(int number);
void insert(void);
void search(void);
void update(void);
void print(void);
/**********************************************************
* main: Prompts the user to enter an operation code, *
* then calls a function to perform the requested *
* action. Repeats until the user enters the *
* command 'q'. Prints an error message if the user *
* enters an illegal code. *
**********************************************************/
int main(void)
{
char code;
for (;;) {
printf("Enter operation code: ");
scanf(" %c", &code);
while (getchar() != '\n') /* skips to end of line */
;
switch (code) {
case 'i': insert();
break;
case 's': search();
break;
case 'u': update();
break;
case 'p': print();
break;
case 'q': return 0;
default: printf("Illegal code\n");
}
printf("\n");
}
}
/**********************************************************
* find_part: Looks up a part number in the inventory *
* array. Returns the array index if the part *
* number is found; otherwise, returns -1. *
**********************************************************/
int find_part(int number)
{
int i;
for (i = 0; i < num_parts; i++)
if (inventory[i].number == number)
return i;
return -1;
}
/**********************************************************
* insert: Prompts the user for information about a new *
* part and then inserts the part into the *
* database. Prints an error message and returns *
* prematurely if the part already exists or the *
* database is full. *
**********************************************************/
void insert(void)
{
int part_number;
if (num_parts == MAX_PARTS) {
printf("Database is full; can't add more parts.\n");
return;
}
printf("Enter part number: ");
scanf("%d", &part_number);
if (find_part(part_number) >= 0) {
printf("Part already exists.\n");
return;
}
inventory[num_parts].number = part_number;
printf("Enter part name: ");
read_line(inventory[num_parts].name, NAME_LEN);
printf("Enter quantity on hand: ");
scanf("%d", &inventory[num_parts].on_hand);
num_parts++;
}
/**********************************************************
* search: Prompts the user to enter a part number, then *
* looks up the part in the database. If the part *
* exists, prints the name and quantity on hand; *
* if not, prints an error message. *
**********************************************************/
void search(void)
{
int i, number;
printf("Enter part number: ");
scanf("%d", &number);
i = find_part(number);
if (i >= 0) {
printf("Part name: %s\n", inventory[i].name);
printf("Quantity on hand: %d\n", inventory[i].on_hand);
} else
printf("Part not found.\n");
}
/**********************************************************
* update: Prompts the user to enter a part number. *
* Prints an error message if the part doesn't *
* exist; otherwise, prompts the user to enter *
* change in quantity on hand and updates the *
* database. *
**********************************************************/
void update(void)
{
int i, number, change;
printf("Enter part number: ");
scanf("%d", &number);
i = find_part(number);
if (i >= 0) {
printf("Enter change in quantity on hand: ");
scanf("%d", &change);
inventory[i].on_hand += change;
} else
printf("Part not found.\n");
}
/**********************************************************
* print: Prints a listing of all parts in the database, *
* showing the part number, part name, and *
* quantity on hand. Parts are printed in the *
* order in which they were entered into the *
* database. *
**********************************************************/
void print(void)
{
int i;
printf("Part Number Part Name "
"Quantity on Hand\n");
for (i = 0; i < num_parts; i++)
printf("%7d %-25s%11d\n", inventory[i].number,
inventory[i].name, inventory[i].on_hand);
}
在main
函数中,格式串" %c"
允许scanf
函数在读入操作码之前跳过空白字符。格式串中的空格是至关重要的,如果没有它,scanf
函数有时会读入前一输入行末尾的换行符。
程序包含一个名为find_part
的函数,main
函数不调用此函数。这个"辅助"函数用于避免多余的代码和简化更重要的函数。通过调用find_part
,insert
函数、search
函数和update
函数可以定位数据库中的零件(或者简单地确定零件是否存在)。
现在还剩下一个细节:
read_line
函数。这个函数用来读零件的名字。13.3节
讨论了书写此类函数时的相关问题,但是那个read_line
函数不能用于这个程序。请思考当用户插入零件时会发生什么:
bash
Enter part number: 528
Enter part name: Disk drive
在输入零件的编号后,用户按回车键,输入零件的名字后再次按了回车键,这样每次都无形中给程序留下一个必须读取的换行符。为了方便讨论,现在假装这些字符都是可见的:
bash
Enter part number: 528¤
Enter part name: Disk drive¤
当调用scanf
函数来读零件编号时,函数读入了5
、2
和8
,但是留下了字符¤
未读。如果试图用原始的read_line
函数来读零件名称,那么函数将立刻遇到字符¤
,并且停止读入。当数值输入的后边跟有字符输入时,这种问题非常普遍。解决办法就是编写read_line
函数,使它在开始往字符串中存储字符之前跳过空白字符。这不仅解决了换行符的问题,而且可以避免存储用户在零件名称的开始处输入的任何空白。
因为read_line
函数与inventory.c
文件中的其他函数无关,而且它在其他程序中有复用的可能,所以我们决定把此函数从inventory.c
中独立出来。read_line
函数的原型将放在头文件readline.h
中:
c
/*
readline.h
*/
#ifndef READLINE_H
#define READLINE_H
/**********************************************************
* read_line: Skips leading white-space characters, then *
* reads the remainder of the input line and *
* stores it in str. Truncates the line if its *
* length exceeds n. Returns the number of *
* characters stored. *
**********************************************************/
int read_line(char str[], int n);
#endif
我们将把read_line
的定义放在readline.c
文件中:
c
/*
readline.c
*/
#include <ctype.h>
#include <stdio.h>
#include "readline.h"
int read_line(char str[], int n)
{
int ch, i = 0;
while (isspace(ch = getchar()))
;
while (ch != '\n' && ch != EOF) {
if (i < n)
str[i++] = ch;
ch = getchar();
}
str[i] = '\0';
return i;
}
表达式isspace(ch = getchar())
控制第一个while
语句。它调用getchar
读取一个字符,把读入的字符存储在ch
中,然后使用isspace
函数(23.5节
)来判断ch
是否是空白字符。如果不是,循环终止,ch
中包含一个非空白字符。15.3节
解释了ch
的类型为int
而不是char
的原因,还解释了判定EOF
的理由。
16.4 联合
像结构一样,
联合(union)
也是由一个或多个成员构成的,而且这些成员可能具有不同的类型。但是,编译器只为联合中最大的成员分配足够的内存空间 。联合的成员在这个空间内彼此覆盖。这样的结果是,给一个成员赋予新值也会改变其他成员的值。
为了说明联合的基本性质,现在声明一个联合变量u
,并且这个联合变量有两个成员:
c
union {
int i;
double d;
} u;
//注意,联合的声明方式非常类似于结构的声明方式:
struct {
int i;
double d;
} s;
事实上,结构变量
s
和联合变量u
只有一处不同:s
的成员存储在不同的内存地址中,而u
的成员存储在同一内存地址中。在结构变量
s
中,成员i
和d
占有不同的内存单元。s
总共占用了12
字节。在联合变量u
中,成员i
和d
互相交叠(i
实际上是d
的前4
个字节),所以u
只占用了8
字节;此外,i
和d
具有相同的地址。
访问联合成员的方法和访问结构成员的方法相同 。为了把数82
存储到u
的成员i
中,可以写成
c
u.i = 82;
//为了把值74.8存储到成员d中,可以写成
u.d = 74.8;
因为编译器把联合成员重叠存储,所以改变一个成员就会使之前存储在任何其他成员中的值发生改变。因此,如果把一个值存储到u.d
中,那么先前存储在u.i
中的值会丢失。(如果测试u.i
的值,那么它会显示出无意义的内容。)类似地,改变u.i
也会影响u.d
。由于这个性质,可以把u
想成存储i
或者存储d
的地方,而不是同时存储二者的地方 。(结构s
允许存储i
和d
。)
联合的性质和结构的性质几乎一样,因此可以用声明结构标记
和类型
的方法来声明联合的标记和类型。像结构一样,联合可以使用运算符=
进行复制,也可以传递给函数,还可以由函数返回。
联合的初始化方式甚至也和结构的初始化很类似。但是,只有联合的第一个成员可以获得初始值。例如,可以用下列方式初始化联合u
的成员i
为0
:
c
union {
int i;
double d;
} u = {0};
注意 !!花括号是必需的。花括号内的表达式必须是常量 。(从C99
开始的规则稍有不同,在18.5节
会看到。)
指示器(我们在讨论数组和结构时介绍过的一种C99特性)也可以用在联合中 。指示器允许我们指定需要对联合中的哪个成员进行初始化。例如,可以像下面这样初始化
u
的成员d
:
c
union {
int i;
double d;
} u = {.d = 10.0};
只能初始化一个成员,但不一定是第一个。
联合有几种应用,现在讨论其中的两种。联合的另外一个应用是用不同的方法观察存储,因为这个应用与机器高度相关,所以推迟到20.3节
再介绍。
16.4.1 用联合来节省空间
在结构中经常使用联合作为节省空间的一种方法。假设打算设计的结构包含通过礼品册售出的商品的信息。礼品册上只有三种商品:图书、杯子和衬衫。每种商品都含有库存量、价格以及与商品类型相关的其他信息。
- 图书:书名、作者、页数。
- 杯子:设计。
- 衬衫:设计、可选颜色、可选尺寸。
最初的设计可能会得到如下结构:
c
struct catalog_item {
int stock_number;
double price;
int item_type;
char title[TITLE_LEN+1];
char author[AUTHOR_LEN+1];
int num_pages;
char design[DESIGN_LEN+1];
int colors;
int sizes;
};
成员item_type
的值将是BOOK
、MUG
或SHIRT
之一。成员colors
和sizes
将存储颜色和尺寸的组合代码。
虽然上述结构十分好用,但是它很浪费空间,因为对礼品册中的所有商品来说只有结构中的部分信息是常用的。比如,如果商品是图书,那么就不需要存储design
、colors
和sizes
。通过在结构catalog_item
内部放置一个联合,可以减少结构所需要的内存空间。联合的成员将是一些特殊的结构,每种结构都包含特定类型的商品所需要的数据:
c
struct catalog_item {
int stock_number;
double price;
int item_type;
union {
struct {
char title[TITLE_LEN+1];
char author[AUTHOR_LEN+1];
int num_pages;
} book;
struct {
char design[DESIGN_LEN+1];
} mug;
struct {
char design[DESIGN_LEN+1];
int colors;
int sizes;
} shirt;
} item;
};
注意,联合(名为item
)是结构catalog_item
的成员,而结构book
、mug
和shirt
则是联合item
的成员。如果c
是表示图书的结构catalog_item
,那么可以用下列方法显示图书的名称:
c
printf("%s", c.item.book.title);
正如上边的例子显示的那样,访问嵌套在结构内部的联合是很困难的:为了定位图书的名称,不得不指明结构的名字(c)
、结构的联合成员的名字(item)
、联合的结构成员的名字(book)
,以及此结构的成员名(title)
。
可以用
catalog_item
结构来说明联合有趣的一面。把值存储在联合的一个成员中,然后通过另一个名字来访问该数据通常不太可取,因为给联合的一个成员赋值会导致其他成员的值不确定。然而,C
标准提到了一种特殊情况:联合的两个或多个成员是结构,而这些结构最初的一个或多个成员是相匹配的。(这些成员的顺序应该相同,类型也要兼容,但名字可以不一样。)如果当前某个结构有效,则其他结构中的匹配成员也有效。
考虑嵌入在catalog_item
结构中的联合。它包含三个结构成员,其中两个结构(mug
和shirt
)的起始成员(design
)相匹配。现在假定我们给其中一个design
成员赋值:
c
strcpy(c.item.mug.design, "Cats");
//另一个结构中的design成员也会被定义,并具有相同的值:
printf("%s", c.item.shirt.design); /* prints "Cats" */
16.4.2 用联合来构造混合的数据结构
联合还有一个重要的应用:创建含有不同类型混合数据的数据结构 。现在假设需要数组的元素是
int
值和double
值的混合。因为数组的元素必须是相同的类型,所以好像不可能产生如此类型的数组。但是利用联合,这件事就相对容易了。首先,定义一种联合类型,它所包含的成员分别表示要存储在数组中的不同数据类型:
c
typedef union {
int i;
double d;
} Number;
//接下来,创建一个数组,使数组的元素是Number类型的值:
Number number_array[1000];
数组number_array
的每个元素都是Number
联合。Number
联合既可以存储int
类型的值又可以存储double
类型的值,所以可以在数组number_array
中存储int
和double
的混合值。例如,假设需要用数组number_array
的0
号元素来存储5
,用1
号元素来存储8.395
。下列赋值语句可以达到期望的效果:
c
number_array[0].i = 5;
number_array[1].d = 8.395;
16.4.3 为联合添加"标记字段"
联合所面临的主要问题是不容易确定联合最后改变的成员,因此所包含的值可能是无意义的。请思考下面这个问题:假设编写了一个函数,用来显示当前存储在联合
Number
中的值。这个函数可能有下列框架:
c
void print_number(Number n)
{
if (n 包含一个整数)
printf("%d", n.i);
else
printf("%g", n.d);
}
但是,没有方法可以帮助函数print_number
来确定n
包含的是整数还是浮点数。
为了记录此信息,可以把联合嵌入一个结构中,并且此结构还含有另一个成员:"标记字段"
或者"判别式"
,它是用来提示当前存储在联合中的内容的 。在本节先前讨论的结构catalog_item
中,item_type
就是用于此目的的。
下面把
Number
类型转换成具有嵌入联合的结构类型:
c
#define INT_KIND 0
#define DOUBLE_KIND 1
typedef struct {
int kind; /* tag field */
union {
int i;
double d;
} u;
} Number;
//Number有两个成员kind和u。kind的值可能是INT_KIND或DOUBLE_KIND。
每次给u
的成员赋值时,也会改变kind
,从而提示修改的是u的
哪个成员。例如,如果n
是Number
类型的变量,对u
的成员i
进行赋值操作可以采用下列形式:
c
n.kind = INT_KIND;
n.u.i = 82;
//注意,对i赋值要求首先选择n的成员u,然后才是u的成员i。
当需要找回存储在Number
型变量中的数时,kind
将表明联合的哪个成员是最后被赋值的。函数print_number
可以利用这种能力:
c
void print_number(Number n)
{
if (n.kind == INT_KIND)
printf("%d", n.u.i);
else
printf("%g", n.u.d);
}
请注意!!每次对联合的成员进行赋值,都由程序负责改变标记字段的内容。
16.4.4 匿名联合(C1X)
从
C11
开始,结构或者联合的成员也可以是另一个没有名字的联合。如果一个结构或者联合包含了这样的成员:
- 没有名称;
- 被声明为联合类型,但是只有成员列表而没有标记。
则这个成员就是一个匿名联合(anonymous union)
。在下例中,struct t
和union u
的第二个成员都是匿名联合。
c
struct t {int i; union {char c; float f;};};
union u {int i; union {char c; float f;};};
现在的问题是,如何才能访问匿名联合的成员?
答案如下:若某个匿名联合U
是结构或者联合X
的成员,则U
的成员被当作X
的成员。进一步,对于多层嵌套的情况,如果符合以上条件,那么可以递归地应用这种关系。(跟结构的情况类似)
在下面的例子中,struct t
包含了一个没有标记、没有名字的联合成员,这个联合的成员c
和f
被认为属于struct t
:
c
struct t
{
int i;
struct s {int j, k:3;}; // 有标记的成员
union {char c; float f;}; // 无标记且未命名的成员(匿名联合)
struct {double d;} s; // 命名的成员
} t;
t.i = 2006;
t.j = 5; // 非法
t.k = 6; // 非法
t.c = 'x'; // 正确
t.f = 2.0; // 正确
t.s.d = 22.2;
出于同样的原因,下面的类型声明将在转换期间得到一个表示错误的诊断信息。因为struct tag
的第二个成员是匿名联合,而匿名联合的成员中又有一个是匿名联合,所以,匿名联合的成员i
和f
被当作struct tag
的成员,这意味着struct tag
有两个成员的名称相同,都是i
:
c
struct tag
{
struct {int i;};
union {union {int i; float f;}; double d;};
char c;
};
16.5 枚举
在许多程序中,我们会需要变量只具有少量有意义的值。例如,布尔变量应该只有
2
种可能的值:"真"
和"假"
。用来存储扑克牌花色的变量应该只有4
种可能的值:"梅花"、"方片"、"红桃"和"黑桃"
。显然可以用声明成整数的方法来处理此类变量,并且用一组编码来表示变量的可能值:
c
int s; /* s will store a suit */
...
s = 2; /* 2 represents "hearts" */
虽然这种方法可行,但是也遗留了许多问题。有些人读程序时可能不会意识到s
只有4
种可能的值,而且不会知道2
的特殊含义。
使用宏来定义牌的花色"类型"和不同花色的名字是一种正确的措施:
c
#define SUIT int
#define CLUBS 0
#define DIAMONDS 1
#define HEARTS 2
#define SPADES 3
//那么前面的示例现在可以变得更加容易阅读:
SUIT s;
...
s = HEARTS;
这种方法有所改进,但它仍然不是最好的解决方案,因为这样做没有为阅读程序的人指出宏表示具有相同"类型"的值。如果可能值的数量很多,那么为每个值定义一个宏是很麻烦的。而且,因为预处理器会删除我们定义的CLUBS
、DIAMONDS
、HEARTS
和SPADES
这些名字,所以在调试期间没法使用这些名字。
C
语言为具有可能值较少的变量提供了一种专用类型。枚举类型(enumeration type)
是一种值由程序员列出("枚举")的类型,而且程序员必须为每个值命名(枚举常量)。以下例子中枚举的值(CLUBS
、DIAMONDS
、HEARTS
和SPADES
)可以赋值给变量s1
和s2
:
c
enum {CLUBS, DIAMONDS, HEARTS, SPADES} s1, s2;
虽然枚举和结构、联合没有什么共同的地方,但是它们的声明方法很类似。但是,与结构或联合的成员不同,枚举常量的名字必须不同于作用域范围内声明的其他标识符。
枚举常量类似于用
#define
指令创建的常量,但是两者又不完全一样。特别地,枚举常量遵循C
语言的作用域规则:如果枚举声明在函数体内,那么它的常量对外部函数来说是不可见的。
16.5.1 枚举标记和类型名
与命名结构和联合的原因相同,我们也常常需要创建枚举的名字。与结构和联合一样,可以用两种方法命名枚举:通过
声明标记
的方法,或者使用typedef
来创建独一无二的类型名。
枚举标记类似于结构和联合的标记 。例如,为了定义标记suit
,可以写成:
c
enum suit {CLUBS, DIAMONDS, HEARTS, SPADES};
//变量suit可以按照下列方法来声明:
enum suit s1, s2;
还可以用typedef
把Suit
定义为类型名:
c
typedef enum {CLUBS, DIAMONDS, HEARTS, SPADES} Suit;
Suit s1, s2;
在C89
中,利用typedef
来命名枚举是创建布尔类型的一种非常好的方法:
c
typedef enum {FALSE, TRUE} Bool;
当然,从C99
开始,我们有内置的布尔类型,所以使用这一新特性的程序员不需要这样定义Bool
类型。
16.5.2 枚举作为整数
在系统内部,
C
语言会把枚举变量和常量作为整数
来处理 。默认情况下,编译器会把整数0, 1, 2, ...
赋给特定枚举中的常量。例如,在枚举suit
的例子中,CLUBS、DIAMONDS、HEARTS
和SPADES
分别表示0、1、2
和3
。
我们可以为枚举常量自由选择不同的值 。现在假设希望CLUBS、DIAMONDS、HEARTS
和SPADES
分别表示1、2、3
和4
,可以在声明枚举时指明这些数:
c
enum suit {CLUBS = 1, DIAMONDS = 2, HEARTS = 3, SPADES = 4};
枚举常量的值可以是任意整数,也可以不用按照特定的顺序列出:
c
enum dept {RESEARCH = 20, PRODUCTION = 10, SALES = 25};
两个或多个枚举常量具有相同的值甚至也是合法的。
当没有为枚举常量指定值时,它的值比前一个常量的值大1
。(第一个枚举常量的值默认为0
。)在下列枚举中,BLACK
的值为0
,LT_GRAY
为7
,DK_GRAY
为8
,而WHITE
为15
:
c
enum EGA_colors {BLACK, LT_GRAY = 7, DK_GRAY, WHITE = 15};
枚举的值只不过是一些稀疏分布的整数,所以C
语言允许把它们与普通整数进行混合:
c
int i;
enum {CLUBS, DIAMONDS, HEARTS, SPADES} s;
i = DIAMONDS; /* i is now 1 */
s = 0; /* s is now 0 (CLUBS) */
s++; /* s is now 1 (DIAMONDS) */
i = s + 2; /* i is now 3 */
//编译器会把s作为整型变量来处理,而CLUBS、DIAMONDS、HEARTS和SPADES只是数0、1、2和3的名字而已。
请注意 !!虽然把枚举的值作为整数使用非常方便,但是把整数用作枚举的值是非常危险的。例如,我们可能会不小心把
4
存储到s
中,而4
不能跟任何花色相对应。
16.5.3 枚举声明"标记字段"
用枚举来解决
16.4节
遇到的问题是非常合适的:用来确定联合中最后一个被赋值的成员。例如,在结构Number
中,可以把成员kind
声明为枚举而不是int
:
c
typedef struct {
enum {INT_KIND, DOUBLE_KIND} kind;
union {
int i;
double d;
} u;
} Number;
这种新结构和旧结构的用法完全一样。这样做的好处是不仅远离了宏INT_KIND
和DOUBLE_KIND
(它们现在是枚举常量),而且阐明了kind
的含义,现在kind
显然应该只有两种可能的值:INT_KIND
和DOUBLE_KIND
。
问与答
问1 :当试图使用
sizeof
运算符来确定结构中的字节数量时,获得的数大于成员加在一起的总数。为什么会这样?
答:看看下面这个例子:
c
struct {
char a;
int b;
} s;
如果char
类型值占1
字节,而int
类型值占4
字节,s
会是多大呢?显而易见的答案(5
字节)不一定正确。一些计算机要求特定数据项的地址是某个字节数(一般是2
、4
或8
,由数据项的类型决定)的倍数。为了满足这一要求,编译器会在邻近的成员之间留"空洞"(即不使用的字节)
,从而使结构的成员"对齐"。如果假设数据项必须从4
字节的倍数开始,那么结构s
的成员a
后面将有3
字节的空洞,从而sizeof(s)
为8
。
顺便说一句,就像在成员间有空洞一样,结构末尾也可以有空洞。例如,结构
c
struct {
int a;
char b;
} s;
可能在成员b
的后边有3
字节的空洞。
问2:结构的开始处是否可能会有"空洞"?
答:不会 。C
标准指明只允许在成员之间或者最后一个成员的后边有空洞。因此可以确保指向结构第一个成员的指针就是指向整个结构的指针。(但是,注意这两个指针的类型不同。)
问3 :使用
==
来判定两个结构是否相等为什么是不合法的?
答:这种操作超出了C语言的范围,因为任何实现都不能确保它始终是和语言的体系相一致的。逐个比较结构成员是极没有效率的。比较结构中的全部字节是相对较好的方法(许多计算机有专门的指令可以用来快速执行此类比较)。然而,如果结构中含有空洞,那么比较字节会产生不正确的结果。即使对应的成员有同样的值,空洞中的废弃值也可能会不同。这个问题可以通过下列方法解决,那就是编译器要确保空洞始终包含相同的值(比如零)。然而,初始化空洞会影响全部使用结构的程序的性能,所以它是不可行的。
问4 :为什么
C
语言提供两种命名结构类型的方法(标记命名
和typedef
命名)?
答:C
语言早期没有typedef
,所以标记是结构类型命名的唯一有效方法。当加入typedef
时,已经太晚了,以致无法删除标记了。此外,当结构的成员是指向同类型结构的指针时(见17.5节
的node
结构),标记仍然是非常必要的。
问5 :结构可否同时有标记名和
typedef
名?
答:可以。事实上,标记名和typedef名甚至可以是一样的,虽然不要求这么做:
c
typedef struct part {
int number;
char name[NAME_LEN+1];
int on_hand;
} part;
问6:如何能在程序的几个文件间共享结构类型呢?
答:把结构标记(如果喜欢也可以用typedef
)的声明放在头文件中,然后在需要结构的地方包含此头文件就可以了 。例如,为了共享结构part
,可以在头文件中放入下列内容:
c
struct part {
int number;
char name[NAME_LEN+1];
int on_hand;
};
//注意,这里只是声明结构标记,而没有声明具有这种类型的变量。
顺便提一句,含有结构标记声明或结构类型声明的头文件可能需要保护,以避免多次包含(15.2节
)。在同一文件中两次声明同一个标记或类型是错误的。类似的说明也适用于联合和枚举。
问7 :如果在两个不同的文件中包含了结构
part
的声明,那么一个文件中的part
类型变量和另一个文件中的part
类型变量是否一样呢?
答:从技术上来说,不一样 。但是,C
标准提到,一个文件中的part
类型变量所具有的类型和另一个文件中的part
类型变量所具有的类型是兼容的。具有兼容类型的变量可以互相赋值,所以在实际中"兼容的"类型和"相同的"类型之间几乎没有差异。
C89
和从C99
开始的标准在有关结构兼容性的法则上稍有不同。在C89
中,对于在不同文件中定义的结构来说,如果它们的成员具有同样的名字并且顺序一样,那么它们是兼容的,相应的成员类型也是兼容的。从C99
开始则更进一步,它要求两个结构要么具有相同的标记,要么都没有标记。
类似的兼容性法则也适用于联合和枚举(在C89
和从C99
开始的标准之间的差异也一样)。
问8:让指针指向复合字面量是否合法?
答:合法 。考虑16.2节
的print_part
函数。目前这个函数的形式参数是一个part
结构。如果将参数修改为指向part
结构的指针,函数的效率会更高。这样,使用该函数来显示复合字面量就可以通过在参数前面加取地址&
运算符的方式来完成:
c
print_part(&(struct part) {528, "Disk drive", 10});
问9 :
C99
允许指针指向复合字面量似乎使我们可以修改该字面量,是这样吗?
答:是的 。虽然很少这么做,但复合字面量是左值,可以修改。
问10 :我在程序中看到,枚举的最后一个常量后面有一个
逗号
,就像这样:
cenum gray_values { BLACK = 0, DARK_GRAY = 64, GRAY = 128, LIGHT_GRAY = 192, };
这样是否合法?
答:从C99
开始,这是合法的(C99
之前的有些编译器也允许这么做)。允许有"尾逗号"可以使修改枚举更方便,因为我们可以直接在枚举的最后增加常量而无须改变已有的代码。例如,我们可能希望在枚举中增加WHITE
:
c
enum gray_values {
BLACK = 0,
DARK_GRAY = 64,
GRAY = 128,
LIGHT_GRAY = 192,
WHITE = 255,
};
//LIGHT_GRAY的定义之后的逗号使得在列表最后增加WHITE很容易。
做出这一修改的原因是,C89
允许在初始化器中使用尾逗号 ,所以在枚举中也提供这一灵活性就显得很一致。 顺便说一句,从C99
开始也允许在复合字面量中使用尾逗号。
问11:枚举类型的值可以用作下标吗?
答:是的,的确可以 。它们是整数
,值(默认)从0
开始逐渐增加,所以是很理想的下标。此外,从C99
开始,枚举常量可以用作指示器中的下标。下面是一个例子:
c
enum weekdays {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY};
const char *daily_specials[] = {
[MONDAY] = "Beef ravioli",
[TUESDAY] = "BLTs",
[WEDNESDAY] = "Pizza",
[THURSDAY] = "Chicken fajitas",
[FRIDAY] = "Macaroni and cheese"
};
写在最后
本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!