第14章 结构和其他数据形式

04-13Ctrl+D 收藏本站

关灯 直达底部

本章介绍以下内容:

关键字:struct、union、typedef

运算符:.、->

什么是C结构,如何创建结构模板和结构变量

如何访问结构的成员,如何编写处理结构的函数

联合和指向函数的指针

设计程序时,最重要的步骤之一是选择表示数据的方法。在许多情况下,简单变量甚至是数组还不够。为此,C提供了结构变量(structure variable)提高你表示数据的能力,它能让你创造新的形式。如果熟悉Pascal的记录(record),应该很容易理解结构。如果不懂Pascal也没关系,本章将详细介绍C结构。我们先通过一个示例来分析为何需要C结构,学习如何创建和使用结构。

14.1 示例问题:创建图书目录

Gwen Glenn要打印一份图书目录。她想打印每本书的各种信息:书名、作者、出版社、版权日期、页数、册数和价格。其中的一些项目(如,书名)可以储存在字符数组中,其他项目需要一个int数组或float数组。用 7 个不同的数组分别记录每一项比较繁琐,尤其是 Gwen 还想创建多份列表:一份按书名排序、一份按作者排序、一份按价格排序等。如果能把图书目录的信息都包含在一个数组里更好,其中每个元素包含一本书的相关信息。

因此,Gwen需要一种即能包含字符串又能包含数字的数据形式,而且还要保持各信息的独立。C结构就满足这种情况下的需求。我们通过一个示例演示如何创建和使用数组。但是,示例进行了一些限制。第一,该程序示例演示的书目只包含书名、作者和价格。第二,只有一本书的数目。当然,别忘了这只是进行了限制,我们在后面将扩展该程序。请看程序清单14.1及其输出,然后阅读后面的一些要点。

程序清单14.1 book.c程序

//* book.c -- 一本书的图书目录 */

#include <stdio.h>

#include <string.h>

char * s_gets(char * st, int n);

#define MAXTITL 41  /* 书名的最大长度 + 1  */

#define MAXAUTL 31  /* 作者姓名的最大长度 + 1*/

struct book {/* 结构模版:标记是 book */

char title[MAXTITL];

char author[MAXAUTL];

float value;

}; /* 结构模版结束*/

int main(void)

{

struct book library; /* 把 library 声明为一个 book 类型的变量 */

printf("Please enter the book title.\n");

s_gets(library.title, MAXTITL);/* 访问title部分*/

printf("Now enter the author.\n");

s_gets(library.author, MAXAUTL);

printf("Now enter the value.\n");

scanf("%f", &library.value);

printf("%s by %s: $%.2f\n", library.title,

library.author, library.value);

printf("%s: \"%s\" ($%.2f)\n", library.author,

library.title, library.value);

printf("Done.\n");

return 0;

}

char * s_gets(char * st, int n)

{

char * ret_val;

char * find;

ret_val = fgets(st, n, stdin);

if (ret_val)

{

find = strchr(st, '\n');  // 查找换行符

if (find)  // 如果地址不是 NULL,

*find = '\0'; // 在此处放置一个空字符

else

while (getchar != '\n')

continue; //处理输入行中剩余的字符

}

return ret_val;

}

我们使用前面章节中介绍的s_gets函数去掉fgets储存在字符串中的换行符。下面是该例的一个运行示例:

Please enter the book title.

Chicken of the Andes

Now enter the author.

Disma Lapoult

Now enter the value.

29.99

Chicken of the Andes by Disma Lapoult: $29.99

Disma Lapoult: "Chicken of the Andes" ($29.99)

Done.

程序清单14.1中创建的结构有3部分,每个部分都称为成员(member)或字段(field)。这3部分中,一部分储存书名,一部分储存作者名,一部分储存价格。下面是必须掌握的3个技巧:

为结构建立一个格式或样式;

声明一个适合该样式的变量;

访问结构变量的各个部分。

14.2 建立结构声明

结构声明(structure declaration)描述了一个结构的组织布局。声明类似下面这样:

struct book {

char title[MAXTITL];

char author[MAXAUTL];

float value;

};

该声明描述了一个由两个字符数组和一个float类型变量组成的结构。该声明并未创建实际的数据对象,只描述了该对象由什么组成。〔有时,我们把结构声明称为模板,因为它勾勒出结构是如何储存数据的。如果读者知道C++的模板,此模板非彼模板,C++中的模板更为强大。〕我们来分析一些细节。首先是关键字 struct,它表明跟在其后的是一个结构,后面是一个可选的标记(该例中是 book),稍后程序中可以使用该标记引用该结构。所以,我们在后面的程序中可以这样声明:

struct book library;

这把library声明为一个使用book结构布局的结构变量。

在结构声明中,用一对花括号括起来的是结构成员列表。每个成员都用自己的声明来描述。例如,title部分是一个内含MAXTITL个元素的char类型数组。成员可以是任意一种C的数据类型,甚至可以是其他结构!右花括号后面的分号是声明所必需的,表示结构布局定义结束。可以把这个声明放在所有函数的外部(如本例所示),也可以放在一个函数定义的内部。如果把结构声明置于一个函数的内部,它的标记就只限于该函数内部使用。如果把结构声明置于函数的外部,那么该声明之后的所有函数都能使用它的标记。例如,在程序的另一个函数中,可以这样声明:

struct book dickens;

这样,该函数便创建了一个结构变量dickens,该变量的结构布局是book。

结构的标记名是可选的。但是以程序示例中的方式建立结构时(在一处定义结构布局,在另一处定义实际的结构变量),必须使用标记。我们学完如何定义结构变量后,再来看这一点。

14.3 定义结构变量

结构有两层含义。一层含义是“结构布局”,刚才已经讨论过了。结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。下一步是创建一个结构变量,即是结构的另一层含义。程序中创建结构变量的一行是:

struct book library;

编译器执行这行代码便创建了一个结构变量library。编译器使用book模板为该变量分配空间:一个内含MAXTITL个元素的char数组、一个内含MAXAUTL个元素的char数组和一个float类型的变量。这些存储空间都与一个名称library结合在一起(见图14.1)。

在结构变量的声明中,struct book所起的作用相当于一般声明中的int或float。例如,可以定义两个struct book类型的变量,或者甚至是指向struct book类型结构的指针:

struct book doyle, panshin, * ptbook;

图14.1 一个结构的内存分配

结构变量doyle和panshin中都包含title、author和value部分。指针ptbook可以指向doyle、panshin或任何其他book类型的结构变量。从本质上看,book结构声明创建了一个名为struct book的新类型。

就计算机而言,下面的声明:

struct book library;

是以下声明的简化:

struct book {

char title[MAXTITL];

char author[AXAUTL];

float value;

} library;  /* 声明的右右花括号后跟变量名*/

换言之,声明结构的过程和定义结构变量的过程可以组合成一个步骤。如下所示,组合后的结构声明和结构变量定义不需要使用结构标记:

struct { /* 无结构标记 */

char title[MAXTITL];

char author[MAXAUTL];

float value;

} library;

然而,如果打算多次使用结构模板,就要使用带标记的形式;或者,使用本章后面介绍的typedef。

这是定义结构变量的一个方面,在这个例子中,并未初始化结构变量。

14.3.1 初始化结构

初始化变量和数组如下:

int count = 0;

int fibo[7] = {0,1,1,2,3,5,8};

结构变量是否也可以这样初始化?是的,可以。初始化一个结构变量(ANSI之前,不能用自动变量初始化结构;ANSI之后可以用任意存储类别)与初始化数组的语法类似:

struct book library = {

"The Pious Pirate and the Devious Damsel",

"Renee Vivotte",

1.95

};

简而言之,我们使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔。因此, title成员可以被初始化为一个字符串,value成员可以被初始化为一个数字。为了让初始化项与结构中各成员的关联更加明显,我们让每个成员的初始化项独占一行。这样做只是为了提高代码的可读性,对编译器而言,只需要用逗号分隔各成员的初始化项即可。

注意 初始化结构和类别储存期

第12章中提到过,如果初始化静态存储期的变量(如,静态外部链接、静态内部链接或静态无链接),必须使用常量值。这同样适用于结构。如果初始化一个静态存储期的结构,初始化列表中的值必须是常量表达式。如果是自动存储期,初始化列表中的值可以不是常量。

14.3.2 访问结构成员

结构类似于一个“超级数组”,这个超级数组中,可以是一个元素为char类型,下一个元素为forat类型,下一个元素为int数组。可以通过数组下标单独访问数组中的各元素,那么,如何访问结构中的成员?使用结构成员运算符——点(.)访问结构中的成员。例如,library.value即访问library的value部分。可以像使用任何float类型变量那样使用library.value。与此类似,可以像使用字符数组那样使用 library.title。因此,程序清单 14.1 中的程序中有 s_gets(library.title, MAXTITL);和scanf("%f", &library.value);这样的代码。

