【小课堂】五、函数(1)

函数是C/C++语言的核心概念,也是学习的重点所在。然而此函数与数学上的函数有所不同(也有称方法的),要理解掌握必须下些工夫。

函数

C/C++中的函数与C/C++语言的程序设计理念密切相关,在引入函数之前先看一个例子:
例5.1:
输入十个整数,输出他们的绝对值的阶乘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;

int main(){
int x,s;
for(int i=0;i<10;i++){
cin>>x;
if(x<0) x = -x;//求x的绝对值
s = 1;
for(int j=2;j<=x;j++)//求x的阶乘
s *= j;
cout<<s<<' ';
}

return 0;
}

输入:
1 -2 3 -4 5 -6 7 -8 9 -10
输出:
1 2 6 24 120 720 5040 40320 362880 3628800

随着需求功能的增加,将各个功能写在一起的代码将变得不利阅读和修改;而如果能将代码模块化,就可以解决这一问题。C/C++语言为此引入函数

定义一个函数

函数是一个命名的语句序列,能够返回计算结果(称为返回值)。
函数的定义语法如下:

1
2
3
4
<返回值类型> 函数名(<参数类型1> 参数1,<参数类型2> 参数2,……){
语句;
return 返回值;
}

可以抽象为:

1
<返回值类型> 函数名(参数表)函数体

这样,上面的例5.1就可以改写成(请先带着疑惑看完):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
using namespace std;

int absolute(int x){
if(x<0)return -x;
else return x;
}

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

int main(){
int x;
for(int i=0;i<10;i++){
cin>>x;
cout<<factorial(absolute(x))<<" ";
}

return 0;
}

可以看到,绝对值模块、阶乘模块被分开实现了,使用时只需调用即可,这样的编程设计思路(编程范式)更接近人类自然的解题思维。

下面将一一解释以上语法的每个部分。
以计算一个数的平方为例:

1
2
3
int square(int x){
return x * x;
}

第一行:
定义一个名为square的函数,需要一个整型参数(并命名为x),返回一个整型返回值
第二行:
函数体部分为一个return语句,即返回值为表达式x * x的结果。

需要注意的是,函数的定义是写在main函数外面的(可以观察例5.1的代码)。毕竟main也是函数,在C/C++代码中的地位与其它函数一样;更一般地、在C/C++语言中,一个函数不能定义在另一个函数内(暂不考虑lambda表达式)。

调用

完成了函数的定义,我们就可以调用(diào)这个函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;

int square(int x){
return x*x;
}

int main(){
int a;
cin>>a;
cout<<square(a)<<endl;
cout<<a;
return 0;
}

输入:
6
输出:
36
6

我们分析一下在调用过程中发生了什么:
程序从main函数开始执行,分别执行了int a;cin>>a;两行。
在执行cout<<square(a);时调用函数square,变量a作为被调用的参数。
于是,square函数中的变量x被赋值为变量a的值,并开始执行square函数:
返回x * x的值(即36),函数执行结束,回到调用的地方
执行cout<<36;最后输出36。
操作后a的值没有改变。

我们称调用时传递给函数的值(上例中的a)为实际参数(简称实参);称接受该值的变量(上例中的x)为形式参数(简称形参)。
形参是函数中接受传入数值的特殊变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
using namespace std;

int add1(int x){
x++;
return x;
}

int main(){
int a;
cin>>a;
cout<<add1(a)<<endl;
cout<<a;
return 0;
}

输入:
6
输出:
7
6
函数调用过程中,a的值被赋给了形参x,x在函数中+1并返回;而a的值仍为6:操作后a的值没有改变。

试试看在main函数中为上例的x赋值,或者输出x,看看会发生什么——

作用域

考察以下代码:

1
2
3
4
5
6
7
8
9
int square(int x){
return x*x;
}

int main(){
x = 10;
cout<<x;
return 0;
}

编译器会告诉你,这个代码是错误的:x没有定义。square函数中定义了变量x,到main函数中却没有了,因为main函数超出了变量x的作用域

要解释作用域这一概念,我认为最好的类比就是数学题中出现的前提条件:
19.已知函数$f(x)=x^2,g(x)$
(1)若$g(x)=2^x$,设$h(x)=…$……
(2)……
发现:
总条件是$f(x)=x^2$,在整道题中不会改变,且都能使用
第一小问中:$g(x)=2^x$,但只能在第一问中使用;另定义了$h(x)$
第二小问中:没有关于$g(x)$的条件,因此它可以为任意函数,$h(x)$甚至没有出现,自然无法使用。

