C现代方法(第16章)笔记——结构、联合和枚举

第16章 结构、联合和枚举

——函数延迟绑定:数据结构导致绑定。记住:在编程过程后期再结构化数据。

本章介绍3种新的类型:结构、联合和枚举结构是可能具有不同类型的值(成员)的集合。联合结构很类似,不同之处在于联合的成员共享同一存储空间。这样的结果是,联合可以每次存储一个成员,但是无法同时存储全部成员。枚举是一种整数类型,它的值由程序员来命名。

在这3种类型中,结构是到目前为止最重要的一种,所以本章的大部分内容是关于结构的。16.1节说明了如何声明结构变量,以及如何对其进行基本操作。随后,16.2节解释了定义结构类型的方法,借助结构类型,我们就可以编写接受结构类型参数或返回结构的函数。16.3节探讨如何实现数组和结构的嵌套。本章的最后两节分别讨论了联合(16.4节)枚举(16.5节)

16.1 结构变量

到目前为止介绍的唯一一种数据结构就是数组。数组有两个重要特性:

  • 首先,数组的所有元素具有相同的类型;
  • 其次,为了选择数组元素需要指明元素的位置(作为整数下标)。

结构所具有的特性与数组有很大不同。结构的元素(在C语言中的说法是结构的成员)可能具有不同的类型。而且,每个结构成员都有名字,因此为了选择特定的结构成员需要指明结构成员的名字而不是它的位置

由于大多数编程语言提供类似的特性,因此结构可能听起来很熟悉。在其他一些语言中,经常把结构称为记录(record),把结构的成员称为字段(field)

16.1.1 结构变量的声明

当需要存储相关数据项的集合时,结构是一种合乎逻辑的选择。例如,假设需要记录存储在仓库中的零件。每种零件需要存储的信息可能包括零件的编号(整数)、零件的名称(字符串)以及现有零件的数量(整数)。为了产生一个可以存储全部3种数据项的变量,可以使用类似下面这样的声明:

struct { 
    int number; 
    char name[NAME_LEN+1]; 
    int on_hand; 
} part1, part2;

//结构的成员在内存中是按照声明的顺序存储的

每个结构变量都有3个成员:number(零件的编号)name(零件的名称)on_hand(现有数量)注意!!这里的声明格式和C语言中其他变量的声明格式一样。struct{...}指明了类型,part1part2是具有这种类型的变量。

每个结构代表一种新的作用域。任何声明在此作用域内的名字都不会和程序中的其他名字冲突。[用C语言的术语可表述为,每个结构都为它的成员设置了独立的名字空间(name space)。]例如,下列声明可以出现在同一程序中:

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;

结构part1part2中的成员number和成员name不会与结构employee1employee2中的成员number和成员name冲突。

16.1.2 结构变量的初始化

和数组一样,结构变量也可以在声明的同时进行初始化。为了对结构进行初始化,要把待存储到结构中的值的列表准备好,并用花括号把它括起来:

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的初始化器:

{528, "Disk drive", 10}

指示器与之类似,但是在初始化时需要按成员的名字来指定初值:

{.number = 528, .name = "Disk drive", .on_hand = 10} 

点号和成员名称的组合也是指示器(数组元素的指示器在形式上有所不同)

指示器有几个优点:

  • 其一,易读且容易进行验证,这是因为读者可以清楚地看出结构中的成员和初始化器中的值之间的对应关系。
  • 其二,初始化器中的值的顺序不需要与结构中成员的顺序一致。以上这个例子可以写为
{.on_hand = 10, .name = "Disk drive", .number = 528} 

因为顺序不是问题,所以程序员不必记住原始声明时成员的顺序。而且成员的顺序在之后还可以改变,不会影响指示器。

指示器中列出来的值前面不一定要有指示器(数组也是如此,见8.1节)。考虑下面的例子:

{.number = 528, "Disk drive", .on_hand = 10} 

"Disk drive"的前面并没有指示器,所以编译器会认为它用于初始化结构中位于number之后的成员。初始化器中没有涉及的成员都设为0

16.1.4 对结构的操作

既然最常见的数组操作是取下标(根据位置选择数组元素),那么也就无须惊讶结构最常用的操作是选择成员了。但是,结构成员是通过名字而不是通过位置访问的

为了访问结构内的成员,首先写出结构的名字,然后写一个句点,再写出成员的名字。例如,下列语句将显示结构part1的成员的值:

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节),所以它们可以出现在赋值运算的左侧,也可以作为自增或自减表达式的操作数:

part1.number = 258; /* changes part1's part number */ 
part1.on_hand++; /* increments part1's quantity on hand */ 

用于访问结构成员的句点实际上就是一个C语言的运算符。它的运算优先级与后缀++和后缀--运算符一样,所以句点运算符的优先级几乎高于所有其他运算符。考虑下面的例子:

scanf("%d", &part1.on_hand);

表达式&part1.on_hand包含两个运算符(即&.)。.运算符的优先级高于&运算符,所以就像希望的那样,&计算的是part1.on_hand的地址。


结构的另一种主要操作是赋值运算

part2 = part1;

这一语句的效果是把part1.number复制到part2.number,把part1.name复制到part2.name,以此类推。

因为数组不能用=运算符进行复制,所以结构可以用=运算符复制应该是一个惊喜。更大的惊喜是,对结构进行复制时,嵌在结构内的数组也被复制。一些程序员利用这种性质来产生“空”结构,以封装稍后将进行复制的数组:

struct { int a[10]; } a1, a2; 

a1 = a2;   /* legal, since a1 and a2 are structures */ 

运算符=仅仅用于类型兼容的结构。两个同时声明的结构(比如part1part2)是兼容的。正如下一节你会看到的那样,使用同样的“结构标记”或同样的类型名声明的结构也是兼容的。

请注意!!除了赋值运算,C语言没有提供其他用于整个结构的操作。特别是不能使用运算符==!=来判定两个结构相等还是不等

16.2 结构类型

16.1节虽然说明了声明结构变量的方法,但是没有讨论一个重要的问题:命名结构类型。假设程序需要声明几个具有相同成员的结构变量。如果一次可以声明全部变量,那么没有什么问题。但是,如果需要在程序中的不同位置声明变量,那么问题就复杂了。如果在某处编写了:

struct { 
    int number; 
    char name[NAME_LEN+1]; 
    int on_hand; 
} part1;

并且在另一处编写了:

struct { 
    int number; 
    char name[NAME_LEN+1]; 
    int on_hand; 
} part2;

那么立刻就会出现问题。重复的结构信息会使程序膨胀。因为难以确保这些声明会保持一致,所以将来修改程序会有风险。

但是这些还不是最大的问题。根据C语言的规则,part1part2不具有兼容的类型(因为没有同时声明),因此不能把part1赋值给part2,反之亦然。而且,因为part1part2的类型都没有名字,所以也就不能把它们用作函数调用的参数。

为了克服这些困难,需要定义表示结构类型(而不是特定的结构变量)的名字。C语言提供了两种命名结构的方法:可以声明“结构标记”,也可以使用typedef来定义类型名[类型定义(7.5节)]。

16.2.1 结构标记的声明

结构标记(structure tag)是用于标识某种特定结构的名字。下面的例子声明了名为part的结构标记:

struct part { 
    int number; 
    char name[NAME_LEN+1]; 
    int on_hand;
};
//注意,右花括号后的分号是必不可少的,它表示声明结束。

请注意!!如果无意间忽略了结构声明结尾的分号,可能会导致奇怪的错误。考虑下面的例子:

struct 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,就可以用它来声明变量了:

struct part part1, part2;

但是,不能通过省略单词struct来缩写这个声明:

part part1, part2;  /*** WRONG ***/

part不是类型名。如果没有单词struct的话,它就没有任何意义。

因为结构标记只有在前面放置了单词struct时才会有意义,所以它们不会和程序中用到的其他名字发生冲突。程序拥有名为part的变量是完全合法的(虽然有点儿容易混淆)。

顺便说一句,结构标记的声明可以和结构变量的声明合并在一起:

struct part { 
    int number; 
    char name[NAME_LEN+1]; 
    int on_hand; 
} part1, part2; 

在这里不仅声明了结构标记part(可能稍后会用part声明更多的变量),而且声明了变量part1part2

所有声明为struct part类型的结构彼此之间是兼容的:

struct part part1 = {528, "Disk drive", 10}; 
struct part part2; 

part2 = part1; /* legal; both parts have the same type */ 

16.2.2 结构类型的定义

除了声明结构标记,还可以用typedef来定义真实的类型名。例如,可以按照如下方式定义名为Part的类型:

typedef struct  { 
    int number; 
    char name[NAME_LEN+1]; 
    int on_hand; 
} Part;

注意,类型Part的名字必须出现在定义的末尾,而不是在单词struct的后边。

可以像内置类型那样使用Part。例如,可以用它声明变量:

Part part1, part2; 

因为类型Parttypedef的名字,所以不允许书写struct Part。无论在哪里声明,所有的Part类型的变量都是兼容的。

