《C Primer Plus》学习笔记—第14章

目录

  • 《C Primer Plus》学习笔记
    • 第14章 结构和其他数据形式
      • 1.示例问题:创建图书目录
        • 1.程序book.c
      • 2.建立结构声明
      • 3.定义结构变量
        • 1.初始化结构
        • 2.访问结构成员
        • 3.结构的初始化
      • 4.结构数组
        • **1.结构和内存**
        • 2.程序manybook.c
        • 3.声明结构数组
        • 4.标识结构数组的成员
        • 5.程序讨论
      • 5.嵌套结构
        • 1.程序friend.c
      • 6.指向结构的指针
        • 1.程序friends.c
        • 2.声明和初始化结构指针
        • 3.用指针访问成员
      • 7.向函数传递结构的信息
        • 1.传递结构成员
          • 1.程序funds1.c
        • 2.传递结构的地址
          • 1.程序funds2.c
        • 3.传递结构
          • 1.程序funds3.c
        • 4.其他结构特性
          • 1.程序names1.c
          • 2.程序names2.c
        • 5.结构和结构指针的选择
        • 6.结构中的字符数组和字符指针
        • 7.结构、指针和malloc()
          • 1.程序names3.c
        • 8.复合字面量和结构(C99)
          • 1.程序complit.c
        • 9.伸缩型数组成员(C99)
          • 1.程序flexmemb.c
        • 10.匿名结构(C11)
        • 11.使用结构数组的函数
          • 1.程序funds4.c
      • 8.把结构内容保存到文件中
        • 1.保存结构的程序示例
          • 1.程序booksave.c
        • 2.程序要点
      • 9.链式结构
      • 10.联合简介
        • 1.使用联合
        • 2.匿名联合(C11)
        • 3.总结:结构和联合运算符
          • 1.**成员运算符: .**
          • 2.间接成员运算符: ->
      • 11.枚举类型
        • 1.enum常量
        • 2.默认值
        • 3.赋值
        • 4.enum的用法
        • 5.程序enum.c
        • 6.共享名称空间
      • 12.typedef简介
      • 13.其他复杂的声明
      • 14.函数和指针
        • 1.程序func_ptr.c
      • 15.关键概念
      • 16.本章小结
      • 17.编程练习
        • 1.exercise1.c
        • 2.exercise2.c
        • 3.exercise3.c
        • 4.exercise4.c
        • 5.exercise5.c
        • 6.exercise6.c
        • 7.exercise7.c
        • 8.exercise8.c
        • 9.exercise9.c
        • 10.exercise10.c
        • 11.exercise11.c
        • 12.问题总结
          • 1.输入验证问题
          • 2.文件问题
          • 3.指针问题

《C Primer Plus》学习笔记

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

1.示例问题:创建图书目录

要打印一份图书目录,打印每本书的各种信息:书名、作者、出版社、版权日期、页数、册数和价格。其中的一些项目(如,书名)可以储存在字符数组中,其他项目需要一个int数组或float数组。用7个不同的数组分别记录每一项比较繁琐,尤其是想创建多份列表:一份按书名排序、一份按作者排序、一份按价格排序等。如果能把图书目录的信息都包含在一个数组里更好,其中每个元素包含一本书的相关信息。因此,需要一种即能包含字符串又能包含数字的数据形式,而且还要保持各信息的独立。C结构就满足这种情况下的需求。通过一个示例演示如何创建和使用数组。但是,示例进行了一些限制。第一,该程序示例演示的书目只包含书名、作者和价格。第二,只有一本书的数目。

1.程序book.c

//* book.c -- 一本书的图书目录 */
#include 
#include 
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)                  
            *find = '\0';          
        else
            while (getchar() != '\n')
                continue;          
    }
    return ret_val;
}

输出示例:

Please enter the book title.
C Primer Plus
Now enter the author.
Stephen
Now enter the value.
108.0
C Primer Plus by Stephen: $108.00
Stephen: "C Primer Plus" ($108.00)
Done.

程序book.c中创建的结构有3部分,每个部分都称为成员(member) 或字段(field)。这3部分中,一部分储存书名,一部分储存作者名,一部分储存价格。下面是必须掌握的3个技巧:
1.为结构建立一个格式或样式;
2.声明一个适合该样式的变量;
3.访问结构变量的各个部分。

2.建立结构声明

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

struct book {
    char title[MAXTITL]; 
    char author[MAXAUTL];
    float value;
};

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

struct book library;

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

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

struct book dickens;

这样,该函数便创建了一个结构变量dickens,该变量的结构布局是book。
结构的标记名是可选的。但是以程序示例中的方式建立结构时(在一处定义结构布局,在另一处定义实际的结构变量),必须使用标记。

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;

《C Primer Plus》学习笔记—第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。
这是定义结构变量的一个方面,在这个例子中,并未初始化结构变量。

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

2.访问结构成员

结构类似于一个“超级数组”,这个超级数组中,可以是一个元素为char类型,下一个元素为forat类型,下一个元素为int数组。使用结构成员运算符——点(.)访问结构中的成员。例如,library.value即访问library的value部分。可以像使用任何float类型变量那样使用library.value。与此类似,可以像使用字符数组那样使用library.title。因此,程序book.c中的程序中有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个成员。注意,程序book.c中的程序以两种不同的格式打印了library结构变量中的内容。这说明可以自行决定如何使用结构成员

3.结构的初始化

C99和C11为结构提供了指定初始化器(designated initialicer),也被称为标记化结构初始化语法,其语法与数组的指定初始化器类似。
但是,结构的指定初始化器使用点运算符和成员名(而不是方括号和下标)标识特定的元素。例如,只初始化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。

4.结构数组

接下来,要把程序book.c的程序扩展成可以处理多本书。显然,每本书的基本信息都可以用一个book类型的结构变量来表示。为描述两本书,需要使用两个变量,以此类推。可以使用这一类型的结构数组来处理多本书。在下一个程序中(程序manybook.c)就创建了一个这样的数组。

1.结构和内存

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

2.程序manybook.c

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

输出示例:

Please enter the book title.
Press [enter] at the start of a line to stop.
C Primer Plus
Now enter the author.
Stephen
Now enter the value.
108.0
Enter the next title.
matlab
Now enter the author.
Li
Now enter the value.
119
Enter the next title.

Here is the list of your books:
C Primer Plus by Stephen: $108.00
matlab by Li: $119.00

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

3.声明结构数组

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

struct book library[MAXBKS];

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

《C Primer Plus》学习笔记—第14章_第2张图片

4.标识结构数组的成员

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

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[1].title[4]

这是library数组第2个结构变量(library[1]部分)中书名的第5个字符(title[4]部分)。以程序manybook.c的输出为例,这个字符是a。该例指出,点运算符右侧的下标作用于各个成员,点运算符左侧的下标作用与结构数组
最后,总结一下:

library//一个book结构的数组
library[2]//一个数组元素,该元素是book结构
library[2].title//一个char数组(library[2]的title成员)
library[2].tit1e[4]//数组中library[2]元素的title成员的一个字符

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

5.程序讨论

较之程序book.c,该程序主要的改动之处是:插入一个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()就可以重新开始下一次输入。

5.嵌套结构

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

1.程序friend.c

// friend.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",
    "over a delicious ",
    " 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
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成员。

6.指向结构的指针

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

1.程序friends.c

/* friends.c -- 使用指向结构的指针 */
#include 
#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: 0240FE60 #2: 0240FEB4
pointer #1: 0240FE60 #2: 0240FEB4
him->income is $68112.00: (*him).income is $68112.00
him->favfood is tripe:  him->handle.last is Swillbelly

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

2.声明和初始化结构指针

声明结构指针很简单:

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。在十六进制中,B4 - 60 = 54(十六进制) = 84(十进制),因为每个guy结构都占用84字节的内存: names.first占用20字节,names.last占用20字节,favfood占用20字节,job占用20字节,income占用4字节(假设系统中float占用4字节)。
顺带一提,在有些系统中,一个结构的大小可能大于它各成员大小之和。这是因为系统对数据进行校准的过程中产生了一些“缝隙”。例如,有些系统必须把每个成员都放在偶数地址上,或4的倍数的地址上。在这种系统中,结构的内部就存在未使用的“缝隙”。

3.用指针访问成员

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

第1种方法也是最常用的方法:使用->运算符。该运算符由一个连接号(-)后跟一个大于号(>)组成。有下面的关系:
如果him == &barney,那么him->income即是barney.income
如果him == &fellow[0], 那么him->income即是fellow[0].income
换句话说,->运算符后面的结构指针和.运算符后面的结构名工作方式相同(不能写成him.income,因为him不是结构名)。
这里要着重理解him是一个指针,但是him->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

7.向函数传递结构的信息

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

1.传递结构成员

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

1.程序funds1.c
/* funds1.c -- 把结构成员作为参数传递 */
#include 
#define FUNDLEN 50

struct funds {
    char   bank[FUNDLEN];
    double bankfund;
    char   save[FUNDLEN];
    double savefund;
};

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种方法是,让被调函数知道自己正在处理一个结构。

2.传递结构的地址

这次把结构的地址作为参数。由于函数要处理funds结构,所以必须声明funds结构。如程序funds2.c所示。

1.程序funds2.c
/* funds2.c -- 传递指向结构的指针 */
#include 
#define FUNDLEN 50

struct funds {
    char   bank[FUNDLEN];
    double bankfund;
    char   save[FUNDLEN];
    double savefund;
};

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

3.传递结构

对于允许把结构作为参数的编译器,可以把程序funds2.c重写为程序funds3.c。

1.程序funds3.c
/* funds3.c -- 传递一个结构 */
#include 
#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.

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

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),函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。把结构作为函数参数可以把结构的信息传送给函数;把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。结构指针也允许这种双向通信

通过另一组程序示例来演示这两种方法。为了对比这两种方法,先编写一个程序以传递指针的方式处理结构,然后以传递结构和返回结构的方式重写该程序。

1.程序names1.c
/* names1.c -- 使用指向结构的指针 */
#include 
#include 

#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)                  
            *find = '\0';          
        else
            while (getchar() != '\n')
                continue;         
    }
    return ret_val;
}

输出示例:

Please enter your first name.
Stephen
Please enter your last name.
Prata
Stephen Prata, your name contains 12 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()。程序names2.c演示了不使用指针的版本。

2.程序names2.c
/* names2.c -- 传递并返回结构 */
#include 
#include 

#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)                  
            *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类型,所以该函数要返回一个结构。

5.结构和结构指针的选择

指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这种方法,而且执行起来很快,只需要传递一个地址。缺点是无法保护数据。被调函数中的某些操作可能会意外影响原来结构中的数据。
不过,ANSI C新增的const限定符解决了这个问题。例如,如果在程序names1.c中,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限定符。按值传递结构是处理小型结构最常用的方法。

6.结构中的字符数组和字符指针

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

#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("is 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.1ast);
puts("Enter the last name of your attorney:");
scanf ("%s", attorney.1ast);/*这里有一个潜在的危险*/

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

7.结构、指针和malloc()

如果使用malloc()分配内存并使用指针储存该地址,那么在结构中使用指针处理字符串就比较合理。这种方法的优点是,可以请求malloc()为字符串分配合适的存储空间。可以要求用4字节储存"Joe"和用18字节储存"Rasolofomasoandro"。 用这种方法改写程序names2.c并不费劲。主要是更改结构声明(用指针代替数组)和提供一个新版本的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(),用于释放程序动态分配的内存。如程序names3.c所示。

1.程序names3.c
// names3.c -- 使用指针和malloc()
#include 
#include    // 提供strcpy(), strlen()原型 
#include    // 提供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)                  
            *find = '\0';          
        else
            while (getchar() != '\n')
                continue;          
    }
    return ret_val;
}

输出示例:

Please enter your first name.
Stephen
Please enter your last name.
Prata
Stephen Prata, your name contains 12 letters.

8.复合字面量和结构(C99)

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

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

程序complit.c中的程序示例,使用复合字面量为一个结构变量提供两个可替换的值。

1.程序complit.c
/* complit.c -- compound literals复合字面量 */
#include 
#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;
}

输出示例:

