第十四章 结构和其他数据形式

文章目录

    • 14.1 示例问题:创建图书目录
    • 14.2 建立结构声明
    • 14.3 定义结构变量
      • 14.3.1 初始化结构
            • 注意 初始化结构和类别储存期
      • 14.3.2 结构的初始化器
    • 14.4 结构数组
            • 结构和内存
    • 14.5 嵌套结构
    • 14.6 指向结构的指针
    • 14.7 向函数传递结构的信息
      • 14.7.1 结构和结构指针的选择
      • 14.8 把结构内容保存到文件中
      • 14.8.1 保存结构的程序示例
      • 14.8.2 程序要点
    • 14.9 链式结构
    • 14.10 联合简介
            • 总结:结构和联合运算符
    • 14.11 枚举类型
      • 14.11.1 enum 常量
      • 14.11.2 默认值
      • 14.11.3 赋值
      • 14.11.4 enum 的用法
      • 14.11.5 共享名称空间
    • 14.12 typedef 简介
    • 14.13 其他复杂的声明
      • 14.14 函数和指针
            • 提示
    • 14.15 关键概念
    • 14.16 本章小结
    • 14.17 复习题
    • 14.18 编程练习

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

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

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

/* 一本书的图书目录 */
#include 
#include 
char * s_gets(char * st, int n);
#define MAXTITL 41 /* 书名的最大长度 */
#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.
Diema Lapoult
Now enter the value.
29.99
Chicken of the Andes by Diema Lapoult: $29.99
Diema Lapoult: “Chicken of the Andes” ($29.99)
Done.

程序清单中创建的结构有 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 结合在一起。

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

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

就计算而言,下面的声明:struct book library;
是一下声明的简化:

struct book{
    char title[MAXTITL];
    char author[MAXAUTL];
    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,2,3,4,5,8};

结构变量是否也可以这样初始化?是的,可以。初始化一个结构变量与初始化数组的语法类似:

struct book library = {
	"The Pious Pirate and the Devious Damsel",
	"Renee Vivotte",
	1.95
};

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

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

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

14.3.2 结构的初始化器

C99 和 C11 结构提供了指定初始化器(designated initializer),其语法与数组的指定初始化器类似。但是,结构的指定初始化器使用点运算符和成员名表示特定的元素。例如,只初始化 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.90。

14.4 结构数组

显然,每本书的基本信息都可以用一个 book 类型的结构变量来表示。为描述两本书,需要使用两个变量,以此类推。可以使用这一类型的结构数组来处理多本书。

结构和内存

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

14.5 嵌套结构

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

/* 嵌套结构示例 */
#include 
#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",
    " and have a few laughs"
};

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 = {
	    {
	        "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
 and have a few laughsgrilled salmon(null).

                                        See you soon,
                                        Shalala

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

该声明表明 handle 是一个 struct name 类型的变量。当然,文件中也应包含结构 names 的声明。其次,注意如何访问嵌套结构的成员,这需要使用两次点运算符:printf("Dear %s, \n\n",fellow.handle.first);

14.6 指向结构的指针

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

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

/* 嵌套结构示例 */
#include 
#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",
    " and have a few laughs"
};

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->inicome 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: 0060FE44 #2: 0060FE98
pointer #1: 0060FE44 #2: 0060FE98
him->inicome is $68112.00; (*him).income is $68112.00
him->favfood is tripe; him->handle.last is Swillbelly

14.7 向函数传递结构的信息

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

14.7.1 结构和结构指针的选择

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

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

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

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

14.8.1 保存结构的程序示例

为了演示如何在程序中使用这些函数,把书名保存在 book.dat 文件中。如果该文件已经存在,程序将显示它当前的内容,然后允许在文件总添加内容。

/* 在文件中保存结构中的内容 */
#include 
#include 
#include 
#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;
}

我们先看几个运行示例,然后在讨论程序中的要点。
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.