需要命名结构时,通常既可以选择声明结构标记,也可以使用typedef。但是,正如稍后将看到的,结构用于链表(17.5节)时,强制使用声明结构标记。在本书的大多数例子中,我使用的是结构标记而不是typedef名。

16.2.3 结构作为参数和返回值

函数可以有结构类型的实际参数和返回值。下面来看两个例子。当把part结构用作实际参数时,第一个函数显示出结构的成员:

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可能的调用方法:

print_part(part1);

第二个函数返回part结构,此结构由函数的实际参数构成:

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可能的调用方法:

part1 = build_part(528, "Disk drive", 10); 

给函数传递结构和从函数返回结构都要求生成结构中所有成员的副本。这样的结果是,这些操作对程序强加了一定数量的系统开销,特别是结构很大的时候。为了避免这类系统开销,有时用传递指向结构的指针来代替传递结构本身是很明智的做法。类似地,可以使函数返回指向结构的指针来代替返回实际的结构。在17.5节的例子中,可以看到用指向结构的指针作为参数或者作为返回值的函数。


除了效率方面的考虑之外,避免创建结构的副本还有其他原因。例如,<stdio.h>定义了一个名为FILE的类型,它通常是结构。每个FILE结构存储的都是已打开文件的状态信息,因此在程序中必须是唯一的。<stdio.h>中每个用于打开文件的函数都返回一个指向FILE结构的指针,每个对已打开文件执行操作的函数都需要用FILE指针作为参数。

有时,可能希望在函数内部初始化结构变量来匹配其他结构(可能作为函数的形式参数)。在下面的例子中,part2的初始化器是传递给函数f的形式参数:

void f(struct part part1) 
{ 
    struct part part2 = part1; 
    ... 
}

C语言允许这类初始化器,因为初始化的结构(此例中的part2)具有自动存储期(10.1节),也就是说它局部于函数并且没有声明为static。初始化器可以是适当类型的任意表达式,包括返回结构的函数调用。

16.2.4 复合字面量(C99)

9.3节介绍过从C99开始引入的新特性复合字面量。在那一节中,复合字面量被用于创建没有名字的数组,这样做的目的通常是将数组作为参数传递给函数。复合字面量同样也可以用于“实时”创建一个结构,而不需要先将其存储在变量中。生成的结构可以像参数一样传递,可以被函数返回,也可以赋值给变量。接下来看两个例子。

首先,使用复合字面量创建一个结构,这个结构将传递给函数。例如,可以按如下方式调用print_part函数:

print_part((struct part) {528, "Disk drive", 10}); 

上面的复合字面量创建了一个part结构,依次包括成员528"Disk drive"10。这个结构之后被传递到print_part显示。

下面的语句把复合字面量赋值给变量:

part1 = (struct part) {528, "Disk drive", 10}; 

//可以赋值是因为结构本身的原因,不适用于数组
int a[3];
a = (int[]){1, 2, 3};  //错误语句

这一语句类似于包含初始化器的声明,但不完全一样——初始化器只能出现在声明中,不能出现在这样的赋值语句中。

一般来说,复合字面量包括用圆括号括住的类型名和后续的初始化器。如果复合字面量代表一个结构,类型名可以是结构标签的前面加上struct(如本例所示)或者typedef名。一个复合字面量的初始化器部分可以包含指示器:

print_part((struct part) {.on_hand = 10, 
                          .name = "Disk drive",  
                          .number = 528}); 

复合字面量不会提供完全的初始化,所以任何未初始化的成员默认值为0

16.2.5 匿名结构(C1X)

C11开始,结构或者联合(16.4节)的成员也可以是另一个没有名字的结构。如果一个结构或者联合包含了这样的成员:

  1. 没有名称
  2. 被声明为结构类型,但是只有成员列表而没有标记

则这个成员就是一个匿名结构(anonymous structure)

在下例中,struct tunion u的第二个成员都是匿名结构:

struct t {int i; struct {char c; float f;};}; 
union u {int i; struct {char c; float f;};}; 

现在的问题是,如何才能访问匿名结构的成员?若某个匿名结构S是结构或者联合X的成员,那么S的成员就被当作X的成员。进一步,对于多层嵌套的情况,如果符合以上条件,则可以递归地应用这种关系。

在下面的例子中,struct t包含了一个没有标记、没有名称的结构成员,这个结构成员的成员cf被认为属于struct t

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的第二个成员是匿名结构,而匿名结构的成员中又有一个是匿名结构,所以,匿名结构的成员if被当作struct tag的成员,这意味着struct tag有两个成员的名称相同,都是i