Enter test score: 99
Your assigned reading:
Crime and Punishment by Fyodor Dostoyevsky: $11.25

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

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.520.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.520.0});

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

9.伸缩型数组成员(C99)

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

首先,声明一个伸缩型数组成员有如下规则:
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; // 访问数组成员的一个元素

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

1.程序flexmemb.c
// flexmemb.c -- 伸缩性数组成员(C99新特性)
#include 
#include 

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

C-Free不支持此特性,在支持C99的编译器中,输出如下:

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标准。这种伸缩型数组成员方法是标准认可的编程技巧。

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.nane.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的成员,删除嵌套循环。匿名特性在嵌套联合中更加有用,我们在本章后面介绍。

11.使用结构数组的函数

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

1.程序funds4.c
/* funds4.c -- 把结构数组传递给函数 */
#include 
#define FUNDLEN 50
#define N 2

struct funds {
    char   bank[FUNDLEN];
    double bankfund;
    char   save[FUNDLEN];
    double savefund;
};

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.可以把数组名作为数组中第1个结构的地址传递给函数
2.然后可以用数组表示法访问数组中的其他结构。注意下面的函数调用与使用数组名效果相同:

sum(&jones[0], N)

因为jones和&jones[0]的地址相同,使用数组名是传递结构地址的一种间接的方法。
3.由于sum()函数不能改变原始数据,所以该函数使用了ANSI C的限定符const.

8.把结构内容保存到文件中

由于结构可以储存不同类型的信息,所以它是构建数据库的重要工具。数据库文件可以包含任意数量的此类数据对象。储存在一个结构中的整套信息被称为记录(record),单独的项被称为字段(field)。本节来探讨这个主题。

或许储存记录最没效率的方法是用fprintf()。例如,回忆程序book.c中的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指向的位置。简而言之,这两个函数一次读写整个记录,而不是一个字段。

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

1.保存结构的程序示例

为了演示如何在程序中使用这些函数,把程序manybook.c修改为一个新的版本(即程序booksave.c),把书名保存在book.dat文件中。如果该文件已存在,程序将显示它当前的内容,然后允许在文件中添加内容。

1.程序booksave.c
/* booksave.c -- 在文件中保存结构的内容 */
#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)                  
            *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.
C Primer plus
Now enter the author.
Stephen
Now enter the value.
109
Enter the next title.
matlab
Now enter the author.
Li
Now enter the value.
118
Enter the next title.

Here is the list of your books:
C Primer plus by Stephen: $109.00
matlab by Li: $118.00
Bye.

第二次运行,输出示例:

Current contents of book.dat:
C Primer plus by Stephen: $109.00
matlab by Li: $118.00
Please add new book titles.
Press [enter] at the start of a line to stop.
softtest
Now enter the author.
zhou
Now enter the value.
35
Enter the next title.

Here is the list of your books:
C Primer plus by Stephen: $109.00
matlab by Li: $118.00
softtest by zhou: $35.00
Bye.

2.程序要点

首先,以"a+b"模式打开文件。a+部分允许程序读取整个文件并在文件的末尾添加内容。

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

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

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

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

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

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

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

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

9.链式结构

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

《C Primer Plus》学习笔记—第14章_第3张图片

考虑一个有10级节点的树的情况。它有2ⁿ-1(此处n为10,共1023)个节点,可以储存1023个单词。如果这些单词以某种规则排列,那么可以从最顶层开始,逐级向下移动查找单词,最多只需移动9次便可找到任意单词。如果把这些单词都放在一个数组中,最多要查找1023个元素才能找出所需的单词。
本章对结构的概念介绍至此为止,下面,介绍C语言中的联合、枚举和typedef。

10.联合简介

联合(union)是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。其典型的用法是,设计一种表以储存既无规律、事先也不知道顺序的混合类型。使用联合类型的数组,其中的联合都大小相等,每个联合可以储存各种数据类型。
创建联合和创建结构的方式相同,需要一个联合模板和联合变量。可以用一个步骤定义联合,也可以用联合标记分两步定义。下面是一个带标记的联合模板:

union hold {
    int digit;
    double bigf1;
    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 va1B = valA;//用另一个联合来初始化
union hold valC = {88};//初始化联合的digit成员
union hold valD = {.bigfl = 118.2}; //指定初始化器

1.使用联合

下面是联合的一些用法:

fit.digit = 23;//把23储存在fit,占2字节
fit.bigfl = 2.0;//清除23,储存2.0,占8字节
fit.1etter = 'h';//清除2.0,储存h,占1字节

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

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

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

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

fit.letter = 'A';
flnum = 3.02*fit.bigf1; //错误

以上语句序列是错误的,因为储存在fit中的是char类型,但是下一行却假定fit中的内容是double类型。
不过,用一个成员把值储存在一个联合中,然后用另一个成员查看内容,这种做法有时很有用。下一章的程序dualview.c就给出了一个这样的例子。

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

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。

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.

3.总结:结构和联合运算符

1.成员运算符: .

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

struct {
    int code; 
    float cost; 
} item;
item.code = 1265;
2.间接成员运算符: ->

一般注释:
该运算符和指向结构或联合的指针一起使用,标识结构或联合的一个成员。假设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

11.枚举类型

可以用枚举类型(emumerated 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变量。

1.enum常量

blue和red是int类型的常量。例如,假定有前面的枚举声明,可以这样写:

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

其输出如下:
red=0,orange=1
red成为一个有名称的常量,代表整数0。类似地,其他标识符都是有名称的常量,分别代表1~5。

只要是能使用整型常量的地方就可以使用枚举常量。例如,在声明数组时,可以用枚举常量表示数组的大小;在switch语句中,可以把枚举常量作为标签。

2.默认值

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

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

3.赋值

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

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

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

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

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

4.enum的用法

枚举类型的目的是为了提高程序的可读性和可维护性。如果要处理颜色,使用red和blue比使用0和1更直观。注意,枚举类型只能在内部使用。如果要输入color中orange的值,只能输入1,而不是单词orange。或者,让程序先读入字符串"orange",再将其转换为orange代表的值。
因为枚举类型是整数类型,所以可以在表达式中以使用整数变量的方式使用enum变量。它们用在case语句中很方便。

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

5.程序enum.c

/* enum.c -- 使用枚举类型的值 */
#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 color, 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)                  
            *find = '\0';          
        else
            while (getchar() != '\n')
                continue;          
    }
    return ret_val;
}

输出示例:

Enter a color (empty line to quit):
orange
Poppies are orange.
Next color, please (empty line to quit):
blue
Bluebells are blue.
Next color, please (empty line to quit):

Goodbye!

6.共享名称空间

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

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

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

12.typedef简介

typedef工具是一个高级数据特性,利用typedef可以为某一类型自定义名称。这方面与#define类似,但是两者有3处不同:
1.与#define不同,typedef创建的符号名只受限于类型,不能用于值。
2.typedef由编译器解释,不是预处理器。
3.在其受限范围内, 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()返回整数类型,但是让实现来决定具体是什么整数类型。标准委员决定建立一个新的类型名(如,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替换uns igned char。但是也有#define没有的功能:

typedef char * STRING;

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

STRING name, sign;

相当于:

char * name, * sign;

但是,如果这样假设:

#define STRING char *

然后,下面的声明:

STRING name, sign;

将被翻译成:

char * name,sign;

这导致只有name才是指针。

还可以把typedef用于结构:

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

然后便可使用COMPLEX类型代替complex结构来表示复数。使用typedef的第1个原因是:为经常出现的类型创建一个方便、易识别的类型名。
用typedef来命名一个结构类型时,可以省略该结构的标签:

typedef struct {double x; double y;} rect;

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

rect r1 = {3.06.0};
rect r2;

以上代码将被翻译成:

struct {double x; double y; } r1= {3.06.0};
struct {double x; double y; } r2;

这两个结构在声明时都没有标记,它们的成员完全相同(成员名及其类型都匹配),C认为这两个结构的类型相同,所以r1和r2间的赋值是有效操作。
使用typedef的第2个原因是: typedef常用于给复杂的类型命名。例如,下面的声明:

typedef char (* FRPTC ()) [5];

把FRPTC声明为一个函数类型,该函数返回一个指针,该指针指向内含5个char类型元素的数组(参见下一节的讨论)。
使用typedef时要记住,typedef并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签。以前面的STRING为例,这意味着我们创建的STRING类型变量可以作为实参传递给以指向char指针作为形参的函数。

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

13.其他复杂的声明

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

《C Primer Plus》学习笔记—第14章_第4张图片

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

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

要看懂以上声明,关键要理解* 、()和[]的优先级。记住下面几条规则。
1.数组名后面的[]和函数名后面的()具有相同的优先级。它们比*(解引用运算符)的优先级高。因此下面声明的risk是一个指针数组,不是指向数组的指针:

int * risks[10];

2.[]和()的优先级相同,由于都是从左往右结合,所以下面的声明中,在应用方括号之前,*先与rusks结合。因此rusks是一个指向数组的指针,该数组内含10个int类型的元素:

int (* rusks) [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是一个3X4的二维数组,每个元素都是指向int的指针。编译器要为12个指针预留存储空间。

现在来看下面的声明:

int (* uuf)[3][4];

圆括号使得*先与uuf结合,说明uuf是一个指针,所以uuf是一个指向3X4的int类型二维数组的指针。编译器要为一个指针预留存储空间。根据这些规则,还可以声明:

char * fump(int); //返回字符指针的函数
char (* frump)(int) ;//指向函数的指针,该函数的返回类型为char
char (* flump[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.函数和指针

通常,函数指针常用作另一个函数的参数,告诉该函数要使用哪一个函数。 例如,排序数组涉及比较两个元素,以确定先后。如果元素是数字,可以使用>运算符;如果元素是字符串或结构,就要调用函数进行比较。C库中的qsort()函数可以处理任意类型的数组,但是要告诉qsort()使用哪个函数来比较元素。为此,qsort()函数参数列表中,有一个参数接受指向函数的指针。然后,qsort()函数使用该函数提供的方案进行排序,无论这个数组中的元素是整数、字符串还是结构。

什么是函数指针?假设有一个指向int类型变量的指针,该指针储存着这个int类型变量储存在内存位置的地址。同样,函数也有地址,因为函数的机器语言实现由载入内存的代码组成。指向函数的指针中储存着函数代码的起始处的地址
其次,声明一个数据指针时,必须声明指针所指向的数据类型。声明一个函数指针时,必须声明指针指向的函数类型。为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型。例如,考虑下面的函数原型:

void ToUpper(char *); //把字符串中的字符转换成大写字符

ToUpper()函数的类型是“带char *类型参数、返回类型是void的函数”。下面声明了一个指针pf指向该函数类型:

void (*pf)(char *);//pf是一个指向函数的指针

从该声明可以看出,第1对圆括号把* 和pf括起来,表明pf是一个指向函数的指针。因此,(pf)是一个参数列表为(char * )、返回类型为void的函数。注意,把*函数名ToUpper替换为表达式(pf)是创建指向函数指针最简单的方式。所以,如果想声明一个指向某类型函数的指针,可以写出该函数的原型后把函数名替换成( pf)形式的表达式,创建函数指针声明。前面提到过,由于运算符优先级的规则,在声明函数指针时必须把*和指针名括起来。如果省略第1个圆括号会导致完全不同的情况:

void *pf(char *); //pf是一个返回字符指针的函数

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

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

最后一条语句是无效的,不仅因为ToLower()不是地址,而且ToLower()的返回类型是void,它没有返回值,不能在赋值语句中进行赋值。注意,指针pf可以指向其他带char *类型参数、返回类型是void的函数,不能指向其他类型的函数。
既然可以用数据指针访问数据,也可以用函数指针访问函数。奇怪的是,有两种逻辑上不一致的语法可以这样做,下面解释:

void ToUpper(char *);
void ToLower(char *);
void (*pf)(char *);
char mis[] = "Nina Metier";
pf = ToUpper;
(*pf)(mis); //把ToUpper作用于(语法1)
pf = ToLower;
pf(mis);//把ToLower作用于(语法2)

先分析第1种方法:由于pf指向ToUpper函数,那么* pf就相当于ToUpper函数,所以表达式(* pf)(mis)和ToUpper(mis)相同。从ToUpper函数和pf的声明就能看出,ToUpper和(* pf)是等价的。

第2种方法:由于函数名是指针,那么指针和函数名可以互换使用,所以pf(mis)和ToUpper(mis)相同。从pf的赋值表达式语句就能看出ToUpper和pf是等价的。

由于历史的原因,贝尔实验室的C和UNIX的开发者采用第1种形式,而伯克利的UNIX推广者却采用第2种形式。K&R C不允许第2种形式。但是,为了与现有代码兼容,ANSI C认为这两种形式(本例中是(* pf)(mis)和pf(mis))等价。后续的标准也延续了这种矛盾的和谐。
作为函数的参数是数据指针最常见的用法之一,函数指针亦如此。例如,考虑下面的函数原型:

void show(void (* fp)(char *), char * str);

它声明了两个形参: fp和str。fp形参是一个函数指针,str是一个数据指针。更具体地说,fp指向的函数接受char *类型的参数,其返回类型为void; str 指向一个char类型的值。因此,假设有上面的声明,可以这样调用函数:

show(ToLower, mis);/* show()使用ToLower()函数: fp = ToLower */
show(pf, mis); /* show()使用pf指向的函数: fp = pf */
show()如何使用传入的函数指针?是用fp()语法还是(*fp)()语法调用函数:
void show(void (* fp)(char *), char * str)
{
    (*fp)(str); /* 把所选函数作用于str */
    puts(str);/*显示结果*/
}

例如,这里的show()首先用fp指向的函数转换str,然后显示转换后的字符串。
顺带一提,把带返回值的函数作为参数传递给另一个函数有两种不同的方法。例如,考虑下面的语句:

function1(sqrt);/*传递sqrt()函数的地址*/
function2(sqrt(4.0)); /* 传递sqrt()函数的返回值*/

第1条语句传递的是sqrt()函数的地址,假设function1()在其代码中会使用该函数。第2条语句先调用sqrt()函数,然后求值,并把返回值(该例中是2.0)传递给function2()。
程序func_ptr.c中的程序通过show()函数来演示这些要点,该函数以各种转换函数作为参数。该程序也演示了一些处理菜单的有用技巧。

1.程序func_ptr.c

// func_ptr.c -- 使用函数指针 
#include 
#include 
#include 
#define LEN 81
char * s_gets(char * st, int n);
char showmenu(void);
void eatline(void);     // 读取至行尾 
void show(void (* fp)(char *), char * str);
void ToUpper(char *);   // 把字符串转换为大写 
void ToLower(char *);   // 把字符串转换为小写
void Transpose(char *); // 大小写转置 
void Dummy(char *);     // 不更改字符串 

int main(void)
{
    char line[LEN];
    char copy[LEN];
    char choice;
    void (*pfun)(char *); // 声明一个函数指针,被指向的函数指向接受char *类型的参数,无返回值 

    puts("Enter a string (empty line to quit):");
    while (s_gets(line, LEN) != NULL && line[0] != '\0')
    {
        while ((choice = showmenu()) != 'n')
        {
            switch (choice )  // switch语句设置指针 
            {
                case 'u' : pfun = ToUpper;   break;
                case 'l' : pfun = ToLower;   break;
                case 't' : pfun = Transpose; break;
                case 'o' : pfun = Dummy;     break;
            }
            strcpy(copy, line);// 为show()函数拷贝一份 
            show(pfun, copy);  // 根据用户的选择,使用选定的函数 
        }
        puts("Enter a string (empty line to quit):");
    }
    puts("Bye!");
    
    return 0;
}

char showmenu(void)
{
    char ans;
    puts("Enter menu choice:");
    puts("u) uppercase       l) lowercase");
    puts("t) transposed case o) original case");
    puts("n) next string");
    ans = getchar();    // 获取用户的输入 
    ans = tolower(ans); // 转换为小写 
    eatline();          // 清理输入行 
    while (strchr("ulton", ans) == NULL)
    {
        puts("Please enter a u, l, t, o, or n:");
        ans = tolower(getchar());
        eatline();
    }
    
    return ans;
}

void eatline(void)
{
    while (getchar() != '\n')
        continue;
}

void ToUpper(char * str)
{
    while (*str)
    {
        *str = toupper(*str);
        str++;
    }
}

void ToLower(char * str)
{
    while (*str)
    {
        *str = tolower(*str);
        str++;
    }
}
void Transpose(char * str)
{
    while (*str)
    {
        if (islower(*str))
            *str = toupper(*str);
        else if (isupper(*str))
            *str = tolower(*str);
        str++;
    }
}

void Dummy(char * str)
{
    // 不改变字符串 
}

void show(void (* fp)(char *), char * str)
{
    (*fp)(str); // 把用户选定的函数作用与str 
    puts(str);  // 显示结果
}

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)                  
            *find = '\0';          
        else
            while (getchar() != '\n')
                continue;          
    }
    return ret_val;
}

输出示例:

Enter a string (empty line to quit):
Dose C make you feel happy?
Enter menu choice:
u) uppercase       l) lowercase
t) transposed case o) original case
n) next string
t
dOSE c MAKE YOU FEEL HAPPY?
Enter menu choice:
u) uppercase       l) lowercase
t) transposed case o) original case
n) next string
l
dose c make you feel happy?
Enter menu choice:
u) uppercase       l) lowercase
t) transposed case o) original case
n) next string
n
Enter a string (empty line to quit):

