第19课 函数


前言

函数是完成某种功能的程序段,是程序模块化的体现。对于一个复杂的问题,可以将其分解成若干个子问题来解决,如果子问题依然很复杂,还可以将它继续分解。直到每个子问题都是一个具有独立任务的模块。以这种方式编写的程序结构清晰,逻辑关系明确,会给编写、阅读、调试以及修改带来很多好处。
一个程序可以有许多函数,包括主函数和非主函数,主函数只能有一个,非主函数可以有多个。主函数自动执行,非主函数只有被调用时才会执行。程序从主函数开始执行到主函数结束而结束,主函数可以调用任何非主函数,非主函数之间可以互相调用,但不能调用主函数。函数调用过程中,调用其他函数的函数叫作主调函数,相应的被调用的函数叫作被调函数。
提示
主函数和主调函数是不同的。主函数特指 main ()函数,主调函数是指调用其他函数的函数。主调函数可以是主函数,也可以是非主函数。

一、函数的定义与声明

二、函数的调用与返回

三、函数的嵌套与递归

1. 函数的嵌套

2. 函数的递归

函数的递归调用是指函数自身调用自身,这是一类特殊的函数嵌套。
递归函数的一般格式

数据类型 函数名(形参列表) {
    if<递归结束条件> return 返回值;
    else return 函数名(形参该变量) 
}

例如

long long factor(int n) {
	if(n==0) return 1;
	else return n*factor(n-1);
}

构成递归需要具备两个条件:

  • 必须有结束条件和结束值
  • 参数该变量使参数向结束条件发展

1. 求n!

非递归的一般实现代码。

long long factorial(int n) {
	long long s=1;
	for(int i=1; i<=n; i++) {
		s *= i;
	}
	return s;
}

递归方式的代码。

long long factorial(int n) {
	if(n==0) {
		return 1;
	}
	else {
		return n*factorial(n-1);
	}
}

下面是factorial(2),即当实参为2时的递归调用过程:

  1. main()调用factor(2)时,main()暂停,进入factor(2);
  2. 执行factor(2)中的return 2*factor(2-1)时, factor(2)暂停,调用factor(1);
  3. 执行factor(1)中的return 1*factor(1-1)时, factor(1)暂停,调用factor(0);
  4. 执行factor(0)中的return 1;语句, factor(0)返回值1,回退至factor(1)调用处;
  5. 执行factor(1)中的return 1*1时, factor(1)返回值1,回退至factor(2)调用处;
  6. 执行factor(2)中的return 2*1时, factor(2)返回值2,返回至main()调用处。

图1是当n==5时的递归过程。
递归求5!
图1 递归求5!

2. Hanoi(汗诺)塔问题

Hanoi (汉诺)塔问题。这是一个古典的数学问题,是一个用递归方法解题的典型例子。问题是这样的:古代有一个梵塔,塔内有3个座 A 、 B 、 C ,开始时 A 座上有64个盘子,盘子大小不等,大的在下,小的在上(图2)。有一个老和尚想把这64个盘子从 A 座移到 C 座,但每次只允许移动一个盘,且在移动过程中在3个座上都始终保持大盘在下,小盘在上。在移动过程中可以利用 B 座,要求编程序输出移动的步骤。
Hanoi(汗诺)塔问题
图2 Hanoi(汗诺)塔问题

为便于理解,先分析将 A 座上3个盘子移到 C 座上的过程:
1 将 A 座上2个盘子移到 B 座上(借助 C );
2 将 A 座上1个盘子移到 C 座上;
3 将 B 座上2个盘子移到 C 座上(借助 A )。
将 A 座上2个盘子移到 B 座上(借助 C )
图3 将 A 座上2个盘子移到 B 座上(借助 C )
将 B 座上2个盘子移到 C 座上(借助 A )
图4 将 B 座上2个盘子移到 C 座上(借助 A )

其中第2步可以直接实现。第1步又可用递归方法分解为:
1.1 将 A 上1个盘子从 A 移到 C ;
1.2 将 A 上1个盘子从 A 移到 B ;
1.3 将 C 上1个盘子从 C 移到 B 。
第3步可以分解为:
3.1 将 B 上1个盘子从 B 移到 A 上;
3.2 将 B 上1个盘子从 B 移到 C 上;
3.3 将 A 上1个盘子从 A 移到 C 上。
将以上综合起来,可得到移动3个盘子的步骤为:
A→C, A→B, C→B, A→C, B→A, B→C, A→C.
共经历7步。由此可推出:移动n个盘子要经历 2 n − 1 2^n-1 2n1步。如移4个盘子经历15步,移5个盘子经历31步,移64个盘子经历 2 64 − 1 2^{64}-1 2641步。