struct tag  
{ 
    struct {int i;};  
    struct {struct {int i; float f;}; double d;};  
    char c; 
};

尽管匿名结构的成员被当作隶属于包含该结构的上层结构的成员,但它的初始化器依然必须采用被花括号包围的形式。

在下例中,尽管匿名结构的成员x被认为属于包含它的那个结构struct t,但它的初始化器仍然需要使用一对花括号

struct t {char c; struct {int x;};}; 
struct t t = {'x', 1};  // 非法 
struct t t = {'x', {1}};   // 合法

16.3 嵌套的数组和结构

结构和数组的组合没有限制。数组可以将结构作为元素,结构也可以包含数组和结构作为成员。我们已经看过数组嵌套在结构内部的示例(结构part的成员name)。下面探讨其他的可能性:成员是结构的结构和元素是结构的数组。

16.3.1 嵌套的结构

把一种结构嵌套在另一种结构中经常是非常有用的。例如,假设声明了如下的结构,此结构用来存储一个人的名、中间名和姓:

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成为结构(而不是把firstmiddle_initiallast作为student结构的成员)的好处之一就是可以把名字作为数据单元来处理,这样操作起来更容易。例如,如果打算编写函数来显示名字,那么只需要传递一个实际参数(person_name结构)而不是三个实际参数:

display_name(student1.name); 

同样,把信息从结构person_name复制给结构student的成员name将只需要一次而不是三次赋值:

struct person_name new_name; 
... 
student1.name = new_name; 

16.3.2 结构数组

数组和结构最常见的组合之一就是其元素为结构的数组这类数组可以用作简单的数据库。例如,下列结构part的数组能够存储100种零件的信息:

struct part inventory[100]; 

为了访问数组中的某种零件,可以使用取下标的方式。例如,为了显示存储在位置i的零件,可以写成

print_part(inventory[i]); 

访问结构part内的成员要求结合使用取下标和成员选择。为了给inventory[i]中的成员number赋值883,可以写成

inventory[i].number = 883; 

访问零件名中的单个字符要求先取下标(选择特定的零件),然后选择成员(选择成员name),再取下标(选择零件名称中的字符)。为了使存储在inventory[i]中的名字变为空字符串,可以写成

inventory[i].name[0] = '\0';

16.3.3 结构数组的初始化

初始化结构数组与初始化多维数组的方法非常相似。每个结构都拥有自己的带有花括号的初始化器,数组的初始化器简单地在结构初始化器的外围括上另一对花括号。

初始化结构数组的原因之一是,我们打算把它作为程序执行期间不改变的信息的数据库。例如,假设程序在打国际长途电话时需要访问国家(地区)代码。首先,设置结构用来存储国家(地区)名和相应代码:

struct dialing_code { 
    char *country; 
    int code; 
}; 

注意,country是指针而不是字符数组。如果计划用dialing_code结构作为变量,则可能有问题,但是这里没这样做。当初始化dialing_code结构时,country会指向字面串。

接下来,声明这类结构的数组并对其进行初始化,从而使此数组包含一些世界上人口最多的国家(地区)的代码:

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,名字暂时为空:

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(退出)分别表示这些操作。与程序的会话可能如下所示:

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的变量。

因为这个程序是以菜单方式驱动的,所以十分容易勾勒出主循环结构:

for (;;) { 
    提示用户输入操作码 
    读操作码 
    switch(操作码){ 
        case 'i': 执行插入操作; break; 
        case 's': 执行搜索操作; break; 
        case 'u': 执行更新操作; break; 
        case 'p': 执行显示操作; break; 
        case 'q': 终止程序; 
        default: 显示出错消息; 
    } 
} 

为了方便起见,接下来将分别设置不同的函数执行插入、搜索、更新和显示操作。因为这些函数都需要访问inventorynum_parts,所以可以把这些变量设置为外部变量。或者把变量声明在main函数内,然后把它们作为实际参数传递给函数。从设计角度来说,使变量局部于函数通常比把它们外部化更好(如果忘记了原因,见10.2节)。然而,在此程序中,把inventorynum_parts放在main函数中只会使程序复杂化。


由于稍后会解释的一些原因,这里决定把程序分割为三个文件:inventory.c文件,它包含程序的大部分内容;readline.h文件,它包含read_line函数的原型;readline.c文件,它包含read_line函数的定义。本节的后面将讨论后两个文件,现在先集中讨论inventory.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_partinsert函数、search函数和update函数可以定位数据库中的零件(或者简单地确定零件是否存在)。