Bye!

注意,ToUpper()、ToLower()、Transpose()和Dummy()函数的类型都相同,所以这4个函数都可以赋给pfun指针。该程序把pfun作为show()的参数,但是也可以直接把这4个函数中的任一个函数名作为参数,如show(Transpose, copy)。
这种情况下,可以使用typedef。例如,该程序中可以这样写:

typedef void (*V_FP_CHARP)(char *);
void show (V_FP_CHARP fp, char *);
V_FP_CHARP pfun;

如果还想更复杂一些,可以声明并初始化一个函数指针的数组:

V_FP_CHARP arpf[4] = {ToUpper, ToLower, Transpose, Dunny};

然后把showmenu ()函数的返回类型改为int,如果用户输入u,则返回0;如果用户输入1,则返回2;如果用户输入t,则返回2,以此类推。可以把程序中的switch语句替换成下面的while循环:

index = showmenu() ;
while (index >= 0 && index <= 3)
{
    strcpy(copy, line);/*为show()拷贝一份*/
    show(arpf[index], copy); /* 使用选定的函数*/
    index = showmenu();
}

虽然没有函数数组,但是可以有函数指针数组。
以上介绍了使用函数名的4种方法:定义函数、声明函数、调用函数和作为指针。图14.4进行了总结。

《C Primer Plus》学习笔记—第14章_第5张图片

至于如何处理菜单,showmenu()函数给出了几种技巧。首先,下面的代码:

ans = getchar();//获取用户输入
ans = tolower(ans); //转换成小写
ans = tolower(getchar());

演示了转换用户输入的两种方法。这两种方法都可以把用户输入的字符转换为一种大小写形式, 这样就不用检测用户输入的是’u’还是’U’,等等。

eatline()函数丢弃输入行中的剩余字符,在处理这两种情况时很有用。第一,用户为了输入一个选择,输入一个字符,然后按下Enter键,将产生一个换行符。如果不处理这个换行符,它将成为下一次读取的第1个字符。第二,假设用户输入的是整个单词uppercase,而不是一个字母u。如果没有eatline()函数,程序会把uppercase中的字符作为用户的响应依次读取。有了eatline(),程序会读取u字符并丟弃输入行中剩余的字符。

其次,showmenu()函数的设计意图是,只给程序返回正确的选项。为完成这项任务,程序使用了string.h头文件中的标准库函数strchr():

while (strchr("ulton", ans) == NULL)

该函数在字符串"ulton"中查找字符ans首次出现的位置,并返回一个指向该字符的指针。如果没有找到该字符,则返回空指针。因此,上面的 while循环头可以用下面的while循环头代替,但是上面的用起来更方便:

while(ans != 'u' && ans != 'l' && ans != 't' && ans != 'o' && ans!='n')

待检查的项越多,使用strchr()就越方便。

15.关键概念

在编程中要表示的信息通常不只是一个数字或一些列数字。程序可能要处理具有多种属性的实体。例如,通过姓名、地址、电话号码和其他信息表示一名客户;或者,通过电影名、发行人、播放时长、售价等表示一部电影DVD。C结构可以把些信息都放在一个单元内。在组织程序时这很重要,因为这样可以把相关的信息都储存在一处,而不是分散储存在多个变量中。

设计结构时,开发一个与之配套的函数包通常很有用。例如,写一个以结构(或结构的地址)为参数的函数打印结构内容,比用一堆printf()语句强得多。因为只需要一个参数就能打印结构中的所有信息。如果把信息放到零散的变量中,每个部分都需要一个参数。另外,如果要在结构中增加一个成员,只需重写函数,不必改写函数调用。这在修改结构时很方便。
联合声明与结构声明类似。但是,联合的成员共享相同的存储空间,而且在联合中同一时间内只能有一个成员。实质上,可以在联合变量中储存一个类型不唯一的值。
enum工具提供一种定义符号常量的方法,typedef工具提供一种为基本或派生类型创建新标识符的方法。
指向函数的指针提供一种告诉函数应使用哪一个函数的方法。

16.本章小结

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

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

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

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

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

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

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

#include  /*提供sin()函数的原型: double sin(double) */
...
double (*pdf) (double) ;
double x;
odf = sin; 
x = (*pdf)(1.2); //调用sin(1.2)
x = pdf(1.2);//同样调用sin(1.2)

17.编程练习

1.exercise1.c

设计一个结构模板存储一个月份名、该月份名的3个字母缩写、该月的天数以及月份号。定义一个数组,内含12个结构并初始化为一个年份(非闰年)。假设在所有函数的外部声明了该结构模板和一个该结构类型的数组。编写一个函数,用户提供月份号,该函数就返回一年中到该月为止(包括该月)的总天数。用月份名的拼写代替月份号(别忘了使用strcmp())。在一个简单的程序中测试该函数。

//exercise14.1
#include 
#include 
#include 

struct month {
    char name[20];
    char abbrev[4];
    int days;
    int monumb;
};//结构模板 
const struct month months[12] = {
    {"January", "Jan", 31, 1},
    {"February", "Feb", 28, 2},
    {"March", "Mar", 31, 3},
    {"April", "Apr", 30, 4},
    {"May", "May", 31, 5},
    {"June", "Jun", 30, 6},
    {"July", "Jul", 31, 7},
    {"August", "Aug", 31, 8},
    {"September", "Sep", 30, 9},
    {"October", "Oct", 31, 10},
    {"November", "Nov", 3, 11},
    {"December", "Dec", 31, 12}
};//结构数组 
int days(char * input);//获取月份名,计算天数 
int main(void)
{
    char input[20];
    int total_days;

    printf("请输入月份名:(按q退出程序)\n");
    while (scanf("%s",input) == 1 && input[0] != 'q')//输入q才停止循环 
    {
        total_days = days(input);//调用函数 
        if (total_days > 0) //总天数大于0,说明有对应月份号 
        {
            printf("总天数是:%d\n",total_days);
        }
        else 
        {
            printf("输入有误。\n");
        }
        printf("请输入下一个月份名:(按q退出程序)\n");
    }
    printf("程序结束\n");

    return 0;
}
int days(char * input)
{
    int i;
    int total = 0;
    int num = 0;

    input[0] = toupper(input[0]);//第一个字母转成大写 
    for (i = 1; input[i] != '\0'; i++) //循环到输入为止,都转为小写字母 
    {
        input[i] = tolower(input[i]);
    }
    for (i = 0; i < 12; i++) 
    {
        if (strcmp(input, months[i].name) == 0) //和结构数组每一个元素的名字比较 
        {
            num = months[i].monumb;//找到对应月份,把月份号赋值给num 
            break;
        }
    }
    if (num == 0) //没有对应月份号 
    {
        total = 0;
    }
    else 
    {
        for (i = 0; i < num; i++) //有对应月份号 
        {
            total += months[i].days;//求和 
        }
    }
    return total;//返回总天数 
}

