第八章 函数
8.1函数概述
函数是用于完成特定任务的程序代码的自包含单元。函数的优点:第一,函数的使用可以省去重复的代码编写。第二,即使某种功能在程序中只使用一次,将其以函数的形式实现也是有必要的,因为函数使得程序更加模块化,从而更有利于程序的阅读、修改和完善。例如,您想编写一个实现以下功能的程序:
●读入一行数字
●对数字进行排序
●找到它们的平均值
●打印出一个柱状图
程序如下:
#include <stdio.h>
#define SIZE 50
int main(void)
{
float list[SIZE];
readlist(list,SIZE);
sort(list,SIZE);
average(list,SIZE);
bargraph(list,SIZE);
return 0;
}
编写代码之前首先需要考虑的是函数的功能以及函数和程序整体上的关系。
8.1.1编写和使用一个简单的函数
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月26日07:26:08
#include <stdio.h>
#define NAME "GIGATHINK INC. "
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolis,CA 94904"
#define WIDTH 40
void starbar(void);//声明函数原型
int main(void)
{
starbar();
printf("%s\n",NAME);
printf("%s\n",ADDRESS);
printf("%s\n",PLACE);
starbar();//使用函数
return 0;
}
void starbar(void)//定义函数
{
int count;
for (count = 1; count <= WIDTH; count++)
putchar('*');
putchar('\n');
}
结果:
GIGATHINK INC.
101 Megabuck Plaza
Megapolis,CA 94904
****************************************
8.1.2程序分析
关于这个程序有以下几点需要注意:
●starbar标识符在不同位置被使用了3次:函数原型告知编译器starbar()的函数类型,函数调用导致该函数的执行,而函数定义则确切制定了该函数的具体功能。
●函数同变量一样有多种类型。任何程序在使用函数之前都需要声明该函数的类型。因此,下面这个ANSI C风格出现在main()函数定义之前:
void starbar(void);
圆括号表示starbar是一个函数名。第一个void是函数返回值类型;第二个void表示该函数不接受任何参数。分号的作用是表示该语句是进行函数声明而不是函数的定义。也就是说,这一行声明了程序将使用一个名为starbar()且函数类型为void的函数,同时通知编译器需要在其他位置找到该类型的定义。
●程序把starbar()原型都置于main()之前;也可将其至于main()之内,可以放置变量声明的任何位置。
●程序在main()中通过使用函数名后跟圆括号和分号的格式调用函数starbar(),语句如下:
starbar();
●程序中starbar()和main()具有相同的定义格式,即首先以类型、名称和圆括号开始,接着是花括号。注意此处的starbar()后没有分号,这告诉编译器您是在定义函数starbar(),而不是在调用它或声明它的原型。
函数头
---------------------------
#include <stdio.h>
#define WIDTH 40
void starbar(void)
---------------------------
函数体
---------------------------
{
int count;
for(count;---)
putchar('*');
putchar('\n');
}
---------------------------
●程序把starbar()和main()放在同一个文件中,也可以将他们放在不同的文件中。单文件形式比较容易编译,而使用两个文件则有利于在不同的程序中使用相同的函数。
●starbar()中count是一个局部变量。这意味着该变量只在starbar()中可用。即使在其他函数(包括main()函数)中使用名称count,也不会出现任何冲突。
8.1.3函数参数
遵循C的设计思想,不应为每个任务编写一个单独的函数,而应该编写一个可以同时胜任这两个任务的更为通用的函数。
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月26日08:08:41
#include <stdio.h>
#include <string.h> //为strlen()提供原型
#define NAME "GIGATHINK,INC. "
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolis, CA 94904"
#define WIDTH 40
#define SPACE ' '
void show_n_char(char ch, int num);
int main()
{
int spaces;
show_n_char('*',WIDTH); //使用常量作为参数
putchar('\n');
show_n_char(SPACE,12); //使用常量作为参数
printf("%s\n",NAME);
spaces = (WIDTH - strlen(ADDRESS))/2;//让程序计算需要跳多少空格
show_n_char(SPACE,spaces); //使用变量作为参数
printf("%s\n",ADDRESS);
show_n_char(SPACE,(WIDTH - strlen(PLACE))/2); //用一个表达式作为参数
printf("%s\n",PLACE);
show_n_char('*',WIDTH);
putchar('\n');
return 0;
}
//show_n_char()定义
void show_n_char(char ch, int num)
{
int count;
for(count = 1; count <= num; count++)
putchar(ch);
}
结果:
****************************************
GIGATHINK,INC.
101 Megabuck Plaza
Megapolis, CA 94904
****************************************
8.1.4定义带有参数的函数:形式参数
函数定义以下面的ANSI C函数头开始:
void show_n_char(char ch, int num)
这行代码通知编译器show_n_char( )使用名为ch和num的两个参数,并且这两个参数的类型分别是char和int。变量ch和num被称为形式参数或形式参量(更正式)。如同函数内部定义的变量一样,形式参量是局部变量,它们是函数所私有的。这意味着可以在其他函数中使用相同的变量名。每当调用函数时,这些变量就会被赋值。
8.1.5带函数声明的原型声明
void show_n_char(char , int );
在原型中使用变量并没有实际地创建变量。这只是说明char代表了一个char类型变量,以此类推。
8.1.6调用带有参数的函数:实际参数
形式参量是被调函数中的变量,而实际参数是调用分配给被调函数变量的特定数值。实际参量可以是常量、变量或一个复杂的表达式。但是无论何种形式的实际参数,执行时首先要计算其值,然后将改值赋值给被调函数中相应的形式参量。
8.1.7使用return从函数中返回一个值
用来测试函数的程序被称为驱动程序。
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月26日09:49:58
#include <stdio.h>
int imin(int ,int );
int main(void)
{
int evil1,evil2;
printf("Enter a pair of integers(q to quit):\n");
while (scanf("%d %d", &evil1, &evil2) == 2)
{
printf("The lesser of %d and %d is %d.\n",evil1,evil2,imin(evil1,evil2));
printf("Enter a pair of integers(q to quit):\n");
}
printf("Bye!\n");
return 0;
}
int imin(int n, int m)
{
int min;
if(n < m)
min = n;
else
min = m;
return min;
}
结果:
Enter a pair of integers(q to quit):
509 333
The lesser of 509 and 333 is 333.
Enter a pair of integers(q to quit):
-9393 6
The lesser of -9393 and 6 is -9393.
Enter a pair of integers(q to quit):
q
Bye!
return的另一个作用是在终止执行函数,并把控制返回给调用函数的一下个语句。
8.1.8函数类型
函数应该进行类型声明。同时其类型和返回值相同。而无返回值的函数应该被称为void类型。
为正确使用函数,程序在首次调用前需要知道该函数的类型。途径之一是在第一次调用之前进行完整的函数定义。但是,这种方式会使得程序难于阅读。而且,需要的函数可能在C库函数或其他文件中。因此,通常的做法是预先对函数进行声明,以便将函数信息通知给编译器。函数声明需要在使用函数之前进行。
在ANSI C标准库中,函数被分为几个系列,每一个系列都有各自的头文件。这些头文件包含了本系列函数的声明部分。
函数声明只是将函数类型告诉编译器,而函数定义部分则是函数的实际实现代码。
8.2函数原型
8.2.1产生的问题
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月26日10:11:28
#include <stdio.h>
int imax();
int main(void)
{
printf("The maxmium of %d and %d is %d.\n",3,5,imax(3));
printf("The maxmium of %d and %d is %d.\n",3,5,imax(3.0,5.0));
return 0;
}
int imax(n,m)
int n,m;
{
int max;
if(n>m)
max = n;
else
max = m;
return max;
}
结果:
The maxmium of 3 and 5 is 9375779.
The maxmium of 3 and 5 is 1074266112.
因为各个操作系统的内部机制不同,所以出现以上错误的具体情况也不相同。当使用PC或VAX时,程序执行过程是这样的:调用函数首先把参数放在一个被称为堆栈(stack)的临时存储区里,然后被调函数从堆栈中读取这些参数。但是这两个过程并没有相互协调进行。调用函数根据调用过程中的实际参数类型确定需要传递的数值类型,但是被调函数是根据其形式参数的类型进行数据读取的,因此,函数调用imax(3)把一个整数放在堆栈中。当函数imax()开始执行时,它会从堆栈中读取两个整数。而实际上只有一个需要的数值被存储在堆栈中,所以第二个读出的数据就是当时恰好在堆栈中的其他数值。
8.2.2ANSI C的解决方案
针对以上出现的问题,ANSI C标准的解决方案是在函数声明中同时说明所使用的参数类型。即使用函数原型来声明返回值类型、参数个数以及各参数的类型。
int imax(int ,int );
int imax(int a,int b);
使用这些函数原型信息,编译器就可以检查函数调用语句是否和其原型声明相一致。比如检查参数个数是否正确,参数类型是否匹配。如果有一个参数类型不匹配但是都是数值类型,编译器会把实际参数值转换成和形式参数类型相同的的数值。
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月26日10:46:07
#include <stdio.h>
int imax(int,int);//原型
int main(void)
{
printf("The maximum of %d and %d is %d.\n",3,5,imax(3));
printf("The maximum of %d and %d is %d.\n",3,5,imax(3.0,5.0));
return 0;
}
int imax(int n, int m)
{
int max;
if(n>m)
max = n;
else
max = m;
return max;
}
编译时程序会给出一个错误信息,声称调用函数imax()时传递地的参数太少。
8.2.3无参数和不确定参数
一些函数(比如printf()和scanf())使用的参数个数是变化的。对于这种情况,ANSI C允许使用不确定的函数原型。例如,对于printf()可以使用下面的原型声明:
int printf(char *,...);
对于参数个数不确定的函数,C库通过stdarg.h头文件提供了定义该类型函数的标准方法。
8.2.4函数原型优点
函数原型是对语言的有力补充。它可以使编译器发现函数使用时可能出现的错误或疏漏。
8.3递归
C允许一个函数调用其本身(递归)。递归可以代替循环语句使用。有些情况下使用循环更好,而有些时候使用递归更有效。递归方法虽然使程序结构优美,但是其执行效率却没有循环语句高。
8.3.1递归的使用
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月27日07:46:28
#include <stdio.h>
void up_and_down(int);
int main(void)
{
up_and_down(1);
return 0;
}
void up_and_down(int n)
{
printf("Level %d: n location %p\n",n,&n);
if(n<4)
up_and_down(n+1);
printf("LEVEL %d: n location %p\n",n,&n);
}
结果:
Level 1: n location 0057F78C
Level 2: n location 0057F6B4
Level 3: n location 0057F5DC
Level 4: n location 0057F504
LEVEL 4: n location 0057F504
LEVEL 3: n location 0057F5DC
LEVEL 2: n location 0057F6B4
LEVEL 1: n location 0057F78C
8.3.2递归的基本原则
第一,每一级的函数调用都有自己的变量.
第二,每一次函数调用都会有一次返回.
第三,递归函数中,位于递归调用前的语句和各级被调用函数具有相同的执行顺序.
第四,递归函数中,位于递归调用后的语句的执行顺序和各个被调用函数的顺序相反.
第五,虽然每一级递归都有自己的变量,但是函数代码并不会得到复制.
8.3.3尾递归
最简单的递归形式是把递归调用语句放在函数结尾恰在return语句之前。由于尾递归的作用相当于一条循环语句,所以它是最简单的递归形式。
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月27日08:00:20
#include <stdio.h>
long fact(int n);
long rfact(int n);
int main(void)
{
int num;
printf("This program calculates factorials.\n");
printf("Enter a value in the 0-12(q to quit):\n");
while ( scanf("%d", &num) == 1 )
{
if(num < 0)
printf("No negative numbers,please.\n");
else if( num > 12 )
printf("Keep intput under 13.\n");
else
{
printf("loop:%d factorial = %ld\n",num,fact(num));
printf("recursion: %d factorial = %ld\n",num,rfact(num));
}
printf("Enter a value in the range 0-12(q to quit):\n");
}
printf("Bye.\n");
return 0;
}
long fact( int n )//使用循环计算阶乘
{
long ans;
for ( ans = 1; n > 1; n-- )
ans *= n;
return ans;
}
long rfact( int n )//使用递归计算阶乘
{
long ans;
if( n > 0 )
ans = n* rfact(n-1);
else
ans = 1;
return ans;
}
结果:
This program calculates factorials.
Enter a value in the 0-12(q to quit):
5
loop:5 factorial = 120
recursion: 5 factorial = 120
Enter a value in the range 0-12(q to quit):
10
loop:10 factorial = 3628800
recursion: 10 factorial = 3628800
Enter a value in the range 0-12(q to quit):
q
Bye.
循环和递归都可以函数。如何选择?一般来讲,选择循环更好一些。首先,因为每次递归调用都拥有自己的变量集合,所以就要占用较多的内存;每次递归调用需要把新的变量集合存储在堆栈中。其次,由于进行每次函数调用需要花费一定的时间,所以调用的执行速度较慢。因为尾递归是最简单的递归形式,比较容易理解;而且在某些情况下,我么不能使用简单的循环语句代替递归,所以就有必要学习递归的方法。
8.3.4递归和反向计算
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月27日08:30:11
#include <stdio.h>
void to_binary( unsigned long n );
int main(void)
{
unsigned long number;
printf("Enter an integer(q to quit): \n");
while( scanf("%lu", &number) == 1 )
{
printf("Binary equivalent:");
to_binary(number);
putchar('\n');
printf("Enter an integer(q to quit): \n");
}
printf("Done.\n");\
return 0;
}
void to_binary(unsigned long n)//递归函数
{
int r;
r = n % 2;
if( n >= 2 )
to_binary( n / 2 );
putchar( '0' + r );
return;
}
结果:
Enter an integer(q to quit):
9
Binary equivalent:1001
Enter an integer(q to quit):
255
Binary equivalent:11111111
Enter an integer(q to quit):
1024
Binary equivalent:10000000000
Enter an integer(q to quit):
q
Done.
8.3.5递归的优缺点
其优点在于为某些编程问题提供了最简单的解决方法,而缺点是一些递归算法很快就耗尽计算机的内存资源。同时,使用递归的程序难于阅读和维护;
8.4多源代码文件
8.4.1头文件的使用
如果把main()函数放在第一个文件中二把自定义函数放在第二个文件中实现,那么第一个文件仍需要使用函数原型。如果把函数原型放在第一个头文件中,就不必每次使用这些函数时输入其原型声明。
编写程序的过程中需要经常使用C的预处理器定义常量。而定义的常量只能用于包含相应的#define语句的文件。如果程序中的函数分别放在不同的文件中,那么就必须使用定义常量的#define指令对每个文件都可用。而直接在每个文件中键入该指令的方法既耗时又容易出错,同时也会带来一个人维护上的问题:即如果修改了一个使用#define定义的数值,那么必须在每一个文件中对其进行修改。比较好的解决方法是把所有的#define指令放在一个头文件中,然后在每个源代码文件中使用#include语句引用该头文件。总之,把函数原型和常量定义放在一个头文件中是一个很好的编程习惯。
程序:
//usehotel.c
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月27日09:07:33
#include <stdio.h>
#include "hotel.h"//定义常量、声明函数
int main(void)
{
int nights;
double hotel_rate;
int code;
while ( (code = menu()) != QUIT )
{
switch (code)
{
case 1:hotel_rate = HOTEL1;break;
case 2:hotel_rate = HOTEL2;break;
case 3:hotel_rate = HOTEL3;break;
case 4:hotel_rate = HOTEL4;break;
default:hotel_rate = 0.0;
printf("Oops!\n");break;
}
nights = getnights();
showprice( hotel_rate, nights );
}
printf("Thank you and goodbye.");
return 0;
}
//hotel.c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include "hotel.h"
int menu(void)
{
int code,status;
printf("\n%s%s\n",STARS,STARS);
printf("Enter the number of the desired hotel:\n");
printf("1) Fairfield Arms 2) Hotel Olympic\n");
printf("3) Chertworthy Plaza 4) The Stockon\n");
printf("5) quit\n");
printf("%s%s\n",STARS,STARS);
while ( (status = scanf("%d", &code)) != 1 || ( code < 1 || code > 5 ) )
{
if( status != 1 )
scanf("%*s");
printf("Enter an integer from 1 to 5,please.\n");
}
return code;
}
int getinights(void)
{
int nights;
printf("How many nights are needed?");
while( scanf("%d", &nights) != 1 )
{
scanf("%*s");
printf("Please enter an integer, such as 2.\n");
}
return nights;
}
void showprice(double rate, int nights)
{
int n;
double total = 0.0;
double factor = 1.0;
for( n = 1; n <= nights; n++, factor *=DISCOUNT )
total += rate * factor;
printf("The total cost will be $%0.2f.\n",total);
}
//hotel.h
#define QUIT 5
#define HOTEL1 80.0
#define HOTEL2 125.0
#define HOTEL3 155.0
#define HOTEL4 200.0
#define DISCOUNT 0.95
#define STARS "******************************************"
//给出选项列表
int menu(void);
//返回天数
int getnights(void);
//按饭店的星级和预定天数计算价格显示出来
void showprice(double, int);
8.5地址运算符
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月28日08:21:20
#include <stdio.h>
void mikado(int); //声明函数
int main(void)
{
int pooh = 2, bah = 5;//main()函数中的局部变量
printf("In main(), pooh = %d and &pooh = %p\n",pooh,&pooh);
printf("In main(), bah = %d and &bah = %p\n",bah,&bah);
mikado(pooh);
return 0;
}
void mikado(int bah)//定义函数
{
int pooh = 10;//mikado()函数中的局部变量
printf("In mikado(), pooh = %d and &pooh = %p\n",pooh,&pooh);
printf("In mikado(), bah = %d and &bah = %p\n",bah,&bah);
}
结果:
In main(), pooh = 2 and &pooh = 0073FAD0
In main(), bah = 5 and &bah = 0073FAC4
In mikado(), pooh = 10 and &pooh = 0073F9DC
In mikado(), bah = 2 and &bah = 0073F9F0
8.6改变调用函数中的变量、
程序:
//swap1.c
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月28日08:29:51
#include <stdio.h>
void interchange( int u, int v );//声明函数
int main(void)
{
int x = 5,y = 10;
printf("Originally x = %d and y = %d.\n",x,y);
interchange(x,y);
printf("Now x = %d and y = %d.\n",x,y);
return 0;
}
void interchange(int u, int v)//定义函数
{
int temp;
temp = u;
u = v;
v = temp;
}
结果:
Originally x = 5 and y = 10.
Now x = 5 and y = 10.
程序:
#define _CRT_SECURE_NO_WARNINGS 1
//2022年4月28日08:52:58
#include <stdio.h>
void interchange( int u, int v );
int main(void)
{
int x = 5, y = 10;
printf("Originally x = %d and y = %d.\n", x, y);
interchange( x, y );
printf("Now x = %d and y = y = %d.\n", x, y);
return 0;
}
void interchange(int u, int v)
{
int temp;
printf("Originally u = %d and v = %d.\n", u, v);
temp = u;
u = v;
v = temp;
printf("Now u = %d and v = %d.\n", u, v);
}
结果:
inally x = 5 and y = 10.
Originally u = 5 and v = 10.
Now u = 10 and v = 5.
Now x = 5 and y = y = 10.
8.6指针简介
程序:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void interchange( int * u, int * v );
int main(void)
{
int x = 5, y = 10;
printf("Originally x = %d and y = %d.\n", x, y);
interchange(&x, &y);//向函数产地地址
printf("Now x = %d and y = %d.\n", x, y);
return 0;
}
void interchange( int * u, int * v )
{
int temp;
temp = *u;
*u = *v;
*v = temp;
}
结果:
Originally x = 5 and y = 10.
Now x = 10 and y = 5.
●变量的地址可以被看作是在计算机中变量的名称
●函数返回值类型指的是函数返回给它的调用函数的数值类型。如果函数返回值的类型和声明的类型不相同时,实际返回值是当把指定要返回的值赋给一个具有所声明的返回类型的变量时得到的数值。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)