《C primer plus》第六版 第九章 函数(附源码)

《C primer plus》第六版 第九章 函数(附源码)

9.1 函数概述

什么是函数?
函数是用于完成特定的任务的程序代码的自包含单元

为什么使用函数?
a.函数可以省去重复的代码编写
b.使程序模块化,从而利于程序的阅读,修改,完善

例如完成如下功能: 1: 读入一行数字 2:对数字进行排序 3:找到他们的平均值 4:打印出一个柱状图

可以编写如下函数
#include <stdio.h>
#define SIZE 50
int main()
{
    float list[SIZE];
    readlist(list,SIZE);
    sort(list,SIZE);
    average(list,SIZE);
    bargraph(list,SIZE);
    return 0;
//以上函数功能需要您自己编写
}

描述性函数可以清楚的表明程序的功能和组织结构

许多程序员喜欢把函数当做黑盒子,而黑盒子的内部暂时不需要考虑。编写代码前需要考虑的是函数的功能以及函数和程序整体上的关系

9.1.1 编写和使用一个简单的函数

源代码

//lethead.c
#include <stdio.h>
#define NAME "GIGATHINK, INC. "
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolils,CA 94904"
#define WIDTH 40

void starbar(void);

int main()
{
	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');
 }

9.1.2 程序分析

void starbar(void);
//分号的作用是表示该语句是进行的函数的声明而不是函数的定义

一些老版本的编译器不能识别void类型,这时需要把没有返回值的函数声明为int类型,并在程序末尾加 return 0; 语句。

流程控制
lethead1.c的程序控制

上面为依次执行每个函数 以及 每个函数可以调用其他函数 (我们可以根据此流程图想一想递归函数的执行)

程序把starbar( ) 和 main( )包含在同一个文件中,您可以把他们放在不同的两个文件当中。单文件形式比较容易编译,而使用两个文件则有利于在不同的程序中使用相同的函数,如果您把函数写在了另一个单独的文件中,则在那个文件中必须加入#define 和 #include指令。

starbar( )中的变量count是一个局部变量(local)变量, 这意味着该变量只在starbar( )中可用。即使其他函数(包括main函数)中使用名称count也不会出现冲突

9.1.3 函数参数

遵循 C 的设计思想,我们不应该为每一个任务编写一个函数,而是应该编写一个可以同时胜任两个任务的更通用的函数。

源代码

//lethead2.c
#include <stdio.h>
#include <string.h>
#define NAME "GIGATHING, 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; // brillant !
	
	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;
}

void show_n_char(char ch, int num)
{
	int count;
	
	for(count=1; count<= num; count++)
	putchar(ch);
}

9.1.4 定义带有参数的函数:形式参量

ANSI C也接受ANSI之前的形式,但将其视为废弃不用的形式

//旧形式举例
void show_n_char(ch, num)
char ch;
int num;
/*
此处花括号是参数列表名,而参数类型的声明是在后面给出的
*/

制定标准的目的是为了淘汰ANSI之前的形式。为了理解之前的代码,您也需要了解ANSI之前的形式,但是以后要尽量使用新的形式

9.1.5 带参数函数的原型声明

//ANSI C 也支持旧的函数声明形式,即圆括号内不带有任何参数
void show_n_char();

9.1.6 调用带有参数的函数:实际参数

9.1.7 黑盒子观点

9.1.8 使用return从函数中返回一个值

…,同时为了检查imin( )函数的执行结果,需要编写一个简单的main( )函数。这种用来测试的函数的程序有时被称作**驱动程序(driver)。**驱动程序实际调用了这个被测试的函数,如果成功通过了测试,那么他将在一个更重要的程序中用到。…

源代码:

