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

发布于 2021-09-20  610 次阅读


设计程序时,最重要的步骤之一是选择表示数据的方法。再许多情况下,简单变量甚至是数组还不够。为此,C提供了结构变量(structure variable)提高你表示数据的能力,它能让你创造新的形式。

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

想要打印一份图书目录。每本书的各种信息:书名、作者、出版社、版权日期、页数、册数和价格。其中的一些项目(如,书名)可以存储在字符数组中,其他项目需要一个int数组或者float数组。用7个不同的数组分别记录每一项比较繁琐,尤其当你还像创建多分列表:一份按照书名排序、一份按照作者排序、一份按照价格排序等等。如果能把图数目录的信息都包含在一个数组里更好,其中每个元素包含一本书的相关信息。
因此,需要一种既能包含字符串又能包含数字的数据形式,而且还要保持各信息的独立。C结构就能满足这种情况下的需求。
//book.c        --一本书的图数目录
#include<stdio.h>
#include<string.h>
char *s_gets(char *st,int n);
#define MAXBOOKNAME 41
#define MAXNAME 31

struct book{        //结构体模板:标记是book
    char title[MAXBOOKNAME];
    char author[MAXNAME];
    float value;
};          //结构体模板结束

int main(void)
{
    struct book library;    //把library声明成一个book类型的变量

    printf("Please Enter the book title.\n");
    s_gets(library.title,MAXBOOKNAME);
    printf("Please Enter the author.\n");
    s_gets(library.author,MAXNAME);
    printf("Please Enter the book's value.\n");
    scanf("%f",&library.value);
    printf("%s by %s:$%.2f\n",library.title,library.author,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;
}

file

14.2 建立结构声明

结构声明(structure declaration)描述了一个结构的组织布局。声明类似下面这样:
struct book{
    char title[MAXBOOKNAME];
    char author[MAXNAME];
    float value;
};
该声明描述了一个由两个字符数组和一个float类型变量组成的结构。该声明并未创建实际的数据对象,只描述了该对象由什么组成。(有时,我们把结构声明称为模板,因为它勾勒出结构是如何存储数据的)
关键词struct,它表明跟在其后的是一个结构,后面是一个可选的标记(该例中是book),稍后程序中可以使用该标记引用该结构。所以,我们下后面的程序中可以这样声明:
struct book library:
这把library声明为一个使用book结构布局的结构变量。
在结构声明中,用一对花括号括起来的是结构成员列表,每个成员列表都用自己的声明来描述。例如:titile部分是一个内含MAXBOOKNAME个元素的char类型数组。成员可以是任意一种C的数据结构,甚至可以是其他结构!右括号后面的分号是声明所必须的,表示结构布局定义结束。可以把这个声明放在所用函数的外部。也可以放在一个函数定义的内部。如果把结构声明置于一个函数内部,它的标记就只能限于该函数的内部使用,如果把结构声明置于函数的外部,那么该声明之后所有的函数都能使用它的标记。例如:在程序的另一个函数中,可以这样声明:
struct book dickens;
这样,该函数创建了一个结构变量dickens,该变量的结构布局是book。
结构的标记名是可选的。但是以程序示例中的方式建立结构时(在一处定义结构布局,在另一处定义实际的结构变量),必须使用标记。

14.3 定义结构变量

结构有两层含义。一层含义是“结构布局”,结构布局告诉编辑器如何表示数据,但是它并未让编译器为数据分配空间。下一步是创建一个结构与变量,即是结构的另一层含义。程序中创建结构变量的一行是:
struct book library;
编译器执行这行代码便创建了一个结构变量library。编译器使用book模板为该变量分配空间:一个内含MAXBOOKNAME个元素的char数组、一个内含MAXAUTL个元素的char数组和一个float类型的变量。这些存储空间都与一个名称library结合在一起。
在结构变量的声明中,struct book所起的作用相当于一般声明中的int或float。例如,可以定义两个类struct book类型的变量,或者甚至是指向struct book类型结构的指针:
struct book doyle,panshin,* ptbook;

file

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

struct book library;
是以下声明的简化:
struct book{
    char title[MAXBOOKNAME];
    char author[MAXNAME];
    float value;
}library;   //声明的左右括号后跟变量名
换言之,声明结构的过程和定义结构变量的过程咳哟组合成一个步骤。如下所示,组合后的结构声明和结构变量不需要使用结构标记:
struct{     //无结构标记
    char title[MAXBOOKNAME];
    char author[MAXNAME];
    float value;
}library;
然而,如果打算多次使用结构模板,就要使用带标记的形式;或者,使用本章后面介绍的typedef。
这是一个定义结构变量的一个方面,在这个例子中,并未初始化结构变量。

14.3.1 初始化结构

初始化变量和数组如下:
int count = 0;
int fibo[7] = {0,1,1,2,3,5,8};
结构变量是否也可以这样初始化?是的,可以。初始化一个结构变量(ANSI 之前,不能用自动变量初始化结构;ANSI 之后可以用任意存储类别)与初始化数组的语法类似:
struct book library = {
    "The Pious Pirate and the Devious Damsel",
    "Renee Vivotte",
    1.95
};
简而言之,我们使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔。因此,title成员可以被初始化为一个字符串,value成员可以被初始化为一个数字。为了让初始化项与结构中各成员的关联更加明显,我们让每个成员的初始化项独占一行。这样做只是为了提高源代码的可读性,对编译器而言,只需要用逗号分隔各成员的初始化项即可。
注意:初始化结构和类别存储期
如果初始化静态存储期的变量(如,静态外部链接、静态内部链接或静态无链接),必须使用常量值。这同样适用于结构。如果初始化一个静态存储期的结构,初始化列表中的值必须是常量表示。如果是自动存储,初始化列表中的值可以不是常量。

14.3.2 访问结构成员

结构类似于一个“超级数组”,这个超级数组中,可以是一个元素为char类型,下一个元素为forat类型,下一个元素为int数组。可以通过数组下标单独访问数组中的各元素。
那么如何访问结构中的成员?
使用结构运算符——点(.)访问结构中的成员。例如,library.value即访问library的value部分。可以使用仍和float类型变量那样使用library.value。与此类似,可以使用非字符数组那样使用library.title。因此程序中s_gets(library.title,MAXNAME);和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,MAXNAME);
s_gets(newt.title,MAXNAME);
.title引用book结构的第1个成员。