本质上,.title、.author和.value的作用相当于book结构的下标。

注意,虽然library是一个结构,但是library.value是一个float类型的变量,可以像使用其他 float 类型变量那样使用它。例如,scanf("%f",...)需要一个 float 类型变量的地址,而&library.float正好符合要求。.比&的优先级高,因此这个表达式和&(library.float)一样。

如果还有一个相同类型的结构变量,可以用相同的方法:

struct book bill, newt;

s_gets(bill.title, MAXTITL);

s_gets(newt.title, MAXTITL);

.title 引用 book 结构的第 1 个成员。注意,程序清单 14.1 中的程序以两种不同的格式打印了library结构变量中的内容。这说明可以自行决定如何使用结构成员。

14.3.3 结构的初始化器

C99和C11为结构提供了指定初始化器(designated initializer)[1],其语法与数组的指定初始化器类似。但是,结构的指定初始化器使用点运算符和成员名(而不是方括号和下标)标识特定的元素。例如,只初始化book结构的value成员,可以这样做:

struct book surprise = { .value = 10.99};

可以按照任意顺序使用指定初始化器:

struct book gift = { .value = 25.99,

.author = "James Broadfool",

.title = "Rue for the Toad"};

与数组类似,在指定初始化器后面的普通初始化器,为指定成员后面的成员提供初始值。另外,对特定成员的最后一次赋值才是它实际获得的值。例如,考虑下面的代码:

struct book gift= {.value = 18.90,

.author = "Philionna Pestle",

0.25};

赋给value的值是0.25,因为它在结构声明中紧跟在author成员之后。新值0.25取代了之前的18.9。在学习了结构的基本知识后,可以进一步了解结构的一些相关类型。

14.4 结构数组

接下来,我们要把程序清单14.1的程序扩展成可以处理多本书。显然,每本书的基本信息都可以用一个 book 类型的结构变量来表示。为描述两本书,需要使用两个变量,以此类推。可以使用这一类型的结构数组来处理多本书。在下一个程序中(程序清单 14.2)就创建了一个这样的数组。如果你使用 Borland C/C++,请参阅本节后面的“Borland C和浮点数”。

结构和内存

manybook.c程序创建了一个内含100个结构变量的数组。由于该数组是自动存储类别的对象,其中的信息被储存在栈(stack)中。如此大的数组需要很大一块内存,这可能会导致一些问题。如果在运行时出现错误,可能抱怨栈大小或栈溢出,你的编译器可能使用了一个默认大小的栈,这个栈对于该例而言太小。要修正这个问题,可以使用编译器选项设置栈大小为10000,以容纳这个结构数组;或者可以创建静态或外部数组(这样,编译器就不会把数组放在栈中);或者可以减小数组大小为16。为何不一开始就使用较小的数组?这是为了让读者意识到栈大小的潜在问题,以便今后再遇到类似的问题,可以自己处理好。

程序清单14.2 manybook.c程序

/* manybook.c -- 包含多本书的图书目录 */

#include <stdio.h>

#include <string.h>

char * s_gets(char * st, int n);

#define MAXTITL  40

#define MAXAUTL  40

#define MAXBKS 100  /* 书籍的最大数量 */

struct book {/* 简历 book 模板*/

char title[MAXTITL];

char author[MAXAUTL];

float value;

};

int main(void)

{

struct book library[MAXBKS];  /* book 类型结构的数组 */

int count = 0;

int index;

printf("Please enter the book title.\n");

printf("Press [enter] at the start of a line to stop.\n");

while (count < MAXBKS && s_gets(library[count].title, MAXTITL) != NULL

&& library[count].title[0] != '\0')

{

printf("Now enter the author.\n");

s_gets(library[count].author, MAXAUTL);

printf("Now enter the value.\n");

scanf("%f", &library[count++].value);

while (getchar != '\n')

continue; /* 清理输入行*/

if (count < MAXBKS)

printf("Enter the next title.\n");

}

if (count > 0)

{

printf("Here is the list of your books:\n");

for (index = 0; index < count; index++)

printf("%s by %s: $%.2f\n", library[index].title,

library[index].author, library[index].value);

}

else

printf("No books? Too bad.\n");

return 0;

}

char * s_gets(char * st, int n)

{

char * ret_val;

char * find;

ret_val = fgets(st, n, stdin);

if (ret_val)

{

find = strchr(st, '\n');  // 查找换行符

if (find)  // 如果地址不是 NULL,

*find = '\0'; // 在此处放置一个空字符

else

while (getchar != '\n')

continue; // 处理输入行中剩余的字符

}

return ret_val;

}

下面是该程序的一个输出示例:

Please enter the book title.

Press [enter] at the start of a line to stop.

My Life as a Budgie

Now enter the author.

Mack Zackles

Now enter the value.

12.95

Enter the next title.

...(此处省略了许多内容)...

Here is the list of your books:

My Life as a Budgie by Mack Zackles: $12.95

Thought and Unthought Rethought by Kindra Schlagmeyer: $43.50

Concerto for Financial Instruments by Filmore Walletz: $49.99

The CEO Power Diet by Buster Downsize: $19.25

C++ Primer Plus by Stephen Prata: $59.99

Fact Avoidance: Perception as Reality by Polly Bull: $19.97

Coping with Coping by Dr.Rubin Thonkwacker: $0.02

Diaphanous Frivolity by Neda McFey: $29.99

Murder Wore a Bikini by Mickey Splats: $18.95

A History of Buvania, Volume 8, by Prince Nikoli Buvan: $50.04

Mastering Your Digital Watch, 5nd Edition, by Miklos Mysz: $28.95

A Foregone Confusion by Phalty Reasoner: $5.99

Outsourcing Government: Selection vs.Election by Ima Pundit: $33.33

Borland C和浮点数

如果程序不使用浮点数,旧式的Borland C编译器会尝试使用小版本的scanf来压缩程序。然而,如果在一个结构数组中只有一个浮点值(如程序清单14.2中那样),那么这种编译器(DOS的Borland C/C++ 3.1之前的版本,不是Borland C/C++ 4.0)就无法发现它存在。结果,编译器会生成如下消息:

scanf : floating point formats not linked

Abnormal program termination

一种解决方案是,在程序中添加下面的代码:

#include <math.h>

double dummy = sin(0.0);

这段代码强制编译器载入浮点版本的scanf。

首先,我们学习如何声明结构数组和如何访问数组中的结构成员。然后,着重分析该程序的两个方面。

14.4.1 声明结构数组

声明结构数组和声明其他类型的数组类似。下面是一个声明结构数组的例子:

struct book library[MAXBKS];

以上代码把library声明为一个内含MAXBKS个元素的数组。数组的每个元素都是一个book类型的数组。因此,library[0]是第1个book类型的结构变量,library[1]是第2个book类型的结构变量,以此类推。参看图14.2 可以帮助读者理解。数组名library本身不是结构名,它是一个数组名,该数组中的每个元素都是struct book类型的结构变量。

图14.2 一个结构数组library[MAXBKS]

14.4.2 标识结构数组的成员

为了标识结构数组中的成员,可以采用访问单独结构的规则:在结构名后面加一个点运算符,再在点运算符后面写上成员名。如下所示:

library[0].value /* 第1个数组元素与value 相关联 */

library[4].title /* 第5个数组元素与title 相关联 */

注意,数组下标紧跟在library后面,不是成员名后面:

library.value[2] // 错误

library[2].value // 正确

使用library[2].value的原因是:library[2]是结构变量名,正如library[1]是另一个变量名。

顺带一提,下面的表达式代表什么?

library[2].title[4]

这是library数组第3个结构变量(library[2]部分)中书名的第5个字符(title[4]部分)。以程序清单14.2的输出为例,这个字符是e。该例指出,点运算符右侧的下标作用于各个成员,点运算符左侧的下标作用与结构数组。

最后,总结一下:

library// 一个book 结构的数组

library[2] // 一个数组元素,该元素是book结构

library[2].title  // 一个char数组(library[2]的title成员)

library[2].title[4]// 数组中library[2]元素的title 成员的一个字符

下面,我们来讨论一下这个程序。

14.4.3 程序讨论

较之程序清单14.1,该程序主要的改动之处是:插入一个while循环读取多个项。该循环的条件测试是:

while (count < MAXBKS && s_gets(library[count].title, MAXTITL) != NULL

&& library[count].title[0] != '\0')

表达式 s_gets(library[count].title, MAXTITL)读取一个字符串作为书名,如果 s_gets尝试读到文件结尾后面,该表达式则返回NULL。表达式library[count].title[0] != '\0'判断字符串中的首字符是否是空字符(即,该字符串是否是空字符串)。如果在一行开始处用户按下 Enter 键,相当于输入了一个空字符串,循环将结束。程序中还检查了图书的数量,以免超出数组的大小。

然后,该程序中有如下几行:

while (getchar != '\n')

