函数
1. 初见函数
我们要求m和n之间素数的和,这个程序写出来是这样的:
#include "stdio.h"
int main(){
int m, n;
int sum = 0;
int cnt = 0;
int i;
scanf("%d %d", &m, &n);
if (m == 1)
{
m = 2;
}
for ( i = m; i <= n; i++)
{
int isPrime = 1;
int k;
for ( k = 2; k < i - 1; k++)
{
if (i % k == 0)
{
isPrime = 0;
break;
}
}
if (isPrime)
{
sum += i;
cnt++;
}
}
printf("%d %d\n", cnt, sum);
return 0;
}
16行到25行代码做的事情是判断i
是不是素数,这段代码使得外层循环显得臃肿,由于这段代码功能单纯,我们可以这样做:
#include "stdio.h"
int isPrime(int i){
int ret = 1;
int k;
for ( k = 2; k < i - 1; k++)
{
if (i % k == 0)
{
ret = 0;
break;
}
}
return ret;
}
int main(){
int m, n;
int sum = 0;
int cnt = 0;
int i;
scanf("%d %d", &m, &n);
if (m == 1)
{
m = 2;
}
for ( i = m; i <= n; i++)
{
if (isPrime(i))
{
sum += i;
cnt++;
}
}
printf("%d %d\n", cnt, sum);
return 0;
}
我们定义了一个自己的函数,专门用来判断一个数是否是素数,在主函数中只要调用该函数,就能判断一个数是不是素数。这样主函数中的循环就变得很简洁,而自定义的函数将来也可用于其他程序调用。
下面举个例子:
- 求出1到10、20到30和35到45的三个和
代码如下:
#include "stdio.h"
int main(){
int i;
int sum;
for (i = 1, sum = 0; i <= 10; i++)
{
sum += i;
}
printf("%d到%d的和是%d\n", 1, 10, sum);
for (i = 20, sum = 0; i <= 30; i++)
{
sum += i;
}
printf("%d到%d的和是%d\n", 20, 30, sum);
for (i = 35, sum = 0; i <= 45; i++)
{
sum += i;
}
printf("%d到%d的和是%d\n", 35, 45, sum);
return 0;
}
在上面的代码中,我们可以看到三段几乎一模一样的代码,这种现象称为“代码复制”,是程序质量不良的体现。当将来做修改或维护时,要修改或维护很多处。
和之前的例子一样,我们可以把这里重复的代码提出来,作一个sum函数,主函数只要在使用时调用一下就可以了。代码如下:
#include "stdio.h"
void sum(int begin, int end){
int i;
int sum = 0;
for (i = begin; i <= end; i++)
{
sum += i;
}
printf("%d到%d的和是%d\n", begin, end, sum);
}
int main(){
sum(1, 10);
sum(2, 30);
sum(35, 45);
return 0;
}
2. 函数的定义和调用
什么是函数?
- 函数是一块代码,接收零个或多个参数,做一件事情,并返回零个或一个值
- 可以先想象成数学中的函数:y = f(x)
函数定义
如上图所示,我们在定义一个函数时,包含函数头和函数体两部分,函数头又包含函数名,返回类型和参数表。
函数调用
- 函数名(参数值)
- ()起到了表示函数调用的重要作用
- 即使没有参数也需要()
- 如果有参数,则需要给出正确的数量和顺序。这些值会被按照顺序依次用来初始化函数中的参数
即使没有参数也需要(),下面我们来看一个例子:
#include "stdio.h"
void cheer(){
printf("cheer\n");
}
int main(){
cheer();
return 0;
}
执行结果如下:
如果没有给出(),程序能运行成功吗?
#include "stdio.h"
void cheer(){
printf("cheer\n");
}
int main(){
cheer;
return 0;
}
结果如下:
程序没有报错而是给了一个warning,运行结果也没有任何输出,这是为什么呢?我们在后面解释指针的时候才能理解。
如果有参数呢?
我们来看看函数是怎么执行的,还是之前的例子:
#include "stdio.h"
void sum(int begin, int end){
int i;
int sum = 0;
for (i = begin; i <= end; i++)
{
sum += i;
}
printf("%d到%d的和是%d\n", begin, end, sum);
}
int main(){
sum(1, 10);
sum(2, 30);
sum(35, 45);
return 0;
}
当我们在sum(1, 10);
前面打个断点,看看程序是如何运行的:
从调试过程可以看出,sum(1, 10);
在执行的过程中,会将括号里面的参数传递到void sum(int begin, int end)
函数中,当函数执行完后,控制台输出1到10的和是55
。
函数返回
- 函数知道每一次是哪里调用它,会返回到正确的地方
3. 从函数中返回
首先看一个例子:
int max(int a, int b){
int ret;
if (a > b)
{
ret = a;
} else
{
ret = b;
}
return ret;
}
该函数会通过return
语句返回一个int
类型的结果,也符合前面提到的“单一出口”理念。
从函数中返回值
- return停止函数的执行,并返回一个值
- return;
- return 表达式;
我们通过一个例子来看一下:
#include "stdio.h"
int max(int a, int b){
int ret;
if (a > b)
{
ret = a;
} else
{
ret = b;
}
return ret;
}
int main(){
int a, b, c;
a = 5;
b = 6;
c = max(10, 12);
c = max(a, b);
c = max(c, 23);
printf("%d\n", max(a, b));
return 0;
}
在19行打上断点,看看c = max(10, 12);
的执行流程是什么样子的,首先变量c
还没有初始值,是一堆乱七八糟的值;进入max
函数后,可以看到10
给了变量a
,12
给了变量b
,然后在if
语句中会执行ret = b;
;最后执行return ret;
,也就将12
返回给了变量c
。
注:一个函数中可以出现多个return语句!
上面的max
函数也可以改成下面的样子:
int max(int a, int b){
int ret;
if (a > b)
{
return a;
} else
{
return b;
}
// return ret;
}
我们之前讲过,有一个“单一出口”的设计理念,但是上面的代码中有多个return
,这就不符合“单一出口”的理念,当然这样做也没有错,但是不建议这样做。
当函数有了返回值之后,我们可以把这个值
- 赋值给变量
- 再传递给函数
- 甚至可以丢弃
我们在之前的代码中,单独做一次max
调用:
#include "stdio.h"
int max(int a, int b){
int ret;
if (a > b)
{
ret = a;
} else
{
ret = b;
}
return ret;
}
int main(){
int a, b, c;
a = 5;
b = 6;
c = max(10, 12);
c = max(a, b);
c = max(c, 23);
max(23, 45);
printf("%d\n", max(a, b));
return 0;
}
我们并没有把max(23, 45);
的值交给任何变量,编译没有任何问题。因为有的时候我们调用函数的目的并不是要看它返回给我们的结果,而是要函数执行中产生的其他作用。
没有返回值的函数
- void 函数名(参数表)
- 不能使用带值的return,可以没有return
- 调用的时候不能做返回值的赋值
注:如果函数是有返回值的,就必须使用带值的return。
4. 函数原型
我们前面有这样的代码:
#include "stdio.h"
void sum(int begin, int end){
int i;
int sum = 0;
for (i = begin; i <= end; i++)
{
sum += i;
}
printf("%d到%d的和是%d\n", begin, end, sum);
}
int main(){
sum(1, 10);
sum(2, 30);
sum(35, 45);
return 0;
}
函数的先后关系
- 像这样把sum()写在上面,是因为:
- C的编译器自上而下顺序分析你的代码
- 在看到sum(1, 10)的时候,它需要知道sum()的样子
- 也就是sum()要几个参数,每个参数的类型如何,返回什么类型
- 所有它必须在执行前看到这个sum(),才能调用
如果调用前不知道这个sum(),也就是把要调用的函数放到下面了,会出现什么情况呢?
#include "stdio.h"
int main(){
sum(1, 10);
sum(2, 30);
sum(35, 45);
return 0;
}
void sum(int begin, int end){
int i;
int sum = 0;
for (i = begin; i <= end; i++)
{
sum += i;
}
printf("%d到%d的和是%d\n", begin, end, sum);
}
结果如下:
我的编译器只是报了一个warning,但大部分编译器都会编译报错(不同的编译器结果不同),怎么办呢?
我们可以把sum()的函数头拷贝到main()函数前面去,像这样:
#include "stdio.h"
void sum(int begin, int end); // 声明
int main(){
sum(1, 10);
sum(2, 30);
sum(35, 45);
return 0;
}
void sum(int begin, int end){ // 定义
int i;
int sum = 0;
for (i = begin; i <= end; i++)
{
sum += i;
}
printf("%d到%d的和是%d\n", begin, end, sum);
}
这次程序执行没有问题。像第3行void sum(int begin, int end);
叫作函数的原型声明。而12到20行叫作函数定义。
声明不是定义,声明只是告诉编译器sum()
是一个函数,它长这个样子,函数名叫作sum
,有两个int
参数,没有返回类型。有了这个声明后,编译器就知道sum
长这个样子的,下面遇到sum(1, 10)
的时候,就会根据声明来判断你对sum()
的调用是否是正确的。
函数原型
- 函数头,以分号“;”结尾,就构成了函数的原型
- 函数原型的目的就是告诉编译器这个函数长什么样,包括名称,参数(数量及类型),返回类型
- 函数原型一般写在调用它的函数前面
- 原型里可以不写参数名字,但是一般会写上
#include "stdio.h"
void sum(int , int );
int main(){
sum(1, 10);
sum(2, 30);
sum(35, 45);
return 0;
}
void sum(int begin, int end){
int i;
int sum = 0;
for (i = begin; i <= end; i++)
{
sum += i;
}
printf("%d到%d的和是%d\n", begin, end, sum);
}
上面的例子中,我们去掉了函数声明里面的变量,只保留了变量类型,程序依旧可以正常运行。
对于原型声明来说,它要告诉编译器,这个sum()
函数有两个参数,都是int
型,至于这两个int
型的变量叫做什么没有任何关系,因为编译器做检查的时候不会检查参数的名称,它只会检查参数的类型。但我们通常会保留参数的名字,和函数头保持一致,方便人类阅读。
5. 参数传递
调用函数
- 如果函数有参数,调用函数时必须传递给它数量、类型正确的值
- 可以传递给函数的值是表达式的结果,这包括:字面量,变量,函数的返回值,计算的结果
int a, b, c;
a = 5;
b = 6;
c = max(10, 12); // 字面量
c = max(a, b); // 变量
c = max(c, 23); // 函数返回值
c = max(max(23, 45), a); // 函数
c = max(23+45, b); // 计算的结果
如果调用函数时给的值和参数的类型不匹配会怎么样呢?
类型不匹配?
- 调用函数时给的参数值与参数的类型不匹配是C语言传统上最大的漏洞
- 编译器总是悄悄替你把类型转换好,但这很可能不是你所期望的
- 后续的语言,C++/Java在这方面很严格
请看下面的例子:
#include "stdio.h"
void cheer(int i)
{
printf("cheer %d\n", i);
}
int main()
{
cheer(2.4);
return 0;
}
结果如下:
程序正常执行(不同的编译器结果不同,有的编译器会报warning),说程序中包含了一个隐藏的从double
到int
的转换,但程序可以运行成功。
下面我们来考虑另一个问题,我们调用函数的时候传过去的到底是什么?请看下面的例子:
#include "stdio.h"
void swap(int a, int b);
int main()
{
int a = 5;
int b = 6;
swap(a, b);
printf("a=%d b=%d\n", a, b);
return 0;
}
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
这样的代码能交换a
和b
的值吗?
当main()
函数在做swap(a, b);
的时候,我们是把a
的值5
,b
的值6
交给了swap()
函数里的a
和b
,**swap()
函数里的a
和b
与main()
函数里的a
和b
没有任何关系!**所有在swap()
函数里对a
和b
做的任何事情,是swap()
函数自己的参数a
和b
的问题,和main()
函数里的a
和b
没有任何关系!这样的代码也不能交换a
和b
的值。
注:C语言在调用函数时,永远只能传值给函数!
传值
- 每个函数都有自己的变量空间,参数也位于这个独立的空间中,和其他函数没关系
- 过去,对于函数参数表中的参数,叫作“形式参数”,调用函数时给的值,叫作“实际参数”
- 由于容易让初学者误会实际参数就是实际在函数中进行计算的参数,误会调用函数时把变量而不是值穿进去了,所以不建议用这种古老的方式来称呼它们
- 我们认为,他们是参数和值的关系
6. 本地变量
什么是本地变量?
本地变量
- 函数的每次运行,就产生了一个独立的变量空间,在这个空间中的变量,是函数的这次运行所独有的,称作本地变量
- 定义在函数内部的变量就是本地变量
- 参数也是本地变量
变量的生存期和作用域
- 生存期:什么时候这个变量开始出现了,到什么时候它消亡了
- 作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
- 对于本地变量,这两个问题的答案是统一的:大括号内——块
可以通过之前的例子来观察变量的生存期和作用域:
#include "stdio.h"
void swap(int a, int b);
int main()
{
int a = 5;
int b = 6;
swap(a, b);
printf("a=%d b=%d\n", a, b);
return 0;
}
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
通过调试来观察变量的生存期和作用域:
如上图所示,如果这个变量不存在它会显示unable to create variable object,如果存在会给出值,可以通过这个机制判断一个变量什么时候开始出现,什么时候开始消亡。当程序在main()
中,变量a
,b
是存在的,x
,y
,t
是不存在的,当程序进入到swap()
中,现在x
,y
,t
是存在的,而a
,b
是不存在的,此时a
,b
还在那里,但我们不能访问他们,但是作为生存来说它们还在,作用来说它们不在当前的作用域了。但程序执行离开swap()
回到main()
后,a
,b
又回来了,而x
,y
,t
就都不存在了。
本地变量的规则
- 本地变量是定义在块内的
- 它可以是定义在函数的块内
- 也可以定义在语句的块内
- 甚至可以随便拉一对大括号来定义变量
- 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
- 块外面定义的变量在里面仍然有效
- 块里面定义了和外面同名的变量则掩盖了外面的
- 不能在一个块内定义同名变量
- 本地变量不会默认初始化
- 参数在进入函数的时候就被初始化了
7. 函数庶事
当函数没有参数时,函数声明写成什么样子呢?
- void f(void);
- 还是 void f();
- 在传统C中,它表示f函数的参数表未知,并不表示没有参数
现在的C99中是什么样子呢?我们在之前的代码上进行更改,将第三行改为void swap();
:
#include "stdio.h"
void swap();
int main()
{
int a = 5;
int b = 6;
swap(a, b);
printf("a=%d b=%d\n", a, b);
return 0;
}
void swap(int x, int y)
{
int t = x;
x = y;
y = t;
}
这样写,编译的时候是通过的,之所以通过是因为,void swap();
只告诉编译器有个swap()
函数,编译器并不知道有什么参数,于是编译器在遇到12行swap(a, b);
时,它猜测swap()
函数要两个int
型变量。
现在我们将函数的变量类型改为double
型:
#include "stdio.h"
void swap();
int main()
{
int a = 5;
int b = 6;
swap(a, b);
printf("a=%d b=%d\n", a, b);
return 0;
}
void swap(double x, double y)
{
int swap;
int t = x;
printf("in swap, a=%f, b=%f\n", x, y);
x = y;
y = t;
}
现在的情况是:在原型声明时没说swap()
函数的参数是什么类型,调用的时候实际给的是两个整数,而函数实际的类型是两个double
。程序运行结果如下:
我们给的数字是5
和6
,而swap()
函数拿到的是两个0
,为什么会这样?
因为我们用函数声明void swap();
欺骗了编译器,我们告诉编译器我们也不知道是什么类型,然后编译器遇到swap(a, b);
时认为swap()
函数要的是两个int
型变量,于是它为swap()
函数调用安排了两个int
型变量的传递。当编译器在编译15行void swap(double x, double y)
时,我们之前说过,原型的作用不仅仅是检查你对编译函数的调用是不是对的,也用于检查你对函数的定义是不是对的,而在检查15行时,函数原型说不确定是什么类型,所以两个double
也是可能类型的一种,所以编译器没有从中发现任何问题。但实际上,函数参数是两个double
,而调用函数时传递了两个int
,所以出现了错误的显示。
所以,建议不要写出void swap();
这样的函数原型,原型里面一定要把参数写全,如果确定函数是没有参数的,就把void放上去!
逗号运算符?
- 调用函数时的逗号和逗号运算符怎么区分?
- 调用函数时的圆括号里的逗号是标点符号,不是运算符
- f(a, b) 中逗号是标点符号
- f((a, b)) 中逗号是运算符
函数里面可以定义函数吗?
- 不可以,C语言不允许函数的嵌套定义,我们可以在一个函数里面放另外一个函数的声明,但不能放另一个函数的body
这是什么?
- int i, j, sum(int a, int b);
- return (i);
int i, j, sum(int a, int b);
定义了int
型的变量i
和j
,声明sum()
函数要两个int
型参数并返回一个int
,这样写是可以的,不会报错,但不建议这样写!建议函数声明单独拿出来。
return (i);
这里的圆括号其实没有任何意义,有圆括号也不会错,但是会让人误解return
是个函数,误解我们是在调用return
函数,所以不要这么写!
关于main
- int main()也是一个函数
- 要不要写成int main(void)?
- return的0有人看吗?
- Windows:if error level 1···
- Unix Bash:echo $?
- Csh:echo $status
main()
也是一个函数,有的地方会说如果不要main()
任何参数就写一个void
。main()
函数虽然是你写的代码中第一个被执行的地方,但并不是程序运行起来第一条运行的代码,在main()
函数之前还有其他的东西,这些其他东西是为你的程序运行做准备的,准备工作做完后就会来调用你的main()
函数。同样的,也是这个原因,所以return 0;
是有意义的,main()
函数在结束的时候要把这个0
返回给调用它的那个地方。