由上面的分析可知:将n个盘子从A座移到C座可以分解为以下3个步骤:
(1) 将A上 n-1个盘借助C座先移到B座上;
(2) 把A座上剩下的一个盘移到C座上;
(3) 将n-1个盘从B座借助于A座移到C座上。
上面第(1)步和第(3)步,都是把n-1个盘从一个座移到另一个座上,采取的办法一样的,只是座的名字不同而已。为使之一般化,可以将第(1)步和第(3)步表示为:将“one”座上n-1个盘移到“two”座(借助“three”座)。只是在第(1)步和第(3)中,one、two、three 和A、B、C的对应关系不同。对第(1)步,对应关系是one对应A,two对应B,three对应C。对第(3)步,是:one对应B, two对应C, three对应A。
因此,可以把上面3个步骤分成两类操作:
(1)当n==1时,直接将1个盘子从一个座上移到另一座上。
(2)当n>1时,将n-1个盘从一个座移到另一个座上。它是一个递归的过程(又可以拆分成3大步骤)。

下面编写程序,分别用两个函数实现以上的两类操作。

  • 函数调用hanoi(n, p1, p2, p3)表示将n个盘子从p1座移到p3座的过程(借助p2座);
  • 函数调用move(n, from, to)表示将1个盘子从 from座移到to座的过程。 from、to是代表 A、B、C 座之一,根据每次不同情况分别取A、B、C 代入。

对应的函数定义如下。

void move(int no, char from, char to) {
	cout << "将" << no << "号盘子从" << from << "移动到" << to << endl;
}

// 将n个碟子从p1挪动到p3, p2是中间的柱子
void hanoi(int n, char p1, char p2, char p3) {
	if(n==1) {
		move(n, p1, p3);
	} else {
		//将p3看作中间柱子,将n-1个碟子从p1移动到p2
		hanoi(n-1, p1, p3, p2);
		//将编号为n的盘子从p1移动到p3
		move(n, p1, p3);
		//将p1看作中间柱子,将n-1个碟子从p2移动到p3
		hanoi(n-1, p2, p1, p3);
	}
}

① 将n-1个盘子从A移动到B
将n-1个盘子从A移动到B
图5 将n-1个盘子从A移动到B

② 将编号为n的盘子从A移动到C
将编号为n的盘子从A移动到C
图6 将编号为n的盘子从A移动到C

③ 将n-1个盘子从B移动到C
将n-1个盘子从B移动到C
图7 将n-1个盘子从B移动到C

步骤①③ 明显是个递归调用

所以,Hanoi(汗诺)塔问题完整的递归实现代码如下。

#include<iostream>
using namespace std;

void move(int no, char from, char to) {
	cout << "将" << no << "号盘子从" << from << "移动到" << to << endl;
}

//将n个碟子从p1挪动到p3, p2为中间的柱子
void hanoi(int n, char p1, char p2, char p3) {
	if(n==1) {
		move(n, p1, p3);
	} else {
		//将p3看作中间柱子,将n-1个碟子从p1移动到p2
		hanoi(n-1, p1, p3, p2);
		//将编号为n的盘子从p1移动到p3
		move(n, p1, p3);
		//将p1看作中间柱子,将n-1个碟子从p2移动到p3
		hanoi(n-1, p2, p1, p3);
	}
}

int main() {
	hanoi(3, 'A', 'B', 'C');
	return 0;
}

当n==3时,程序输出如下。

1号盘子从A移动到C
将2号盘子从A移动到B
将1号盘子从C移动到B
将3号盘子从A移动到C
将1号盘子从B移动到A
将2号盘子从B移动到C
将1号盘子从A移动到C

四、局部变量与全局变量

1. 局部变量

  • 局部变量是指在函数内部定义的变量。前面所有例题中的变量都是在函数内定义的,因此都是局部变量。
  • 局部变量的作用域(即起作用的范围)是从定义点到函数结束,即局部变量只在定义它的函数内有效。
  • 由于局部变量的作用域仅局限于本函数内,因此,在不同函数中局部变量名可以相同,它们分别代表不同的对象,互不干扰。

2. 全局变量

  • 全局变量是指在函数外定义的变量。其作用域是从变量的定义点到程序结束,即从变量定义之后的所有函数都可以访问该全局变量。
  • 全局变量和局部变量可以同名,但在局部变量的作用域内全局变量无效。
  • 当全局变量没有初始化时默认初始值为0,局部变量没有初始化时默认初始值不确定。
  • 由于全局变量的作用域可以覆盖整个程序,因此可以作为共享数据在不同函数中传递数据。在函数调用中,可以通过参数从主调函数向被调函数传递数据(实参传给形参),也可以通过返回值从被调函数向主调函数传递数据,但二者都是单向传递,即只能向一个方向传递数据。而全局变量既可以在主调函数中使用,又可以在被调函数中使用,实现了数据的双向传递。