continue; /* 清理输入行 */

前面章节介绍过,这段代码弥补了scanf函数遇到空格和换行符就结束读取的问题。当用户输入书的价格时,可能输入如下信息:

12.50[Enter]

其传送的字符序列如下:

12.50\n

scanf函数接受1、2、.、5和0,但是把\n留在输入序列中。如果没有上面两行清理输入行的代码,就会把留在输入序列中的换行符当作空行读入,程序以为用户发送了停止输入的信号。我们插入的这两行代码只会在输入序列中查找并删除\n,不会处理其他字符。这样s_gets就可以重新开始下一次输入。

14.5 嵌套结构

有时,在一个结构中包含另一个结构(即嵌套结构)很方便。例如,Shalala Pirosky创建了一个有关她朋友信息的结构。显然,结构中需要一个成员表示朋友的姓名。然而,名字可以用一个数组来表示,其中包含名和姓这两个成员。程序清单14.3是一个简单的示例。

程序清单14.3 friend.c程序

// friend.c -- 嵌套结构示例

#include <stdio.h>

#define LEN 20

const char * msgs[5] =

{

"  Thank you for the wonderful evening, ",

"You certainly prove that a ",

"is a special kind of guy.We must get together",

"over a delicious ",

" and have a few laughs"

};

struct names { // 第1个结构

char first[LEN];

char last[LEN];

};

struct guy {// 第2个结构

struct names handle;  // 嵌套结构

char favfood[LEN];

char job[LEN];

float income;

};

int main(void)

{

struct guy fellow = { // 初始化一个结构变量

{ "Ewen", "Villard" },

"grilled salmon",

"personality coach",

68112.00

};

printf("Dear %s, \n\n", fellow.handle.first);

printf("%s%s.\n", msgs[0], fellow.handle.first);

printf("%s%s\n", msgs[1], fellow.job);

printf("%s\n", msgs[2]);

printf("%s%s%s", msgs[3], fellow.favfood, msgs[4]);

if (fellow.income > 150000.0)

puts("!!");

else if (fellow.income > 75000.0)

puts("!");

else

puts(".");

printf("\n%40s%s\n", " ", "See you soon,");

printf("%40s%s\n", " ", "Shalala");

return 0;

}

下面是该程序的输出:

Dear Ewen,

Thank you for the wonderful evening, Ewen.

You certainly prove that a personality coach

is a special kind of guy.We must get together

over a delicious grilled salmon and have a few laughs.

See you soon,

Shalala

首先,注意如何在结构声明中创建嵌套结构。和声明int类型变量一样,进行简单的声明:

struct names handle;

该声明表明handle是一个struct name类型的变量。当然,文件中也应包含结构names的声明。

其次,注意如何访问嵌套结构的成员,这需要使用两次点运算符:

printf("Hello, %s!\n", fellow.handle.first);

从左往右解释fellow.handle.first:

(fellow.handle).first

也就是说,找到fellow,然后找到fellow的handle的成员,再找到handle的first成员。

14.6 指向结构的指针

喜欢使用指针的人一定很高兴能使用指向结构的指针。至少有 4 个理由可以解释为何要使用指向结构的指针。第一,就像指向数组的指针比数组本身更容易操控(如,排序问题)一样,指向结构的指针通常比结构本身更容易操控。第二,在一些早期的C实现中,结构不能作为参数传递给函数,但是可以传递指向结构的指针。第三,即使能传递一个结构,传递指针通常更有效率。第四,一些用于表示数据的结构中包含指向其他结构的指针。

下面的程序(程序清单14.4)演示了如何定义指向结构的指针和如何用这样的指针访问结构的成员。

程序清单14.4 friends.c程序

/* friends.c -- 使用指向结构的指针 */

#include <stdio.h>

#define LEN 20

struct names {

char first[LEN];

char last[LEN];

};

struct guy {

struct names handle;

char favfood[LEN];

char job[LEN];

float income;

};

int main(void)

{

struct guy fellow[2] = {

{ { "Ewen", "Villard" },

"grilled salmon",

"personality coach",

68112.00

},

{ { "Rodney", "Swillbelly" },

"tripe",

"tabloid editor",

432400.00

}

};

struct guy * him; /* 这是一个指向结构的指针 */

printf("address #1: %p #2: %p\n", &fellow[0], &fellow[1]);

him = &fellow[0]; /* 告诉编译器该指针指向何处 */

printf("pointer #1: %p #2: %p\n", him, him + 1);

printf("him->income is $%.2f: (*him).income is $%.2f\n",

him->income, (*him).income);

him++;  /* 指向下一个结构*/

printf("him->favfood is %s: him->handle.last is %s\n",

him->favfood, him->handle.last);

return 0;

}

该程序的输出如下:

address #1: 0x7fff5fbff820 #2: 0x7fff5fbff874

pointer #1: 0x7fff5fbff820 #2: 0x7fff5fbff874

him->income is $68112.00: (*him).income is $68112.00

him->favfood is tripe: him->handle.last is Swillbelly

我们先来看如何创建指向guy类型结构的指针,然后再分析如何通过该指针指定结构的成员。

14.6.1 声明和初始化结构指针

声明结构指针很简单:

struct guy * him;

首先是关键字 struct,其次是结构标记 guy,然后是一个星号(*),其后跟着指针名。这个语法和其他指针声明一样。

该声明并未创建一个新的结构,但是指针him现在可以指向任意现有的guy类型的结构。例如,如果barney是一个guy类型的结构,可以这样写:

him = &barney;

和数组不同的是,结构名并不是结构的地址,因此要在结构名前面加上&运算符。

在本例中,fellow 是一个结构数组,这意味着 fellow[0]是一个结构。所以,要让 him 指向fellow[0],可以这样写:

him = &fellow[0];

输出的前两行说明赋值成功。比较这两行发现,him指向fellow[0],him + 1指向fellow[1]。注意,him加1相当于him指向的地址加84。在十六进制中,874 - 820 = 54(十六进制)= 84(十进制),因为每个guy结构都占用84字节的内存:names.first占用20字节,names.last占用20字节,favfood占用20字节,job占用20字节,income占用4字节(假设系统中float占用4字节)。顺带一提,在有些系统中,一个结构的大小可能大于它各成员大小之和。这是因为系统对数据进行校准的过程中产生了一些“缝隙”。例如,有些系统必须把每个成员都放在偶数地址上,或4的倍数的地址上。在这种系统中,结构的内部就存在未使用的“缝隙”。

14.6.2 用指针访问成员

指针him指向结构变量fellow[0],如何通过him获得fellow[0]的成员的值?程序清单14.4中的第3行输出演示了两种方法。

第1种方法也是最常用的方法:使用->运算符。该运算符由一个连接号(-)后跟一个大于号(>)组成。我们有下面的关系:

如果him == &barney,那么him->income 即是 barney.income

如果him == &fellow[0],那么him->income 即是 fellow[0].income

换句话说,->运算符后面的结构指针和.运算符后面的结构名工作方式相同(不能写成him.incone,因为him不是结构名)。

这里要着重理解him是一个指针,但是hime->income是该指针所指向结构的一个成员。所以在该例中,him->income是一个float类型的变量。

第2种方法是,以这样的顺序指定结构成员的值:如果him == &fellow[0],那么*him == fellow[0],因为&和*是一对互逆运算符。因此,可以做以下替代:

fellow[0].income == (*him).income

必须要使用圆括号,因为.运算符比*运算符的优先级高。

总之,如果him是指向guy类型结构barney的指针,下面的关系恒成立:

barney.income == (*him).income == him->income // 假设 him == &barney

接下来,我们来学习结构和函数的交互。

14.7 向函数传递结构的信息

函数的参数把值传递给函数。每个值都是一个数字——可能是int类型、float类型,可能是ASCII字符码,或者是一个地址。然而,一个结构比一个单独的值复杂,所以难怪以前的C实现不允许把结构作为参数传递给函数。当前的实现已经移除了这个限制,ANSI C允许把结构作为参数使用。所以程序员可以选择是传递结构本身,还是传递指向结构的指针。如果你只关心结构中的某一部分,也可以把结构的成员作为参数。我们接下来将分析这3种传递方式,首先介绍以结构成员作为参数的情况。

14.7.1 传递结构成员

只要结构成员是一个具有单个值的数据类型(即,int及其相关类型、char、float、double或指针),便可把它作为参数传递给接受该特定类型的函数。程序清单14.5中的财务分析程序(初级版本)演示了这一点,该程序把客户的银行账户添加到他/她的储蓄和贷款账户中。

程序清单14.5 funds1.c程序

/* funds1.c -- 把结构成员作为参数传递 */

#include <stdio.h>

#define FUNDLEN 50

struct funds {

char bank[FUNDLEN];

doublebankfund;

char save[FUNDLEN];

doublesavefund;

};

double sum(double, double);

int main(void)