输入示例:

请输入月份名:(按q退出程序)
febRuary
总天数是:59
请输入下一个月份名:(按q退出程序)
q
程序结束

2.exercise2.c

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

//exercise14.2
#include 
#include 
#include 

struct month {
    char name[20];
    char abbrev[4];
    int days;
    int monumb;
};//结构模板 
struct month months[12] = {
    {"January", "Jan", 31, 1},
    {"February", "Feb", 28, 2},
    {"March", "Mar", 31, 3},
    {"April", "Apr", 30, 4},
    {"May", "May", 31, 5},
    {"June", "Jun", 30, 6},
    {"July", "Jul", 31, 7},
    {"August", "Aug", 31, 8},
    {"September", "Sep", 30, 9},
    {"October", "Oct", 31, 10},
    {"November", "Nov", 3, 11},
    {"December", "Dec", 31, 12}
};//结构数组 
int days(int day, char * input);//获取月份名,计算天数
void leapyear(int year); 
int main(void)
{
	int day, year;
    char input[20];
    int total_days;

    printf("请输入日,月份名,年:(按q退出程序)\n");
    while (scanf("%d %s %d", &day, input, &year) == 3 && input[0] != 'q') 
    {
    	leapyear(year);
        total_days = days(day, input);//调用函数 
        if (total_days > 0) //总天数大于0,说明有对应月份号 
        {
            printf("总天数是:%d\n",total_days);
        }
        else 
        {
            printf("输入有误。\n");
        }
        months[1].days = 28;//将2月份重置为28天 
        printf("请输入下一个日,月份名,年:(按q退出程序)\n");
    }
    printf("程序结束\n");

    return 0;
}
int days(int day, char * input)
{
    int i;
    int total = 0;
    int num = 0;

    input[0] = toupper(input[0]);//第一个字母转成大写 
    for (i = 1; input[i] != '\0'; i++) //循环到输入为止,都转为小写字母 
    {
        input[i] = tolower(input[i]);
    }
    for (i = 0; i < 12; i++) 
    {
        if (strcmp(input, months[i].name) == 0) //和结构数组每一个元素的名字比较 
        {
            num = months[i].monumb;//找到对应月份,把月份号赋值给num 
            break;
        }
    }
    if (num == 0) //没有对应月份号 
    {
        total = 0;
    }
    else 
    {
        for (i = 0; i < num; i++) //有对应月份号 
        {
            total += months[i].days;//求和 
        }
        total += day;//再加上日期数 
    }
    return total;//返回总天数 
}
void leapyear(int year)
{
	if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)//判断是否为闰年 
	{
		months[1].days = 29;//闰年2月为29天 
	}
}

输出示例:

请输入日,月份名,年:(按q退出程序)
1 march 2020
总天数是:92
请输入下一个日,月份名,年:(按q退出程序)
1 march 2019
总天数是:91
请输入下一个日,月份名,年:(按q退出程序)
q
程序结束

没有做过多的输入验证,默认是按照正常输入。

3.exercise3.c

修改程序manybook.c中的图书目录程序,使其按照输入图书的顺序输出图书的信息,然后按照书名的字母顺序输出图书的信息,最后按照价格的升序输出图书的信息。

