14.7向函数传递结构的信息
函数的参数把值传递给函数。每个值都是一个数字---可能是int类型、float类型、可能是ASCII字符码,或者是一个地址。ANSI C之前不允许把结构作为参数传递给函数。当前的实现已经移除了这个限制,ANSI C允许把结构作为参数使用。所以程序员可以选择是传递结构本身,还是传递指向结构的指针。如果你只关心结构中的一部分,也可以把结构的成员作为参数。接下来将分析这3种传递方式,首先介绍以结构成员作为参数的情况。
14.7.1 传递结构成员
只要结构的成员是一个具有单个值的数据类型,便可把它作为参数传递给接受该特定类型的函数。程序清单14.5中的财务分析程序演示了这一点,该程序把客户的银行账户添加到他/她的储蓄和贷款账户中。
程序清单14.5 funds1.c程序
/* funds1.c -- passing structure members as arguments */
#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;
}
/* adds two double numbers */
double sum(double x, double y)
{
return(x + y);
}
/* 输出:
*/
注意,sum()函数既不知道也不关心实际的参数是否是结构的成员,它只要求传入的数据是double类型。
当然,如果需要在被调用函数中修改主调函数中成员的值,就要传递成员的地址:
modify( &stan.bankfund );
这是一个更改银行账户的函数。
把结构的信息告诉函数的第2种方法是,让被调函数知道自己正在处理一个结构。
14.7.2 传递结构的地址
这次把结构的地址作为参数。
程序清单14.6 funds2.c程序
/* funds2.c -- passing a pointer to a structure */
#include <stdio.h>
#define FUNDLEN 50
struct funds {
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(const struct funds *); /* argument is a pointer */
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);
}
/* 输出:
*/
注意,由于该函数不能改变所指向值的内容,所以把money声明为一个指向const的指针。
14.7.3 传递结构
程序清单14.7 funds3.c程序
/* funds3.c -- passing a structure */
#include <stdio.h>
#define FUNDLEN 50
struct funds {
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(struct funds moolah); /* argument is a structure */
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);
}
/* 输出:
*/
14.7.4 其他结构特性
现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做。也就是说,如果n_data和o_data都是相同类型的结构,可以这样做:
o_data = n_data; //把一个结构赋值给另一个结构
这条语句把n_data的每个成员的值都赋值给o_data的相应成员。即使成员是数组,也能完成赋值。另外,还可以把一个结构初始化为相同类型的另一个结构:
struct names right_field = {
"Ruthie", "George"
};
struct names captain = right_field; //把一个结构初始化为另一个结构
现在的C(包括ANSI C),函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。把结构作为函数参数可以把结构的信息传送给函数;把结构作为返回值的函数能把结构的信息从被调用函数传回主调函数。结构指针允许这种双向通信,因此可以选择一种方法来解决编程问题。我们通过另一组程序示例来演示这两种方法。
先编写一个程序以传播指针的方式处理结构(见程序清单14.8),然后以传递结构和返回结构的方式重写该程序。
程序清单14.8 names1.c程序
/* names1.c -- uses pointers to a structure */
#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'); // look for newline
if (find) // if the address is not NULL,
*find = '\0'; // place a null character there
else
while (getchar() != '\n')
continue; // dispose of rest of line
}
return ret_val;
}
/* 输出:
*/
makeinfo()函数使用双向传输方式传送信息。
所有这些操作中,只有一个结构变量person,每个函数都使用该结构变量的地址来访问它。一个函数把信息从自身传回主调函数,一个函数把信息从主调函数传给本身,一个函数通过双向传输来传递信息。
现在,我们来看如何使用结构参数和返回的值来完成相同的任务。第一,为了传递结构本身,函数的参数必须是person,而不是&person。那么,相应的形式参数应声明为struct namect,而不是指向该类型的指针。第二,可以通过返回一个结构,把结构的信息返回给main()。
程序清单14.9 names2.c程序
/* names2.c -- passes and returns structures */
#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'); // look for newline
if (find) // if the address is not NULL,
*find = '\0'; // place a null character there
else
while (getchar() != '\n')
continue; // dispose of rest of line
}
return ret_val;
}
/* 输出:
*/
程序中的每个函数都创建了自己的person备份,所以该程序使用了4个不同的结构,不想前面的版本只使用一个结构。
该函数的makeinfo()函数的返回机制弥补了这一点。这行代码为:
return info;
与main()中的这行结合:
person = makeinfo( person );
把存储在info中的值拷贝到person中。
14.7.5 结构和结构指针的选择
两者各有优缺点。
把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这种方法,而且执行起来很快,只需要传递一个地址。缺点是无法保护数据。被调用函数中的某些操作可能会意外影响结构中的数据。不过,ANSI C新增的const限定符解决了这个问题。例如,把函数参数声明为const形式。
把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚。假设定义了下面的结构类型:
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_vector( const struct vector *, const struct vector *, struct vector * );
...
sum vect( &a, &b, &ans );
传递结构的两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构存储给函数,而它只使用结构中的一两个成员时特别浪费。这种情况下传递指针或只传递函数所需的成员更合理。
通常,程序员为了追求效率会使用结构指针作为函数参数,如需防止原始数据被意外修改,使用const限定符,按值传递结构是处理小型结构最常用的方法。
14.7.6 结构中的字符数组和字符指针
到目前为止,我们在结构中都使用字符数组来存储字符串。是否可以说明指向char的指针来代替字符数组?例如,程序清单14.3中有如下声明:
#define LEN 20
struct names{
char first[LEN];
char second[LEN];
};
其中的结构声明是否可以这样写:
struct names{
char *first;
char *last;
};
当然可以,但是如果不理解这样做的含义,可能会有麻烦。考虑下面的代码:
struct names veep = {
"Talia", "Summers"
};
struct pnames treas = {
"Brad", "Fallingjaw"
};
printf( "%s and %s\n", veep.first, treas.first );
以上代码都没问题,也能正常运行,但是思考一下字符串本存储在何处。对于struct names类型的结构变量veep,以上字符串都存储在结构内部,结构总共要分配40字节存储姓名。然而,对于struct pnames类型的结构变量treas,以上字符串存储在编译器存储常量的地方。结构本身只存储了两个地址,在我们的系统中共16字节。尤其是,struct pnames结构不用为字符串分配任何存储空间,它使用的是存储在别处的字符串(如,字符串常量或数组中的字符串)。简而言之,在pnames结构变量中的指针应该只用来在程序中管理那些已分配和在别处分配的字符串。
我们来看看这种限制在什么情况下出问题。考虑下面的代码:
struct names accountant;
struct pnames attorney;
puts( "Enter the last name of your accountant:" );
scanf( "%s", accountant.last );
puts( "Enter the last name of your accountant:" );
scanf( "%s", attorney.last );
就语法而言,这段代码没问题。但是,用户的输入存储到哪里去了?对于会计师(accountant),它的名存储在accountant结构变量的last成员中,该结构中有一个存储字符串的数组。对于律师(attorney),scanf()把字符串放到attorney.last表示的地址上。由于这是未经初始化的变量,地址可以是任何值,因此程序可以把名放到任何地方。如果走运的话,程序不会出问题,至少暂时不会出问题,否则这一操作会导致程序崩溃。实际上,如果程序能正常运行并不是好事,因为这意味着一个未被觉察的危险潜伏在程序中。
因此,如果要用结构存储字符串,用字符数组作为成员比较简单。用指向char的指针也行,但是误用会导致严重的问题。
14.7.7 结构、指针和malloc()
如果使用malloc()分配内存并使用指针存储改地址,那么在结构中使用指针处理字符串就比较合理。这种方法的优点是,可以请求malloc()为字符串分配合适的存储空间。用这种方法改写程序清单14.9并不费劲。主要是更改结构声明(用指针代替数组)和提供一个新版本的getinfo()
函数。新的结构声明如下:
struct namect{
char *fname; //用指针代替数组
char *lname;
int letters;
};
新版本的getinfo()把用户的输入读入临时数组中,调用malloc()函数分配存储空间,并把字符串拷贝到新分配的存储空间中。对名和姓都要这样做:
void getinfo( struct namect *pst ){
char temp[SLEN];
printf( "Please enter your first name.\n" );
s_gets( temp, SLEN );
//分配内存存储名
pst->fname = (char *)malloc( strlen( temp ) + 1 );
//把名拷贝到已分配的内存
strcpy( pst->fname, temp );
printf( "Please enter your last name.\n" );
s_gets( temp, SLEN );
pst->lname = (char *)malloc( strlen( temp ) + 1 );
//把名拷贝到已分配的内存
strcpy( pst->lname, temp );
}
要理解这两个字符串都未存储在结构中,它们存储在malloc()分配的内存块中。然而,结构中存储这两个字符串的地址,处理字符串的函数通常都要使用字符串的地址。因此,不用修改程序中的其他函数。
第12章建议应该成对使用malloc()和free()。因此,还要在程序中添加一个新的函数cleanup(),用于释放程序动态分配的内存。如程序清单14.10所示.
程序清单14.10 names3.c程序
// names3.c -- use pointers and malloc()
#include <stdio.h>
#include <string.h> // for strcpy(), strlen()
#include <stdlib.h> // for malloc(), free()
#define SLEN 81
struct namect {
char * fname; // using pointers
char * lname;
int letters;
};
void getinfo(struct namect *); // allocates memory
void makeinfo(struct namect *);
void showinfo(const struct namect *);
void cleanup(struct namect *); // free memory when done
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);
// allocate memory to hold name
pst->fname = (char *) malloc(strlen(temp) + 1);
// copy name to allocated memory
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'); // look for newline
if (find) // if the address is not NULL,
*find = '\0'; // place a null character there
else
while (getchar() != '\n')
continue; // dispose of rest of line
}
return ret_val;
}
/* 输出:
*/
14.7.8 复合字面量和结构(C99)
C99的复合字面量特性可用于结构和数组。如果只需要一个临时结构值,复合字面量很好用。例如,可以使用复合字面量创建一个结构作为函数的参数或赋给另一个结构。语法是把类型名放在圆括号中,后面紧跟一个用花括号括起来的初始化列表。例如,下面是struct book类型的复合字面量:
(struct book){
"The Idiot", "Fyodor Dostoyevsky", 6.99
}
程序清单14.11中的程序示例,使用复合字面量为一个结构变量提供两个可替换的值(不是所有的编译器都支持这个特性,不过这是时间的问题)。
程序清单14.11 complit.c程序
/* complit.c -- compound literals */
#include <stdio.h>
#define MAXTITL 41
#define MAXAUTL 31
struct book { // structure template: tag is book
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
int main(void)
{
struct book readfirst;
int score;
printf("Enter test score: ");
scanf("%d",&score);
if(score >= 84)
readfirst = (struct book) {"Crime and Punishment",
"Fyodor Dostoyevsky",
11.25};
else
readfirst = (struct book) {"Mr. Bouncy's Nice Hat",
"Fred Winsome",
5.99};
printf("Your assigned reading:\n");
printf("%s by %s: $%.2f\n",readfirst.title,
readfirst.author, readfirst.value);
return 0;
}
/* 输出:
*/
还可以把复合字面量作为函数的参数。如果函数接受一个结构,可以把复合字面量作为实际参数传递:
struct rect {
double x;
double y;
};
double rect_area( struct rect r ){
return r.x * r.y;
}
...
double area;
area = rect_area( (struct rect){10.5, 20.0} );
如果函数接受一个地址,可以传递复合字面量的地址:
struct rect {
double x;
double y;
};
double rect_areap( struct rect *rp ){
return rp->x * rp->y;
}
...
double area;
area = rect_area( (struct rect){10.5, 20.0} );
复合字面量在所有函数的外部,具有静态存储期;如果复合字面量在块中,则具有自动存储期。复合字面量和普通初始化列表的语法规则相同。这意味着,可以在复合字面量中使用指定初始化器。
14.7.9 伸缩性数组成员(C99)
C99新增了一个特性:伸缩性数组成员(flexible array member),利用这项特性声明的结构,其最后一个数组成员有一些特性。第1个特性是,该数组不会立即存在。第2个特性是,使用这个伸缩性数组成员可以编写合适的代码,就好像它确实存在并具有所需要数目的元素一样。这可能听起来很奇怪,所以我们来一步步地创建和使用一个带伸缩性数组成员的结构。
首先,声明一个伸缩性数组成员有如下规则:
*伸缩型数组成员必须是结构的最后一个成员;
*结构中必须至少有一个成员;
*伸缩数组的声明类似于普通数组,只是它的方括号中是空的。
下面用一个示例来解释以上几点:
struct flex
{
int count;
double average;
double score[]; //伸缩性数组成员
};
声明一个struct flex类型的结构变量时,不能用scores做任何事,因为没有给这个数组预留存储空间。实际上,C99的意图并不是让你声明struct flex类型的变量,而是希望你声明一个指向struct flex类型的指针,然后用malloc()来分配足够的空间,以存储struct flex类型、结构的常规内容和伸缩性数组成员所需的额外空间。例如,假设用scores表示一个内含5个double类型值的数组,可以这样做:
struct flex *pf; //声明一个指针
//请求为一个结构和一个数组分配存储空间
pf = malloc( sizoef(struct flex) + 5 * sizeof(double) );
现在有足够的存储空间count、average和一个内含5个double类型值的数组。可以用指针pf访问这些成员:
pf->count = 5; //设置count成员
pf->scores[2] = 18.5; //访问数组成员的一个元素
程序清单14.12进一步扩展了这个例子,让伸缩型数组在第1中情况下表示5个值,在第2种情况下代表9个值。该程序也演示了如何编写一个函数处理带伸缩性数组元素的结构。
程序清单14.12 flexmemb.c程序
// flexmemb.c -- flexible array member (C99 feature)
#include <stdio.h>
#include <stdlib.h>
struct flex
{
size_t count;
double average;
double scores[]; // flexible array member
};
void showFlex(const struct flex * p);
int main(void)
{
struct flex * pf1, *pf2;
int n = 5;
int i;
int tot = 0;
// allocate space for structure plus array
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);
}
/* 输出:
*/
带伸缩性数组成员的结构确实有一些特殊的处理要求。
第一,不能用结构进行赋值或拷贝:
struct flex *pf1, *pf2; //*pf1和*pf2都是结构
...
*pf2 = *pf1; //不要这样做
这样做只能拷贝除伸缩性数组成员以外的其他成员。确实要进行拷贝,应使用memcpy()函数(第16章介绍)。
第二,不要以按值方式把这种结构传递给结构。原因相同,按值传递一个参数与赋值类似。要把结构的地址传递给函数。
第三,不要使用带伸缩性数组成员的结构作为数组成员或另一个结构的成员。
这种类似于在结构中最后一个成员是伸缩型数组的情况,称为struct hack。除了伸缩型数组成员在声明时用空的方括号外,struct hack特指大小为0的数组。然而,struct hack是针对特殊编译器(GCC)的,不属于C标准。这种伸缩型成员方法是标准认可的编程技巧。
14.7.10 匿名结构(C11)
匿名结构是一个没有名称的结构成员。为了理解它的工作原理,我们先考虑如何创建嵌套结构:
struct names{
char first[20];
char last[20];
};
struct person{
int id;
struct names name; //嵌套结构成员
};
struct person ted = {
8483, {"Ted", "Grass"}
};
这里,name成员是一个嵌套结构,可以通过类似ted.name.first的表达式访问"ted":
在C11中,可以通过用嵌套的匿名成员结构定义person:
struct person{
int id;
struct {
char first[20];
char last[20];
}; //匿名结构
};
初始化ted的方式相同:
struct person ted = {
8483, {"Ted", "Grass"}
};
但是,在访问ted时简化了步骤,只需把first看作是person的成员那样使用它:
puts( ted.first );
当前,也可以把first和last直接作为person的成员,删除嵌套循环。匿名特性在嵌套联合中更加有用,本章后面介绍。
14.7.11 使用结构数组的函数
程序清单14.13把前面的金融程序扩展为两人,所以需要一个内含funds结构的数组。
程序清单14.13 funds4.c程序
/* funds4.c -- passing an array of structures to a function */
#include <stdio.h>
#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);
}
/* 输出:
*/
下面是几个要点:
*可以把数组名作为数组中第1个结构的地址传递给函数。
*然后可以用数组名表示法访问数组中的其他结构。注意下面的函数调用与使用数组名效果相同:
sum( &jones[0], N );
因为jones和&jones[0]的地址相同,使用数组名是传递结构地址的一种间接的方法。
*由于sum()函数不能改变原始数据,所以该函数使用了ANSI C的限定符const。