{

struct funds stan = {

"Garlic-Melon Bank",

4032.27,

"Lucky's Savings and Loan",

8543.94

};

printf("Stan has a total of $%.2f.\n",

sum(stan.bankfund, stan.savefund));

return 0;

}

/* 两个double类型的数相加 */

double sum(double x, double y)

{

return(x + y);

}

运行该程序后输出如下:

Stan has a total of $12576.21.

看来,这样传递参数没问题。注意,sum函数既不知道也不关心实际的参数是否是结构的成员,它只要求传入的数据是double类型。

当然,如果需要在被调函数中修改主调函数中成员的值,就要传递成员的地址:

modify(&stan.bankfund);

这是一个更改银行账户的函数。

把结构的信息告诉函数的第2种方法是,让被调函数知道自己正在处理一个结构。

14.7.2 传递结构的地址

我们继续解决前面的问题,但是这次把结构的地址作为参数。由于函数要处理funds结构,所以必须声明funds结构。如程序清单14.6所示。

程序清单14.6 funds2.c程序

/* funds2.c -- 传递指向结构的指针 */

#include <stdio.h>

#define FUNDLEN 50

struct funds {

char bank[FUNDLEN];

doublebankfund;

char save[FUNDLEN];

doublesavefund;

};

double sum(const struct funds *); /* 参数是一个指针 */

int main(void)

{

struct funds stan = {

"Garlic-Melon Bank",

4032.27,

"Lucky's Savings and Loan",

8543.94

};

printf("Stan has a total of $%.2f.\n", sum(&stan));

return 0;

}

double sum(const struct funds * money)

{

return(money->bankfund + money->savefund);

}

运行该程序后输出如下:

Stan has a total of $12576.21.

sum函数使用指向funds结构的指针(money)作为它的参数。把地址&stan传递给该函数,使得指针money指向结构stan。然后通过->运算符获取stan.bankfund和stan.savefund的值。由于该函数不能改变指针所指向值的内容,所以把money声明为一个指向const的指针。

虽然该函数并未使用其他成员,但是也可以访问它们。注意,必须使用&运算符来获取结构的地址。和数组名不同,结构名只是其地址的别名。

14.7.3 传递结构

对于允许把结构作为参数的编译器,可以把程序清单14.6重写为程序清单14.7。

程序清单14.7 funds3.c程序

/* funds3.c -- 传递一个结构 */

#include <stdio.h>

#define FUNDLEN 50

struct funds {

char  bank[FUNDLEN];

double bankfund;

char  save[FUNDLEN];

double savefund;

};

double sum(struct funds moolah); /* 参数是一个结构 */

int main(void)

{

struct funds stan = {

"Garlic-Melon Bank",

4032.27,

"Lucky's Savings and Loan",

8543.94

};

printf("Stan has a total of $%.2f.\n", sum(stan));

return 0;

}

double sum(struct funds moolah)

{

return(moolah.bankfund + moolah.savefund);

}

下面是运行该程序后的输出:

Stan has a total of $12576.21.

该程序把程序清单14.6中指向struct funds类型的结构指针money替换成struct funds类型的结构变量moolah。调用sum时,编译器根据funds模板创建了一个名为moolah的自动结构变量。然后,该结构的各成员被初始化为 stan 结构变量相应成员的值的副本。因此,程序使用原来结构的副本进行计算,然而,传递指针的程序清单14.6使用的是原始的结构进行计算。由于moolah是一个结构,所以该程序使用moolah.bankfund,而不是moolah->bankfund。另一方面,由于money是指针,不是结构,所以程序清单14.6使用的是monet->bankfund。

14.7.4 其他结构特性

现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做。也就是说,如果n_data和o_data都是相同类型的结构,可以这样做:

o_data = n_data; // 把一个结构赋值给另一个结构

这条语句把n_data的每个成员的值都赋给o_data的相应成员。即使成员是数组,也能完成赋值。另外,还可以把一个结构初始化为相同类型的另一个结构:

struct names right_field = {"Ruthie", "George"};

struct names captain = right_field; // 把一个结构初始化为另一个结构

现在的C(包括ANSI C),函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。把结构作为函数参数可以把结构的信息传送给函数;把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。结构指针也允许这种双向通信,因此可以选择任一种方法来解决编程问题。我们通过另一组程序示例来演示这两种方法。

为了对比这两种方法,我们先编写一个程序以传递指针的方式处理结构,然后以传递结构和返回结构的方式重写该程序。

程序清单14.8 names1.c程序

/* names1.c -- 使用指向结构的指针 */

#include <stdio.h>

#include <string.h>

#define NLEN 30

struct namect {

char fname[NLEN];

char lname[NLEN];

int letters;

};

void getinfo(struct namect *);

void makeinfo(struct namect *);

void showinfo(const struct namect *);

char * s_gets(char * st, int n);

int main(void)

{

struct namect person;

getinfo(&person);

makeinfo(&person);

showinfo(&person);

return 0;

}

void getinfo(struct namect * pst)

{

printf("Please enter your first name.\n");

s_gets(pst->fname, NLEN);

printf("Please enter your last name.\n");

s_gets(pst->lname, NLEN);

}

void makeinfo(struct namect * pst)

{

pst->letters = strlen(pst->fname) +strlen(pst->lname);

}

void showinfo(const struct namect * pst)

{

printf("%s %s, your name contains %d letters.\n",

pst->fname, pst->lname, pst->letters);

}

char * s_gets(char * st, int n)

{

char * ret_val;

char * find;

ret_val = fgets(st, n, stdin);

if (ret_val)

{

find = strchr(st, '\n');  // 查找换行符

if (find)  // 如果地址不是 NULL,

*find = '\0'; // 在此处放置一个空字符

else

while (getchar != '\n')

continue; // 处理输入行的剩余字符

}

return ret_val;

}

下面是编译并运行该程序后的一个输出示例:

Please enter your first name.

Viola

Please enter your last name.

Plunderfest

Viola Plunderfest, your name contains 16 letters.

该程序把任务分配给3个函数来完成,都在main中调用。每调用一个函数就把person结构的地址传递给它。

getinfo函数把结构的信息从自身传递给main。该函数通过与用户交互获得姓名,并通过pst指针定位,将其放入 person 结构中。由于 pst->lname 意味着 pst 指向结构的 lname 成员,这使得pst->lname等价于char数组的名称,因此做s_gets的参数很合适。注意,虽然getinfo给main提供了信息,但是它并未使用返回机制,所以其返回类型是void。

makeinfo函数使用双向传输方式传送信息。通过使用指向 person 的指针,该指针定位了储存在该结构中的名和姓。该函数使用C库函数strlen分别计算名和姓中的字母总数,然后使用person的地址储存两数之和。同样,makeinfo函数的返回类型也是void。

showinfo函数使用一个指针定位待打印的信息。因为该函数不改变数组的内容,所以将其声明为const。

所有这些操作中,只有一个结构变量 person,每个函数都使用该结构变量的地址来访问它。一个函数把信息从自身传回主调函数,一个函数把信息从主调函数传给自身,一个函数通过双向传输来传递信息。

现在,我们来看如何使用结构参数和返回值来完成相同的任务。第一,为了传递结构本身,函数的参数必须是person,而不是&person。那么,相应的形式参数应声明为struct namect,而不是指向该类型的指针。第二,可以通过返回一个结构,把结构的信息返回给main。程序清单14.9演示了不使用指针的版本。

程序清单14.9 names2.c程序

/* names2.c -- 传递并返回结构 */

#include <stdio.h>

#include <string.h>

#define NLEN 30

struct namect {

char fname[NLEN];

char lname[NLEN];

int letters;

};

struct namect getinfo(void);

struct namect makeinfo(struct namect);

void showinfo(struct namect);

char * s_gets(char * st, int n);

int main(void)

{

struct namect person;

person = getinfo;

person = makeinfo(person);

showinfo(person);

return 0;

}

struct namect getinfo(void)

{

struct namect temp;

printf("Please enter your first name.\n");

s_gets(temp.fname, NLEN);

printf("Please enter your last name.\n");

s_gets(temp.lname, NLEN);

return temp;

}

struct namect makeinfo(struct namect info)

{

info.letters = strlen(info.fname) + strlen(info.lname);

return info;

}

void showinfo(struct namect info)

{

printf("%s %s, your name contains %d letters.\n",

info.fname, info.lname, info.letters);

}

char * s_gets(char * st, int n)

{

char * ret_val;

char * find;

ret_val = fgets(st, n, stdin);

if (ret_val)

{

find = strchr(st, '\n');  // 查找换行符

if (find)  // 如果地址不是 NULL,

*find = '\0'; // 在此处放置一个空字符

else

while (getchar != '\n')

continue; // 处理输入行的剩余部分

}

return ret_val;

}

该版本最终的输出和前面版本相同,但是它使用了不同的方式。程序中的每个函数都创建了自己的person备份,所以该程序使用了4个不同的结构,不像前面的版本只使用一个结构。

