C语言学习笔记(六)函数

函数

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给了变量a12给了变量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),说程序中包含了一个隐藏的从doubleint的转换,但程序可以运行成功。

下面我们来考虑另一个问题,我们调用函数的时候传过去的到底是什么?请看下面的例子:

#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;
}

这样的代码能交换ab的值吗?
在这里插入图片描述

main()函数在做swap(a, b);的时候,我们是把a的值5b的值6交给了swap()函数里的ab,**swap()函数里的abmain()函数里的ab没有任何关系!**所有在swap()函数里对ab做的任何事情,是swap()函数自己的参数ab的问题,和main()函数里的ab没有任何关系!这样的代码也不能交换ab的值。

注: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()中,变量ab是存在的,xyt是不存在的,当程序进入到swap()中,现在xyt是存在的,而ab是不存在的,此时ab还在那里,但我们不能访问他们,但是作为生存来说它们还在,作用来说它们不在当前的作用域了。但程序执行离开swap()回到main()后,ab又回来了,而xyt就都不存在了。

本地变量的规则

  • 本地变量是定义在块内的
    • 它可以是定义在函数的块内
    • 也可以定义在语句的块内
    • 甚至可以随便拉一对大括号来定义变量
  • 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
  • 块外面定义的变量在里面仍然有效
  • 块里面定义了和外面同名的变量则掩盖了外面的
  • 不能在一个块内定义同名变量
  • 本地变量不会默认初始化
  • 参数在进入函数的时候就被初始化了

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。程序运行结果如下:

在这里插入图片描述
在这里插入图片描述

我们给的数字是56,而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型的变量ij,声明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()任何参数就写一个voidmain()函数虽然是你写的代码中第一个被执行的地方,但并不是程序运行起来第一条运行的代码,在main()函数之前还有其他的东西,这些其他东西是为你的程序运行做准备的,准备工作做完后就会来调用你的main()函数。同样的,也是这个原因,所以return 0;是有意义的main()函数在结束的时候要把这个0返回给调用它的那个地方。

  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值