//exercise14.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;
};
void print(struct book library[MAXBKS], int count);//原样打印 
void print_al(struct book library[MAXBKS], int count);//按字母顺序打印 
void print_price(struct book library[MAXBKS], int count);//按价格升序打印 
int main(void)
{
    struct book library[MAXBKS]; /* book类型的结构数组 */
    int count = 0;
    
    printf("请输入书名:\n");
    printf("在开始处按enter键结束程序。\n");
    while (count < MAXBKS && s_gets(library[count].title, MAXTITL) != NULL
           && library[count].title[0] != '\0')
    {
        printf("输入作者名:\n");
        s_gets(library[count].author, MAXAUTL);
        printf("输入书的价格\n");
        scanf("%f", &library[count++].value);
        while (getchar() != '\n')
            continue;          /* 清理输入行  */
        if (count < MAXBKS)
            printf("输入下一本书的名字:\n");
    }
    
    if (count > 0)
    {
    	printf("这是你的书的顺序:\n");
        print(library, count);
        print_al(library, count);
        print_price(library, count);
    }
    else
    	printf("没有书。\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;
}
void print(struct book library[MAXBKS], int count)
{
	int index;
    
	for (index = 0; index < count; index++)
	{
		printf("%s by %s: $%.2f\n", library[index].title,
                   library[index].author, library[index].value);
	}        
}
void print_al(struct book library[MAXBKS], int count)
{
	struct book temp;//标记 
	int index;
    int i;
    
    printf("书按字母顺序输出:\n");
	for (index = 0; index < count - 1; index++)//冒泡排序 
    {
        for (i = index + 1; i < count; i++)
        {
        	if (strcmp(library[index].title, library[i].title) > 0)
				//后面的书的字母排在前面的书的前面 
			{
				temp = library[index];//交换 
				library[index] = library[i];
				library[i] = temp;
			}
		}
	}
	print(library, count);//打印排序后的 
}
void print_price(struct book library[MAXBKS], int count)
{
	struct book temp;
	int index;
    int i;
    
    printf("书按价格顺序输出:\n");
	for (index = 0; index < count - 1; index++)
    {
        for (i = index + 1; i < count; i++)
        {
        	if (library[index].value > library[i].value)
			//前面的书价格高于后面的书 
			{
				temp = library[index];
				library[index] = library[i];
				library[i] = temp;
			}
		}
	}
	print(library, count);
}

输出示例:

请输入书名:
在开始处按enter键结束程序。
c study
输入作者名:
sttphen
输入书的价格
56
输入下一本书的名字:
python
输入作者名:
peter
输入书的价格
23
输入下一本书的名字:
java
输入作者名:
lily
输入书的价格
35
输入下一本书的名字:

这是你的书的顺序:
c study by sttphen: $56.00
python by peter: $23.00
java by lily: $35.00
书按字母顺序输出:
c study by sttphen: $56.00
java by lily: $35.00
python by peter: $23.00
书按价格顺序输出:
python by peter: $23.00
java by lily: $35.00
c study by sttphen: $56.00

4.exercise4.c

编写一个程序,创建一个有两个成员的结构模板:
a. 第1个成员是社会保险号,第2个成员是一个有3个成员的结构,第1个成员代表名,第2个成员代表中间名,第3个成员表示姓。创建并初始化一个内含5个该类型结构的数组。该程序以下面的格式打印数据:
Dribble, Flossie M. – 302039823
如果有中间名,只打印它的第1个字母,后面加一个点(.); 如果没有中间名,则不用打印点。编写一个程序进行打印,把结构数组传递给这个函数。
b. 修改a部分,传递结构的值而不是结构的地址。

//exercise14.4a
#include 
#include  
#define LEN 20

struct names {
	char fname[LEN];
	char mname[LEN];
	char lname[LEN];
};

struct message {
	char sonum[LEN];
	struct names name; 
};
char * s_gets(char * st, int n); 
void print(const struct message num[], int n);
int main(void)
{
	struct message num[5];//结构数组 
	int count = 0;
	
	printf("请输入保险号:\n");
    printf("在开始处按enter键结束程序。\n");
    while (count < 5 && s_gets(num[count].sonum, LEN) && num[count].sonum[0] != '\0')
    {//输入不超过5个,或者直接输入enter退出 
    	printf("请输入名:\n");
    	s_gets(num[count].name.fname, LEN);
    	printf("请输入中间名:\n");
    	s_gets(num[count].name.mname, LEN);
    	printf("请输入姓:\n");
    	s_gets(num[count].name.lname, LEN);
    	if (count < 5)//输入不超过5个,继续输入 
    	{	
    		printf("请输入下一个保险号:\n");
		}
		count++;
	}
	if (count > 0)//输入了成员 
	{
		printf("以下是所有成员:\n");
		print(num, count);	
	}
	else//没输入成员 
	{
		printf("没有成员。\n");
	}
	printf("程序结束。\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;
}
void print(const struct message num[], int n)
{
	int i;
	
	for (i = 0; i < n; i++)
	{
		if (num[i].name.mname[0] == '\0')//中间名是空 
		{
			printf("%s,%s . -- %s\n", num[i].name.fname, num[i].name.lname, num[i].sonum);
		}		 
		else
		{
			printf("%s,%s %c. -- %s\n", num[i].name.fname, num[i].name.lname, num[i].name.mname[0], num[i].sonum); 
		}
	}
}

输出示例:

请输入保险号:
在开始处按enter键结束程序。
302039823
请输入名:
Dribble
请输入中间名:
Mit
请输入姓:
Flossie
请输入下一个保险号:
845145656
请输入名:
san
请输入中间名:

请输入姓:
zhang
请输入下一个保险号:
123456789
请输入名:
er
请输入中间名:
xiao
请输入姓:
li
请输入下一个保险号:
963852741
请输入名:
wu
请输入中间名:
xiao
请输入姓:
wang
请输入下一个保险号:
159263487
请输入名:
qi
请输入中间名:

请输入姓:
tian
请输入下一个保险号:
以下是所有成员:
Dribble,Flossie M. -- 302039823
san,zhang . -- 845145656
er,li x. -- 123456789
wu,wang x. -- 963852741
qi,tian . -- 159263487
程序结束。
//exercise14.4b
#include 
#include  
#define LEN 20

struct names {
	char fname[LEN];
	char mname[LEN];
	char lname[LEN];
};

struct message {
	char sonum[LEN];
	struct names name; 
};
char * s_gets(char * st, int n); 
void print(const struct message nu);//传递结构 
int main(void)
{
	struct message num[5];//结构数组 
	int count = 0;
	int i; 
	
	printf("请输入保险号:\n");
    printf("在开始处按enter键结束程序。\n");
    while (count < 5 && s_gets(num[count].sonum, LEN) && num[count].sonum[0] != '\0')
    {//输入不超过5个,或者直接输入enter退出 
    	printf("请输入名:\n");
    	s_gets(num[count].name.fname, LEN);
    	printf("请输入中间名:\n");
    	s_gets(num[count].name.mname, LEN);
    	printf("请输入姓:\n");
    	s_gets(num[count].name.lname, LEN);
    	if (count < 5)//输入不超过5个,继续输入 
    	{	
    		printf("请输入下一个保险号:\n");
		}
		count++;
	}
	if (count > 0)//输入了成员 
	{
		printf("以下是所有成员:\n");
		for (i = 0; i < count; i++)
		{
			print(num[i]);//单独打印每个成员 
		}	
	}
	else//没输入成员 
	{
		printf("没有成员。\n");
	}
	printf("程序结束。\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;
}
void print(const struct message nu)
{
	if (nu.name.mname[0] == '\0')//中间名是空 
	{
		printf("%s,%s . -- %s\n", nu.name.fname, nu.name.lname, nu.sonum);
	}		 
	else
	{
		printf("%s,%s %c. -- %s\n", nu.name.fname, nu.name.lname, nu.name.mname[0], nu.sonum); 
	}
}

5.exercise5.c

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

//exercise14.5
#include 
#define CSIZE 4
#define LEN 20 

struct name {
	char surname[LEN];
	char name[LEN];	
};

struct student {
	struct name na;
	float grade[3];
	float average; 
};

void get_grade(struct student stu[], int n);//获取每个学生的分数 
void get_average(struct student stu[], int n);//获取每个学生的平均分数 
void print_student(struct student stu[], int n);//打印每个学生的信息 
void print_classarg(struct student stu[], int n);//计算班级平均分并打印 
int main(void)
{
	struct student stu[CSIZE] = 
	{
		{"zhang", "san"}, 
		{"li", "si"},
		{"wang", "wu"},
		{"tian", "qi"}
	
	};//初始化结构数组 
	
	get_grade(stu, CSIZE);
	get_average(stu, CSIZE);
	print_student(stu, CSIZE);
	print_classarg(stu, CSIZE);
	
	return 0;
}
void get_grade(struct student stu[], int n)
{
	int i, j;
	
	for (i = 0; i < n; i++)
	{
		printf("请输入%s %s的三个分数:\n", stu[i].na.surname, stu[i].na.name);
		for (j = 0; j < 3; j++)
		{
			while (scanf("%f", &stu[i].grade[j]) != 1)
			{
				while (getchar() != '\n')
				{
					continue;
				}
				printf("输入下一个正确的分数:\n");
			}
		}
	}
}
void get_average(struct student stu[], int n)
{
	int i;
	
	for (i = 0; i < n; i++)
	{
		stu[i].average = (stu[i].grade[0] + stu[i].grade[1] + stu[i].grade[2]) / 3;
	}
}
void print_student(struct student stu[], int n)
{
	int i;
	
	printf("以下是所有学生的信息:\n");
	for (i = 0; i < n; i++)
	{
		printf("%s %s的三个分数:%.1f %.1f %.1f,平均分:%.1f\n", stu[i].na.surname, stu[i].na.name
		, stu[i].grade[0], stu[i].grade[1], stu[i].grade[2], stu[i].average);
	}
}
void print_classarg(struct student stu[], int n)
{
	int i;
	float total = 0.0; 
	float class_average;
	printf("班级的平均分数:");
	for (i = 0; i < n; i++)
	{
		total += stu[i].average;
	}
	class_average = total / n;
	printf("%.2f。\n", class_average);
}

输出示例:

请输入zhang san的三个分数:
56 23 25
请输入li si的三个分数:
23.5 62.5 23
请输入wang wu的三个分数:
98 85 69
请输入tian qi的三个分数:
98 97 96
以下是所有学生的信息:
zhang san的三个分数:56.0 23.0 25.0,平均分:34.7
li si的三个分数:23.5 62.5 23.0,平均分:36.3
wang wu的三个分数:98.0 85.0 69.0,平均分:84.0
tian qi的三个分数:98.0 97.0 96.0,平均分:97.0
班级的平均分数:63.00。

6.exercise6.c

一个文本文件中保存着一个垒球队的信息。每行数据都是这样排列:
4 Jessie Joybat 5 2 1 1
第1项是球员号,为方便起见,其范围是0~18。第2项是球员的名。第3项是球员的姓。名和姓都是一个单词。第4项是官方统计的球员上场次数。接着3项分别是击中数、走垒数和打点(RBI)。
文件可能包含多场比赛的数据,所以同一位球员可能有多行数据,而且同一位球员的多行数据之间可能有其他球员的数据。编写一个程序,把数据储存到一个结构数组中。该结构中的成员要分别表示球员的名、姓、上场次数、击中数、走垒数、打点和安打率(稍后计算)。可以使用球员号作为数组的索引。该程序要读到文件结尾,并统计每位球员的各项累计总和。
世界棒球统计与之相关。例如,一次走垒和触垒中的失误不计入上场次数,但是可能产生一个RBI。但是该程序要做的是像下面描述的一样读取和处理数据文件,不会关心数据的实际含义。
要实现这些功能,最简单的方法是把结构的内容都初始化为零,把文件中的数据读入临时变量中, 然后将其加入相应的结构中。程序读完文件后,应计算每位球员的安打率,并把计算结果储存到结构的相应成员中。计算安打率是用球员的累计击中数除以上场累计次数。这是一个浮点数计算。最后,程序结合整个球队的统计数据,一行显示一位球员的累计数据。

//exercise14.6
#include 
#include 
#include 
#define LEN 19
typedef struct {
	int id;
	char name[LEN];
	char surname[LEN];
	int time;
	int beat;
	int walk;
	int RBI;
	float rate;
} TEAM; //定义结构 
static TEAM players[LEN];//创建球员数组,最多不超过19个人 

int read_data(FILE * fp, TEAM players[], int n);//从文件中获取球员信息,并返回球员个数 
int get_rate(TEAM players[], int n);//获取每个球员的安打率 
void print(TEAM players[], int n);//打印球员信息
void print_pre(FILE * fp);//打印文件信息 
int main(void)
{
	FILE * fp;
	int line;
	
	if ((fp = fopen("exercise6.txt", "r")) == NULL)//打开文件失败就报错,读模式 
	{
		fprintf(stderr,"打开文件失败。\n");
		exit(EXIT_FAILURE); 
	}
	print_pre(fp);
	rewind(fp);//定位到文件开始处 
	line = read_data(fp, players, LEN);//调用函数执行相对应功能 
	get_rate(players, line);
	print(players, line);	
	if (fclose(fp) != 0)//关闭文件错误时则报错 
    {
        fprintf(stderr, "关闭exercise6.txt失败。\n");
    }
	return 0;
}
void print_pre(FILE * fp)
{
	int get_id, beat, time, walk, RBI;//获取每行对应的信息 
	char name[LEN];
	char surname[LEN];
	
	printf("以下是文件的数据:\n");
	while (fscanf(fp, "%d %18s %18s %d %d %d %d", &get_id, name, surname, &time, &beat, &walk, &RBI) == 7
		&& !feof(fp))
	{
		printf("%d %s %s %d %d %d %d\n", get_id, name, surname, time, beat, walk, RBI);//打印文件数据 
	}
}
int read_data(FILE * fp, TEAM players[], int n)
{
	int count = 0;//计数,用来获取文件一共储存了多少行数据 
	int get_id, beat, time, walk, RBI;//获取每行对应的信息 
	char name[LEN];
	char surname[LEN];
	int temp[LEN] = {-1}; //临时数组,存放球员的id 
	int total;//用于循环计数,数组下标 
	int flag = 1;//标记位,表示没有读取到已经存进了数组的id 
	int temp_count = 0;//返回值,保存有几个球员 

	while (fscanf(fp, "%d %18s %18s %d %d %d %d", &get_id, name, surname, &time, &beat, &walk, &RBI) == 7
		&& !feof(fp) && count < n) 
		//feof()当上一次输入调用检测到文件结尾时,feof()函数返回一个非零值
	{
		for (total = 0;total < count + 1; total++)//从0开始,一直到已经读取的行数为止 
		{
			if (temp[total] == get_id)//如果读取的id已经存进了数组 
			{
				players[total].time += time;//在对应的球员下,累计相应的上场次数等需要累加的值 
				players[total].beat += beat;
				players[total].walk += walk;
				players[total].RBI += RBI;
				flag = 0;//标记位赋值0,表示这行数据的球员已经存了 
				break;//跳出循环 
			}
		}
		if (flag == 1)//表示这是一个新球员 
		{
			players[temp_count].id = get_id;//给相应的球员结构赋值 
			strcpy(players[temp_count].name,name);
			strcpy(players[temp_count].surname,surname);
			players[temp_count].time = time;
			players[temp_count].beat = beat;
			players[temp_count].walk = walk;
			players[temp_count].RBI = RBI;
			temp[temp_count] = get_id;
			temp_count++;//新球员,数组存了一个,下一个数组元素下标 
		}
		flag = 1;//重新置为1,进行下一行数据读取 
		count++;//读取行数+1 
	} 
	return temp_count;//返回球员数 
}
int get_rate(TEAM players[], int n)
{
	int i;
	
	for (i = 0; i < n; i++)
	{
		players[i].rate = 1.0 *players[i].beat / players[i].time;
		//这样计算就不会舍入,会得到一个浮点数 
	}
}
void print(TEAM players[], int n)
{
	int i;
	
	if (n == 0)
	{
		printf("没有球员数据。\n");
	}
	else
	{
		printf("以下是球员数据:\n");
		for (i = 0; i < n; i++)
		{
			printf("%d %s %s %d %d %d %d %.2f\n",
			players[i].id, players[i].name, players[i].surname, players[i].time,
			players[i].beat, players[i].walk, players[i].RBI, players[i].rate);
		}
	}	
}

输出示例:

以下是文件的数据:
4 Jessie Joybat 5 2 1 1
3 Jee Jot 6 3 2 1
6 Zhang san 6 3 2 1
3 Jee Jot 7 1 2 1
1 Lily Neo 10 3 6 2
以下是球员数据:
4 Jessie Joybat 5 2 1 1 0.40
3 Jee Jot 13 4 4 2 0.31
6 Zhang san 6 3 2 1 0.50
1 Lily Neo 10 3 6 2 0.30

7.exercise7.c

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

//exercise14.7
#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 is_delete;//标记是否删除,是的话,值为1 
};
char get_char();//获取输入的信息 
void change(struct book * p);//更改书籍的信息 
int main(void)
{
    struct book library[MAXBKS]; //存放书籍 
    int count = 0;
    int index, filecount;
    FILE * pbooks;
    int size = sizeof (struct book);
    char ch;//获取输入的选项 
    struct book library_temp[MAXBKS]; //暂时存放 
    int i = 0;
    
    if ((pbooks = fopen("exercise7.txt", "r+")) == NULL)//r+模式 
    {
        fprintf(stderr, "打不开文件。\n");
        exit(EXIT_FAILURE);
    } 
    rewind(pbooks);            /* 定位到文件开始处 */
    while (count < MAXBKS &&  fread(&library[count], size, 1, pbooks) == 1)
    {//从文件中读取书 
        if (count == 0)
        {
        	puts("文件中已经有的:");//先逐个打印书籍信息 
		}
		library[count].is_delete = 0;    
        printf("%s by %s: $%.2f\n",library[count].title,library[count].author, library[count].value);
        
        puts("要修改或删除这条记录吗?(要输入y,不要就输入n)"); 
        ch = get_char();//获取y或n 
		if (ch == 'y')
		{
			puts("修改记录输入y,删除记录输入n:");
			ch = get_char();//继续利用这个函数 
			if (ch == 'y') 
			{
				change(&library[count]);//修改这本书的信息 
			}
			else
			{
				library[count].is_delete = 1;//这本书删除掉,标记置为1 
				printf("%s by %s在文件已经被删除。\n", library[count].title, library[count].author);
			}
		}
        count++;//读取下一本 
    }
    filecount = count;//文件中一共读取的书籍数量 
    if (count == MAXBKS)//等于最大数量 
    {
        fputs("exercise7.txt空间已经满了。", stderr);
        exit(EXIT_FAILURE);//退出 
    }  
    puts("输入新的书名(在一行开始处按回车退出):");
    while (count < MAXBKS && s_gets(library[count].title, MAXTITL) != NULL
           && library[count].title[0] != '\0')
    {
    	library[count].is_delete = 0;//先给书的删除标记赋值 
        puts("输入作者名:");
        s_gets(library[count].author, MAXAUTL);
        puts("输入书的价格:");
        scanf("%f", &library[count].value);
        count++;//书籍总数量+1,包括文件中已经存了的 
        while (getchar() != '\n')
            continue;                
        if (count < MAXBKS)
            puts("输入下一本书的书名:");
    }   
    if (count > 0)
    {
        puts("这是书的清单:");
        for (index = 0; index < count; index++)
        {//循环打印所有书 
        	if (library[index].is_delete == 0)//没删除的书籍才打印 
			{
				printf("%s by %s: $%.2f\n",library[index].title,library[index].author, library[index].value);
			} 
		}		
		for (index = 0; index < count; index++)//没删除的书籍存放在临时结构中 
        {
        	if (library[index].is_delete == 0)
			{
				 library_temp[i] = library[index];
				 i++;//存进临时结构就+1 
			} 
		}
		for (index = 0; index < i; index++)
        {
			library[index] = library_temp[index];
			//再把临时结构的存在静态结构中,下次运行程序读取书籍 
		}
		if (fclose(pbooks) != 0)//把文件关了 
		{
			fprintf(stderr, "关闭文件失败。\n");
        	exit(EXIT_FAILURE);
		}
		if ((pbooks = fopen("exercise7.txt", "w")) == NULL)//再以写模式打开文件 
	    {
	        fprintf(stderr, "打开文件失败。\n");
	        exit(EXIT_FAILURE);
	    }
		for (index = 0; index < i; index++)
        {//剩余书籍存进文件中 
			fwrite(&library[index], size, 1, pbooks);//把没有删除的书逐个写入文件 
		}            
    }
    else
    {
    	puts("没有书。\n");
	} 	    
    if (fclose(pbooks) != 0)
	{
		fprintf(stderr, "关闭文件失败。\n");
        exit(EXIT_FAILURE);
	}
    puts("程序结束。");
    
    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)                  
            *find = '\0';          
        else
            while (getchar() != '\n')
                continue;          
    }
    return ret_val;
}
char get_char()
{
	char temp[10];//输入暂存在这个数组 
	
	while (s_gets(temp, 10) == NULL || (temp[0] != 'y' && temp[0] != 'n') || temp[1] != '\0')
	{//输入为空或输入的第一个字符不为y、n中的一个或第二个字符不是换行符 
        puts("请输入y或n。");  
	}
	return temp[0];//返回y或n 
}
void change(struct book * p)
{
	puts("输入修改后的书名(在一行开始处按回车退出):");
	s_gets(p->title, MAXAUTL);
 	puts("输入修改后的作者名:");
    s_gets(p->author, MAXAUTL);
    puts("输入修改后的书的价格:");
    scanf("%f", &p->value);
    while (getchar() != '\n')
    {
    	continue;
	}
}