例如,考虑makeinfo函数。在第1个程序中,传递的是person的地址,该函数实际上处理的是person的值。在第2个版本的程序中,创建了一个新的结构info。储存在person中的值被拷贝到info中,函数处理的是这个副本。因此,统计完字母个数后,计算结果储存在info中,而不是person中。然而,返回机制弥补了这一点。makeinfo中的这行代码:

return info;

与main中的这行结合:

person = makeinfo(person);

把储存在info中的值拷贝到person中。注意,必须把makeinfo函数声明为struct namect类型,所以该函数要返回一个结构。

14.7.5 结构和结构指针的选择

假设要编写一个与结构相关的函数,是用结构指针作为参数,还是用结构作为参数和返回值?两者各有优缺点。

把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这种方法,而且执行起来很快,只需要传递一个地址。缺点是无法保护数据。被调函数中的某些操作可能会意外影响原来结构中的数据。不过,ANSI C新增的const限定符解决了这个问题。例如,如果在程序清单14.8中,showinfo函数中的代码改变了结构的任意成员,编译器会捕获这个错误。

把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚。假设定义了下面的结构类型:

struct vector {double x; double y;};

如果用vector类型的结构ans储存相同类型结构a和b的和,就要把结构作为参数和返回值:

struct vector ans, a, b;

struct vector sum_vect(struct vector, struct vector);

...

ans = sum_vect(a,b);

对程序员而言,上面的版本比用指针传递的版本更自然。指针版本如下:

struct vector ans, a, b;

void sum_vect(const struct vector *, const struct vector *, struct vector *);

...

sum_vect(&a, &b, &ans);

另外,如果使用指针版本,程序员必须记住总和的地址应该是第1个参数还是第2个参数的地址。

传递结构的两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构传递给函数,而它只使用结构中的一两个成员时特别浪费。这种情况下传递指针或只传递函数所需的成员更合理。

通常,程序员为了追求效率会使用结构指针作为函数参数,如需防止原始数据被意外修改,使用const限定符。按值传递结构是处理小型结构最常用的方法。

14.7.6 结构中的字符数组和字符指针

到目前为止,我们在结构中都使用字符数组来储存字符串。是否可以使用指向 char 的指针来代替字符数组?例如,程序清单14.3中有如下声明:

#define LEN 20

struct names {

char first[LEN];

char last[LEN];

};

其中的结构声明是否可以这样写:

struct pnames {

char * first;

char * last;

};

当然可以,但是如果不理解这样做的含义,可能会有麻烦。考虑下面的代码:

struct names veep = {"Talia", "Summers"};

struct pnames treas = {"Brad", "Fallingjaw"};

printf("%s and %s\n", veep.first, treas.first);

以上代码都没问题,也能正常运行,但是思考一下字符串被储存在何处。对于struct names类型的结构变量veep,以上字符串都储存在结构内部,结构总共要分配40字节储存姓名。然而,对于struct pnames类型的结构变量treas,以上字符串储存在编译器储存常量的地方。结构本身只储存了两个地址,在我们的系统中共占16字节。尤其是,struct pnames结构不用为字符串分配任何存储空间。它使用的是储存在别处的字符串(如,字符串常量或数组中的字符串)。简而言之,在pnames结构变量中的指针应该只用来在程序中管理那些已分配和在别处分配的字符串。

我们看看这种限制在什么情况下出问题。考虑下面的代码:

struct names accountant;

struct pnames attorney;

puts("Enter the last name of your accountant:");

scanf("%s", accountant.last);

puts("Enter the last name of your attorney:");

scanf("%s", attorney.last);/* 这里有一个潜在的危险 */

就语法而言,这段代码没问题。但是,用户的输入储存到哪里去了?对于会计师(accountant),他的名储存在accountant结构变量的last成员中,该结构中有一个储存字符串的数组。对于律师(attorney),scanf把字符串放到attorney.last表示的地址上。由于这是未经初始化的变量,地址可以是任何值,因此程序可以把名放在任何地方。如果走运的话,程序不会出问题,至少暂时不会出问题,否则这一操作会导致程序崩溃。实际上,如果程序能正常运行并不是好事,因为这意味着一个未被觉察的危险潜伏在程序中。

因此,如果要用结构储存字符串,用字符数组作为成员比较简单。用指向 char 的指针也行,但是误用会导致严重的问题。

14.7.7 结构、指针和malloc

如果使用malloc分配内存并使用指针储存该地址,那么在结构中使用指针处理字符串就比较合理。这种方法的优点是,可以请求malloc为字符串分配合适的存储空间。可以要求用4字节储存"Joe"和用18字节储存"Rasolofomasoandro"。用这种方法改写程序清单14.9并不费劲。主要是更改结构声明(用指针代替数组)和提供一个新版本的getinfo函数。新的结构声明如下:

struct namect {

char * fname; // 用指针代替数组

char * lname;

int letters;

};

新版本的getinfo把用户的输入读入临时数组中,调用malloc函数分配存储空间,并把字符串拷贝到新分配的存储空间中。对名和姓都要这样做:

void getinfo (struct namect * pst)

{

char temp[SLEN];

printf("Please enter your first name.\n");

s_gets(temp, SLEN);

// 分配内存储存名

pst->fname = (char *) malloc(strlen(temp) + 1);

// 把名拷贝到已分配的内存

strcpy(pst->fname, temp);

printf("Please enter your last name.\n");

s_gets(temp, SLEN);

pst->lname = (char *) malloc(strlen(temp) + 1);

strcpy(pst->lname, temp);

}

要理解这两个字符串都未储存在结构中,它们储存在 malloc分配的内存块中。然而,结构中储存着这两个字符串的地址,处理字符串的函数通常都要使用字符串的地址。因此,不用修改程序中的其他函数。

第12章建议,应该成对使用malloc和free。因此,还要在程序中添加一个新的函数cleanup,用于释放程序动态分配的内存。如程序清单14.10所示。

程序清单14.10 names3.c程序

// names3.c -- 使用指针和 malloc

#include <stdio.h>

#include <string.h>  // 提供 strcpy、strlen 的原型

#include <stdlib.h>  // 提供 malloc、free 的原型

#define SLEN 81

struct namect {

char * fname; // 使用指针

char * lname;

int letters;

};

void getinfo(struct namect *); // 分配内存

void makeinfo(struct namect *);

void showinfo(const struct namect *);

void cleanup(struct namect *); // 调用该函数时释放内存

char * s_gets(char * st, int n);

int main(void)

{

struct namect person;

getinfo(&person);

makeinfo(&person);

showinfo(&person);

cleanup(&person);

return 0;

}

void getinfo(struct namect * pst)

{

char temp[SLEN];

printf("Please enter your first name.\n");

s_gets(temp, SLEN);

// 分配内存以储存名

pst->fname = (char *) malloc(strlen(temp) + 1);

// 把名拷贝到动态分配的内存中

strcpy(pst->fname, temp);

printf("Please enter your last name.\n");

s_gets(temp, SLEN);

pst->lname = (char *) malloc(strlen(temp) + 1);

strcpy(pst->lname, temp);

}

void makeinfo(struct namect * pst)

{

pst->letters = strlen(pst->fname) +

strlen(pst->lname);

}

void showinfo(const struct namect * pst)

{

printf("%s %s, your name contains %d letters.\n",

pst->fname, pst->lname, pst->letters);

}

void cleanup(struct namect * pst)

{

free(pst->fname);

free(pst->lname);

}

char * s_gets(char * st, int n)

{

char * ret_val;

char * find;

ret_val = fgets(st, n, stdin);

if (ret_val)

{

find = strchr(st, '\n');  // 查找换行符

if (find)  // 如果地址不是 NULL,

*find = '\0'; // 在此处放置一个空字符

else

while (getchar != '\n')

continue; // 处理输入行的剩余部分

}

return ret_val;

}

下面是该程序的输出:

Please enter your first name.

Floresiensis

Please enter your last name.

Mann

Floresiensis Mann, your name contains 16 letters.

14.7.8 复合字面量和结构(C99)

C99 的复合字面量特性可用于结构和数组。如果只需要一个临时结构值,复合字面量很好用。例如,可以使用复合字面量创建一个数组作为函数的参数或赋给另一个结构。语法是把类型名放在圆括号中,后面紧跟一个用花括号括起来的初始化列表。例如,下面是struct book类型的复合字面量:

(struct book) {"The Idiot", "Fyodor Dostoyevsky", 6.99}

程序清单14.11中的程序示例,使用复合字面量为一个结构变量提供两个可替换的值(在撰写本书时,并不是所有的编译器都支持这个特性,不过这是时间的问题)。

程序清单14.11 complit.c程序

/* complit.c -- 复合字面量 */

#include <stdio.h>

#define MAXTITL 41

#define MAXAUTL 31