现在还剩下一个细节:read_line函数。这个函数用来读零件的名字。13.3节讨论了书写此类函数时的相关问题,但是那个read_line函数不能用于这个程序。请思考当用户插入零件时会发生什么:

Enter part number: 528 
Enter part name: Disk drive 

在输入零件的编号后,用户按回车键,输入零件的名字后再次按了回车键,这样每次都无形中给程序留下一个必须读取的换行符。为了方便讨论,现在假装这些字符都是可见的:

Enter part number: 528¤
Enter part name: Disk drive¤

当调用scanf函数来读零件编号时,函数读入了528,但是留下了字符¤未读。如果试图用原始的read_line函数来读零件名称,那么函数将立刻遇到字符¤,并且停止读入。当数值输入的后边跟有字符输入时,这种问题非常普遍。解决办法就是编写read_line函数,使它在开始往字符串中存储字符之前跳过空白字符。这不仅解决了换行符的问题,而且可以避免存储用户在零件名称的开始处输入的任何空白。

因为read_line函数与inventory.c文件中的其他函数无关,而且它在其他程序中有复用的可能,所以我们决定把此函数从inventory.c中独立出来。read_line函数的原型将放在头文件readline.h中:

/*
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文件中:

/*
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,并且这个联合变量有两个成员:

union { 
    int i;  
    double d; 
} u;
//注意,联合的声明方式非常类似于结构的声明方式:
struct { 
    int i;  
    double d; 
} s; 

事实上,结构变量s和联合变量u只有一处不同:s的成员存储在不同的内存地址中,而u的成员存储在同一内存地址中

在结构变量s中,成员id占有不同的内存单元。s总共占用了12字节。在联合变量u中,成员id互相交叠(i实际上是d的前4个字节),所以u只占用了8字节;此外,id具有相同的地址。

访问联合成员的方法和访问结构成员的方法相同。为了把数82存储到u的成员i中,可以写成

u.i = 82;

//为了把值74.8存储到成员d中,可以写成
u.d = 74.8; 

因为编译器把联合成员重叠存储,所以改变一个成员就会使之前存储在任何其他成员中的值发生改变。因此,如果把一个值存储到u.d中,那么先前存储在u.i中的值会丢失。(如果测试u.i的值,那么它会显示出无意义的内容。)类似地,改变u.i也会影响u.d由于这个性质,可以把u想成存储i或者存储d的地方,而不是同时存储二者的地方。(结构s允许存储id。)


联合的性质和结构的性质几乎一样,因此可以用声明结构标记类型的方法来声明联合的标记和类型。像结构一样,联合可以使用运算符=进行复制,也可以传递给函数,还可以由函数返回

联合的初始化方式甚至也和结构的初始化很类似。但是,只有联合的第一个成员可以获得初始值。例如,可以用下列方式初始化联合u的成员i0

union { 
    int i; 
    double d; 
} u = {0};

注意!!花括号是必需的。花括号内的表达式必须是常量。(从C99开始的规则稍有不同,在18.5节会看到。)

指示器(我们在讨论数组和结构时介绍过的一种C99特性)也可以用在联合中。指示器允许我们指定需要对联合中的哪个成员进行初始化。例如,可以像下面这样初始化u的成员d

union { 
    int i; 
    double d; 
} u = {.d = 10.0}; 

只能初始化一个成员,但不一定是第一个

联合有几种应用,现在讨论其中的两种。联合的另外一个应用是用不同的方法观察存储,因为这个应用与机器高度相关,所以推迟到20.3节再介绍。

16.4.1 用联合来节省空间

在结构中经常使用联合作为节省空间的一种方法。假设打算设计的结构包含通过礼品册售出的商品的信息。礼品册上只有三种商品:图书、杯子和衬衫。每种商品都含有库存量、价格以及与商品类型相关的其他信息。

  • 图书:书名、作者、页数。
  • 杯子:设计。
  • 衬衫:设计、可选颜色、可选尺寸。

最初的设计可能会得到如下结构:

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的值将是BOOKMUGSHIRT之一。成员colorssizes将存储颜色和尺寸的组合代码。

虽然上述结构十分好用,但是它很浪费空间,因为对礼品册中的所有商品来说只有结构中的部分信息是常用的。比如,如果商品是图书,那么就不需要存储designcolorssizes。通过在结构catalog_item内部放置一个联合,可以减少结构所需要的内存空间。联合的成员将是一些特殊的结构,每种结构都包含特定类型的商品所需要的数据:

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的成员,而结构bookmugshirt则是联合item的成员。如果c是表示图书的结构catalog_item,那么可以用下列方法显示图书的名称:

printf("%s", c.item.book.title); 

正如上边的例子显示的那样,访问嵌套在结构内部的联合是很困难的:为了定位图书的名称,不得不指明结构的名字(c)、结构的联合成员的名字(item)、联合的结构成员的名字(book),以及此结构的成员名(title)


可以用catalog_item结构来说明联合有趣的一面。把值存储在联合的一个成员中,然后通过另一个名字来访问该数据通常不太可取,因为给联合的一个成员赋值会导致其他成员的值不确定。然而,C标准提到了一种特殊情况:联合的两个或多个成员是结构,而这些结构最初的一个或多个成员是相匹配的。(这些成员的顺序应该相同,类型也要兼容,但名字可以不一样。)如果当前某个结构有效,则其他结构中的匹配成员也有效。

考虑嵌入在catalog_item结构中的联合。它包含三个结构成员,其中两个结构(mugshirt)的起始成员(design)相匹配。现在假定我们给其中一个design成员赋值:

strcpy(c.item.mug.design, "Cats"); 

//另一个结构中的design成员也会被定义,并具有相同的值:
printf("%s", c.item.shirt.design); /* prints "Cats" */ 