//lesser.c
#include <stdio.h>
int imin(int, int);
int main()
{
	int evill,evil2;
	
	printf("Enter a pair of integers ( q to quit ): \n");
	while(scanf("%d %d",&evill, &evil2)==2) //beautiful code!
	{
		printf("The lesser of %d and %d is %d.\n",evill, evil2, imin(evill, 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;
}

imin( ) 是函数中的局部变量。函数调用imin( evil1, evil2)只是复制了两个变量的值

如果函数返回值类型和函数声明的类型不一致会怎样呢?请看下面分析程序

int what_if(int n)
{
	double z = 100.0/(double)n;
	return z;
}

假如输入的 n 的值为 64, z 的最终结果是多少呢?函数的最终返回值应该是多少呢?

正确答案为:这将把数值 1.5625 赋值给 z, 然后,return 语句返回的则是int 类型的数值 1

许多程序员更倾向于只在函数的结尾写一个return语句,因为这样做更有利于阅读程序的人明白函数的执行过程。

return;
这个语句会终止执行函数并把控制返回给调用他的函数。因为return后面没有任何表达,所以这种形式上只能用于void类型的函数中

9.1.9 函数类型

C99标准不再支持函数的int 类型的默认设置

9.2 ANSI C的函数原型

9.2.1 产生的问题

源代码

//misuse.c
#include <stdio.h>
int main()
{
	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(n,m)
int n,m;
{
	int max;
	if(n>m)
		max=n;
	else
		max=m;
	
	return max;
}

使用两种编译器都可以编译通过,只不过他们因为没有使用函数原型而产生了错误。
运行的时候发生了什么呢?
因为操作系统的内部机制不同,所以出现以上错误的具体情况也不相同。当使用PC 或 VAX时, 程序执行过程是这样的:调用函数首先把参数放在一个被称为堆栈(stack)的临时存储区域里,然后被调用函数从堆栈中读取这些参数。但是两个过程并没有相互协调进行。调用函数根据调用过程中的实际参数类型确定需要传递的数值类型,但是被调用函数是根据其形式参数进行数据读取的,因此,函数调用imax(3)把一个整数放在堆栈中。当函数imax()开始执行时,它会从堆栈中读取两个整数。而实际上只有一个需要的数值被存储在堆栈中,所以第二个读出来的数据就是当时正好在堆栈中的其他数值。
第二次使用函数imax( )函数时,传递的是float()类型的数值。这时两个double类型的数值被放在堆栈中,(回忆一下,作为参数传递时,float类型数据会被转换为double类型数据),而在我们使用的操作系统中,这意味着两个64位的数值,即共128位的数据存储在堆栈中。因为这个系统中的int类型是32位,所以当imax()从堆栈中读取两个int类型的数值时,它会读出堆栈的前64位数据,把这些数据对应于两个整数,其中较大的一个就是107426612

9.2.2 ANSI的解决方案

第一种形式使用逗号对参数类型进行了分割;而第二种形式在类型后加入了变量名。需要注意的是这些变量名只是虚设的名字,他们不必和函数定义中使用的类型名相匹配。

源代码

#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.5,5.0));
	return 0;
}

int imax(int n, int m)
{
	int max;
	
	if(n>m)
		max=n;
	else
		max=m;
	
	return max;
}

9.2.3 无参数和不确定参数

9.2.4 函数原型的优点

9.3 递归

当一个函数调用自己时,如果编程中没有设定可以终止递归的条件检测,他会无限制的进行递归调用,所以要进行谨慎处理。

递归一般可以代替循环语句使用

9.3.1 递归的使用

概念:第一级递归,第二级递归,…

源代码

//recur.c
#include <stdio.h>
void up_and_down(int);
int main()
{
	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);
}

如果对此递归过程感到疑惑,可以进行一系列函数调用,即使用fun1()调用了fun2(), fun2() 调用了fun3(), fun4()执行完后,fun3()会继续执行。而fun3()执行完后,开始继续执行fun2()。最后fun2()返回到fun1()中并执行后续代码。递归过程也是如此,只不过fun1(),fun2(),fun3(),fun4()在这里是相同的函数。

9.3.2 递归的基本原理

第一每一级的函数调用都有自己的变量
第二每一次函数调用都有一次返回
第三递归函数中,位于递归调用前的语句和各级被调用函数具有相同的执行顺序
第四递归函数中,位于递归调用后的语句执行顺序和各个被调用函数的顺序相反
第五虽然每一级别的函数都有自己的变量,但是函数代码并不会得到赋值,函数代码是一系列的计算机指令,而函数调用就是从头到尾执行这个指令集的一条命令
最后递归函数必须包含可以终止递归调用的语句

9.3.3 尾递归

最简单的递归形式就是把递归调用语句放在函数结尾,即恰好在return语句之前,这种形式的递归被称为尾递归
由于尾递归相当于是一个循环语句,所以他是最简单的递归形式

源代码

//factor.c
#include <stdio.h>
long fact (int n);
long rfact(int n);
int main()
{
	int num;
	
	printf("This program calculates factors.\n");
	printf("Enter a value in the range 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 input nuder 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;
 }

使用循环方法的函数把ans初始化为 1

既然循环和递归都可以实现函数,那么用谁呢?请看下面分析
一般而言,循环更好
首先,因为每次递归都需要拥有自己的变量集合,所以就需要占用较多的每次调用函数需要把新的变量集合存储在堆栈中,
其次,由于每次进行函数的调用需要花费时间,所以递归的执行速度较慢

9.3.4 递归和反向计算

…,为了得出下一个数字,需要把原数值除以2.这种计算就相当于在十进制下把小数点左移一位。如果此时得出的数值是偶数,则下一个数值的二进制的数值是0;若得到的是计数,则下一个二进制数值就是1,…

//binary.c
#include <stdio.h>
void to_binary(unsigned long n);

int main()
{
	unsigned long number;
	printf("Enter an intger ( q to quit ): \n");
	while(scanf("%ul",&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 ;
}

当然,不适用递归函数也能实现这个算法,但是由于本算法先计算出最后一位数值,所以在显示结果之前必须对所有的数值进行存储(比如放在一个数组中),

9.3.5 递归的优缺点

优点在于为某些编程问题提供了最简单的解决问题的方法,而缺点在于一些递归算法会很快的耗尽计算机资源。同时使用递归的程序难于阅读与维护。

关于递归深度问题,书中举例了斐波那契数列,点此查看代码
另外关于递归函数的使用:递归求阶乘
递归将十进制数转换为二进制
这个函数使用了双重递归:也就是函数对本身进行了两次调用。
但是这样做会很容易耗尽计算机资源

9.4 多源代码文件程序的编译

9.4.1 UNIX

首先假设UNIX系统下安装了标准的UNIX C编译器 cc。文件file1.c 和文件file2.c 中包含有C 函数。下面的命令将这两个文件编译在一起并生成可执行文件 a.out:

$ cc file1.c file2.c

另外还将生成两个目标文件file1.o 和 file2.o。如果随后只更改了文件file1.c 而file2.c 没有改变,使用下面命令编译第一个文件并将其链接到第二个文件的目标代码:

$ cc file1.c file2.o

9.4.2 Linux

首先假设UNIX系统下安装了标准的GUN C编译器 gcc。文件file1.c 和文件file2.c 中包含有C 函数。下面的命令将这两个文件编译在一起并生成可执行文件 a.out:

$ gcc file1.c file2.c

另外还将生成两个目标文件file1.o 和 file2.o。如果随后只更改了文件file1.c 而file2.c 没有改变,使用下面命令编译第一个文件并将其链接到第二个文件的目标代码:

$ gcc file1.c file2.o

9.4.3 DOS命令行编译器

大多数DOS文件命令行编译器的工作机制同UNIX系统下的cc命令行编译器类似。一个不同之处在于DOS系统下的目标文件扩展名为.obj而不是.o。而且有些编译器并不生成目标文件代码,而是生成汇编语言和其他特殊代码的中间文件

9.4.4 Windows 和 Macintosh编译器@

使用这两种编译器运行单文件程序是,必须创建工程。而对于多文件程序,需要使用相应的菜单命令行将源代码文件加入一个工程中。而且,工程中必须包含所有源代码文件(扩展名为.c的文件)。但是头文件(扩展名为.h的文件)不能包含在工程中。因为工程只管理所使用的的源文件的代码。而使用了哪些头文件则由源代码文件中的#include指令确定

9.4.5 头文件的使用

总之,把函数原型和常量定义放在一个头文件中是一个很好的编程习惯。

源代码

//usehotel.c
//与程序清单9.10一起编译
#include <stdio.h>
#include "hotel.h"
int main()
{
	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
#include <stdio.h>
#inlcude "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 Stockton\n");
	ptintf("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 getnights(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.00
#define HOTEL2 125.00
#define HOTEL3 155.00
#define HOTEL4 200.00
#define DISCOUNT 0.95
#define STARS*******************

int menu(void);

int getnights(void);

void showprice(double, int);

9.5 地址运算符:&

%p 是输出地址的说明符

源代码

#include <stdio.h>
void mikado(int);
int main()
{
	int pooh = 2, bah = 5;

	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;

	printf("In main(), pooh = %d and &pooh = %p\n",pooh, &pooh);
	printf("In main(), bah = %d and &bah = %p\n",bah, &bah);
}

从这里我们可以很好的体会到这种传递只进行了数值传递

但是并非所有语言都是这样,例如在FORTRAN中,子程序会改变调用程序中的原变量中的数值,尽管在子程序中变量的名称可能不同,但是其地址是相同的。

9.6 改变调用函数中的变量

源代码

#include <stdio.h>
void interchange(int u, int v);
int main()
{
	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;
}

运行上面程序我们发现数值并没有发生变化

#include <stdio.h>
void interchange(int u, int v);
int main()
{
	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;
	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);
}

根据运行结果我们知道在函数内部两个数值确实得到了交换,但是现在没有把结果传递到main( )函数中。这是因为Interchange()使用的变量独立于函数main()。
想要实现传递两个数值,我们需要用到下面的指针

9.7 指针

9.7.1 间接运算符: *

这是一个一元运算符

9.7.2 指针声明

9.7.3 使用指针在函数间通信

源代码

#include <stdio.h>
void interchange(int u, int v);
int main()
{
	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;
}

尽管interchange( )只是局部变量,但是通过使用指针,该函数可以操作main()中的变量的值。

编写程序时,一个函数一般有两种属性:变量名和数值
程序被编译和加载后,同一个变量 在计算机中的两个属性是地址和数值。
变量的地址可以被看做是在计算机中的变量的名称

普通的变量和它的数值作为基本数值量,儿通过使用运算符& 将它的地址作为间接数量值。但是对于外部来讲,地址是它的基本数值量,使用运算符*后,该地址中的存储的数值是它的间接数值量。

9.8 关键概念

9.9 总结

9.10 复习题

发布了22 篇原创文章 · 获赞 39 · 访问量 4035

猜你喜欢

转载自blog.csdn.net/weixin_44895666/article/details/102885935