struct book { // 结构模版:标记是 book

char title[MAXTITL];

char author[MAXAUTL];

float value;

};

int main(void)

{

struct book readfirst;

int score;

printf("Enter test score: ");

scanf("%d", &score);

if (score >= 84)

readfirst = (struct book) {"Crime and Punishment",

"Fyodor Dostoyevsky",

11.25};

else

readfirst = (struct book) {"Mr.Bouncy's Nice Hat",

"Fred Winsome",

5.99};

printf("Your assigned reading:\n");

printf("%s by %s: $%.2f\n", readfirst.title,

readfirst.author, readfirst.value);

return 0;

}

还可以把复合字面量作为函数的参数。如果函数接受一个结构,可以把复合字面量作为实际参数传递:

struct rect {double x; double y;};

double rect_area(struct rect r){return r.x * r.y;}

...

double area;

area = rect_area( (struct rect) {10.5, 20.0});

值210被赋给area。

如果函数接受一个地址,可以传递复合字面量的地址:

struct rect {double x; double y;};

double rect_areap(struct rect * rp){return rp->x * rp->y;}

...

double area;

area = rect_areap( &(struct rect) {10.5, 20.0});

值210被赋给area。

复合字面量在所有函数的外部,具有静态存储期;如果复合字面量在块中,则具有自动存储期。复合字面量和普通初始化列表的语法规则相同。这意味着,可以在复合字面量中使用指定初始化器。

14.7.9 伸缩型数组成员(C99)

C99新增了一个特性:伸缩型数组成员(flexible array member),利用这项特性声明的结构,其最后一个数组成员具有一些特性。第1个特性是,该数组不会立即存在。第2个特性是,使用这个伸缩型数组成员可以编写合适的代码,就好像它确实存在并具有所需数目的元素一样。这可能听起来很奇怪,所以我们来一步步地创建和使用一个带伸缩型数组成员的结构。

首先,声明一个伸缩型数组成员有如下规则:

伸缩型数组成员必须是结构的最后一个成员;

结构中必须至少有一个成员;

伸缩数组的声明类似于普通数组,只是它的方括号中是空的。

下面用一个示例来解释以上几点:

struct flex

{

int count;

double average;

double scores; // 伸缩型数组成员

};

声明一个struct flex类型的结构变量时,不能用scores做任何事,因为没有给这个数组预留存储空间。实际上,C99的意图并不是让你声明struct flex类型的变量,而是希望你声明一个指向struct flex类型的指针,然后用malloc来分配足够的空间,以储存struct flex类型结构的常规内容和伸缩型数组成员所需的额外空间。例如,假设用scores表示一个内含5个double类型值的数组,可以这样做:

struct flex * pf; // 声明一个指针

// 请求为一个结构和一个数组分配存储空间

pf = malloc(sizeof(struct flex) + 5 * sizeof(double));

现在有足够的存储空间储存count、average和一个内含5个double类型值的数组。可以用指针pf访问这些成员:

pf->count = 5; // 设置 count 成员

pf->scores[2] = 18.5; // 访问数组成员的一个元素

程序清单14.13进一步扩展了这个例子,让伸缩型数组成员在第1种情况下表示5个值,在第2种情况下代表9个值。该程序也演示了如何编写一个函数处理带伸缩型数组元素的结构。

程序清单14.12 flexmemb.c程序

// flexmemb.c -- 伸缩型数组成员(C99新增特性)

#include <stdio.h>

#include <stdlib.h>

struct flex

{

size_t count;

double average;

double scores ; // 伸缩型数组成员

};

void showFlex(const struct flex * p);

int main(void)

{

struct flex * pf1, *pf2;

int n = 5;

int i;

int tot = 0;

// 为结构和数组分配存储空间

pf1 = malloc(sizeof(struct flex) + n * sizeof(double));

pf1->count = n;

for (i = 0; i < n; i++)

{

pf1->scores[i] = 20.0 - i;

tot += pf1->scores[i];

}

pf1->average = tot / n;

showFlex(pf1);

n = 9;

tot = 0;

pf2 = malloc(sizeof(struct flex) + n * sizeof(double));

pf2->count = n;

for (i = 0; i < n; i++)

{

pf2->scores[i] = 20.0 - i / 2.0;

tot += pf2->scores[i];

}

pf2->average = tot / n;

showFlex(pf2);

free(pf1);

free(pf2);

return 0;

}

void showFlex(const struct flex * p)

{

int i;

printf("Scores : ");

for (i = 0; i < p->count; i++)

printf("%g ", p->scores[i]);

printf("\nAverage: %g\n", p->average);

}

下面是该程序的输出:

Scores : 20 19 18 17 16

Average: 18

Scores : 20 19.5 19 18.5 18 17.5 17 16.5 16

Average: 17

带伸缩型数组成员的结构确实有一些特殊的处理要求。第一,不能用结构进行赋值或拷贝:

struct flex * pf1, *pf2;  // *pf1 和*pf2 都是结构

...

*pf2 = *pf1; // 不要这样做

这样做只能拷贝除伸缩型数组成员以外的其他成员。确实要进行拷贝,应使用memcpy函数(第16章中介绍)。

第二,不要以按值方式把这种结构传递给结构。原因相同,按值传递一个参数与赋值类似。要把结构的地址传递给函数。

第三,不要使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员。

这种类似于在结构中最后一个成员是伸缩型数组的情况,称为struct hack。除了伸缩型数组成员在声明时用空的方括号外,struct hack特指大小为0的数组。然而,struct hack是针对特殊编译器(GCC)的,不属于C标准。这种伸缩型数组成员方法是标准认可的编程技巧。

14.7.10 匿名结构(C11)

匿名结构是一个没有名称的结构成员。为了理解它的工作原理,我们先考虑如何创建嵌套结构:

struct names

{

char first[20];

char last[20];

};

struct person

{

int id;

struct names name;// 嵌套结构成员

};

struct person ted = {8483, {"Ted", "Grass"}};

这里,name成员是一个嵌套结构,可以通过类似ted.name.first的表达式访问"ted":

puts(ted.name.first);

在C11中,可以用嵌套的匿名成员结构定义person:

struct person

{

int id;

struct {char first[20]; char last[20];}; // 匿名结构

};

初始化ted的方式相同:

struct person ted = {8483, {"Ted", "Grass"}};

但是,在访问ted时简化了步骤,只需把first看作是person的成员那样使用它:

puts(ted.first);

当然,也可以把first和last直接作为person的成员,删除嵌套循环。匿名特性在嵌套联合中更加有用,我们在本章后面介绍。

14.7.11 使用结构数组的函数

假设一个函数要处理一个结构数组。由于数组名就是该数组的地址,所以可以把它传递给函数。另外,该函数还需访问结构模板。为了理解该函数的工作原理,程序清单14.13把前面的金融程序扩展为两人,所以需要一个内含两个funds结构的数组。

程序清单14.13 funds4.c程序

/* funds4.c -- 把结构数组传递给函数 */

#include <stdio.h>

#define FUNDLEN 50

#define N 2

struct funds {

char bank[FUNDLEN];

doublebankfund;

char save[FUNDLEN];

doublesavefund;

};

double sum(const struct funds money , int n);

int main(void)

{

struct funds jones[N] = {

{

"Garlic-Melon Bank",

4032.27,

"Lucky's Savings and Loan",

8543.94

},

{

"Honest Jack's Bank",

3620.88,

"Party Time Savings",

3802.91

}

};

printf("The Joneses have a total of $%.2f.\n",sum(jones, N));

return 0;

}

double sum(const struct funds money , int n)

{

double total;

int i;

for (i = 0, total = 0; i < n; i++)

total += money[i].bankfund + money[i].savefund;

return(total);

}

该程序的输出如下:

The Joneses have a total of $20000.00.

(读者也许认为这个总和有些巧合!)

数组名jones是该数组的地址,即该数组首元素(jones[0])的地址。因此,指针money的初始值相当于通过下面的表达式获得:

money = &jones[0];

因为money指向jones数组的首元素,所以money[0]是该数组的另一个名称。与此类似,money[1]是第2个元素。每个元素都是一个funds类型的结构,所以都可以使用点运算符(.)来访问funds类型结构的成员。

下面是几个要点。

可以把数组名作为数组中第1个结构的地址传递给函数。

然后可以用数组表示法访问数组中的其他结构。注意下面的函数调用与使用数组名效果相同:

sum(&jones[0], N)

因为jones和&jones[0]的地址相同,使用数组名是传递结构地址的一种间接的方法。

由于sum函数不能改变原始数据,所以该函数使用了ANSI C的限定符const。

14.8 把结构内容保存到文件中

由于结构可以储存不同类型的信息,所以它是构建数据库的重要工具。例如,可以用一个结构储存雇员或汽车零件的相关信息。最终,我们要把这些信息储存在文件中,并且能再次检索。数据库文件可以包含任意数量的此类数据对象。储存在一个结构中的整套信息被称为记录(record),单独的项被称为字段(field)。本节我们来探讨这个主题。