阅读下面的代码,给出代码运行的结果。

#include<iostream>
using namespace std;

int x, y;

int fun1(int s) {
	int x=10;
	y=x*s;
	return x+y;
}

int main() {
	int n;
	cin >> n;
	cout << "fun1(" << n << ")=" << fun1(n) << endl;
	cout << "x=" << x << ",  y=" << y << endl;
	return 0;
}

代码运行输出为

5
fun1(5)=60
x=0, y=50

五、课后练习

1. 求3个数的最小公倍数

#include<iostream>
using namespace std;

int max(int x1, int x2, int x3) {
	if(x1>x2 && x1>x3) return x1;
	else if(x2>x1 && x2>x3) return x2;
	else return x3;
}

int main() {
	int x, y, z;
	int biggest, m, i=1;
	cin >> x >> y >> z;
	biggest = max(x, y, z);
	while(true) {
		m = biggest * i;
		if(m%x==0 && m%y==0 && m%z==0) break;
		i++;
	}
	cout << m << endl;
	return 0;
}

运行程序,输入
2 4 8
会输出
8

2. 求第5个人的年龄是多少

有5个人坐在一起,问第5个人多少岁?他说比第4个人大2岁。问第4个人岁数,他说比第3个人大2岁。问第3个人,又说比第2个人大2岁。问第2个人,说比第1个人大2岁。最后问第1个人,他说是10岁。请问第5个人多大。

显然,这是一个递归问题。
要求第5个人的年龄,就必须先知道第4个人的年龄,而第4个人的年龄也不知道,要求第4个人的年龄必须先知道第3个人的年龄,而第3个人的年龄又取决于第2个人的年龄,第2个人的年龄取决于第1个人的年龄。而且每一个人的年龄都比其前1个人的年龄大2。即:
age(5)= age(4)+2
age(4)= age(3)+2
age(3)= age(2)+2
age(2)= age(1)+2
age(1)=10
可以用数学公式表述如下:
a g e ( n ) = {   10 n = 10   a g e ( n − 1 ) + 2 n > 1 age(n)= \begin{cases} \ 10 & n=10\\ \ age(n-1)+2 & n>1 \end{cases} age(n)={ 10 age(n1)+2n=10n>1
公式本身就是一个递归式,过程图示如图8所示:
求第5个人的年龄
图8 求第5个人的年龄

据此,可以直接给出C++的递归实现。

#include<iostream>
using namespace std;

int age(int n) {
	if(n==1) return 10;
	else return age(n-1)+2;
}

int main() {
	int n=5;
	cout << age(n);	// 18
	return 0;
}

2. Hanoi(汗诺)塔问题

见前面相关内容。


补充练习

1. 用自定义函数difference_abs(m,n)求两个数之差的绝对值

2. 用自定义函数matrix_max(m, n)求一个m*n阶矩阵中最大的元素值

3. 用函数递归的方式求斐波那契数列(Fibonacci sequence)的第20项

斐波那契数列数值为:1, 1, 2, 3, 5, 8, 13, 21, 34, …在数学上,这一数列以如下递推的方法定义:F(0)=1, F(1)=1, F(n)=F(n-1)+F(n-2)(n≥2,n∈N),即
f ( n ) = {   0 n = 0   1 n = 1   f ( n − 1 ) + f ( n − 2 ) n > 1 f(n)= \begin{cases} \ 0 & n=0\\ \ 1 & n=1\\ \ f(n-1)+f(n-2) & n>1 \end{cases} f(n)=  0 1 f(n1)+f(n2)n=0n=1n>1

4. 输出矩阵形式的杨辉三角——帕斯卡三角形

帕斯卡三角形,是一个三角形矩阵,其顶端是 1,视为(row0)。第1行(row1)(1&1)两个1,这两个1是由他们上头左右两数之和 (不在三角形内的数视为0)产生。依此类推产生第2行(row2): 0+1=1; 1+1=2; 1+0=1。第3行(row3): 0+1=1; 1+2=3; 2+1=3; 1+0=1。循此法可以产生其余行元素——即 ( a + b ) n (a+b)^n (a+b)n的展开式中的各项系数(n=0表示杨辉三角的第0行row0)。

自定义函数YanghuiTriangle(a[][7])输出行数N=7的杨辉三角,如图9所示。
输出行数N=7的杨辉三角
图9 输出的行数N=7的杨辉三角

  • 36
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值