Current contents of book.dat:
Metric Merriment by Polly Poetica: $18.99
Deadly Farce by Dudley Forse: $15.99
Please add new book titles.
Press [Enter[ at the start of a line to stop.
[Enter]
Here is the list of your books:
Metric Merriment by Polly Poetica: $18.99
Deadly Farce by Dudley Forse: $15.99
Bye.
再次运行程序把这 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 按下循环提示用户进行输入,并接受用户的输入。当数组已满或用户在一行的开始处按下 Enter 键时,循环结束。注意,该循环开始时 count 变量的值是第 1 个循环结束后的值。该循环把新输入项添加到数组的末尾。

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

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

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

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

14.9 链式结构

我们想简要介绍一下结构的多种用途之一:创建新的数据形式。计算机用户已经开发出的一些数据形式比我们提到过的数组和简单结构更有效地解决特定的问题。这些形式包括队列、二叉树、堆、哈希表和图表。许多这样的形式都由链式结构(linked structure)组成。通常,每个结构都包含一两个数据项和一两个指向其他同类型结构的指针。这些指针把一个结构和另一个结构连接起来,并提供一种路径能遍历整个彼此链接的结构。如图演示了一个二叉树结构,每个单独的结构(或节点)都和它下面的两个结构(或节点)相连。
第十四章 结构和其他数据形式_第1张图片
图中显示的分级或树状的结构是否比数组高效?考虑一个有 10 级节点的树情况。它有 210 -1(1023)个节点,可以储存 1023 个单词。如果这些单词以某种规则排列,那么可以从最顶层开始,逐级向下移动查找单词,最多只需要移动 9 次便可找到任意单词。如果把这些单词都放在一个数组中,最多要查找 1023 个元素才能找出所需的单词。

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

本章对结构的概念介绍至此为止,第 17 章中会给出链式结构的例子。

14.10 联合简介

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

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

union hold {
	int digit;
	double bigfl;
	char letter;
};

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

总结:结构和联合运算符

成员运算符
一般注释
该运算符与结构或联合名一起使用,指定结构或联合的一个成员。如果 name 是一个结构的名称,member 是该结构模板指定的一个成员名,下面标识了该结构的这个成员:name.member
name.member 的类型就是 member 的类型。联合使用成员运算符的方式与结构详图。
示例:

struct {
	int fode;
	float cost;
} item;
item.code = 1265;

间接成员运算符:->
一般注释:
该运算符和指向结构或联合的指针一起使用,标识结构或联合的一个成员。假设 ptrstr 是指向结构的指针,member 是该结构模板指定的一个成员,那么:ptrstr->member
标识了指向结构的成员,联合使用间接成员运算符的方式与结构相同。
示例:

struct {
	int fode;
	float cost;
} item, * ptrstr;
ptrstr = &item;
ptestr->code = 3451;

最后一条语句把一个 int 类型的值赋给 item 的 code 成员。如下 3 个表达式是等价的:
1、ptrstr->code
2、item.code
3、(*ptrstr).code

14.11 枚举类型

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

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

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

int c;
color = blue;
if(color == yellow)
	...;
for(color = red; color <= violet; color++)
	...;

虽然枚举符是 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 语句中很方便。

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

/* 使用枚举类型的值 */
#include 
#include  /* 提供 strcmp()、strchr() 函数的原型 */
#include  /* 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];
	enum spectrum color;
	bool color_is_found = false;

	puts("Enter a color (empty line to quit):");
	while(s_gets(choice,LEN) != NULL && choice[0] != '\0')
    {
        for(color = red; color <= violet; color++)
        {
            if(strcmp(choice,colors[color]) == 0)
            {
                color_is_found = true;
                break;
            }
        }
        if(color_is_found)
            switch(color)
        {
            case red : puts("Roses are red.");
                break;
            case orange: puts("Poppies are orange.");
                break;
            case yellow: puts("Sunflowers are yellow.");
                break;
            case green: puts("Grass is green.");
                break;
            case blue: puts("Bluebells are blue.");
                break;
            case violet: puts("Violets are violet.");
                break;
        }
        else
            printf("I don't know about the color %s.\n",choice);
        color_is_found = false;
        puts("Next colot, please (empty line to quit):");

    }
    puts("Goodbye!");

    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;
}

当输入的字符串与 color 数组的成员指向的字符串相匹配时,for 循环结束。如果循环找到匹配的颜色,程序接用枚举变量的值与作为 case 标签的枚举常量匹配。下面是该程序的一个运行示例:
Enter a color (empty line to quit):
blue
Bluebells are blue.
Next colot, please (empty line to quit):
orange
Poppies are orange.
Next colot, please (empty line to quit):
purple
I don’t know about the color purple.
Next colot, please (empty line to quit):
[Enter]
Goodbye!

14.11.5 共享名称空间

C 语言使用名称空间(namespace)标识程序中的各部分,即通过名称类识别。作用域是名称空间概念的一部分:两个不同作用域的同名变量不冲突;两个相同作用域的同名变量冲突。名称空间是分类别的。在特定作用域中的结构标记、联合标记和枚举标记都共享相同的名称空间,该名称空间与普通变量使用的空间不同。这意味着咋相同作用域中变量和标记的名称可以相同,不会引起冲突,但是不能在相同作用域中声明两个同名标签或同名变量。例如,在 C 中,下面的代码不会产生冲突:

struct rect {double x;double y;};
int rect; // 在 C 中不会产生冲突

尽管如此,以两种不同的方式使用的标识符会造成混乱。另外,C++ 不允许这样做,因为它把标记名和变量名放在相同的名称空间中。

14.12 typedef 简介

typedef 工具是一个高级数据特性,利用 typedef 可以为某一类型自定义名称。这方面与 #define 类似,但是两者有 3 处不同:

  • 与 #define 不同,typedef 创建的符号名只受限于类型,不能用于值。
  • typedef 由编译器解释,不是预处理器。
  • 在其受限范围内,typedef 比 #define 更灵活。

下面介绍 typedef 的工作原理。假设要用 BYTE 表示 1 字节的数组。只需要定义个 char 类型变量一样定义 BYTE,然后在定义前面加上关键字 typedef 即可:typedef unsigned char BYTE;

随后,便可使用 BYTE 来定义变量:BYTE x, y[10], *z;

该定义的作用域取决于 typedef 定义所在的位置。如果定义在函数中,就具有局部作用域,受限于定义所在的函数。如果定义在函数外面,就具有文件作用域。

通常,typedef 定义中用大写字母表示被定义的名称,以提醒用户这个类型名实际上是一个符号缩写。当然,也可以用小写:typedef unsigned char byte;

typedef 中使用的名称遵循变量的命名规则。

为现有类型创建一个名称,看上去真是多此一举,但是它有时的确很有用。在前面的示例中,用 BYTE 代替 unsigned char 表明你打算用 BYTE 类型的变量表示数字,而不是字符码。使用 typedef 还能提高程序的可移植性。例如,我们之前提到的 sizeof 运算符的返回类型:size_t 类型,以及 time() 函数的返回类型:time_t 类型。C 标准规定 sizeof 和 time() 返回整数类型,但是让实现来决定具体是什么整数类型。其原因是,C 标准委员会认为没有哪个类型对于所有的计算机平台都是最优选择。所以,标准委员会决定建立一个新的类型名(如,time_t),并让实现使用 typedef 来设置它的具体类型。以这样的方式,C 标准提供以下通用原型:time_t time(time_t *);

time_t 在一个系统中是 unsigned long,在另一个系统中可以是 unsigned long long。只要包含 time.h 头文件,程序就能访问合适的定义,你也可以在代码中声明 time_t 类型的变量。

typedef 的一些特性与 #define 的功能重合。例如:#define BYTE unsigned char

这使预处理器用 BYTE 替换 unsigned char。但是也有 #define 没有的功能:typedef char * STRING;

没有 typedef 关键字,编译器将把 STRING 识别为一个指向 char 的指针变量。有了 typedef 关键字,编译器则把 STRING 解释成一个类型的标识符,该类型是指向 char 的指针。因此,相当于:char * name, * sign;

但是,如果这样假设:#define STRING char * 然后,下面的声明:STRING name, sign; 将被翻译成:char * name, sing; 这导致只有 name 才是指针。

还可以把 typedef 用于结构:

typedef struct comples {
	float real;
	float imag;
} COMPLEX;

然后便可使用 COMPLEX 类型代替 complex 结构来表示复数。使用 typedef 的第 1 个原因是:为经常出现的类型创建一个方便、易识别的类型名。例如,前面的例子中,许多人更倾向于使用 STRING 或与其等价的标记。

用 typedef 来命名一个结构类型时,可以省略该结构的标签:typedef struct {double x; double y;} rect;

假设这样使用 typedef 定义的类型名:

rect r1 = {3.0, 6/0};
rect r2;

以上代码将被翻译成:

struct {double x; double y;} r1 = {3.0, 6.0};
struct {double x; double y;} r2;
r2 = r1;

这两个结构在声明时都没有标记,它们的成员完全相同(成员名及其类型都匹配),C 认为这两个结构的类型相同,所以 r1 和 r2 间的赋值是有效操作。

使用 typedef 的第 2 个原因是:typedef 常用于给复杂的类型命名。例如,下面的声明:typedef char (* FRPTC ()) [5];

把 FRPTC 声明为一个函数类型,该函数返回一个指针,该指针指向内含 5 个char 类型元素的数组。

使用 typedef 时要记住,typedef 并没有创建任何新类型,它只是为某个已存在的类型增加一个方便使用的标签。以前面的 STRING 为例,这意味着我们创建的 STRING 类型变量可以作为实参传递给以指向 char 指针作为形参的函数。

通过结构、联合和 typedef,C 提供了有效处理数据的工具和处理可移植数据的工具。

14.13 其他复杂的声明

C 允许用户自定义数据形式。虽然我们常见的是一些简单的形式,但是根据需要有时还会用到一些复杂的形式。在一些复杂的声明中,常包含下面的符号,如表所示:

符号 含义
* 表示一个指针
() 表示一个函数
[] 表示一个数组

下面是一些较复杂的声明示例:

int board[8][8]; // 声明一个内含 int 数组的数组
int ** ptr; // 声明一个指向指针的指针,被指向的指针指向 int
int * risks[10]; // 声明一个内含 10 个元素的数组,每个元素都是一个指向 int 的指针
int (* rusks)[10]; // 声明一个指针数组的指针,该数组内含 10 个 int 类型的值
int * off[3][4]; // 声明一个 3 x 4 二维数组,每个元素都是指向 int 的指针
int (* uuf)[3][4]; // 声明一个指向 3 x 4 二维数组的指针,该数组中内含 int 类型值
int (* uof[3])[4]; // 声明一个内含 3 个指针元素的数组,其中每个指针都指向一个内含 4 个 int 类型元素的数组

要看懂以上声明,关键要理解 、() 和 [] 的优先级。记住下面几条规则。
1、数组名后面的 [] 和函数名后面的 () 具有相同的优先级。它们比 * (解引用运算符)优先级高。因此下面声明的 risk 是一个指针数组,不是指向数组的指针:int * risks[10];
2、[] 和 () 的优先级相同,由于都是从左往右结合,所以下面的声明中,在应用方括号之前,
先与 risks 结合。因此 risks 是一个指向数组的指针,该数组内含 10 个 int 类型的元素:int (* risks)[10];
3、[] 和 () 都是从左往右结合。因此下面声明的 goods 是一个由 12 个内含 50 个 int 类型值的数组组成的二维数组,不是一个有 50 个内含 12 个 int 类型值的数组组成的二维数组:int goods[12][50];

把以上规则应用于下面的声明:int * oof[3][4];

[3] 比 * 的优先级高,由于从左往右结合,所以 [3] 先与 oof 结合。因此,oof 首先是一个内含 3 个元素的数组。然后再与 [4] 结合,所以 oof 的每个元素都是内含 4 个元素的数组。* 说明这些元素都是指针。最后,int 表明了这 4 个元素都是指向 int 的指针。因此,这条声明要表达的是:foo 是一个内含 3 个元素的数组,其中每个元素是由 4 个指向 int 的指针组成的数组。简而言之,oof 是一个 3 x 4 的二维数组,每个元素都是指向 int 的指针。编译器要为 12 个指针预留存储空间。

现在来看下面的声明:int (* uuf)[3][4]; 圆括号使得 * 先与 uuf 结合,说明 uuf 是一个指针,所以 uuf 是一个指向 3 x 4 的 int 类型二维数组的指针。编译器要为一个指针预留存储空间。

根据这些规则,还可以声明:

char * fump(int); // 返回字符指针的函数
char (*frump)(int); // 指向函数的指针,该函数的返回类型为 char
char (* frump[3])(int); // 内含 3 个指针的数组,每个指针都指向返回类型为 char 的函数

这 3 个函数都接受 int 类型的参数。可以使用 typedef 建立一系列相关类型:

typedef int arr5[5];
typedef arr5 * p_arr5;
typedef p_arr5 arrp10[10];
arr5 togs; // togs 是一个内含 5 个 int 类型值的数组
p_arr5 p2; // p2 是一个指向数组的指针,该数组内含 5 个 int 类型的值
arrp10 ap; // ap 是一个内含 10 个指针的数组,每个指针都指向一个内含 5 个 int 类型值的数组

14.14 函数和指针

首先,什么是函数指针?假设有一个指向 int 类型变量的指针,该指针储存着这个 int 类型变量储存在内存位置的地址。同样,函数也有地址,因为函数的机器语言实现由载入内存的代码组成。指向函数的指针中储存着函数代码的起始处的地址。

其次,声明一个数据指针时,必须声明指针所指向的数据类型。声明一个函数指针时,必须声明指针指向的函数类型。为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型。

提示

要声明一个指向特定类型函数的指针,可以先声明一个该类型的函数,然后把函数名替换成指针(*pf)形式的表达式。然后,pf 就称为指向该类型函数的指针。

声明了函数指针后,可以把类型匹配的函数地址赋给它。在这种上下文中,函数名可以用于表示函数的地址:

void ToUpper(char *);
void ToLower(char *);
int round(double);
void (*pf)(char *);
pf = ToUpper; // 有效,ToUpper 是该类型函数的地址
pf = ToLower; // 有效,ToLower 是该类型函数的地址
pf = round; // 无效,round 与指针类型不匹配
pf = ToLower(); // 无效,ToLower() 不是地址

14.15 关键概念

我们在编程中要表示的信息通常不只是一个数字或一些列数字。程序可能要处理具有多种属性的实体。C 结构可以把这些信息都放在一个单元内。在组织程序是这很重要,因为这样可以把相关的信息都储存在一处,而不是分散储存在多个变量中。

设计结构时,开发一个与之配套的函数包通常很有用。例如,写一个以结构(或结构的地址)为参数的函数打印结构内容,比用一堆 printf() 语句强得多。因为只需要一个参数就能打印结构中的所有信息。如果把信息放到零散的变量中,每个部分都需要一个参数。另外,如果要在结构中增加一个成员,只需重写函数,不必改写函数调用。这在修改结构时很方便。

联合声明与结构声明类似。但是,联合的成员共享相同的存储空间,而且在联合中同一时间内只能有一个成员。实质上,可以在联合变量中储存一个类型不唯一的值。

enum 工具提供一种定义符号常量的方法,typedef 工具提供一种为基本或派生类型创建新标识符的方法。

指向函数的指针提供一种高速函数应使用哪一个函数的方法。

14.16 本章小结

C 结构提供在相同的数据对象中储存多个不同类型数据项的方法。可以使用标记来标识一个具体的结构模板,并声明该类型的变量。通过成员点运算符(.)可以使用结构模板中的标签来访问结构的各个成员。

如果有一个指向结构的指针,可以用该指针和间接成员运算符(->)代替结构名和点运算符来访问结构的各成员。和数组不同,结构名不是结构的地址,要在结构名前使用 & 运算符才能获得结构的地址。

一贯以来,与结构相关的函数都使用指向结构的指针作为参数。现在的 C 允许把结构作为参数传递,作为返回值和同类型结构之间赋值。然而,传递结构的地址通常更有效。

联合使用与结构相同的语法。然而,联合的成员共享一个共同的存储空间。联合同一时间内只能储存一个单独的数据项,不像结构那样同时储存多种数据类型也就是说,结构可以同时储存一个 int 类型数据、一个 double 类型数据和一个 char 类型数据,而相应的联合只能保存一个 int 类型数据,或者一个 double 类型数据,或者一个 char 类型数据。

通过枚举可以创建一系列代码整型常量(枚举常量)的符号和定义相关联的枚举类型。

typedef 工具可用于建立 C 标准烈性的别名或缩写。

函数名代表函数的地址,可以把函数的地址作为参数传递给其他函数,然后这些函数就可以使用被指向的函数。如果把特定函数的地址赋给一个名为 pf 的函数指针,可以通过以下两种方式调用该函数:

#include 
...
double (*pdf)(double);
double x;
pdf = sin;
x = (*pdf)(1.2); // 调用 sin(1.2)
x = pdf(1.2); // 同样调用 sin(1.2)

14.17 复习题

1、下面的结构模板有什么问题:

struct{
	char itable;
	int num[20];
	char * togs;
}

2、下面是程序的一部分,输出是什么?

#include 
struct house {
	float sqft;
	int rooms;
	int stories;
	char address[40];
};
int main(void)
{
	struct house fruzt = {1560.0,6,1,"22 Spiffo Road"};
	struct house *sign;
	sign = &fruzt;
	pirntf("%d %d\n",fruzt.name,sign->stories);
	pirntf("%s \n",fruzt.address);
	pirntf("%c %c\n",sign->address[3],fruzt.address[4]);
	return 0;

3、设计一个结构模板储存一个月份名、该月份的 3 个字母缩写、该月的天数以及月份号。

4、定义一个数组,内含 12 个结构(第 3 题的结构类型)并初始化一个年份(非闰年)。

5、编写一个函数,用户提供月份号,该函数就返回一年中到该月为止(包括该月)的总天数。假设在所有函数的外部声明了第 3 提的结构模板和一个该类型结构的数组。

6、
a、假设有下面的 typedef,声明一个内含 10 个指定结构的数组。然后,单独给成员赋值(或等价字符串),使第 3 个元素表示一个焦距长度有 500mm,孔径为 f/2.0 的 Remarkata 镜头。

typedef struct lens {
	float foclen; /* 描述镜头 */
	float fstop; /* 焦距长度,单位为 mm */
	char brand[30]; /* 品牌名称 */
} LENS;

b、重写 a,在声明中使用一个待指定初始化器的初始化列表,而不是对每个成员单独赋值。

7、考虑下面程序片段:

struct name {
	char first[20];
	char last[20];
};
struct bem {
	int limbs;
	struct name title;
	char type[30];
};
struct ben * pb;
struct bem deb = {
	6,
	{"Berbnazel","Gwolkapwolk"},
	"Arcturan"
};

pb = &deb;

a、下面的语句分别打印什么?
b、如何用结构表示法(两种方法)表示“Gwolkapwolk”?
c、编写一个函数,以 bem 结构的地址作为参数,并以下面的形式输出结构的内容(假定结构模板在一个名为 starfolk.h 的头文件中):Berbnazel Gwolkapwolk is a 6-limbed Arcturan.

8、考虑下面的声明:

struct fullname {
	char fname[20];
	char lname[20];
};
struct bard {
	struct fullname name;
	int born;
	int died;
};
struct bard willie;
struct bard *pt = &willie;

a、用 willie 标识符标识 willie 结构的 born 成员。
b、用 pt 标识符标识 willie 结构的 born 成员。
c、调用 scanf() 读入一个用 willie 标识符标识的 born 成员的值。
d、调用 scanf() 读入一个用 pt 标识符标识的 born 成员的值。
e、调用 scanf() 读入一个用 willie 标识符标识的 name 成员中 lname 成员的值。
f、调用 scanf() 读入一个用 pt 标识符标识的 name 成员中 lname 成员的值。

9、定义一个结构模板以储存这些项:汽车名、马力、EPA(美国环保局)城市交通 MPG(每加仑燃料形式的英里数)评级、轴距和出厂年份。使用 car 作为该模板的标记。

10、假设有如下结构:

struct gas {
	float distance;
	float gals;
	float mpg;
};

a、设计一个函数,接受 struct gas 类型的参数。假设传入的结构包含 distance 和 gals 信息。该函数为 mpg 成员计算正确的值,并把值返回该结构。
b、设计一个函数,接受 struct gas 类型的参数。假设传入的结构包含 distance 和 gals 信息。该函数为 mpg 成员计算正确的值,并把该值赋给合适的成员。

11、声明一个标记为 choices 的枚举,把枚举常量 no、yes 和 maybe 分别设置为 0、1、2。

12、声明一个指向函数的指针,该函数返回指向 char 的指针,接受一个指向 char 的指针和一个 char 类型的值。

13、声明 4 个函数,并初始化一个指向这些函数的指针数组。每个函数都接受两个 double 类型的参数,返回 double 类型的值。另外,用两种方法使用该数组调用带 10.0 和 2.5 实参的第 2 个函数。

14.18 编程练习

1、重新编写复习题 5,用月份名的拼写代替月份号(别忘了使用 strcmp())。在一个简单的程序中测试该函数。

2、编写一个函数,提示用户输入日、月和年。月份可以是月份号、月份名或月份名缩写。然后该程序应返回一年中到用户指定日子(包括这一天)的总天数。

3、修改程序清单中的图书目录程序,使其按照输出图书的顺序输出图书的信息,然后按照标题字母的声明输出图书的信息,最后按照价格的升序输出图书的信息。

/* 包含多本书的图书目录 */
#include 
#include 
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;
}

4、编写一个程序,创建一个有两个成员的结构模板:
a、第 1 个程序是社会保险号,第 2 个成员是一个有 3 个成员的结构,第 1 个成员代码名,第 2 个成员代码中间名,第 3 个成员表示姓。创建并初始化一个内含 5 个该类型结构的数组。该程序以下面的格式打印数据:Dribble, Flossie M. – 302039823

如果有中间名,只打印它的第 1 个字母,后面加一个点(.);如果没有中间名,则不用打印点。编写一个程序进行打印,把结构数组传递给这个函数。

b、修改 a 部分,传递结构的值而不是结构的地址。

5、编写一个程序满足下面的要求。
a、外部定义一个有两个成员的结构模板 name:一个字符串储存名,一个字符串储存姓。
b、外部定义一个有 3 个成员的结构模板 student:一个 name 类型的结构,一个 grade 数组储存 3 个浮点型分数,一个变量储存 3 个分数平均数。
c、在 main() 函数总声明一个内含 CSIZE(CSIZE = 4)个 student 类型结构的数组,并初始化这些结构的名字部分。用函数执行 g、e、f 和 g 中描述的任务。
d、以交互的方式获取每个学生的成绩,提示用户输入学生的姓名和分数。把分数储存到 grade 数组相应的结构中。可以在 main() 函数或其他函数中用循环来完成。
e、计算每个结构的平均分,并把计算后的值赋给合适的成员。
f、打印每个结构的信息。
g、打印班级的平均分,即所有结构的数值成员的平均值。

6、一个文本文件中保存这一个垒球队的信息,每行数据都是这样排列: 4 Jessis Joybat 5 2 1 1

第 1 项是球员号,为方便起见,其范围是 0 ~ 18。第 2 项是球员的名。第 3 项是球员的姓。名和姓都是一个单词。第 4 项是官方统计的球员上场次数。接着 3 项分别是击中数、走垒数和打点(RBI)。文件可能包含多场比赛的数据,所以同一位球员可能有多行数据,而且同一位球员的多行数据之间可能有其他球员的数据。编写一个程序,把数据储存到一个结构数组中。该结构中的成员要分别表示球员的名、姓、上场次数、击中数、走垒数、打点和安打率。可以使用球员号作为数组的索引。该程序要读到文件结尾,并统计每位球员的各项累计总和。

世界棒球统计与之相关。例如,一次走垒和触垒中的失误不计入上场次数,但是可能产生一个 RBI。但是该程序要做的是像下面描述的一样读取和处理数据文件,不会关心数据的实际含义。

要实现这些功能,最简单的方法是吧结构的内容都初始化为零,把文件中的数据读入临时变量中,然后将其加入相应的结构中。程序读完文件后,应结算每位球员的安打率,并把计算结果储存到结构的相应成员中。计算安打率是用球员的累计击中数除以上场累计次数。这是一个浮点数计算。最后,程序结合整个球队的统计数据,一行显示一位球员的累计数据。

7、修改程序,从文件中读取每条记录并显示出来,允许用户删除记录或修改记录的内容。如果删除记录,把控出来的空间留给下一个要读入的记录。要修改现有的文件内容,必须用 “r+b” 模式,而不是 “a+b” 模式。而且,必须更加注意定位文件指针,防止新加入的记录覆盖现有记录。最简单的方法是改动储存在内存中的所有数据,然后在把最后的信息写入文件。跟踪的一个方法是在 book 结构中添加一个成员表示是否该项被删除。

8、巨人航空公司的机群由 12 座位的飞机组成。它每天飞行一个航班。根据下面的要求,编写一个座位预订程序。
a、该程序使用一个内含 12 个结构的数组。每个结构中包括:一个成员表示座位编号、一个成员表示座位是否已被预订、一个成员表示预订人的名、一个成员表示预订人的姓。

b、该程序显示下面的菜单:
To choose a function, enter its letter label:
a) Show number of empty seats
b) Show list of empty seats
c) Show alphabetical list of seats
d) Assign a customer to a seat assignment
e) Delete a seat assignment
f) Quit