第一次运行程序,添加两本书:

输入新的书名(在一行开始处按回车退出):
c
输入作者名:
zhangsan
输入书的价格:
11
输入下一本书的书名:
c++
输入作者名:
lisi
输入书的价格:
22
输入下一本书的书名:

这是书的清单:
c by zhangsan: $11.00
c++ by lisi: $22.00
程序结束。

第二次运行程序,再添加三本书:

文件中已经有的:
c by zhangsan: $11.00
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
c++ by lisi: $22.00
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
输入新的书名(在一行开始处按回车退出):
java
输入作者名:
wangwu
输入书的价格:
33.5
输入下一本书的书名:
python
输入作者名:
maliu
输入书的价格:
44.5
输入下一本书的书名:
c#
输入作者名:
tianqi
输入书的价格:
66.2
输入下一本书的书名:

这是书的清单:
c by zhangsan: $11.00
c++ by lisi: $22.00
java by wangwu: $33.50
python by maliu: $44.50
c# by tianqi: $66.20

第三次运行程序,修改一本书:

文件中已经有的:
c by zhangsan: $11.00
要修改或删除这条记录吗?(要输入y,不要就输入n)
y
修改记录输入y,删除记录输入n:
y
输入修改后的书名(在一行开始处按回车退出):
c++++
输入修改后的作者名:
zhaojiu
输入修改后的书的价格:
77
c++ by lisi: $22.00
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
java by wangwu: $33.50
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
python by maliu: $44.50
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
c# by tianqi: $66.20
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
输入新的书名(在一行开始处按回车退出):

这是书的清单:
c++++ by zhaojiu: $77.00
c++ by lisi: $22.00
java by wangwu: $33.50
python by maliu: $44.50
c# by tianqi: $66.20
程序结束。

删除两本书:

文件中已经有的:
c++++ by zhaojiu: $77.00
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
c++ by lisi: $22.00
要修改或删除这条记录吗?(要输入y,不要就输入n)
y
修改记录输入y,删除记录输入n:
n
c++ by lisi在文件已经被删除。
java by wangwu: $33.50
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
python by maliu: $44.50
要修改或删除这条记录吗?(要输入y,不要就输入n)
y
修改记录输入y,删除记录输入n:
n
python by maliu在文件已经被删除。
c# by tianqi: $66.20
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
输入新的书名(在一行开始处按回车退出):

这是书的清单:
c++++ by zhaojiu: $77.00
java by wangwu: $33.50
c# by tianqi: $66.20
程序结束。

再运行看看:

文件中已经有的:
c++++ by zhaojiu: $77.00
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
java by wangwu: $33.50
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
c# by tianqi: $66.20
要修改或删除这条记录吗?(要输入y,不要就输入n)
n
输入新的书名(在一行开始处按回车退出):

这是书的清单:
c++++ by zhaojiu: $77.00
java by wangwu: $33.50
c# by tianqi: $66.20
程序结束。

输入验证没有做的完善;一本书一本书提示用户是否需要修改或者删除其实是不好的,应该打印所有书籍,然后输入书籍的名称去进行匹配,再进行修改删除,这样的话更合理,但是代码量加大很多,留待后续。

创建一个临时结构去存放剩下的书籍其实也比较低效,应该使用链表,在删除一本书后,把前一本书的指针指向删除的那本书的后面的书,这属于数据结构内容,留待以后。

8.exercise8.c

巨人航空公司的机群由12个座位的飞机组成。它每天飞行一个航班。根据下面的要求,编写一个座位预订程序。
a.该程序使用一个内含12个结构的数组。每个结构中包括:一个成员表示座位编号、一个成员表示座位是否已被预订、一个成员表示预订人的名、一个成员表示预订人的姓。
b.该程序显示下面的菜单:
To choose a function, enter its letter label: 要选择功能,请输入其字母标签:
a)Show number of empty seats a)显示空座位的数量
b)Show list of empty seats b)显示空座位列表
c)Show alphabetical list of seats c)按字母顺序显示座位列表
d) Assign a customer to a seat assignment d)为客户分配座位
e) Delete a seat assignment e)删除一个已经分配的座位
f) Quit f)退出

c.该程序能成功执行上面给出的菜单。选择d)和e)要提示用户进行额外输入,每个选项都能让用户中止输入。
d.执行特定程序后,该程序再次显示菜单,除非用户选择f)。

//exercise14.8
#include 
#include 
#include 
#define SEAT 12
#define LEN 20
struct planes {               
    int id;
    int is_book;
    char fname[LEN];
    char lname[LEN];
};
void menu();//菜单显示函数 
char get_char();//获取输入的菜单选项 
char * s_gets(char * st, int n);
int a_num(const struct planes plane[], int n);
void b_list(const struct planes plane[], int n);
void c_show(const struct planes plane[], int n);
void d_assign(struct planes *plane, int n);
void no_assign(const struct planes plane[], int n, int *arr); 
int get_id(int *arr, int n);
void e_delete(struct planes *plane, int n);
void is_assign(const struct planes plane[], int n, int *arr); 
int main(void)
{
    struct planes plane[SEAT];
    char choice;
    FILE *fp;
    int size = sizeof(struct planes);
    int i;
    int empty;
    
    if ((fp = fopen("exercise8.txt", "r+")) == NULL)//读写模式打开 
    {
    	fprintf(stderr,"打开文件exercise8.txt失败\n");
    	exit(EXIT_FAILURE);
	}
	if (fgetc(fp) == EOF) 
	{/*先读一下文件,如果文件为空,也就是第一次运行程序时
	 int fgetc(FILE *stream)从指定的流 stream 获取下一个字符(一个无符号字符)
	 ,并把位置标识符往前移动,如果到达文件末尾或发生读错误,则返回 EOF*/
		for (i = 0; i < SEAT; i++)//先给12个座位编号1-12,然后都是未预定 
		{
			plane[i].id = i + 1; 
			plane[i].is_book = 0;//座位没被预定 
		}
		fwrite(plane, size, SEAT, fp);//把这12个座位编号并且都写进文件 
	}//就只有第一次运行程序时会执行这一段,后面文件就有内容了
	rewind(fp);//回到文件开始 
	fread(plane, size, SEAT, fp);//把文件内容读入结构数组中 
	if (ferror(fp) != 0)
	{
		fprintf(stderr, "写入文件exercise8.txt失败,\n");
		exit(EXIT_FAILURE);
	}
	
    menu();//显示菜单 
    choice = get_char();//获取选项 
    while (choice != 'f')//输入的不是f,就一直循环 
    {
    	switch (choice)
    	{
    		case 'a':
    				printf("一共有%d个空座位。\n", a_num(plane, SEAT));
    				break;
    		case 'b':
    				b_list(plane, SEAT);
    				break;
    		case 'c':
    				c_show(plane, SEAT);
    				break;
    		case 'd':
    				d_assign(plane, SEAT);
    				break;
    		case 'e':
    				e_delete(plane, SEAT);
    				break;
    		default:
    				printf("程序出错了。\n");
    				break;
		}
		menu();
		choice = get_char();
	}
	if (fclose(fp) != 0)//关闭文件 
	{
		fprintf(stderr, "关闭文件exercise8.txt失败,\n");
		exit(EXIT_FAILURE);
	}
	if ((fp = fopen("exercise8.txt", "w")) == NULL)//写模式打开 
    {
    	fprintf(stderr,"打开文件exercise8.txt失败\n");
    	exit(EXIT_FAILURE);
	}
	fwrite(plane, size, SEAT, fp);
	//把做了更改后的座位信息又重新写回该文件,下次打开程序会保留上次程序运行后的结果 
	if (ferror(fp) != 0)
	{
		fprintf(stderr, "写入文件exercise8.txt失败,\n");
		exit(EXIT_FAILURE);
	}
	if (fclose(fp) != 0)
	{
		fprintf(stderr, "关闭文件exercise8.txt失败,\n");
		exit(EXIT_FAILURE);
	}
    puts("程序结束。");
    
    return 0;
}
void menu()
{
	printf("要选择功能,请输入相应字母:\n");
	printf("a)显示空座位的数量\n");
	printf("b)显示空座位列表\n");
	printf("c)按字母顺序显示座位列表\n");
	printf("d)为客户分配座位\n");
	printf("e)删除一个已经分配的座位\n");
	printf("f)退出\n");
}
char get_char()
{
	char temp[3];//输入暂存在这个数组 
	
	while (s_gets(temp, 3) == NULL || temp[1] != '\0' || temp[0] > 'f' || temp[0] < 'a')
	{//输入的不是单个字母,或不是在a-f之间的字母,就循环输入 
		printf("请输入正确的字母:");
	}
	return temp[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)                  
            *find = '\0';          
        else
            while (getchar() != '\n')
                continue;          
    }
    return ret_val;
}
int a_num(const struct planes plane[], int n)//a功能, 显示有几个空座位 
{
	int i;
	int count = 0;//空座位计数 
	
	for (i = 0; i < n; i++)
	{
		if (plane[i].is_book == 0)//座位的预定标记是0,则是空座位 
		{
			count++; 
		}
	}	
	return count;
}
void b_list(const struct planes plane[], int n)//b功能,显示未被预定的座位编号 
{
	int i;
	int count;//空座位计数 
	
	count = a_num(plane, n);
	if (count == 0)
	{
		printf("没有空座位。\n");
	}
	else
	{
		printf("%d个空座位的编号:", count);
		for (i = 0; i < n; i++)
		{
			if (plane[i].is_book == 0)//座位是空座位 
			{
				printf("%d ", plane[i].id);
			}
		}
		printf("\n");
	}	
}
void c_show(const struct planes plane[], int n)//c功能,以名字的顺序排序展示 
{
	struct planes temp;//临时结构
	struct planes temp_plane[n];//临时结构数组 
	int i, j;
	int top;
	int count = 0;
	
	if (a_num(plane, n) == SEAT)
	{
		printf("全是空座位。\n");
	}
	else
	{
		for (i = 0; i < n; i++)//赋值 
		{
			if (plane[i].is_book == 1)//是预定的座位 
			{
				temp_plane[count] = plane[i];//存入临时结构数组 
				count++;//临时结构数组存放座位+1 
			}
		}
		for (i = 0; i < count; i++)//临时结构数组冒泡排序 
		{
			for (top = i + 1; top < count; top++)//内层循环 
			{
				if (strcmp(temp_plane[i].fname, temp_plane[top].fname) > 0)
				//比较两个座位预定人的名的顺序 
				{
					temp = temp_plane[i];//就是交换顺序 
					temp_plane[i] = temp_plane[top];
					temp_plane[top] =temp;
				}
			}			
		}
		printf("以下是按字母顺序排列的座位列表:\n");
		for (i = 0; i < count; i++)
		{	
			printf("%d %d %s %s\n",
				temp_plane[i].id, temp_plane[i].is_book, temp_plane[i].fname, temp_plane[i].lname);
		}
	}	
}
void d_assign(struct planes *plane, int n)//d功能,预定一个座位 
{	
	int arr[n];//存放空座位 
	int getid;//获取想预定的座位编号 
	int i;
	
	if (a_num(plane, n) == 0)
	{
		printf("没有空座位。\n");
	}
	else
	{
		no_assign(plane, SEAT, arr);//调用此函数,把存放空座位的数组存储空座位的编号	
		printf("请输入你要选择的座位编号:");
		getid = get_id(arr, n);//获取想预定的座位编号 
		while (getid == 0)//此作为不是空座位,重新选一个座位 
		{
			printf("请输入没被预定的座位号:");
			getid = get_id(arr, n);
		}
		printf("请输入您的名:");//填写相关信息 
		s_gets(plane[getid - 1].fname, n);
		printf("请输入您的姓:");
		s_gets(plane[getid - 1].lname, n);
		plane[getid - 1].is_book = 1;//标记这个座位成功被预定了 
	} 
}
void no_assign(const struct planes plane[], int n, int *arr)//没被预定的座位 
{	
	int i;
	
	for (i = 0; i < n; i++)
	{
		if (plane[i].is_book == 0)//座位没被预定 
		{
			arr[i] = plane[i].id;//存在空座位数组中 
		}
	}	
}
int get_id(int *arr, int n)//获取输入的编号 
{
	int input;
	int i; 

	while (scanf("%d", &input) != 1 || getchar() != '\n' || input > 12 || input < 1)
	{//如果输入的不是数字或者数字不在1-12中 ,循环输入 
		printf("请输入正确的数字:", input);
		while (getchar() != '\n')
		{
			continue;
		}
	}
	for (i = 0; i < n; i++)
	{
		if (input == arr[i])//输入的值和数组中的值逐个比较,满足则说明输入有效 
		{
			return input;//返回这个有效值	
		}
	}
	return 0;
}
void e_delete(struct planes *plane, int n)//e功能,删除一个座位预订信息 
{	
	int arr[n];//被预定的座位数组 
	int getid;
	int i;
	
	if (a_num(plane, n) == SEAT)
	{
		printf("全是空座位。\n");
		return; 
	}
	else
	{
		is_assign(plane, SEAT, arr);	
		printf("请输入你要删除的座位编号:");
		getid = get_id(arr, n);
		while (getid == 0)
		{
			printf("请输入已经预定的座位号:");
			getid = get_id(arr, n);
		}
		strcpy(plane[getid - 1].fname, "");//删除座位信息 
		strcpy(plane[getid - 1].lname, "");
		plane[getid - 1].is_book = 0;//座位标记为空座位,删除座位成功 
	} 
}
void is_assign(const struct planes plane[], int n, int *arr)//已经被预定的座位 
{
	
	int i;
	
	for (i = 0; i < n; i++)
	{
		if (plane[i].is_book == 1)//座位被预定 
		{
			arr[i] = plane[i].id;//存在预定数组中 
		}
	}	
}