16.4.2 用联合来构造混合的数据结构

联合还有一个重要的应用:创建含有不同类型混合数据的数据结构。现在假设需要数组的元素是int值和double值的混合。因为数组的元素必须是相同的类型,所以好像不可能产生如此类型的数组。但是利用联合,这件事就相对容易了。首先,定义一种联合类型,它所包含的成员分别表示要存储在数组中的不同数据类型:

typedef union { 
    int i; 
    double d; 
} Number; 

//接下来,创建一个数组,使数组的元素是Number类型的值:
Number number_array[1000]; 

数组number_array的每个元素都是Number联合。Number联合既可以存储int类型的值又可以存储double类型的值,所以可以在数组number_array中存储intdouble的混合值。例如,假设需要用数组number_array0号元素来存储5,用1号元素来存储8.395。下列赋值语句可以达到期望的效果:

number_array[0].i = 5; 
number_array[1].d = 8.395; 

16.4.3 为联合添加“标记字段”

联合所面临的主要问题是不容易确定联合最后改变的成员,因此所包含的值可能是无意义的。请思考下面这个问题:假设编写了一个函数,用来显示当前存储在联合Number中的值。这个函数可能有下列框架:

void print_number(Number n) 
{   
    if (n 包含一个整数) 
        printf("%d", n.i); 
    else 
        printf("%g", n.d); 
} 

但是,没有方法可以帮助函数print_number来确定n包含的是整数还是浮点数。

为了记录此信息,可以把联合嵌入一个结构中,并且此结构还含有另一个成员:“标记字段”或者“判别式”,它是用来提示当前存储在联合中的内容的。在本节先前讨论的结构catalog_item中,item_type就是用于此目的的。

下面把Number类型转换成具有嵌入联合的结构类型:

#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的哪个成员。例如,如果nNumber类型的变量,对u的成员i进行赋值操作可以采用下列形式:

n.kind = INT_KIND; 
n.u.i = 82;
//注意,对i赋值要求首先选择n的成员u,然后才是u的成员i。

当需要找回存储在Number型变量中的数时,kind将表明联合的哪个成员是最后被赋值的。函数print_number可以利用这种能力:

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开始,结构或者联合的成员也可以是另一个没有名字的联合。如果一个结构或者联合包含了这样的成员:

  1. 没有名称
  2. 被声明为联合类型,但是只有成员列表而没有标记

则这个成员就是一个匿名联合(anonymous union)。在下例中,struct tunion u的第二个成员都是匿名联合。

struct t {int i; union {char c; float f;};}; 
union u {int i; union {char c; float f;};}; 

现在的问题是,如何才能访问匿名联合的成员?答案如下:若某个匿名联合U是结构或者联合X的成员,则U的成员被当作X的成员。进一步,对于多层嵌套的情况,如果符合以上条件,那么可以递归地应用这种关系。(跟结构的情况类似)

在下面的例子中,struct t包含了一个没有标记、没有名字的联合成员,这个联合的成员cf被认为属于struct t

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的第二个成员是匿名联合,而匿名联合的成员中又有一个是匿名联合,所以,匿名联合的成员if被当作struct tag的成员,这意味着struct tag有两个成员的名称相同,都是i:

struct tag 
{ 
    struct {int i;};  
    union {union {int i; float f;}; double d;};  
    char c; 
};

