C语言辅导:解答之函数 |
|
www.nanhushi.com 佚名 不详 |
函数是C语言的基本构件,要成为一个优秀的程序员,必须很好地掌握函数的编写方法和使用方法。本章将集中讨论与函数有关的问题,例如什么时候说明函数,怎样说明函数,使用函数的种种技巧,等等。 在阅读本章时,请回忆你曾编写过的函数,看看你是否已尽可能提高了这些函数的效率;如果没有,请应用本章所介绍的一些技术,以提高你的程序的速度和效率。此外,请注意本章所介绍的一些实用编程技巧,其中的一些例子能有效地帮助你提高编写函数的技能。 8. 1 什么时候说明函数? 只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。例如,如果函数stat_func()只在源文件stat.c中使用,应该这样说明:
/* stat.c */ # include <atdio.h> atatic int atat_func(int,int); /* atatic declaration of atat-funcO */ void main (void); viod main (void) { ...... rc=stat_func(1,2); ...... } /* definition (body) of stat-funcO */ static int stat-funcdnt argl,int arg2) { return rc; }
在上例中,函数stat_func()只在源文件stat.c中使用,因此它的原型(或说明)在源文件stat.c以外是不可见的,为了避免与其它源文件中可能出现的同名函数发生冲突,应该将其说明为内部函数。 在下例中,函数glob_func()在源文件global.c中定义和使用,并且还要在源文件extern,c中使用,因此应该在一个头文件(本例中为proto.h)中说明,而源文件global.c和extern.c 中都应包含这个头文件。
File: proto.h /* proto.h */ int glob_func(int,int); /* declaration of the glob-funcO function * /
File: global. c /* global. c */ # include <stdio.h> # include "proto. h" /*include this file for the declaration of glob_func() */ viod main(void); viod main (void) { rc_glob_func(l,2); } /* deHnition (body) of the glob-funcO function */ int glob_func(int argl,int arg2) { return rc; }
File extern. c /* extin.c */ # include <atdio.h> # include "proto. h" /*include thia file for the declaration of glob_func() */ void ext_func(void); void ext_func(void) { /* call glob_func(), which ia deHncd in the global, c source file * / rc=glob_func(10,20); }
在上例中,在头文件proto.h中说明了函数glob_func(),因此,只要任意一个源文件包含了该头文件,该源文件就包含了对函数glob_func()的说明,这样编译程序就能检查在该源文件中glob_func()函数的参数和返回值是否符合要求。请注意,包含头文件的语句总是出现在源文件中第一条说明函数的语句之前。 请参见; 8.2 为什么要说明函数原型? 8.3 一个函数可以有多少个参数? 8.4 什么是内部函数? 8.2 为什么要说明函数原型?
函数原型能告诉编译程序一个函数将接受什么样的参数,将返回什么样的返回值,这样编译程序就能检查对函数的调用是否正确,是否存在错误的类型转换。例如,现有以下函数原型; int some_func(int,char·,long); 编译程序就会检查所有对该函数的引用(包括该函数的定义)是否使用了三个参数并且返回一个int类型的值。如果编译程序发现函数的调用或定义与函数原型不匹配,编译程序就会报告出错或警告消息。例如,对上述函数原型来说,当编译程序检查以下语句时,就会报告出错或警告消息:
x = some_func(l); /* not enough arguments passed */ x = somc_funcC*HELLOl", 1, "DUDE:"); /* wrong type of arguments used */ x = aome_funcd, sir, 2879, "T"); /* too many arguments passed */
下例中的函数调用同样是不正确的,因为函数some_func()的返回值不是一个long*类型的值。 lValue=some_func(1,str,2879); /*some_rune()returns anint,not a long* */ 同样,编译程序还能检查函数的定义(或函数体)是否与函数原型匹配。例如,当编译程序检查以下函数定义时,就会报告出错或警告消息: int some_func(char *string,longlValue,int iValue) /* wrong order Of { parameters */ ...... } 总之,在源文件中说明函数原型提供了一种检查函数是否被正确引用的机制。目前许多流行的编译程序都会检查被引用的函数的原型是否已在源文件中说明过,如果没有,就会发出警告消息。 请参见: 8.1什么时候说明函数?
8.3一个函数可以有多少个参数? 8.4什么是内部函数? 8.3 一个函数可以有多少个参数? 一个函数的参数的数目没有明确的限制,但是参数过多(例如超过8个)显然是一种不可取的编程风格。参数的数目直接影响调用函数的速度,参数越多,调用函数就越慢。另一方面,参数的数目少,程序就显得精练、简洁,这有助于检查和发现程序中的错误。因此,通常应该尽可能减少参数的数目,如果一个函数的参数超过4个,你就应该考虑一下函数是否编写得当。 如果一个函数不得不使用很多参数,你可以定义一个结构来容纳这些参数,这是一种非常好的解决方法。在下例中,函数print_report()需要使用10个参数,然而在它的说明中并没有列出这些参数,而是通过一个RPT_PARMS结构得到这些参数。
# include <atdio. h> typedef struct ( int orientation ; char rpt_name[25]; char rpt_path[40]; int destination; char output_file[25]; int starting_page; int ending_page; char db_name[25]; char db_path[40]; int draft_quality; )RPT_PARMS; void main (void); int print_report (RPT_PARMS* ); void main (void) { RPT_PARMS rpt_parm; /*define the report parameter structure variable * / /* set up the report parameter structure variable to pass to the print_report 0 function */ rpt_parm. orientation = ORIENT_LANDSCAPE; rpt_parm.rpt_name = "QSALES.RPT"; rpt_parm. rpt_path = "Ci\REPORTS" rpt_parm. destination == DEST_FILE; rpt_parm. output_file = "QSALES. TXT" ; rpt_parm. starting_page = 1; rpt_pann. ending_page = RPT_END; rpt_pann.db_name = "SALES. DB"; rpt_parm.db_path = "Ci\DATA"; rpt_pann. draft_quality = TRUE; /*call the print_report 0 function; paaaing it a pointer to the parameteM inatead of paMing it a long liat of 10 aeparate parameteM. * / ret_code = print_report(cu*pt_parm); } int print_report(RPT_PARMS*p) { int rc; /*acccM the report parametcra paaaed to the print_report() function */ oricnt_printcr(p->orientation); Kt_printer_quality((p->draft_quality == TRUE) ? DRAFT ; NORMAL); return rc; }
上例唯一的不足是编译程序无法检查引用print_report()函数时RPT_PARMS结构的10个成员是否符合要求。
请参见: 8.1 什么时候说明函数? 8.2 为什么要说明函数原型? 8.3 什么是内部函数? 8.4 什么是内部函数? 内部函数(用static关键字说明)是作用域只限于说明它的源文件的函数。作用域指的是函数或变量的可见性。如果一个函数或变量在说明它的源文件以外也是可见的,那么就称它具有全局或外部作用域;如果一个函数或变量只在说明它的源文件中是可见的,那么就称它具有局部或内部作用域。 内部函数只能在说明它的源文件中使用。如果你知道或希望一个函数不会在说明它的源文件以外被使用,你就应该将它说明为内部函数,这是一种好的编程习惯,因为这样可以避免与其它源文件中可能出现的同名函数发生冲突。 请看下例:
#include <stdio.h> int open_customer_table(void); /*global function, callable from any module * / static int open_customer_indexes(void); /*local function, used only in this module * / int open_customer_table(void) { int ret_code; /* open the customer table * / ...... if (ret_code == OK) { ret_code = opcn_customer_indexes(); } return ret_code; } static int open_customer_indexes(void) { int ret_code; /* open the index files used for this table * / ...... return ret_code; }
在上例中,函数open_customer_table()是一个外部函数,它可以被任何模块调用,而函数open_customer_indexes()是一个内部函数,它永远不会被其它模块调用。之所以这样说明这两个函数,是因为函数open_customer_indexes()只需被函数open_customer_table()调用,即只需在上例所示的源文件中使用。 请参见: 8.1 什么时候说明函数? 8.2 为什么要说明函数原型? 8.3 一个函数可以有多少个参数? 8.5 如果一个函数没有返回值,是否需要加入return语句? 在C语言中,用void关键字说明的函数是没有返回值的,并且也没有必要加入return语句。 在有些情况下,一个函数可能会引起严重的错误,并且要求立即退出该函数,这时就应该加入一个return语句,以跳过函数体内还未执行的代码。然而,在函数中随意使用return语句是一种不可取的编程习惯,因此,退出函数的操作通常应该尽量集中和简洁。 请参见: 8.8 用PASCAL修饰符说明的函数与普通C函数有什么不同? 8.9 exit()和return有什么不同? 8.6 怎样把数组作为参数传递给函数? 在把数组作为参数传递给函数时,有值传递(by value)和地址传递(by reference)两种方式。在值传递方式中,在说明和定义函数时,要在数组参数的尾部加上一对方括号([]),调用函数时只需将数组的地址(即数组名)传递给函数。例如,在下例中数组x[]是通过值传递方式传递给byval_func()函数的:
# include <atdio.h> voidbyval_func(int[]); /*the byval_func() function is passed an integer array by value * / void main (void); void main (void) { int x[10]; int y; /* Set up the integer array. * / for (y=0; y<10; y++) x[y] = y; /* Call byval_func() ,passing the x array by value. * / byval_func(x); } /* The byval_function receives an integer array by value. * / void byval_func(int i[]) { int y; /* print the content: of the integer array. * / for (y=0; y<10; y++) printf("%d\n", i[y]); }
在上例中,定义了一个名为x的数组,并对它的10个元素赋了初值。函数byval_func()的说明如下所示: intbyval_func(int []); 参数int[]告诉编译程序byval_func()函数只有一个参数,即一个由int类型值组成的数组。在调用byval_func()函数时,只需将数组的地址传递给该函数,即: byval_func(x); 在值传递方式中,数组x将被复制一份,复制所得的数组将被存放在栈中,然后由byval_func()函数接收并打印出来。由于传递给byal_func()函数的是初始数组的一份拷贝,因此在byval_func()函数内部修改传递过来的数组对初始数组没有任何影响。 值传递方式的开销是非常大的,其原因有这样几点:第一,需要完整地复制初始数组并将这份拷贝存放到栈中,这将耗费相当可观的运行时间,因而值传递方式的效率比较低;第二,初始数组的拷贝需要占用额外的内存空间(栈中的内存);第三,编译程序需要专门产生一部分用来复制初始数组的代码,这将使程序变大。 地址传递方式克服了值传递方式的缺点,是一种更好的方式。在地址传递方式中,传递给函数的是指向初始数组的指针,不用复制初始数组,因此程序变得精练和高效,并且也节省了栈中的内存空间。在地址传递方式中,只需在函数原型中将函数的参数说明为指向数组元素数据类型的一个指针。请看下例:
# include <atdio. h> void conat_func(const int* ); void main (void); void main(void) { int x[10]; int y; /* Set up the integer array. * / for (y=0; y<10; y++) x[y] = y; /* Call conat_func(), passing the x array by reference. */ conat_func(x); } /*The const_function receives an integer array by reference. Notice that the pointer i» declared aa const, which renders it unmodif table by the conat_funcO function. * / void conat_func(conat int* i) { int y; / * print the contents of the integer array. * / for (y=0; y<10; y++) printf(""%d\n", *(i+y)); }
在上例中,同样定义了一个名为x的数组,并对它的10个元素赋了初始值。函数const_func()的说明如下所示: int const_func(const int·); 参数constint·告诉编译程序const_func()函数只有一个参数,即指向一个int类型常量的指针。在调用const_func()函数时,同样只需将数组的地址传递给该函数,即: const_rune(x); 在地址传递方式中,没有复制初始数组并将其拷贝存放在栈中,const_rune()函数只接收到指向一个int类型常量的指针,因此在编写程序时要保证传递给const_func()函数的是指向一个由int类型值组成的数组的指针。const修饰符的作用是防止const_func()函数意外地修改初始数组中的某一个元素。 地址传递方式唯一的不足之处是必须由程序本身来保证将一个数组传递给函数作为参数,例如,在函数const—rune()的原型和定义中,都没有明确指示该函数的参数是指向一个由int类型值组成的数组的指针。然而,地址传递方式速度快,效率高,因此,在对运行速度要求比较高时,应该采用这种方式。
请参见: 8.8用PASCAL修饰符说明的函数与普通C函数有什么不同? 8.7 在程序退出main()函数之后,还有可能执行一部分代码吗? 可以,但这要借助C库函数atexit()。利用atexit()函数可以在程序终止前完成一些“清理”工作——如果将指向一组函数的指针传递给atexit()函数,那么在程序退出main()函数后(此时程序还未终止)就能自动调用这组函数。下例的程序中就使用了atexit()函数:
# include <stdio.h> # include <atdlib. h> void close_files(void); void print_regiatration_message(void); int main(int, char ** ); int main (int argc, char** argv) { atcxitCprint_regiatration_message); atexit(cloae_files) ; while (rec_count <max_recorda) { process_one_record ( ); } exit (0); }
在上例中,通过atexit()函数指示程序在退出main()函数后自动调用函数close_files() 和print_registration_message(),分别完成关闭文件和打印登记消息这两项工作。 在使用atexit()函数时你要注意这样两点:第一,由atexit()函数指定的要在程序终止前 执行的函数要用关键字void说明,并且不能带参数;第二,由atexit()函数指定的函数在入栈 时的顺序和调用atexit()函数的顺序相同,即它们在执行时遵循后进先出(LIFO)的原则。例 如,在上例中,由atexit()函数指定的函数在入栈时的顺序如下所示: atexit(print_registration_message); atexit(close_files); 根据LIFO原则,程序在退出main()函数后将先调用close_files()函数,然后调用print_ registration_message()函数。 利用atexit()函数,你可以很方便地在退出main()函数后调用一些特定的函数,以完成一 些善后工作(例如关闭程序中用到的数据文件)。 请参见: 8.9 exit()和return有什么不同? 8.8 用PASCAL修饰符说明的函数与普通C函数有什么不同? 用PASCAL修饰符说明的函数的调用约定与普通函数有所不同。对于普通的C函数,参数是自右至左传递的,而根据PASCAL调用约定,参数是自左至右传递的。下例是一个普通的C函数: int regular_func(int,char*,long); 根据普通C函数的调用约定,函数参数入栈时的顺序为自右至左,因此,在调用regular()函数时,其参数的入栈顺序如下所示: long char· int 当regular_func()函数返回时,调用regular_func()函数的函数负责恢复栈。 下例是一个用PASCAL修饰符说明的函数: int PASCAL pascal_func(int,char *,long); 根据PASCAL调用约定,函数参数入栈时的顺序为自左至右,因此,在调用‘pascal—func()函数时,其参数的入栈顺序如下所示: int char * long 当pascal_func()函数返回时,调用pascal_func()函数的函数负责恢复栈指针。 采用PASCAL调用约定的函数比普通C函数的效率要高一些——前者的函数调用要稍快一些。MicrosoftWindows就是一个采用PASCAL调用约定的操作环境的例子,WindowsSDK中有数百个用PASCAL修饰符说明的函数。 当Windows的第一个版本于80年代末期编写成功时,使用PASCAL修饰符能明显提高程序的执行速度。现在,计算机的运行速度已经相当快,PASCAL修饰符对程序运行速度的作用已经很小了。事实上,Microsoft在其WindowsNT操作系统中已经放弃了PASCAL调用约定。 在大多数情况下,采用PASCAL调用约定对程序的运行速度几乎没有明显的作用,因此,采用普通C函数的调用约定完全能满足编程要求。但是,当几个毫秒的运行时间对你的程序也很重要时,你就应该用PASCAL修饰符来说明你的函数。 请参见: 8.6怎样把数组作为参数传递给函数? 8.9 exit()和return有什么不同? 用exit()函数可以退出程序并将控制权返回给操作系统,而用return语句可以从一个函数中返回并将控制权返回给调用该函数的函数。如果在main()函数中加入return语句,那么在执行这条语句后将退出main()函数并将控制权返回给操作系统,这样的一条return语句和exit()函数的作用是相同的。下例是一个使用了exit()函数和return语句的程序:
#include <stdio.h> #include <stdlib.h> int main (int, char** ); int do_processing (void); int do_something_daring(); int main (int argc, char** argv) { int ret_code; if (argc <3) { printf ("Wrong number of arguments used ! \n"); /* return 1 to the operating system * / exit(1); } ret_code = do_processing (); ...... /* return 0 to the operating system * / exit(0); } int do_processing(void) { int rc; rc = do_aomcthing_daring(); if (rc == ERROR) { printf ("Something fiahy ia going on around here... *\n); /* return rc to the operating syatem * / exit (re); } /* return 0 to the calling function * / return 0; }
在上例的main()函数中,如果argc小于3,程序就会退出。语句“exit(1)”指示程序在退出时将数字1返回给操作系统。操作系统有时会根据程序的返回值进行一些相关的操作,例如许多DOS批处理文件会通过一个名为ERRORLEVEL的全局变量来检查可执行程序的返回值。 请参见: 8.5如果一个函数没有返回值,是否需要加入return语句?
|
|
|
文章录入:杜斌 责任编辑:杜斌 |
|
上一篇文章: C语言编程常见问题解答之调试 下一篇文章: 计算机等级考试C语辅导:数组 |
【字体:小 大】【发表评论】【加入收藏】【告诉好友】【打印此文】【关闭窗口】 |
|
|