我们把函数看做是一个盒子,它会有以下几个特性:
-
- 开始执行前,函数可以输入一些值
- 执行过程中,函数可以做一些事情
- 结束执行后,函数可以返回一些值
函数的写法公式:
函数返回值类型 函数名(参数1,参数2,参数n){ 函数体; return 函数返回值; }
花括号包括的被称为函数体 ,注意函数体一定要被花括号包括且不可省略。花括号上面的函数名、函数参数及返回值被称作函数头 。
例如:我们想要计算两个变量a
和b
相加的结果,可以将其写作一个函数:
-
- 输入一些值:输入
a
,b
- 做一些事情:计算
a+b
- 返回一些值:返回
a
与b
的和
- 输入一些值:输入
我们将这个函数命名为add
,代码如下:
int add(int a, int b){ return a+b; }
注意:每个输入参数必须指明其变量类型,不能省略变量类型。
int add(int a, int b) // 正确 int add(int a, b) // 错误
我们把函数名取名为add
。当然自定义函数的函数名可以按照自己的喜好来写,就算写成aaaaa
也行。 不过,为了函数名拥有语义化,方便人阅读理解,我们一般使用纯英文来作为函数名。
2. 函数的调用
这一段代码被称之为add
函数的定义 ,直接运行这段代码你会发现程序并没有任何结果,因为函数需要被调用才能执行。
int add(int a, int b){ return a+b; }
函数调用的公式:
函数名(参数1,参数2,参数n);
例如:我们用 main
函数来调用 add
函数。 在这个程序中, main
被称作主调函数,add
被称作被调函数。
#include <iostream> using namespace std; int add(int a, int b){ return a+b; } int main(){ int c = add(1,2); // 函数调用 cout<<c; return 0; }
在main
中,将 1
,2
两个参数传入了add
函数,并调用add
函数。
在add
函数头中,标明了函数的返回值类型为int
,说明这个函数被调用后将返回一个int
类型的结果。所以,我们使用int
类型的变量c
存放add
函数的返回值。
函数调用的注意事项:
-
- 对函数的调用,也是一个表达式
- 函数调用表达式的值,由函数内部
return
语句决定
3. 函数的返回值
函数向函数体外传递数据信息,因此函数结束的位置必须用return
语句返回正确的值。
return
语句的格式为:
return 表达式;
返回值的类型的类型有:void
,int
,double
,float
,char
,bool
其中void
表示空类型,即没有返回值。 你也可以在函数参数的括号中填上void
,明确表示函数不需要参数。
例如:
#include<iostream> using namespace std; void start(void){ //无参数且无返回值的函数 cout<<"********"<<endl; return; //无返回值,可以省略 } int main() { start(); //调用无参数且无返回值的函数 return 0; }
输出结果:
对于没有返回值的函数,可以省略return
。 函数运行完花括号内的语句后,就自动结束。
return
的作用是返回表达式的值, 若函数需要返回值,则必须使用return
带回一个返回值才能正常通过编译。
最后,不要忘记了,return
可以出现在函数的任意位置。一旦程序执行到return
,就会停止函数的执行, 返回主调函数。
#include<iostream> using namespace std; void start(void){ cout<<"****"<<endl; cout<<"******"<<endl; cout<<"********"<<endl; return; //函数体内return以下语句不再执行 cout<<"**"<<endl; cout<<"*"<<endl; } int main() { start(); return 0; }
输出结果:
4. 函数的声明
思考:如果函数A内部调用了B,B内部调用了A,哪个写前面?
#include<iostream> using namespace std; void functionA(){ //函数A中调用了函数B functionB(); return; } void functionB(){ //函数B中调用了函数A functionA(); return; } int main() { functionA(); functionB(); return 0; }
运行结果:
一般来说函数的定义必须出现在函数调用语句之前,否则调用语句编译出错。 在函数functionA()
调用前,编译器没有读到任何有关于functionB()
的说明 。 编译器不知道functionB()
代表什么,它究竟是变量还是函数或者是其他实体。这让编译器十分困惑,因此无法成功编译。
在一个源文件中,如果函数调用前没有函数定义,那么可以使用函数声明通知编译器,告诉编译器有这个函数存在。
函数声明的写法非常简单:函数头 + 分号
例如:
#include<iostream> using namespace std; void functionB(); //函数声明 void functionA(){ //函数A中调用了函数B functionB(); return; } void functionB(){ //函数B中调用了函数A functionA(); return; } int main() { functionA(); functionB(); return 0; }
Tips: 函数声明也被称作函数原型。
总结
- 函数三要素:输入一些值、做一些事情、返回一些值
- 函数的调用公式:
函数名(参数1,参数2,参数n);
- 函数返回值格式:
return 表达式;
- 函数声明的格式:
函数头 + 分号
第二节 函数的参数
1. 形参与实参
让我们再回到add
函数,add
函数的两个参数分别为 int
类型,返回值也为 int
类型。
#include <iostream> using namespace std; int add(int a, int b){ return a+b; } int main(){ int c = add(1,2); // 函数调用 cout<<c; return 0; }
add
函数参数列表中的a
、b
被称作形式参数,简称形参。它指代函数参数的类型,以及参数进入add
后,需要经历的处理步骤,没有确定值。
而在函数调用中 add(1, 2)
的2
,3
被称作实际参数,简称实参,它们将确定形式参数的值具体是什么。
2. 参数的自动转换
一般而言,形式参数与实际参数的类型是一致的,但是也允许不一致的情况。
我们将实际参数 1.1
,2.2
传递给形式参数int a
, int b
,编译运行一下看看会发生什么。
#include <iostream> using namespace std; int add(int a, int b){ return a+b; } int main(){ int c = add(1.1,2.2); cout<<c; return 0; }
输出结果:
从编译结果可以得出,将实际参数 1.1
,2.2
传递给形式参数int a
,int b
时,编译器会尝试将实参转换为形参的类型。
若可以转换,那么将编译通过。转换过程中可能出现数据丢失 ,例如:1.1
、2.2
被转换为了整型 1
、2
,小数部分被丢失。
若无法转换,那么编译失败。
3. 返回值的自动转换
现在我们把返回值改为了double
类型。但是,参数仍然保持int
类型。
#include <iostream> using namespace std; double add(int a, int b){ return a+b; } int main(){ int c = add(1,2); cout<<c; return 0; }
a
与b
相加的结果为int
类型,返回时,会尝试将int
转换为double
。int
可以被转换为double
,所以编译通过。
4. 代码封装成函数
相信你学完函数后心里肯定有这么一个疑问: 为什么要将代码封装成函数?
如果程序需要多次完成某项任务,那么你有两个选择:
1. 将同样的代码复制多份。
2. 将代码封装为一个函数,在需要的地方调用这个函数,提高代码的复用性。
显然,后者会更加便利于我们的日常开发工作。
5. 范例:海伦公式封装
接下来我们将演示,如何用代码求三角形面积。并探讨,将求三角形面积代码封装成函数的优点。
【问题描述】假设在平面内,有一个三角形,边长分别为a、b、c,三角形的面积S可由以下公式求得:
p = (a+b+c)/2
S = (p(p-a)(p-b)(p-c))½
这种求三角形面积的公式被称作海伦公式。
另外,三角形a,b,c任意两边的和大于第三边,否则无法构成一个三角形。即下列不等式需同时成立:
1. a + b > c
2. a + c > b
3. b + c > a
要求设计程序:从键盘中输入三个浮点数a、b、c,判断是否能组成三角形,如果能,输出这个三角形的面积。
【样例输入】3 4 5
【样例输入】It's a triangle
6
第一步, 首先我们应当判断三边能否构成一个三角形。
if (a + b > c && a + c > b && b + c > a){ cout<<"It's a triangle"<<endl; } else{ cout<<"Not a triangle"<<endl; }
第二步, 根据三边a
,b
,c
求三角形面积。这里会遇到求一个数的平方根,我们可以使用函数 sqrt
。若要使用 sqrt
,需要包含头文件 math.h
。
p = (a + b + c) / 2; s = sqrt(p * (p - a) * (p - b) * (p - c));
完整代码:
#include <iostream> #include <math.h> //sqrt 需要包含头文件math.h using namespace std; int main(){ double a,b,c,p,s; cin>>a>>b>>c; //判断能否构成三角形 if (a + b > c && a + c > b && b + c > a){ cout<<"It's a triangle"<<endl; } else{ cout<<"Not a triangle"<<endl; } //海伦公式求三角形面积 p = (a + b + c) / 2; s = sqrt(p * (p - a) * (p - b) * (p - c)); //输出结果 cout<<s<<endl; return 0; }
输出结果:
在求三角形面积的代码中,可以被分成两个可复用部分:
1. 判断三边是否能构成三角形
2. 求三角形的面积
也许我们就是想知道三边能否构成三角形,而不需要求其面积,所以将其提取为一个函数。可以增加代码的复用性。
#include <iostream> #include <math.h> //sqrt 需要包含头文件math.h using namespace std; //判断能否构成三角形的函数 void isTriangle (double a,double b,double c){ if (a + b > c && a + c > b && b + c > a){ cout<<"It's a triangle"<<endl; } else{ cout<<"Not a triangle"<<endl; } } double areaOfTriangle(double a,double b,double c){ //海伦公式求三角形面积 double p,s; p = (a + b + c) / 2; s = sqrt(p * (p - a) * (p - b) * (p - c)); //输出结果 return s; } int main(){ double a,b,c,p,s; cin>>a>>b>>c; //判断是否构成三角形 isTriangle(a,b,c); //求三角形面积 s = areaOfTriangle(a,b,c); cout<<s<<endl; return 0; }
总结
- 形参指代函数参数的类型,以及参数进入函数后,需要经历的处理步骤,没有确定值
- 实参将确定形式参数的值具体是什么
- 形参与实参若可以转换,那么将编译通过,转换过程中有可能出现数据丢失。 若无法转换,那么编译失败。
- 代码封装成函数的意义:将代码封装为一个函数,在需要的地方调用这个函数,提高代码的复用性
第三节 函数的递归
本节内容
本节介绍在函数内部调用自身的过程
本节目标
- 掌握函数递归调用
- 掌握函数递归的条件
- 掌握函数递推与回归过程
1.函数递归调用
函数需要被另一个函数调用才会执行,进一步发散思维,函数内部能否调用自己?
接下来我们写一个名为add
的函数,然后在函数内部调用自己。
#include <iostream> using namespace std; void add(int a) { cout<<a<<endl; add(a+1); } int main(){ add(0); return 0; }
输出结果:
编译成功,控制台中依次输出0、1、2、3.......
说明在C++中的函数内部是可以调用自己的,这种调用称为函数递归。
我们来分析一下add
函数递归调用的过程:
-
add
函数在主函数main
中被调用,传入参数0
- 进入
add
函数后,形参a
的值为0
,a
被cout
语句输出 - 将
a+1
作为参数传入add
函数,开始不断调用自己
由于add
函数首尾相接,它将导致程序陷入死循环。就像一条衔尾蛇,头咬住了尾巴,整条蛇构成了一个闭环。
还记得怎么样手动打断程序执行吗?如果程序陷入了死循环,请使用Ctrl+c
组合键结束程序。
如果你不打断程序执行,那么过不了多久,程序将出现栈溢出异常,导致程序异常结束
2.函数递归的条件
我们现在已经明白递归的特点了,很显然还不够,我们需要程序能正常结束。
add
函数里已经有一个递推规则——每次a
都增加1,那么我们还缺少什么呢? ?我们缺少一个递推结束条件 。
要完成递归函数的两个要素:
-
- 递推规则
- 递推结束条件
我们知道可以用break
关键字结束循环,而要结束递推就要用return
关键字,因为一旦函数执行到return
,就会停止函数的执行,现在我们给程序加上递推结束条件,当a
等于5时,递推结束。
#include <iostream> using namespace std; void add(int a){ if(a==5){ // 递推结束条件 return; } cout<<a<<endl; add(a+1); } int main(){ add(0); return 0; }
输出结果:
3. 递推与回归
当a
小于5
之前,一直递推至下级函数;
当a
等于5
时,从下级函数开始回归。
我们在add
函数前后各放置一个cout
语句,用于探究函数递归调用时的递推与回归过程。
#include <iostream> using namespace std; void add(int a){ if(a==5){ return; } cout<<"递推输出:"<<a<<endl; //递推时输出 add(a+1); cout<<"回归输出:"<<a<<endl; //回归时输出 } int main(){ add(0); return 0; }
输出结果:
程序执行后,先执行五次递推输出,值分别为0,1,2,3,4。之后,再执行五次回归输出,值分别为4,3,2,1,0。
下面的图中用红色线条画出了递推进入下级函数的流程,蓝色线条画出了从下级回归的流程。标号数字代表cout
语句的执行顺序。
标号为①②③④⑤的cout
语句在递推过程中依次执行,而标号为⑥⑦⑧⑨⑩的cout
语句必须在回归过程执行, 由于回归过程与递推过程是逆向的,所以输出的a
值是逆序的。
对于此 add
函数,放在递归调用前的语句将在递推过程中执行,而放在递归调用后的语句将在回归过程中执行。
4.使用递归计算阶乘
一个正整数的阶乘是所有小于及等于该数的正整数的积,并且0的阶乘为1,负数没有阶乘。阶乘被记为 n!。
例如:
4! = 4*3*2*1
3! = 3*2*1
2! = 2*1
1! = 1
0! = 1
根据这个规律,我们还能将阶乘这样计算:
4! = 4*3!
3! = 3*2!
2! = 2*1!
1! = 1
0! = 1
得出递推规律:
-
- 当
n
为0或者1时,n
的阶乘为1
- 当
n
大于1时,n
的阶乘为n*(n-1)!
- 当
那么假设有这么一个函数 func(n)
,这个函数传入一个整数n
,返回n
的阶乘n!
。
递推结束条件:当n
为0或者1时,func(n)
返回1
;
递推规律:当n
大于1时,func(n)=n*func(n-1)
。
#include <iostream> using namespace std; int func(int n){ if(n==0 || n==1){ return 1; } return n*func(n-1); } int main(){ int r = func(4); cout<<r<<endl; return 0; } int func(int n){ if(n == 8){ return 1; } return func(n+1)+n; } int main(){ int r = func(2); cout<<r<<endl; return 0; }
总结
- 在C++中的函数内部是可以调用自己的,这种调用称为函数递归
- 完成递归函数的两个要素:递推规则和递推结束条件
- 递归调用前的语句将在递推过程中执行,而放在递归调用后的语句将在回归过程中执行。