14.3.3 结构的初始化器

C99和C11为了结构提供了指定初始化器(designated initializer),其语法与数组的指定初始化器类似。但是,结构的指定初始化器使用点运算符和成员名标识特定的元素。例如,只初始化book结构的value成员,可以这样做:
struct book suprise = {.value = 10.99};
可以按照任意顺序使用指定初始化器:
struct book qift = {.value = 25.99,
.author = "James Broadfool",
.title = "Rue for the Toad"};
与数组类似,在指定初始化器后面的普通初始化器,为指定成员后面的成员提供初始值。另外,对特定成员的最后一次赋值才是它实际获得的值。例如:
struct book gift = {.value = 18.90,
.author = "Philionna Pestle",
0.25};
赋给value的值是0.25,因为它在结构声明中紧跟在author成员之后。新值0.25取代了之前的18,9.

14.4 结构数组

要把上述代码扩展乘可以处理多本书。显然,每本书的基本信息都可以用一个book类型的结构变量来表示。
为描述两本书需要使用两个变量,以此类推。可以使用这一类的结构数组来处理多本书。
结构与内存
    manybook.c程序创建了一个内含100个结构变量的数组。由于该数组就是自动存储类别的对象。其中的信息被存储再栈中。如此大的数组需要很大一块内存,可能会导致一些问题。如果再运行时出现错误,可能会抱怨栈大小或溢出,你的编译器可能使用了一个默认大小的栈,这个栈对于该例而言太小。要修正这个问题。可以把编译器选项设置栈大小10000,以容纳这个结构数组;或者可以创建静态或外部数组(这样,编译器就不会把数组放在栈中了)。或者可以减小数组大小未16.为何不一开始就使用较小的数组?这是为了让读者意识到栈大小潜在的问题。
//manybook.c    --包含多本书的图数目录
#include<stdio.h>
#include<string.h>
char *s_gets(char *st,int n);
#define MAXBOOKNAME 40
#define MAXNAME 20
#define MAXBOOKNU 100   //建立book模板
struct book{
    char title[MAXBOOKNAME];
    char author[MAXNAME];
    float value;
}; 

int main(void)
{
    struct book library[MAXBOOKNU];
    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 <MAXBOOKNU && s_gets(library[count].title,MAXBOOKNAME)!=NULL&&library[count].title[0]!='\0')
    {
        printf("Now enter the author.\n");
        s_gets(library[count].author,MAXNAME);
        printf("Now enter the value.\n");
        scanf("%f",&library[count++].value);
        while(getchar() !='\n')
            continue;       //清理输入行
        if(count < MAXBOOKNU)
            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)
            *find = '\0';
        else
            while(getchar()!='\n')
                continue;
    }
    return ret_val;
}

file

14.4.1 声明结构数组

声明结构数组和声明其他类型的数组类似。下面是一个声明结构数组的例子:
struct book library[MAXBOOKNU];
以上代码把library声明为一个内含MAXBOOKNU个元素的数组。每个数组的每个元素都是一个book类型的数组。因此,library[0]是第1个book类型的结构变量。library[2]是第2个book类型的结构变量,以此类推。数组名library本身不是结构名,他是一个数组名,该数组中的每个元素都是struct book类型的结构变量。

file

14.4.2 标识结构数组的成员

为了标识结构数组中的成员,可以采用访问单独结构的规则:在结构名后面加一个点运算符,再在点运算符后面写上成员名:
library[0].value;   //第1个元素与value相关联
library[4].title;   //第5个元素与title相关联
注意:数组下标紧跟在library后面,不是跟成员名后面:
library.value[2]    //错误
librart[2].value    //正确
使用library[2].value的原因是:library[2]是结构变量名,正如library[1]是另外一个变量名字。
library[2].title[4]表达代表什么?
这是library数组第3个结构变量(library[2]部分中书名的第5个字符(title[4]部分)。点运算符右侧的下标作用于各个成员,点运算符左侧的下标作用于结构数组。
最后总结一下:
library             //一个book结构的数组
library[2]          //一个数组元素,该元素是book结构
library[2].title    //一个char数组(library[2]的title成员)
library[2].title[4] //数组中library[2]元素的title成员的一个字符。

14.4.3 程序讨论

插入一个while循环读取多项。该循环的测试挑条件是:
while(count <MAXBOOKNU && s_gets(library[count].title,MAXBOOKNAME)!=NULL&&library[count].title[0]!='\0')
表达式s_gets(library[count].title,MAXTNAME)读取一个字符串作为书名,如果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()就可以重新开始下一次的输入。

擦肩而过的概率