第一次运行程序,空的exercise8.txt文件会填充12个座位信息,展示b功能:

要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
b
9个空座位的编号:1 3 4 6 7 9 10 11 12
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
f
程序结束。

第二次运行程序,预定三个座位,展示d功能:

要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
b
12个空座位的编号:1 2 3 4 5 6 7 8 9 10 11 12
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
d
请输入你要选择的座位编号:2
请输入您的名:san
请输入您的姓:zhang
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
d
请输入你要选择的座位编号:5
请输入您的名:si
请输入您的姓:li
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
d
请输入你要选择的座位编号:8
请输入您的名:liu
请输入您的姓:ma
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
f
程序结束。

第三次运行程序,展示a、c、e功能:

要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
a
一共有9个空座位。
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
c
以下是按字母顺序排列的座位列表:
8 1 liu ma
2 1 san zhang
5 1 si li
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
e
请输入你要删除的座位编号:2
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
c
以下是按字母顺序排列的座位列表:
8 1 liu ma
5 1 si li
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)退出
f
程序结束。

可以用fgetc()函数判断文件是否为空,然后填充空文件;需要修改结构数组本身的内容时,要传入指针,否则传入结构数组即可,而且要加const限定符,以免内容被篡改;输入验证做的还不是很到位,因为在使用d功能时,就得把修改给做完。无法中途终止这个功能并回到菜单,应该提示q就终止这个功能,这必须要用到判定输入内容的函数,暂时没找到解决方法。

9.exercise9.c

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

//exercise14.9
#include 
#include 
#include 
#define LEN 20
#define SEAT 12

struct planes {               
    int id;
    int is_book;
    char fname[LEN];
    char lname[LEN];
};

char get_char();//获取输入的菜单选项
char * s_gets(char * st, int n);//输入字符串
void airlane_menu();//进入航班后,选择相应功能 
void plane_deal(char choice);//进入相应的航班处理程序
void plane_menu();//响应航班菜单显示函数
int a_num(const struct planes plane[], int n);//a功能,显示有几个空座位
void b_list(const struct planes plane[], int n);//b功能,显示未被预定的座位编号
void c_show(const struct planes plane[], int n);//c功能,以名字的顺序排序展示
void d_assign(struct planes *plane, int n);//d功能,预定一个座位
void no_assign(const struct planes plane[], int n, int *arr); //没被预定的座位
int get_id(int *arr, int n);//获取输入的编号
void e_delete(struct planes *plane, int n);//e功能,删除一个座位预订信息
void is_assign(const struct planes plane[], int n, int *arr);//已经被预定的座位
void f_confirm(const struct planes plane[], int n); //f功能,显示已分配的座位座位
int main(void)
{
	char choice;

	airlane_menu();
    choice = get_char();//获取选项 
    while (choice != 'e')
    {
    	if (choice < 'a' || choice > 'e')
		{
			printf("请输入正确的字母:");
			choice = get_char();
			break; 
		}
		plane_deal(choice);//进入对应航班 
		airlane_menu(); 
		choice = get_char();//获取选项
	}
	
	puts("程序结束。");
	return 0; 
}
char get_char()
{
	char temp[3];//输入暂存在这个数组 
	
	while (s_gets(temp, 3) == NULL || temp[1] != '\0' || temp[0] > 'z' || temp[0] < 'a')
	{//输入的不是单个字母,或不是在a-f之间的字母,就循环输入 
		printf("请输入正确的字母:");
	}
	return temp[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)                  
            *find = '\0';          
        else
            while (getchar() != '\n')
                continue;          
    }
    return ret_val;
}
void airlane_menu()
{
	puts("a)102    b)311    c)444    d)519    e)退出程序");
	printf("要选择航班,请输入相应字母:");
}
void plane_deal(char choice)
{
	char filename[LEN];//文件名,存储对应航班 
	FILE *fp;
	struct planes plane[SEAT];
    int size = sizeof(struct planes);
    int i;
    int empty;
    char choice_two;
    int num;//存储航班号 
	 
	switch (choice)
    {
  		case 'a':
    			puts("以下是102航班:");
    			num = 102;//120航班 
    			strcpy(filename, "exercise9a.txt");//102航班信息存储在对应文件中 
    			break;
    	case 'b':
    			puts("以下是311航班:");
    			num = 311;
    			strcpy(filename, "exercise9b.txt");
    			break;
    	case 'c':
    			puts("以下是444航班:");
    			num = 444;
    			strcpy(filename, "exercise9c.txt");
    			break;
    	case 'd':
    			puts("以下是519航班:");
    			num = 519;
    			strcpy(filename, "exercise9d.txt");
    			break;
    		default:
    			printf("程序出错了。\n");
    			break;
	}
	if ((fp = fopen(filename, "r+")) == NULL)//读写模式打开对应文件 
    {
    	fprintf(stderr,"打开文件%s.失败\n", filename); 
    	exit(EXIT_FAILURE);
	}
	if (fgetc(fp) == EOF) 
	{/*先读一下文件,如果文件为空,也就是第一次运行程序时
	 int fgetc(FILE *stream)从指定的流 stream 获取下一个字符(一个无符号字符)
	 ,并把位置标识符往前移动,如果到达文件末尾或发生读错误,则返回 EOF*/
		for (i = 0; i < SEAT; i++)//先给12个座位编号1-12,然后都是未预定 
		{
			plane[i].id = i + 1; 
			plane[i].is_book = 0;//座位没被预定 
		}
		fwrite(plane, size, SEAT, fp);//把这12个座位编号并且都写进文件 
	}//就只有第一次运行程序时会执行这一段,后面文件就有内容了
	rewind(fp);//回到文件开始 
	fread(plane, size, SEAT, fp);//把文件内容读入结构数组中 
	if (ferror(fp) != 0)
	{
		fprintf(stderr, "写入文件%s失败。\n", filename);
		exit(EXIT_FAILURE);
	}
	plane_menu();//打开功能菜单 
	choice_two = get_char();//获取选项 
    while (choice_two != 'g')//输入的不是g,就一直在本航班内循环 
    {
    	if (choice_two < 'a' || choice_two > 'g')
		{
			printf("请输入正确的字母:");
			choice_two = get_char();
			break; 
		}
    	switch (choice_two)
    	{
    		case 'a':
    				printf("一共有%d个空座位。\n", a_num(plane, SEAT));
    				break;
    		case 'b':
    				b_list(plane, SEAT);
    				break;
    		case 'c':
    				c_show(plane, SEAT);
    				break;
    		case 'd':
    				d_assign(plane, SEAT);
    				break;
    		case 'e':
    				e_delete(plane, SEAT);
    				break;
			case 'f':
    				f_confirm(plane, SEAT);
    				break;
    		default:
    				printf("程序出错了。\n");
    				break;
		}
		printf("以下是%d航班:", num);//提示用户还在此航班中操作 
		plane_menu();
		choice_two = get_char();
	}
	if (fclose(fp) != 0)//关闭对应航班的文件 
	{
		fprintf(stderr, "关闭文件%s失败,\n", filename);
		exit(EXIT_FAILURE);
	}
	if ((fp = fopen(filename, "w")) == NULL)//写模式打开 
    {
    	fprintf(stderr,"打开文件%s失败\n", filename);
    	exit(EXIT_FAILURE);
	}
	fwrite(plane, size, SEAT, fp);
	//把做了更改后的座位信息又重新写回该文件,下次打开程序会保留上次程序运行后的结果 
	if (ferror(fp) != 0)
	{
		fprintf(stderr, "写入文件%s失败,\n", filename);
		exit(EXIT_FAILURE);
	}
	if (fclose(fp) != 0)
	{
		fprintf(stderr, "关闭文件%s失败,\n", filename);
		exit(EXIT_FAILURE);
	}
}
void plane_menu()
{
	printf("要选择功能,请输入相应字母:\n");
	printf("a)显示空座位的数量\n");
	printf("b)显示空座位列表\n");
	printf("c)按字母顺序显示座位列表\n");
	printf("d)为客户分配座位\n");
	printf("e)删除一个已经分配的座位\n");
	printf("f)确认分配座位\n");
	printf("g)退出\n");
}
int a_num(const struct planes plane[], int n)//a功能, 显示有几个空座位 
{
	int i;
	int count = 0;//空座位计数 
	
	for (i = 0; i < n; i++)
	{
		if (plane[i].is_book == 0)//座位的预定标记是0,则是空座位 
		{
			count++; 
		}
	}	
	return count;
}
void b_list(const struct planes plane[], int n)//b功能,显示未被预定的座位编号 
{
	int i;
	int count;//空座位计数 
	
	count = a_num(plane, n);
	if (count == 0)
	{
		printf("没有空座位。\n");
	}
	else
	{
		printf("%d个空座位的编号:", count);
		for (i = 0; i < n; i++)
		{
			if (plane[i].is_book == 0)//座位是空座位 
			{
				printf("%d ", plane[i].id);
			}
		}
		printf("\n");
	}	
}
void c_show(const struct planes plane[], int n)//c功能,以名字的顺序排序展示 
{
	struct planes temp;//临时结构
	struct planes temp_plane[n];//临时结构数组 
	int i, j;
	int top;
	int count = 0;
	
	if (a_num(plane, n) == SEAT)
	{
		printf("全是空座位。\n");
	}
	else
	{
		for (i = 0; i < n; i++)//赋值 
		{
			if (plane[i].is_book == 1)//是预定的座位 
			{
				temp_plane[count] = plane[i];//存入临时结构数组 
				count++;//临时结构数组存放座位+1 
			}
		}
		for (i = 0; i < count; i++)//临时结构数组冒泡排序 
		{
			for (top = i + 1; top < count; top++)//内层循环 
			{
				if (strcmp(temp_plane[i].fname, temp_plane[top].fname) > 0)
				//比较两个座位预定人的名的顺序 
				{
					temp = temp_plane[i];//就是交换顺序 
					temp_plane[i] = temp_plane[top];
					temp_plane[top] =temp;
				}
			}			
		}
		printf("以下是按字母顺序排列的座位列表:\n");
		for (i = 0; i < count; i++)
		{	
			printf("%d %d %s %s\n",
				temp_plane[i].id, temp_plane[i].is_book, temp_plane[i].fname, temp_plane[i].lname);
		}
	}	
}
void d_assign(struct planes *plane, int n)//d功能,预定一个座位 
{	
	int arr[n];//存放空座位 
	int getid;//获取想预定的座位编号 
	int i;
	
	if (a_num(plane, n) == 0)
	{
		printf("没有空座位。\n");
	}
	else
	{
		no_assign(plane, SEAT, arr);//调用此函数,把存放空座位的数组存储空座位的编号	
		printf("请输入你要选择的座位编号:");
		getid = get_id(arr, n);//获取想预定的座位编号 
		while (getid == 0)//此作为不是空座位,重新选一个座位 
		{
			printf("请输入没被预定的座位号:");
			getid = get_id(arr, n);
		}
		printf("请输入您的名:");//填写相关信息 
		s_gets(plane[getid - 1].fname, n);
		printf("请输入您的姓:");
		s_gets(plane[getid - 1].lname, n);
		plane[getid - 1].is_book = 1;//标记这个座位成功被预定了 
	} 
}
void no_assign(const struct planes plane[], int n, int *arr)//没被预定的座位 
{	
	int i;
	
	for (i = 0; i < n; i++)
	{
		if (plane[i].is_book == 0)//座位没被预定 
		{
			arr[i] = plane[i].id;//存在空座位数组中 
		}
	}	
}
int get_id(int *arr, int n)//获取输入的编号 
{
	int input;
	int i; 

	while (scanf("%d", &input) != 1 || getchar() != '\n' || input > 12 || input < 1)
	{//如果输入的不是数字或者数字不在1-12中 ,循环输入 
		printf("请输入正确的数字:", input);
		while (getchar() != '\n')
		{
			continue;
		}
	}
	for (i = 0; i < n; i++)
	{
		if (input == arr[i])//输入的值和数组中的值逐个比较,满足则说明输入有效 
		{
			return input;//返回这个有效值	
		}
	}
	return 0;
}
void e_delete(struct planes *plane, int n)//e功能,删除一个座位预订信息 
{	
	int arr[n];//被预定的座位数组 
	int getid;
	int i;
	
	if (a_num(plane, n) == SEAT)
	{
		printf("全是空座位。\n");
		return; 
	}
	else
	{
		is_assign(plane, SEAT, arr);	
		printf("请输入你要删除的座位编号:");
		getid = get_id(arr, n);
		while (getid == 0)
		{
			printf("请输入已经预定的座位号:");
			getid = get_id(arr, n);
		}
		strcpy(plane[getid - 1].fname, "");//删除座位信息 
		strcpy(plane[getid - 1].lname, "");
		plane[getid - 1].is_book = 0;//座位标记为空座位,删除座位成功 
	} 
}
void is_assign(const struct planes plane[], int n, int *arr)//已经被预定的座位 
{
	
	int i;
	
	for (i = 0; i < n; i++)
	{
		if (plane[i].is_book == 1)//座位被预定 
		{
			arr[i] = plane[i].id;//存在预定数组中 
		}
	}	
}
void f_confirm(const struct planes plane[], int n)//f功能,显示已分配的座位座位
{
	int i;
	
	puts("已分配的座位:");
	for (i = 0; i < n; i++)
	{
		if (plane[i].is_book == 1)//座位的预定标记是0,则是已分配座位 
		{
			 printf("%d %d %s %s\n",
				plane[i].id, plane[i].is_book, plane[i].fname, plane[i].lname);
		}
	} 
}