或许储存记录最没效率的方法是用fprintf。例如,回忆程序清单14.1中的book结构:

#define MAXTITL 40

#define MAXAUTL 40

struct book {

char title[MAXTITL];

char author[MAXAUTL];

float value;

};

如果pbook标识一个文件流,那么通过下面这条语句可以把信息储存在struct book类型的结构变量primer中:

fprintf(pbooks, "%s %s %.2f\n", primer.title,primer.author, primer.value);

对于一些结构(如,有 30 个成员的结构),这个方法用起来很不方便。另外,在检索时还存在问题,因为程序要知道一个字段结束和另一个字段开始的位置。虽然用固定字段宽度的格式可以解决这个问题(例如,"%39s%39s%8.2f"),但是这个方法仍然很笨拙。

更好的方案是使用fread和fwrite函数读写结构大小的单元。回忆一下,这两个函数使用与程序相同的二进制表示法。例如:

fwrite(&primer, sizeof(struct book), 1, pbooks);

定位到 primer 结构变量开始的位置,并把结构中所有的字节都拷贝到与 pbooks 相关的文件中。sizeof(struct book)告诉函数待拷贝的一块数据的大小,1 表明一次拷贝一块数据。带相同参数的fread函数从文件中拷贝一块结构大小的数据到&primer指向的位置。简而言之,这两个函数一次读写整个记录,而不是一个字段。

以二进制表示法储存数据的缺点是,不同的系统可能使用不同的二进制表示法,所以数据文件可能不具可移植性。甚至同一个系统,不同编译器设置也可能导致不同的二进制布局。

14.8.1 保存结构的程序示例

为了演示如何在程序中使用这些函数,我们把程序清单14.2修改为一个新的版本(即程序清单14.14),把书名保存在book.dat文件中。如果该文件已存在,程序将显示它当前的内容,然后允许在文件中添加内容(如果你使用的是早期的Borland编译器,请参阅程序清单14.2后面的“Borland C和浮点数”)。

程序清单14.14 booksave.c程序

/* booksave.c -- 在文件中保存结构中的内容 */

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#define MAXTITL 40

#define MAXAUTL 40

#define MAXBKS 10 /* 最大书籍数量 */

char * s_gets(char * st, int n);

struct book { /* 建立 book 模板 */

char title[MAXTITL];

char author[MAXAUTL];

float value;

};

int main(void)

{

struct book library[MAXBKS]; /* 结构数组 */

int count = 0;

int index, filecount;

FILE * pbooks;

int size = sizeof(struct book);

if ((pbooks = fopen("book.dat", "a+b")) == NULL)

{

fputs("Can't open book.dat file\n", stderr);

exit(1);

}

rewind(pbooks);  /* 定位到文件开始 */

while (count < MAXBKS && fread(&library[count], size,

1, pbooks) == 1)

{

if (count == 0)

puts("Current contents of book.dat:");

printf("%s by %s: $%.2f\n", library[count].title,

library[count].author, library[count].value);

count++;

}

filecount = count;

if (count == MAXBKS)

{

fputs("The book.dat file is full.", stderr);

exit(2);

}

puts("Please add new book titles.");

puts("Press [enter] at the start of a line to stop.");

while (count < MAXBKS && s_gets(library[count].title, MAXTITL) != NULL

&& library[count].title[0] != '\0')

{

puts("Now enter the author.");

s_gets(library[count].author, MAXAUTL);

puts("Now enter the value.");

scanf("%f", &library[count++].value);

while (getchar != '\n')

continue; /* 清理输入行 */

if (count < MAXBKS)

puts("Enter the next title.");

}

if (count > 0)

{

puts("Here is the list of your books:");

for (index = 0; index < count; index++)

printf("%s by %s: $%.2f\n", library[index].title,

library[index].author, library[index].value);

fwrite(&library[filecount], size, count - filecount,

pbooks);

}

else

puts("No books? Too bad.\n");

puts("Bye.\n");

fclose(pbooks);

return 0;

}

char * s_gets(char * st, int n)

{

char * ret_val;

char * find;

ret_val = fgets(st, n, stdin);

if (ret_val)

{

find = strchr(st, '\n');  // 查找换行符

if (find) // 如果地址不是 NULL,

*find = '\0'; // 在此处放置一个空字符

else

while (getchar != '\n')

continue; // 清理输入行

}

return ret_val;

}

我们先看几个运行示例,然后再讨论程序中的要点。

$ booksave

Please add new book titles.

Press [enter] at the start of a line to stop.

Metric Merriment

Now enter the author.

Polly Poetica

Now enter the value.

18.99

Enter the next title.

Deadly Farce

Now enter the author.

Dudley Forse

Now enter the value.

15.99

Enter the next title.

[enter]

Here is the list of your books:

Metric Merriment by Polly Poetica: $18.99

Deadly Farce by Dudley Forse: $15.99

Bye.

$ booksave

Current contents of book.dat:

Metric Merriment by Polly Poetica: $18.99

Deadly Farce by Dudley Forse: $15.99

Please add new book titles.

The Third Jar

Now enter the author.

Nellie Nostrum

Now enter the value.

22.99

Enter the next title.

[enter]

Here is the list of your books:

Metric Merriment by Polly Poetica: $18.99

Deadly Farce by Dudley Forse: $15.99

The Third Jar by Nellie Nostrum: $22.99

Bye.

$

再次运行booksave.c程序把这3本书作为当前的文件记录打印出来。

14.8.2 程序要点

首先,以"a+b"模式打开文件。a+部分允许程序读取整个文件并在文件的末尾添加内容。b 是 ANSI的一种标识方法,表明程序将使用二进制文件格式。对于不接受b模式的UNIX系统,可以省略b,因为UNIX只有一种文件形式。对于早期的ANSI实现,要找出和b等价的表示法。

我们选择二进制模式是因为fread和fwrite函数要使用二进制文件。虽然结构中有些内容是文本,但是value成员不是文本。如果使用文本编辑器查看book.dat,该结构本文部分的内容显示正常,但是数值部分的内容不可读,甚至会导致文本编辑器出现乱码。

rewrite函数确保文件指针位于文件开始处,为读文件做好准备。

第1个while循环每次把一个结构读到结构数组中,当数组已满或读完文件时停止。变量filecount统计已读结构的数量。

第2个while按下循环提示用户进行输入,并接受用户的输入。和程序清单14.2一样,当数组已满或用户在一行的开始处按下Enter键时,循环结束。注意,该循环开始时count变量的值是第1个循环结束后的值。该循环把新输入项添加到数组的末尾。

然后for循环打印文件和用户输入的数据。因为该文件是以附加模式打开,所以新写入的内容添加到文件现有内容的末尾。

我们本可以用一个循环在文件末尾一次添加一个结构,但还是决定用 fwrite一次写入一块数据。对表达式count - filecount求值得新添加的书籍数量,然后调用fwrite把结构大小的块写入文件。由于表达式&library[filecount]是数组中第1个新结构的地址,所以拷贝就从这里开始。

也许该例是把结构写入文件和检索它们的最简单的方法,但是这种方法浪费存储空间,因为这还保存了结构中未使用的部分。该结构的大小是2×40×sizeof(char)+sizeof(float),在我们的系统中共84字节。实际上不是每个输入项都需要这么多空间。但是,让每个输入块的大小相同在检索数据时很方便。

另一个方法是使用可变大小的记录。为了方便读取文件中的这种记录,每个记录以数值字段规定记录的大小。这比上一种方法复杂。通常,这种方法涉及接下来要介绍的“链式结构”和第16章的动态内存分配。

14.9 链式结构

在结束讨论结构之前,我们想简要介绍一下结构的多种用途之一:创建新的数据形式。计算机用户已经开发出的一些数据形式比我们提到过的数组和简单结构更有效地解决特定的问题。这些形式包括队列、二叉树、堆、哈希表和图表。许多这样的形式都由链式结构(linked structure)组成。通常,每个结构都包含一两个数据项和一两个指向其他同类型结构的指针。这些指针把一个结构和另一个结构链接起来,并提供一种路径能遍历整个彼此链接的结构。例如,图14.3演示了一个二叉树结构,每个单独的结构(或节点)都和它下面的两个结构(或节点)相连。

图14.3 一个二叉树结构

图14.3中显示的分级或树状的结构是否比数组高效?考虑一个有10级节点的树的情况。它有210−1(或1023)个节点,可以储存1023个单词。如果这些单词以某种规则排列,那么可以从最顶层开始,逐级向下移动查找单词,最多只需移动9次便可找到任意单词。如果把这些单词都放在一个数组中,最多要查找1023个元素才能找出所需的单词。

