预计阅读本页时间:-
回忆一下,函数的参数可以向函数传递值。每个值是个数字,可能是int型、float型、ASCII字符编码,或者是个地址。结构比一个单值要复杂一些,所以也难怪早期的C实现不允许把结构作为参数传递给函数。较新的C实现取消了这个限制,ANSI C允许把结构作为参数。因此,现在的C实现允许把结构作为参数传递,或把指向结构的指针作为参数传递。如果只关心结构的一部分,还可以将结构成员作为参数传递给函数。这三种方法我们都将进行研究,首先看看把结构成员作为参数来传递。
14.7.1 传递结构成员
只要结构成员是具有单个值的数据类型(即:int及其相关类型、char、float, double或指针),就可以把它作为参数传递给一个接受这个特定类型的函数。程序清单14.5所示的金融分析雏形程序就说明了这一点。这个程序是把客户的银行账户加到他(她)的储蓄和贷款账户中。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
程序清单14.5 funds1.c程序
下面是该程序的运行结果:
哈,起作用了。注意,函数sum()既不知道也不关心实际参数是不是结构的成员,它只要求参数是double类型的。
当然,如果想让被调函数影响调用函数中的成员的值,可以传递成员地址:
这是一个改变Stan的银行账户的函数。
下一个向函数传递结构信息的方法将使被调函数知道自己正在处理一个结构。
14.7.2 使用结构地址
我们还是解决前面那个问题,不过这一次我们把结构的地址作为参数。因为函数要处理funds结构,所以它也要使用funds声明。请看程序清单14.6:
程序清单14.6 funds2.c程序
这个程序同样产生下面的输出:
sum()函数使用一个指向fund结构的指针(money)作为它惟一的参数。把地址&stan传递给该函数使指针money指向结构stan。然后,使用->运算符来获取stan.bankfund和stan.savefund的值。因为函数没有改变所指向的值的内容,所以它把money声明为一个指向const的指针。
虽然没有这样使用,但是这个函数也可以访问结构的其他成员。注意,必须使用&运算符才能得到结构的地址。和数组名不一样,单独的结构名不是该结构地址的同义词。
14.7.3 把结构作为参数传递
对于允许把结构作为参数传递的编译器来讲,上一个例子可以改写为程序清单14.7所示的程序。
程序清单14.7 funds3.c程序
输出仍是这样的:
我们用struct funds类型的变量moolah代替了指向struct funds变量的指针money。调用sum()时,会根据funds模板创建一个自动变量moolah。然后,这个结构的成员被初始化为stan结构的相应成员取值的副本。因此,将使用原有结构的副本完成计算,而前面的程序(使用指针的那个)使用的是原有结构本身。因为moolah是一个结构,所以程序用的是moolah.bankfund,而不是moolah->bankfund。相反,程序清单14.6中使用了money->bankfund,因为money是一个指针,而不是一个结构。
14.7.4 其他结构特性
现在的C允许把一个结构赋值给另一个结构,不能对数组这样做。也就是说,如果n_data和o_data是同一类型的结构,可以像下面这样做:
这就使o_data的每个成员都被赋成n_data相应成员的值,即使有一个成员是数组也照样完成赋值。也可以把一个结构初始化为另一个同样类型的结构:
在现在的C(包括ANSI C)中,结构不仅可以作为参数传递给函数,也可以作为函数返回值返回。把结构作为函数参数可以将结构信息传递给一个函数,使用函数返回结构可以将结构信息从被调用函数传递给调用函数。同时,结构指针也允许双向通信,因此可以使用任一种方法解决编程问题。我们来看看另一组说明这两种方法的例子。
为了对比这两种方法,我们写一个用指针处理结构的简单程序;然后再用结构传递和结构返回来重写这个程序。程序本身要求您输入名和姓,然后告诉您姓名中的字母总数。这个程序原本并不需要结构,但是它提供了一个说明其工作原理的简单的框架。程序清单14.8给出了指针版的程序。
程序清单14.8 names1.c程序
编译并执行程序,产生如下结果:
程序的工作分配给3个由main()调用的函数来完成。person结构的地址被传递给了每个函数。
getinfo()函数把信息从它自身传递给main()。具体地,它从用户处获取姓名,通过使用指针pst定位把姓名放入person结构中。回忆一下,pst->lname是pst所指向的结构的lname成员。这就使pst->lname相当于一个char数组的名字,因此适合做gets()的参数。注意,虽然getinfo()给主程序提供了信息,但是它并没有使用返回机制,因此它是void类型的。
函数makeinfo()执行信息的双向传送。它通过使用一个指向person的指针来确定结构中存储的姓和名的位置。它使用C函数库里的函数strlen()来计算姓和名中的字母总数,然后使用person的地址存储这个总数。它的类型也是void型。最后,showinfo()函数使用一个指针定位要打印的信息。因为这个函数不改变数组的内容,所以它把指针声明为const。
在所有的操作中,只有一个结构变量person,每个函数都使用该结构的地址访问它。其中的一个函数将信息从函数自身传递给调用程序。一个函数将信息从调用程序传递给函数自身,一个函数两个工作都做。
现在,我们来看看如何使用结构参数和返回值来完成这个任务。第一,为了传递结构本身,需要使用参数person而不是&person。这样,相应的形式参数应被声明为struct namect类型,而不是声明为指向该类型的指针。第二,要把结构的值提供给main()函数,可以返回一个结构。程序清单14.9给出了不使用指针的版本。
程序清单14.9 names2.c程序
该版本的最终结果和前面的版本相同,但它使用了不同的方法。3个函数中的每一个都创建了自己的person副本,因此该程序不是只使用了 1个结构,而是使用了 4个不同的结构。
例如,考虑函数makeinfo()。在第一个程序中,传递进来的是person的地址,函数处理的是实际的person的值。在第二个版本中,创建了一个名为info的新的结构变量。person中存储的值被复制到info中,函数处理这个副本。因此,在计算字母总数时,将把值存储到info里,而不是person里。然而,返回机制弥补了这一点。makeinfo()中的下面这一行:
与main()中的行:
相结合,将info里存储的值复制到person里。注意,必须把makeinfo()函数声明为struct namect类型,因为它返回一个结构。
14.7.5 结构,还是指向结构的指针
假设您必须写一个与结构有关的函数。应该用结构指针作为参数,还是用结构作为参数和返回值呢?每种方法都有它的长处和不足。
把指针作为参数的方法的两个优点是:它既工作在较早的C实现上,也工作在较新的C实现上,而且执行起来很快;只须传递一个单个地址。缺点是缺少对数据的保护。被调函数中的一些操作可能不经意地影响到原来结构中的数据。不过,ANSI C新增的const限定词解决了这个问题。例如,如果在showinfo()函数中写入了改变结构中任何成员的代码,编译器会把它作为一个错误捕获出来。
把结构作为参数传递的一个优点是函数处理的是原始数据的副本,这就比直接处理原始数据安全。编程风格也往往更清晰。假设定义了下列结构类型:
要设置矢量ans为矢量a和矢量b的和,可以编写一个传递结构和返回结构的函数。程序就像下面这样:
对一位工程师来说,上面的形式比指针形式更自然,指针形式就像下面这样:
而且,在指针形式中,用户必须记住总和的地址应该作为第一个还是最后一个参数。
传递结构的两个主要缺点是早期的C实现可能不处理这种代码,并且这样做浪费时间和空间。把很大的结构传递给函数,但函数只使用一个或两个结构成员,这尤其浪费时间和空间。在这种情况下,传递指针或只将所需的成员作为参数传递会更合理。
通常,程序员为了追求效率而使用结构指针作为函数参数;当需要保护数据、防止意外改变数据时对指针使用const限定词。传递结构值是处理小型结构最常用的方法。
14.7.6 在结构中使用字符数组还是字符指针
前面的例子都是在结构中使用字符数组来存储字符串。您可能想知道是否可以用指向字符的指针代替字符数组。例如,程序清单14.3中有这样的声明:
可以改写成下面这样吗?
答案是可以,但是可能会遇到麻烦,除非您理解其含义。考虑下面的代码:
这是一段正确的代码,也能正常运行,但是想想字符串存储在哪里。对于struct names变量veep来说,字符串存储在结构内部;这个结构总共分配了 40字节来存放两个字符串。然而,对于struct pnames变量treas来说,字符串存储在编译器存储字符串常量的任何地方。这个结构中存放的只是两个地址而已,在我们的系统中它总共占用8个字节。struct pnames结构不为字符串分配任何存储空间。它只适用于在另外的地方已经为字符串分配了空间(例如字符串常量或数组中的字符串)。简单地说,pnames结构中的指针应该只用来管理那些已创建的而且在程序其他地方已经分配过空间的字符串。
我们来看看这个限制条件在什么情况下会升级为问题。考虑下面的代码:
从语法上看,这段代码是正确的。但是把输入存储到哪里去了?对会计师,他的名字存储在accountant变量的最后一个成员中;这个结构有一个用来存放字符串的数组。对律师,scanf()把字符串放到由attorney.last给出的地址中。因为这是个未经初始化的变量,所以该地址可能是任何值,程序就可以把名字放在任何地方。如果幸运的话,程序至少有些时候可以正常运行。否则这个操作可以使程序彻底停止。实际上,如果程序运行,那是很不幸的,因为程序中会含有您未觉察到的危险的编程错误。
因此,如果需要一个结构来存储字符串,请使用字符数组成员。存储字符指针有它的用处,但也有被严重误用的可能。
14.7.7 结构、指针和malloc()
在结构中使用指针处理字符串的一个有意义的例子是使用malloc()分配内存,并用指针来存放地址。这个方法的优点是可以请求malloc()分配刚好满足字符串需求数量的空间。可以请求4字节来存储“Joe”,请求 18字节来存储“Rasolofomasoandro”。
把程序清单14.9改写成这种方法不用很费劲。主要的两个变化是改变结构定义,使用指针而不是使用数组;然后给出getinfo()函数的新形式。
新的结构定义如下所示:
getinfo()的新形式把输入读进一个临时数组中;用malloc()分配存储空间,然后把字符串复制到新分配的空间里。对每个名字都要这样做:
要确保理解两个字符串都不是被存储在结构中,它们被保存在由malloc()管理的内存块中。然而,两个字符串的地址被存储在结构中,这些地址是字符串处理函数通常处理的对象。这样,程序中其余的函数就不必做任何改变了。
但是,就像第12章中建议的那样,在调用malloc()之后还应该调用free(),因此程序加入一个名为cleanup()的新函数,在程序使用完内存后释放内存。在程序清单14.10中可以找到这个新的函数以及程序的其余部分。
程序清单14.10 names3.c程序
下面是一个输出样本:
14.7.8 复合文字和结构(C99)
C99新增的复合文字特性不仅适用于数组,也适用于结构。可以使用复合文字创建一个被用来作为函数参数或被赋值给另一个结构的结构。语法是把类型名写在圆括号中,后跟一个用花括号括起来的初始化项目列表。例如,下面是一个struct book类型的复合文字:
程序清单14.11给出一个使用复合文字来有选择地给结构变量赋值的例子(在写本书时,许多但不是全部C编译器支持这个特性,但是时间可以解决这个问题)。
程序清单14.11 complit.c程序
也可以把复合文字作为函数参数使用。如果函数需要一个结构,可以把复合文字作为实际参数传递给它:
这就把area赋值为210.0。
如果函数需要一个地址,可以把一个复合文字的地址传递给它:
这就把area赋值为210.0。
出现在所有函数外面的复合文字具有静态存储时期,而出现在一个代码块内部的复合文字具有自动存储时期。适用于常规初始化项目列表的语法规则同样也适用于复合文字。这意味着,例如,可以在复合文字中使用指定初始化项目。
14.7.9 伸缩型数组成员(C99)
C99具有一个称为伸缩型数组成员(flexible array member)的新特性。利用这一新特性可以声明最后一个成员是一个具有特殊属性的数组的结构。该数组成员的特殊属性之一是它不存在,至少不立即存在。第二个特殊属性是您可以编写适当的代码使用这个伸缩型数组成员,就像它确实存在并且拥有您需要的任何数目的元素一样。可能这听起来有些奇怪,因此让我们开始一步一步地创建和使用具有伸缩型数组成员的结构。
首先看看声明一个伸缩型数组成员的规则:
● 伸缩型数组成员必须是最后一个数组成员。
● 结构中必须至少有一个其他成员。
● 伸缩型数组就像普通数组一样被声明,除了它的方括号内是空的。
下面是一个说明这些规则的例子:
如果声明了一个struct flex类型的变量,您不能使用scores做任何事情,因为没有为它分配任何内存空间。实际上,C99的意图并不是让您声明struct flex类型的变量;而是希望您声明一个指向struct flex类型的指针,然后使用malloc()来分配足够的空间,以存放struct flex结构的常规内容和伸缩型数组成员需要的任何额外空间。例如,假设想要用scores表示含有5个double型数值的数组,那么就要这样做:
现在您已经有一块足够大的内存,以存储count、average和一个含有5个double型数值的数组。可以使用指针pf来访问这些成员。
程序清单14.12更进一步拓展了这个例子,让伸缩型数组成员在第一种情况下代表5个数值,在第二种情况下代表9个数值。它也说明了如何编写一个处理带有伸缩型数组元素的结构的函数(目前,编译器对伸缩型数组成员的支持比对复合文字结构的支持要普遍)。
程序清单14.12 flexmemb.c程序
下面是输出:
14.7.10 使用结构数组的函数
假设需要用一个函数处理结构数组。因为数组的名称等同于它的地址,所以可以把数组名传递给函数。再一次,函数需要访问结构模板。要说明这如何工作,程序清单14.13将有关货币的程序扩展到两个人,以具有一个含有两个funds结构的数组。
程序清单14.13 funds4.c程序
输出如下:
(多么凑巧的一个总额啊!估计您会认为这些数据是杜撰出来的。)
数组名jones是数组的地址。具体地,它是数组第一个元素,即结构jones[0]的地址。因此,指针money最初是由这个表达式给出的:
因为money指向数组jones的第一个元素,所以money[0]是该数组的第一个元素的另一个名称。同样,money[1]是第二个元素。每个元素是一个funds结构,所以每个元素都可以使用点(.)运算符来访问其结构成员。
下面这些是要点:
● 可以用数组名把数组中第一个结构的地址传递给函数。
● 然后可以使用数组的方括号符号来访问数组中的后续结构。注意下面的函数调用和使用数组名有同样的效果:
因为二者都指向同一地址。使用数组名只是传递结构地址的一种间接方法。
● 因为函数sum()不用来改变原来的数据,所以我们使用ANSI C的限定词const。