16.5 枚举

在许多程序中,我们会需要变量只具有少量有意义的值。例如,布尔变量应该只有2种可能的值:“真”“假”。用来存储扑克牌花色的变量应该只有4种可能的值:“梅花”、“方片”、“红桃”和“黑桃”。显然可以用声明成整数的方法来处理此类变量,并且用一组编码来表示变量的可能值:

int s;     /* s will store a suit */ 
... 
s = 2;     /* 2 represents "hearts" */ 

虽然这种方法可行,但是也遗留了许多问题。有些人读程序时可能不会意识到s只有4种可能的值,而且不会知道2的特殊含义。

使用宏来定义牌的花色“类型”和不同花色的名字是一种正确的措施:

#define SUIT       int 
#define CLUBS      0 
#define DIAMONDS   1 
#define HEARTS     2 
#define SPADES     3 

//那么前面的示例现在可以变得更加容易阅读:
SUIT s; 
... 
s = HEARTS;

这种方法有所改进,但它仍然不是最好的解决方案,因为这样做没有为阅读程序的人指出宏表示具有相同“类型”的值。如果可能值的数量很多,那么为每个值定义一个宏是很麻烦的。而且,因为预处理器会删除我们定义的CLUBSDIAMONDSHEARTSSPADES这些名字,所以在调试期间没法使用这些名字。


C语言为具有可能值较少的变量提供了一种专用类型。枚举类型(enumeration type)是一种值由程序员列出(“枚举”)的类型,而且程序员必须为每个值命名(枚举常量)。以下例子中枚举的值(CLUBSDIAMONDSHEARTSSPADES)可以赋值给变量s1s2

enum {CLUBS, DIAMONDS, HEARTS, SPADES} s1, s2; 

虽然枚举和结构、联合没有什么共同的地方,但是它们的声明方法很类似。但是,与结构或联合的成员不同,枚举常量的名字必须不同于作用域范围内声明的其他标识符。

枚举常量类似于用#define指令创建的常量,但是两者又不完全一样。特别地,枚举常量遵循C语言的作用域规则:如果枚举声明在函数体内,那么它的常量对外部函数来说是不可见的


16.5.1 枚举标记和类型名

与命名结构和联合的原因相同,我们也常常需要创建枚举的名字。与结构和联合一样,可以用两种方法命名枚举:通过声明标记的方法,或者使用typedef来创建独一无二的类型名。

枚举标记类似于结构和联合的标记。例如,为了定义标记suit,可以写成:

enum suit {CLUBS, DIAMONDS, HEARTS, SPADES}; 

//变量suit可以按照下列方法来声明:
enum suit s1, s2;

还可以用typedefSuit定义为类型名:

typedef enum {CLUBS, DIAMONDS, HEARTS, SPADES} Suit; 
Suit s1, s2;

C89中,利用typedef来命名枚举是创建布尔类型的一种非常好的方法:

typedef enum {FALSE, TRUE} Bool; 

当然,从C99开始,我们有内置的布尔类型,所以使用这一新特性的程序员不需要这样定义Bool类型。


16.5.2 枚举作为整数

在系统内部,C语言会把枚举变量和常量作为整数来处理。默认情况下,编译器会把整数0, 1, 2, ...赋给特定枚举中的常量。例如,在枚举suit的例子中,CLUBS、DIAMONDS、HEARTSSPADES分别表示0、1、23

我们可以为枚举常量自由选择不同的值。现在假设希望CLUBS、DIAMONDS、HEARTSSPADES分别表示1、2、34,可以在声明枚举时指明这些数:

enum suit {CLUBS = 1, DIAMONDS = 2, HEARTS = 3, SPADES = 4}; 

枚举常量的值可以是任意整数,也可以不用按照特定的顺序列出

enum dept {RESEARCH = 20, PRODUCTION = 10, SALES = 25}; 

两个或多个枚举常量具有相同的值甚至也是合法的。

当没有为枚举常量指定值时,它的值比前一个常量的值大1。(第一个枚举常量的值默认为0。)在下列枚举中,BLACK的值为0LT_GRAY7DK_GRAY8,而WHITE15

enum EGA_colors {BLACK, LT_GRAY = 7, DK_GRAY, WHITE = 15}; 

枚举的值只不过是一些稀疏分布的整数,所以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

typedef struct { 
    enum {INT_KIND, DOUBLE_KIND} kind; 
    union { 
        int i; 
        double d; 
    } u; 
} Number; 