如果你对这些高级概念感兴趣,可以阅读一些关于数据结构的书籍。使用C结构,可以创建和使用那些书中介绍的各种数据形式。另外,第17章中也介绍了一些高级数据形式。

本章对结构的概念介绍至此为止,第17章中会给出链式结构的例子。下面,我们介绍C语言中的联合、枚举和typedef。

14.10 联合简介

联合(union)是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。其典型的用法是,设计一种表以储存既无规律、事先也不知道顺序的混合类型。使用联合类型的数组,其中的联合都大小相等,每个联合可以储存各种数据类型。

创建联合和创建结构的方式相同,需要一个联合模板和联合变量。可以用一个步骤定义联合,也可以用联合标记分两步定义。下面是一个带标记的联合模板:

union hold {

int digit;

double bigfl;

char letter;

};

根据以上形式声明的结构可以储存一个int类型、一个double类型和char类型的值。然而,声明的联合只能储存一个int类型的值或一个double类型的值或char类型的值。

下面定义了3个与hold类型相关的变量:

union hold fit;// hold类型的联合变量

union hold save[10];  // 内含10个联合变量的数组

union hold * pu; // 指向hold类型联合变量的指针

第1个声明创建了一个单独的联合变量fit。编译器分配足够的空间以便它能储存联合声明中占用最大字节的类型。在本例中,占用空间最大的是double类型的数据。在我们的系统中,double类型占64位,即8字节。第2个声明创建了一个数组save,内含10个元素,每个元素都是8字节。第3个声明创建了一个指针,该指针变量储存hold类型联合变量的地址。

可以初始化联合。需要注意的是,联合只能储存一个值,这与结构不同。有 3 种初始化的方法:把一个联合初始化为另一个同类型的联合;初始化联合的第1个元素;或者根据C99标准,使用指定初始化器:

union hold valA;

valA.letter = 'R';

union hold valB = valA; // 用另一个联合来初始化

union hold valC = {88}; // 初始化联合的digit 成员

union hold valD = {.bigfl = 118.2}; // 指定初始化器

14.10.1 使用联合

下面是联合的一些用法:

fit.digit = 23; //把 23 储存在 fit,占2字节

fit.bigfl = 2.0; // 清除23,储存 2.0,占8字节

fit.letter = 'h'; // 清除2.0,储存h,占1字节

点运算符表示正在使用哪种数据类型。在联合中,一次只储存一个值。即使有足够的空间,也不能同时储存一个char类型值和一个int类型值。编写代码时要注意当前储存在联合中的数据类型。

和用指针访问结构使用->运算符一样,用指针访问联合时也要使用->运算符:

pu = &fit;

x = pu->digit; // 相当于 x = fit.digit

不要像下面的语句序列这样:

fit.letter = 'A';

flnum = 3.02*fit.bigfl; // 错误

以上语句序列是错误的,因为储存在 fit 中的是 char 类型,但是下一行却假定 fit 中的内容是double类型。

不过,用一个成员把值储存在一个联合中,然后用另一个成员查看内容,这种做法有时很有用。下一章的程序清单15.4就给出了一个这样的例子。

联合的另一种用法是,在结构中储存与其成员有从属关系的信息。例如,假设用一个结构表示一辆汽车。如果汽车属于驾驶者,就要用一个结构成员来描述这个所有者。如果汽车被租赁,那么需要一个成员来描述其租赁公司。可以用下面的代码来完成:

struct owner {

char socsecurity[12];

...

};

struct leasecompany {

char name[40];

char headquarters[40];

...

};

union data {

struct owner owncar;

struct leasecompany leasecar;

};

struct car_data {

char make[15];

int status; /* 私有为0,租赁为1 */

union data ownerinfo;

...

};

假设flits是car_data类型的结构变量,如果flits.status为0,程序将使用flits.ownerinfo.owncar.socsecurity,如果flits.status为1,程序则使用flits.ownerinfo.leasecar.name。

14.10.2 匿名联合(C11)

匿名联合和匿名结构的工作原理相同,即匿名联合是一个结构或联合的无名联合成员。例如,我们重新定义car_data结构如下:

struct owner {

char socsecurity[12];

...

};

struct leasecompany {

char name[40];

char headquarters[40];

...

};

struct car_data {

char make[15];

int status; /* 私有为0,租赁为1 */

union {

struct owner owncar;

struct leasecompany leasecar;

};

.

};

现在,如果 flits 是 car_data 类型的结构变量,可以用 flits.owncar.socsecurity 代替flits.ownerinfo.owncar.socsecurity。

总结:结构和联合运算符

成员运算符:.

一般注释:

该运算符与结构或联合名一起使用,指定结构或联合的一个成员。如果name是一个结构的名称, member是该结构模版指定的一个成员名,下面标识了该结构的这个成员:

name.member

name.member的类型就是member的类型。联合使用成员运算符的方式与结构相同。

示例:

struct {

int code;

float cost;

} item;

item.code = 1265;

间接成员运算符:->

一般注释:

该运算符和指向结构或联合的指针一起使用,标识结构或联合的一个成员。假设ptrstr是指向结构的指针,member是该结构模版指定的一个成员,那么:

ptrstr->member

标识了指向结构的成员。联合使用间接成员运算符的方式与结构相同。

示例:

struct {

int code;

float cost;

} item, * ptrst;

ptrst = &item;

ptrst->code = 3451;

最后一条语句把一个int类型的值赋给item的code成员。如下3个表达式是等价的:

ptrst->code item.code(*ptrst).code

14.11 枚举类型

可以用枚举类型(enumerated type)声明符号名称来表示整型常量。使用enum关键字,可以创建一个新“类型”并指定它可具有的值(实际上,enum常量是int类型,因此,只要能使用int类型的地方就可以使用枚举类型)。枚举类型的目的是提高程序的可读性。它的语法与结构的语法相同。例如,可以这样声明:

enum spectrum {red, orange, yellow, green, blue, violet};

enum spectrum color;

第1个声明创建了spetrum作为标记名,允许把enum spetrum作为一个类型名使用。第2个声明使color作为该类型的变量。第1个声明中花括号内的标识符枚举了spectrum变量可能有的值。因此, color 可能的值是 red、orange、yellow 等。这些符号常量被称为枚举符(enumerator)。然后,便可这样用:

int c;

color = blue;

if (color == yellow)

...;

for (color = red; color <= violet; color++)

...;

虽然枚举符(如red和blue)是int类型,但是枚举变量可以是任意整数类型,前提是该整数类型可以储存枚举常量。例如,spectrum的枚举符范围是0~5,所以编译器可以用unsigned char来表示color变量。

顺带一提,C枚举的一些特性并不适用于C++。例如,C允许枚举变量使用++运算符,但是C++标准不允许。所以,如果编写的代码将来会并入C++程序,那么必须把上面例子中的color声明为int类型,才能C和C++都兼容。

14.11.1 enum常量

blue和red到底是什么?从技术层面看,它们是int类型的常量。例如,假定有前面的枚举声明,可以这样写:

printf("red = %d, orange = %d\n", red, orange);

其输出如下:

red = 0, orange = 1

red成为一个有名称的常量,代表整数0。类似地,其他标识符都是有名称的常量,分别代表1~5。只要是能使用整型常量的地方就可以使用枚举常量。例如,在声明数组时,可以用枚举常量表示数组的大小;在switch语句中,可以把枚举常量作为标签。

14.11.2 默认值

默认情况下,枚举列表中的常量都被赋予0、1、2等。因此,下面的声明中nina的值是3:

enum kids {nippy, slats, skippy, nina, liz};

14.11.3 赋值

在枚举声明中,可以为枚举常量指定整数值:

enum levels {low = 100, medium = 500, high = 2000};

如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值。例如,假设有如下的声明:

enum feline {cat, lynx = 10, puma, tiger};

那么,cat的值是0(默认),lynx、puma和tiger的值分别是10、11、12。

14.11.4 enum的用法

枚举类型的目的是为了提高程序的可读性和可维护性。如果要处理颜色,使用red和blue比使用0和1更直观。注意,枚举类型只能在内部使用。如果要输入color中orange的值,只能输入1,而不是单词orange。或者,让程序先读入字符串"orange",再将其转换为orange代表的值。

因为枚举类型是整数类型,所以可以在表达式中以使用整数变量的方式使用enum变量。它们用在case语句中很方便。

程序清单14.15演示了一个使用enum的小程序。该程序示例使用默认值的方案,把red的值设置为0,使之成为指向字符串"red"的指针的索引。

程序清单14.15 enum.c程序

/* enum.c -- 使用枚举类型的值 */

#include <stdio.h>

#include <string.h>// 提供 strcmp、strchr函数的原型

#include <stdbool.h>  // C99 特性

char * s_gets(char * st, int n);

enum spectrum { red, orange, yellow, green, blue, violet };

const char * colors = { "red", "orange", "yellow",

"green", "blue", "violet" };

#define LEN 30

int main(void)

{

char choice[LEN];