c、该程序能成功执行上面给出的菜单。选择 d) 和 e) 要提示用户进行额外输入,每个选项都能让用户中止输入。

d、执行特定程序后,该程序再次显示菜单,除非用户现在 f)。

9、巨人航空公司需要另一架飞机(容量相同),每天飞 4 班(航班 102、311、444 和 519)。把程序扩展为可以处理 4 个航班。用一个顶层菜单提供航班选择和退出。选择一个特定航班,就会出现和编程练习 8 类似的菜单。但是该菜单要添加一个新选项:确认座位分配。而且,菜单中的退出是返回顶层菜单。每次显示都要指明当前正在处理的航班号。另外,座位分配显示要指明确认状态。

10、编写一个程序,通过一个函数指针数组实现菜单。例如,选择菜单中的 a,将激活有该数组第 1 个元素指向的函数。

11、编写一个名为 transform() 的函数,接受 4 个参数:内含 double 类型数据的源数组名、内含 double 类型数据的目标数组名、一个表示数组元素个数的 int 类型参数、函数名(或等价的函数指针)。transform() 函数应把指定函数应用于源数组中的每个元素,并把返回值储存在目标数组中。例如:transform(source, target, 100, sin);

该声明会把 target[0] 设置为 sin(source[0]),等等,共有 100 个元素。在一个程序中调用 transform() 4 次,以测试该函数。分别使用 math.h 函数库中的两个函数以及自定义的两个函数作为参数。

你可能感兴趣的:(C,Primer,Plus)