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

目录

《C Primer Plus》学习笔记

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

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

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

1.程序book.c
//* book.c -- 一本书的图书目录 */
#include <stdio.h>
#include <string.h>
char * s_gets(char * st, int n);
#define MAXTITL  41      /* 书名的最大长度 + 1 */
#define MAXAUTL  31      /* 作者姓名的最大长度 + 1 */

struct book {
               /* 结构模板:标记是book */
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};                       /* 结构模板结束 */

int main(void)
{
   
    struct book library; /* 把library声明为一个book类型的变量  */
    
    printf("Please enter the book title.\n");
    s_gets(library.title, MAXTITL); /* 访问title部分 */
    printf("Now enter the author.\n");
    s_gets(library.author, MAXAUTL);
    printf("Now enter the value.\n");
    scanf("%f", &library.value);
    printf("%s by %s: $%.2f\n",library.title,
           library.author, library.value);
    printf("%s: \"%s\" ($%.2f)\n", library.author,
           library.title, library.value);
    printf("Done.\n");
    
    return 0;
}

char * s_gets(char * st, int n)
{
   
    char * ret_val;
    char * find;
    
    ret_val = fgets(st, n, stdin);
    if (ret_val)
    {
   
        find = strchr(st, '\n');   
        if (find)                  
            *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;

结构的内存分配

结构变量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 <stdio.h>
#include <string.h>
char * s_gets(char * st, int n);
#define MAXTITL   40
#define MAXAUTL   40
#define MAXBKS   100              /* 书籍的最大数量  */

struct book {
                        /* 建立book模板   */
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};

int main(void)
{
   
    struct book library[MAXBKS]; /* book类型的结构数组 */
    int count = 0;
    int index;
    
    printf("Please enter the book title.\n");
    printf("Press [enter] at the start of a line to stop.\n");
    while (count < MAXBKS && s_gets(library[count].title, MAXTITL) != NULL
           && library[count].title[0] != '\0')
    {
   
        printf("Now enter the author.\n");
        s_gets(library[count].author, MAXAUTL);
        printf("Now enter the value.\n");
        scanf("%f", &library[count++].value);
        while (getchar() != '\n')
            continue;          /* 清理输入行  */
        if (count < MAXBKS)
            printf("Enter the next title.\n");
    }
    
    if (count > 0)
    {
   
        printf("Here is the list of your books:\n");
        for (index = 0; index < count; index++)
            printf("%s by %s: $%.2f\n", library[index].title,
                   library[index].author, library[index].value);
    }
    else
    	printf("No books? Too bad.\n");
    
    return 0;
}

char * s_gets(char * st, int n)
{
   
    char * ret_val;
    char * find;
    
    ret_val = fgets(st, n, stdin);
    if (ret_val)
    {
   
        find = strchr(st, '\n');   // 查找换行符 
        if (find)                  // 如果地址不是NULL,
            *find = '\0';          // 在此处放置一个空字符 
        else
            while (getchar() != '\n')
                continue;          // 处理输入行中剩余的字符 
    }
    return ret_val;
}

输出示例:

Please enter the book title.
Press [enter] at the start of a line to stop.
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类型的结构变量。

结构数组的存储

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 <stdio.h>
#define LEN 20
const char * msgs[5] =
{
   
    "    Thank you for the wonderful evening, ",
    "You certainly prove that a ",
    "is a special kind of guy. We must get together",
    "over a delicious ",
    " and have a few laughs"
};

struct names {
                        // 第一个结构 
    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 <stdio.h>
#define LEN 20

struct names {
   
    char first[LEN];
    char last[LEN];
};

struct guy {
   
    struct names handle;
    char favfood[LEN];
    char job[LEN];
    float income;
};

int main(void)
{
   
    struct guy fellow[2] = {
   
        {
   {
    "Ewen", "Villard"},
            "grilled salmon",
            "personality coach",
            68112.00
        },
        {
   {
   "Rodney", "Swillbelly"},
            "tripe",
            "tabloid editor",
            432400.00
        }
    };
    struct guy * him;    /* 这是一个指向结构的指针 */
    
    printf("address #1: %p #2: %p\n", &fellow[0], &fellow[1]);
    him = &fellow[0];    /* 告诉编译器该指针指向何处  */
    printf("pointer #1: %p #2: %p\n", him, him + 1);
    printf("him->income is $%.2f: (*him).income is $%.2f\n",
           him->income, (*him).income);
    him++;               /* 指向下一个结构 */
    printf("him->favfood is %s:  him->handle.last is %s\n",
           him->favfood, him->handle.last);
    
    return 0;
}

输出:

address #1: 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 <stdio.h>
#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 <stdio.h>
#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 <stdio.h>
#define FUNDLEN 50

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

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

int main(void)
{
   
    struct funds stan = {
   
        "Garlic-Melon Bank",
        4032.27,
        "Lucky's Savings and Loan",
        8543.94
    };
    
    printf("Stan has a total of $%.2f.\n", sum(stan));
    
    return 0;
}

double sum(struct funds moolah)
{
   
    return(moolah.bankfund + moolah.savefund);
}

输出:

Stan has a total of $12576.21.

该程序把程序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 <stdio.h>
#include <string.h>

#define NLEN 30
struct namect {
   
    char fname[NLEN];
    char lname[NLEN];
    int letters;
};

void getinfo(struct namect *);
void makeinfo(struct namect *);
void showinfo(const struct namect *);
char * s_gets(char * st, int n);

int main(void)
{
   
    struct namect person;
    
    getinfo(&person);
    makeinfo(&person);
    showinfo(&person);
    return 0;
}

void getinfo (struct namect * pst)
{
   
    printf("Please enter your first name.\n");
    s_gets(pst->fname, NLEN);
    printf("Please enter your last name.\n");
    s_gets(pst->lname, NLEN);
}

void makeinfo (struct namect * pst)
{
   
    pst->letters = strlen(pst->fname) +
    strlen(pst->lname);
}

void showinfo (const struct namect * pst)
{
   
    printf("%s %s, your name contains %d letters.\n",
           pst->fname, pst->lname, pst->letters);
}

char * s_gets(char * st, int n)
{
   
    char * ret_val;
    char * find;
    
    ret_val = fgets(st, n, stdin);
    if (ret_val)
    {
   
        find = strchr(st, '\n');   
        if (find)                  
            *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 <stdio.h>
#include <string.h>

#define NLEN 30
struct namect {
   
    char fname[NLEN];
    char lname[NLEN];
    int letters;
};

struct namect getinfo(void);
struct namect makeinfo(struct namect);
void showinfo(struct namect);
char * s_gets(char * st, int n);

int main(void)
{
   
    struct namect person;
    
    person = getinfo();
    person = makeinfo(person);
    showinfo(person);
    
    return 0;
}

struct namect getinfo(void)
{
   
    struct namect temp;
    printf("Please enter your first name.\n");
    s_gets(temp.fname, NLEN);
    printf("Please enter your last name.\n");
    s_gets(temp.lname, NLEN);
    
    return temp;
}

struct namect makeinfo(struct namect info)
{
   
    info.letters = strlen(info.fname) + strlen(info.lname);
    
    return info;
}

void showinfo(struct namect info)
{
   
    printf("%s %s, your name contains %d letters.\n",
           info.fname, info.lname, info.letters);
}

char * s_gets(char * st, int n)
{
   
    char * ret_val;
    char * find;
    
    ret_val = fgets(st, n, stdin);
    if (ret_val)
    {
   
        find = strchr(st, '\n');   
        if (find)                  
            *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 <stdio.h>
#include <string.h>   // 提供strcpy(), strlen()原型 
#include <stdlib.h>   // 提供malloc(), free()原型 
#define SLEN 81
struct namect {
   
    char * fname;  // 使用指针 
    char * lname;
    int letters;
};

void getinfo(struct namect *);        // 分配内存空间 
void makeinfo(struct namect *);
void showinfo(const struct namect *);
void cleanup(struct namect *);        // 调用该函数时释放内存 
char * s_gets(char * st, int n);

int main(void)
{
   
    struct namect person;
    
    getinfo(&person);
    makeinfo(&person);
    showinfo(&person);
    cleanup(&person);
    
    return 0;
}

void getinfo (struct namect * pst)
{
   
    char temp[SLEN];
    printf("Please enter your first name.\n");
    s_gets(temp, SLEN);
    // 分配内存一存储名 
    pst->fname = (char *) malloc(strlen(temp) + 1);
    // 把名拷贝到动态分配的内存中 
    strcpy(pst->fname, temp);
    printf("Please enter your last name.\n");
    s_gets(temp, SLEN);
    pst->lname = (char *) malloc(strlen(temp) + 1);
    strcpy(pst->lname, temp);
}

void makeinfo (struct namect * pst)
{
   
    pst->letters = strlen(pst->fname) +
    strlen(pst->lname);
}

void showinfo (const struct namect * pst)
{
   
    printf("%s %s, your name contains %d letters.\n",
           pst->fname, pst->lname, pst->letters);
}

void cleanup(struct namect * pst)
{
   
    free(pst->fname);
    free(pst->lname);
}

char * s_gets(char * st, int n)
{
   
    char * ret_val;
    char * find;
    
    ret_val = fgets(st, n, stdin);
    if (ret_val)
    {
   
        find = strchr(st, '\n');   
        if (find)                  
            *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 <stdio.h>
#define MAXTITL  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值