这种新结构和旧结构的用法完全一样。这样做的好处是不仅远离了宏INT_KINDDOUBLE_KIND(它们现在是枚举常量),而且阐明了kind的含义,现在kind显然应该只有两种可能的值:INT_KINDDOUBLE_KIND


问与答

问1:当试图使用sizeof运算符来确定结构中的字节数量时,获得的数大于成员加在一起的总数。为什么会这样?

答:看看下面这个例子:

struct { 
    char a; 
    int b; 
} s; 

如果char类型值占1字节,而int类型值占4字节,s会是多大呢?显而易见的答案(5字节)不一定正确。一些计算机要求特定数据项的地址是某个字节数(一般是248,由数据项的类型决定)的倍数。为了满足这一要求,编译器会在邻近的成员之间留“空洞”(即不使用的字节),从而使结构的成员“对齐”。如果假设数据项必须从4字节的倍数开始,那么结构s的成员a后面将有3字节的空洞,从而sizeof(s)8

顺便说一句,就像在成员间有空洞一样,结构末尾也可以有空洞。例如,结构

struct { 
    int a; 
    char b; 
} s; 

可能在成员b的后边有3字节的空洞。

问2:结构的开始处是否可能会有“空洞”?

答:不会C标准指明只允许在成员之间或者最后一个成员的后边有空洞。因此可以确保指向结构第一个成员的指针就是指向整个结构的指针。(但是,注意这两个指针的类型不同。)

问3:使用==来判定两个结构是否相等为什么是不合法的?

答:这种操作超出了C语言的范围,因为任何实现都不能确保它始终是和语言的体系相一致的。逐个比较结构成员是极没有效率的。比较结构中的全部字节是相对较好的方法(许多计算机有专门的指令可以用来快速执行此类比较)。然而,如果结构中含有空洞,那么比较字节会产生不正确的结果。即使对应的成员有同样的值,空洞中的废弃值也可能会不同。这个问题可以通过下列方法解决,那就是编译器要确保空洞始终包含相同的值(比如零)。然而,初始化空洞会影响全部使用结构的程序的性能,所以它是不可行的。

问4:为什么C语言提供两种命名结构类型的方法(标记命名typedef命名)?

答:C语言早期没有typedef,所以标记是结构类型命名的唯一有效方法。当加入typedef时,已经太晚了,以致无法删除标记了。此外,当结构的成员是指向同类型结构的指针时(见17.5节node结构),标记仍然是非常必要的。

问5:结构可否同时有标记名和typedef名?

答:可以。事实上,标记名和typedef名甚至可以是一样的,虽然不要求这么做:

typedef struct part { 
    int number; 
    char name[NAME_LEN+1]; 
    int on_hand; 
} part; 

问6:如何能在程序的几个文件间共享结构类型呢?

答:把结构标记(如果喜欢也可以用typedef)的声明放在头文件中,然后在需要结构的地方包含此头文件就可以了。例如,为了共享结构part,可以在头文件中放入下列内容:

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结构的指针,函数的效率会更高。这样,使用该函数来显示复合字面量就可以通过在参数前面加取地址&运算符的方式来完成:

print_part(&(struct part) {528, "Disk drive", 10}); 

问9C99允许指针指向复合字面量似乎使我们可以修改该字面量,是这样吗?

答:是的。虽然很少这么做,但复合字面量是左值,可以修改

问10:我在程序中看到,枚举的最后一个常量后面有一个逗号,就像这样:

enum gray_values { 
   BLACK = 0, 
   DARK_GRAY = 64, 
   GRAY = 128, 
   LIGHT_GRAY = 192, 
};

这样是否合法?

答:从C99开始,这是合法的(C99之前的有些编译器也允许这么做)。允许有“尾逗号”可以使修改枚举更方便,因为我们可以直接在枚举的最后增加常量而无须改变已有的代码。例如,我们可能希望在枚举中增加WHITE

enum gray_values { 
    BLACK = 0, 
    DARK_GRAY = 64, 
    GRAY = 128, 
    LIGHT_GRAY = 192, 
    WHITE = 255, 
};
//LIGHT_GRAY的定义之后的逗号使得在列表最后增加WHITE很容易。

做出这一修改的原因是,C89允许在初始化器中使用尾逗号,所以在枚举中也提供这一灵活性就显得很一致。 顺便说一句,从C99开始也允许在复合字面量中使用尾逗号。

问11:枚举类型的值可以用作下标吗?

答:是的,的确可以。它们是整数,值(默认)从0开始逐渐增加,所以是很理想的下标。此外,从C99开始,枚举常量可以用作指示器中的下标。下面是一个例子:

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!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

New_Teen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值