在编程练习8的基础上进行修改,把4个航班的信息放在4个不同的文件中,分别是exercise9a.txt、exercise9b.txt、exercise9c.txt、exercise9d.txt第一次运行:

a)102    b)311    c)444    d)519    e)退出程序
要选择航班,请输入相应字母:a
以下是102航班:
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
f
全是空座位。
以下是102航班:要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
d
请输入你要选择的座位编号:5
请输入您的名:san
请输入您的姓:zhang
以下是102航班:要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
g
a)102    b)311    c)444    d)519    e)退出程序
要选择航班,请输入相应字母:b
以下是311航班:
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
d
请输入你要选择的座位编号:5
请输入您的名:si
请输入您的姓:li
以下是311航班:要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
d
请输入你要选择的座位编号:9
请输入您的名:liu
请输入您的姓:ma
以下是311航班:要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
c
以下是按字母顺序排列的座位列表:
9 1 liu ma
5 1 si li
以下是311航班:要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
g
a)102    b)311    c)444    d)519    e)退出程序
要选择航班,请输入相应字母:f
请输入正确的字母:e
程序结束。

第二次运行,还能读取上次运行后的数据:

a)102    b)311    c)444    d)519    e)退出程序
要选择航班,请输入相应字母:b
以下是311航班:
要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
f
已分配的座位:
5 1 si li
9 1 liu ma
以下是311航班:要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
e
请输入你要删除的座位编号:5
以下是311航班:要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
f
已分配的座位:
9 1 liu ma
以下是311航班:要选择功能,请输入相应字母:
a)显示空座位的数量
b)显示空座位列表
c)按字母顺序显示座位列表
d)为客户分配座位
e)删除一个已经分配的座位
f)确认分配座位
g)退出
g
a)102    b)311    c)444    d)519    e)退出程序
要选择航班,请输入相应字母:e
程序结束。

10.exercise10.c

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

//exercise14.10
#include 
#include 
#define LEN 2

void show(void (*pf)(int, int), int x, int y);
void a_f(int x, int y);
void b_f(int x, int y);
void menu(void);
char get_char(void);
char * s_gets(char * st, int n); 
int main(void)
{
	char ch;
	void (*pf[LEN])(int, int);//函数指针数组 
	int x, y;
	
	menu();
	puts("请输入两个整数:"); 
	while (scanf("%d %d", &x, &y) != 2)
	{
		while (getchar() != '\n')
		{
			continue;
		}
		puts("请输入两个整数:");
	} 
	while (getchar() != '\n')
	{
		continue;
	}
	puts("请输入相应的选项选择功能。");
	ch = get_char();
	while (ch != 'c')
	{
		switch(ch)
		{
			case 'a':
					pf[0] = a_f;//指针赋值 
					show(pf[0], x, y);//调用函数 
					break;
			case 'b':
					pf[1] = b_f;
					show(pf[1], x, y);
					break;
			default:
					printf("程序错误。\n");
					break;
		}
		puts("请输入相应的选项选择功能。");
		ch = get_char();
	}
    puts("请输入相应的选项选择功能。");
	return 0;
} 
void menu(void)
{
	puts("请输入相应的选项选择功能。");
	puts("a)计算两个整数的和");
	puts("b)计算两个整数的差");
	puts("c)退出");
}
void a_f(int x, int y)
{
	printf("%d + %d = %d\n", x, y, x + y);
}
void b_f(int x, int y)
{
	printf("%d - %d = %d\n", x, y, x - y);
}
char get_char(void)
{
	char temp[3];//输入暂存在这个数组 
	
	while (s_gets(temp, 3) == NULL || temp[1] != '\0' || temp[0] > 'c' || temp[0] < 'a')
	{//输入的不是单个字母,或不是在a-f之间的字母,就循环输入 
		printf("请输入正确的字母:");
	}
	return temp[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)                  
            *find = '\0';          
        else
            while (getchar() != '\n')
                continue;          
    }
    return ret_val;
}
void show(void (*pf)(int, int), int x, int y)
{
	(*pf)(x, y);//使用函数指针调用函数 
}

输出示例:

请输入相应的选项选择功能。
a)计算两个整数的和
b)计算两个整数的差
c)退出
请输入两个整数:
5 6
请输入相应的选项选择功能。
a
5 + 6 = 11
请输入相应的选项选择功能。
b
5 - 6 = -1
请输入相应的选项选择功能。
c
程序结束。

简单示范,注意声明函数指针的时候,各种参数要对应上。

11.exercise11.c

编写一个名为transform()的函数,接受4个参数:内含double类型数据的源数组名、内含double类型数据的目标数组名、一个表示数组元素个数的int类型参数、函数名(或等价的函数指针)。transform()函数应把指定函数应用于源数组中的每个元素,并把返回值储存在目标数组中。例如:
transform(source, target, 100, sin);
该声明会把target[0]设置为sin(source[0]),等等,共有100个元素。在一个程序中调用transform()4次,以测试该函数。分别使用math.h函数库中的两个函数以及自定义的两个函数作为参数。

//exercise14.11
#include 
#include 
#define LEN 10
void transform(double *source, double *target, int n, double (*p)(double));
double add(double x);//加1 
double subtract(double x);//减1 
void show(const double arr[], int n);
int main(void)
{	
	double source[LEN] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 ,8.0, 9.0, 10.0};
	double target[LEN] = {0.0};
	//double (*p)(double);
	
	puts("源数组:");
	show(source, LEN);
	puts("目标数组:");
	show(target, LEN); 
	
	puts("执行add函数后目标数组:");
	transform(source, target, LEN, add);
	show(target, LEN);
	
	puts("执行subtract函数后目标数组:");
	transform(source, target, LEN, subtract);
	show(target, LEN);
	
	puts("执行sqrt函数后目标数组:");
	transform(source, target, LEN, sqrt);
	show(target, LEN);
	
	puts("执行sin函数后目标数组:");
	transform(source, target, LEN, sin);
	show(target, LEN);
	
	
	puts("程序结束。");	
	return 0;
} 
double add(double x)
{
	return x + 1;
}
double subtract(double x)
{
	return x - 1;
}
void show(const double arr[], int n)
{
	int i;
	
	for (i = 0; i < n; i++)
	{
		printf("%g ", arr[i]);
	}
	printf("\n");
}
void transform(double *source, double *target, int n, double (*p)(double))
{
	int i;
	
	for (i = 0; i < n; i++)
	{
		target[i] = p(source[i]);//使用函数指针调用函数 
	}
}

输出示例:

源数组:
1 2 3 4 5 6 7 8 9 10
目标数组:
0 0 0 0 0 0 0 0 0 0
执行add函数后目标数组:
2 3 4 5 6 7 8 9 10 11
执行subtract函数后目标数组:
0 1 2 3 4 5 6 7 8 9
执行sqrt函数后目标数组:
1 1.41421 1.73205 2 2.23607 2.44949 2.64575 2.82843 3 3.16228
执行sin函数后目标数组:
0.841471 0.909297 0.14112 -0.756802 -0.958924 -0.279415 0.656987 0.989358 0.412118 -0.544021
程序结束。

12.问题总结

1.输入验证问题

一般是菜单选择,输入相应的字母或数字,响应相应的功能,在这里,可以把输入当成是字符串,存在一个临时数组中,必须满足输入条件;比如只想输入a才是正确的,a前面有空格或者有其他字符都不行,那么这个字符串数组第一个元素必须是a且第二个元素必须是空格;选项的获取可以使用switch,相应的输入响应相应的功能。

2.文件问题

如果一个文件要读取和写,那么要是有r+模式,空文件的判定可以用fgetc()函数,如果为空文件,就在第一次运行时把相应数据写入文件;从文件获取数据后,可以把文件先关闭,如果是要把更改后的数据再写会文件,可以用r模式打开文件,这样会清空文件数据再把想要的数据重新写入该文件。

3.指针问题

限定符const很关键,以后不需要更改数据内容的数组或结构等,作为函数参数时,数组可以用数组表示法,这当然不会改变数组本身内容,不需要加const,但是用指针表示法或者结构作为函数参数时,要加上const,以免更改了内容编译器也不提醒。

你可能感兴趣的:(《C,Primer,Plus》,c语言,学习,算法,开发语言,程序人生)