与之类比,C/C++有全局量局部量的概念,定义在所有函数外的变量为全局量,作用域为从声明变量开始,到代码的末尾;而定义在函数内的变量为局部量(包括形参也是特殊的局部量),作用域仅在该函数内。因此,上例中的main函数无法使用square中的x。
全局量和局部量可以同名,但同一作用域不能出现两个同名全局量或局部量,请看下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
using namespace std;

int x;

int add1(int x){
return x+1;
}

int main(){
x = 20;
cout<<add1(10)<<endl;
cout<<add1(x)<<endl;
return 0;
}

输出:
11
21

我们先在所有函数外定义了一个变量x,所以这个x为全局量;然后定义了一个函数add1,参数表中也声明了一个变量x作为形式参数,而这个x在函数内,故为局部量,作用域仅在add1函数内;
程序从main函数开始运行:
先将x赋值为20:此时main函数中没有x的定义,因此这个x是指全局量x(全局量x赋值为20)。
调用add1(10):局部量x(形参)被赋值为10,执行add1函数,return x+1;那么这个x是10(局部量)还是20(全局量)呢?输出结果告诉我们,此处的x是函数内的局部量——一般地,如果有同名的局部量和全局量,那个变量名将会优先定向到局部量,只有没有这个局部量时,它才代指那个全局量。
调用add1(x):此时的x是局部量,因此值为20,并传给形参x,最后返回21。

C/C++的这一特性保证了函数是一个安全的小环境,在里面定义、使用任何名字都不用担心与外面的代码冲突,也让我们能够在定义多个函数时放心的使用诸如x等一些简单的名字而不冲突。

return语句

return语句控制着函数的返回值,写在函数体中,语法为:

1
return 返回值;

因此,return后的返回值类型必须与函数定义时写的类型一致。

同时,return语句也控制着函数的退出,函数体执行到return语句就会立即退出,销毁所有局部量(包括形参和在函数体内定义的局部量),并将return后的返回值代回调用处:

1
2
3
4
5
int f(int t){
return t+1;
t++;
return t+2;
}

上面函数f将返回t+1,而后面的语句将永远不会执行。

无返回值函数(过程)

特别地,一个C/C++函数也可以没有返回值,在定义函数时返回值类型为void(空类型),如:

1
2
3
void f(int x){
……
}

同理,函数也可以没有参数,即参数为void(也可以省略):

1
2
3
4
5
6
7
void g1(void){
……
}
//或者
void g2(){
……
}

那么问题来了,一个没有返回值的函数有什么用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
using namespace std;
int t;
void scan_t(){
cin>>t;
}

void tadd(int x){
t+=x;
}

void print_t(){
cout<<t;
}

void execute(){
scan_t();
tadd(5);
print_t();
}

int main(){
execute();
return 0;
}

输入:
9
输出:
14

而如例4.5,就可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;

void printline(int n){
for(int i=1;i<=n;i++)
cout<<"*";
cout<<"\n";
}

int main(){
int i,k;
cin>>k;
for(i=1;i<=k;i++)
printline(i);

return 0;
}

我们发现,无返回值函数仍然能实现一段有功用的代码,这也使得代码模块化了,因而无返回值函数也被称为过程(procedure)。因为没有返回值,一个过程中可以在任何地方使用return语句退出,但禁止return语句后带返回值。然而限于目前工具有限,我们目前仅能够通过全局量让过程修改一个变量,给代码的其他部分使用;在学习了指针引用后,过程将能够发挥更大作用。

我们发现,main函数就是一个无参数(也可以有参数)、返回值为int(正常运行是返回0)的函数。

作业

思考:

1、什么是C/C++函数,它与数学中的函数有什么不同?
2、我们为什么要引入函数?使用函数编程有什么好处?
3、什么是形参?什么是实参?C/C++语言函数为什么需要形参?
4、什么是作用域?为什么要引入作用域?
5、执行一句return语句会发生哪些事情?
6、什么是过程?

习题:

(以下习题,除main外应至少编写一个函数实现)
1、输入整数n,输出$[2,n]$中的所有质数
2、输入整数n,输出$[2,n]$中所有回文质数
3、计算组合数$C^m_n$的值
4、输入整数n,和n个整数,输入两个整数i,j,将位置i到j间的数升序排列,其余不变
例:
输入:
5
3 5 2 8 6
2 4
输出:
3 2 5 8 6
*5、输入十进制正整数n,转化二进制输出