C语言程序设计学习笔记:P7-函数


一、函数的定义和使用

1.1 初见函数

在第四章我们写过一个求素数的程序,在这个程序中,判断当前这个数是否是素数的那个循环使得我的整个代码看着很冗余。同时,如果其他地方需要用到这个功能,又得把这些代码写一遍,很不方便。

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);

因此,我们可以做一些处理,将判断当前数是否是素数的代码提出来,将其封装成一个函数。

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

最后我们的代码就比较简单了,在主函数中直接调用我们的函数即可。

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);

现在我们有个例子,分别求出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;
}

可以看出,我们有三段几乎一模一样的代码,这种叫做代码复制。代码复制是程序质量不良的表现,如果以后需要维护,则需要维护许多地方。于是我们可以像前面判断素数那样,把这些代码提出来,然后我们主函数只需调用写的函数即可。

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(20,30);
	sum(35,45);
	return 0;
}

1.2 函数的定义和使用

函数是一块代码,接收零个或多个参数,做一件事情,并返回零个或一个值。可以先想像成数学中的函数: y = f(x)。

函数由函数头和函数体组成。函数头又包括返回类型、函数名、参数表。
在这里插入图片描述

当我们要调用函数时,注意事项如下:

• 调用方式:函数名(参数值);
• ()起到了表示函数调用的重要作用
	• 即使没有参数也需要()
• 如果有参数,则需要给出正确的数量和顺序
	• 这些值会被按照顺序依次用来初始化函数中的参数
• 函数知道每一次是哪里调用它,会返回到正确的地方

我们写段代码来看看上面求和函数执行过程中的具体情况:

#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(20,30);
	sum(30,45);
	return 0;
}

我们单步调试,可以看出到了函数那一行会直接跳入函数的定义中,且传递给函数的值分别被赋值给函数的参数列表中的各参数。函数执行完后又正确地跳转到调用函数的那行代码后。
在这里插入图片描述

1.3 从函数中返回

在我们那个判定素数的函数中,需要返回一个int类型的值。而在函数中有一个变量ret,这个ret最开始为1,然后进行一些列操作最终返回这个ret值。

	int isPrime(int i) 
	{
    
    
		int ret = 1;
		int k;
		for (k = 2; k < i - 1; k++) {
    
    
			if (i%k == 0) {
    
    
				isPrime = 0;
				break;
			}
		}
	}

返回关键字return的作用:

return停止函数的执行,并送回一个值。因此,return有两种使用法:
return;
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;
}

我们调试运行,可以看到每次调用max的返回值都赋给c。
在这里插入图片描述


函数的返回值可以赋值给变量,可以再传递给函数,甚至可以丢弃,如下图所示。丢弃的原因在有的时候要检查函数是否有副作用,而不关心其返回什么值。

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+35, b);

如果函数没有返回值,那么前面的返回类型需要是void,具体用法如下:

void 函数名(参数表)
函数内不能使用带值的return
可以没有return
调用的时候不能做返回值的赋值


二、函数的参数和变量

2.1 函数原型

在前面我们定义函数一般都这样写,将函数定义放在main函数前面。

	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(20,30);
		sum(35,45);
		return 0;
	}

像这样把sum()写在上面,是因为:

• C的编译器自上而下顺序分析你的代码
• 在看到sum(1,10)的时候,它需要知道sum()的样子
• 也就是sum()要几个参数,每个参数的类型如何,返回什么类型
• 这样它才能检查你对sum()的调用是否正确

如果将函数定义写在main函数后面,则会报错
在这里插入图片描述


如果把所有函数定义都放main函数前面不太好看,有时候我们想一开始就能看见main函数。此时我们的做法是:将函数头复制粘贴至main函数前面并加上分号。我们复制上去的那一行函数头叫做函数的原型声明,12-21行的叫做函数的定义。现在我们再来编译,没问题。
在这里插入图片描述


关于函数的声明,总结如下:

1、函数头,以分号“;”结尾,就构成了函数的原型
2、函数原型的目的是告诉编译器这个函数长什么样
	• 名称
	• 参数(数量及类型)
	• 返回类型
3、旧标准习惯把函数原型写在调用它的函数里面
4、现在一般写在调用它的函数前面
5、原型里可以不写参数的名字,但是一般仍然写上

2.2 参数传递

如果函数有参数,调用函数时必须传递给它数量、类型正确的值。可以传递给函数的值是表达式的结果,这包括:

• 字面量
• 变量
• 函数的返回值
• 计算的结果

示例如下:

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+35, b);

调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞。这是由于编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的。后续的语言,C++/Java在这方面很严格。我们测试一下类型不匹配情况,发现编译器给我做了类型转换。
在这里插入图片描述
但是会有warning,提示我们类型转换可能会丢失数据。
在这里插入图片描述


调用函数时,传过去的值到底是什么。之前我们说过要交换两个变量的值,需要利用第三个变量。那现在我将交换的代码封装成一个函数,是否可以完成两个变量值的交换?代码如下:

#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 x, int y)
{
    
    
	int  t = x;
	x = y;
	y = t;
}

我们调试运行,可以看出:

a和b的初始值为5和6
进入函数时,没有a和b这两个变量
跳出函数后,没有x和y这两个变量
最终a和b的值仍为5和6
在这里插入图片描述


因此,C语言在调用函数时,永远只能传值给函数。 关于函数,有以下注意事项:

• 每个函数有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系
• 过去,对于函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫做“实际参数”
• 由于容易让初学者误会实际参数就是实际在函数中进行计算的参数,误会调用函数的时候把变量而不是值传进去了,所以我们不建议继续用这种古老的方式来称呼它们
• 我们认为,它们是参数和值的关系
在这里插入图片描述


2.3 本地变量

函数的每一次运行,就会产生一个独立的运行空间。在这个空间中的变量,是这个函数这一次运行所独有的,我们将其称作本地变量。定义在函数内部的变量就是本地变量,参数也是本地变量。

变量的生存期和作用域

• 生存期:什么时候这个变量开始出现了,到什么时候它消亡了
• 作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
• 对于本地变量,这两个问题的答案是统一的:大括号内 — 块

本地变量使用的规则:

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

我们来测试一下,在if语句的大括号内定义一个变量,并在外面使用它。

int a = 5;
int b = 6;
if (a < b)
{
    
    
	int i = 11;
}
i++;

此时会报错,说i未声明。i是在if那个大括号内定义的,出了那个大括号就不在了。
在这里插入图片描述
我们再来测试下直接写个大括号,然后在里面定义变量i,并在大括号外面使用。

	int a = 5;
	int b = 6;
	{
    
    
		int i = 11;
	}
	i++;

可以看出,仍然报相同的错误。
在这里插入图片描述


我们测试一下在块外和块内都定义一个变量a,看块内是否会覆盖块外。

	int i = 1;
	{
    
    
		int i = 2;
		printf("%d\n", i);
	}

可以看出,成功覆盖。
在这里插入图片描述

2.4 函数庶事

当函数没有参数时,是写成void f(void)还是void f()呢?当你在参数表内放个void明确代表这个函数不接受任何参数。当你在参数表里不放东西,在传统C中,它表示f函数的参数表未知,并不表示没有参数。那么在C99里面会怎样呢?我们来看看。

现在我把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函数,但是我不确定这个swap到底要什么参数。于是编译器遇到swap(a,b)时,它猜测我的swap函数需要两个int。
在这里插入图片描述


如果我把swap函数定义的参数列表改为两个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  t = x;
	printf("in swap, a=%f b=%f\n", x, y);
	x = y;
	y = t;
}

我们运行,可以看出我们给swap函数5和6,但是它拿到的是0.00000和一个非常大的数。
在这里插入图片描述
我们分析原因如下:

• 这是因为我们用void swap()这个函数声明(原型)去欺骗了编译器告诉编译器我也不知道swap函数要什么参数。
• 然后编译器看到swap(a,b)时认为swap函数要两个int,于是它为swap调用安排了两个int传递。
• 当编译器看到void swap(double x, double y)时,由于前面的函数声明(原型)不仅仅检查函数的调用是否正确,也要检查对函数的定义是否正确。现在原型看到这一行时,也不确定是什么类型(两个double也是可能的一种不确定)。
• 所以编译器从中没有发现任何问题,因此两个int传到double这里来就错了。

因此,不要用void swap()这种。如果确定函数里面没参数,就用void swap(void)。


还有一个小问题。调用函数时参数表里面也有逗号,我们也说过逗号是运算符。调用函数时的逗号和逗号运算符怎么区分?

调用函数时的圆括号里的逗号是标点符号,不是运算符
对于f(a,b)和f((a,b))  第一个传了两个参数,第二个传递了一个参数。

C语言不允许函数嵌套定义:C语言不允许在函数内部定义函数,但是可以放函数的声明。


我们来看两个例子都分别是什么:

• int i,j,sum(int a, int b);
声明了两个int变量和一个返回int类型值得sum函数。(不建议这种写法)
• return (i);
和return i一样,这个括号无意义。(不建议这种写法,让人误解return是个函数)

关于main函数的返回值

main函数中的return 0是有意义的。main函数是我们写的代码最先被执行的地方,但是它并不是程序运行起来第一条运行的代码。在main函数之前还有其他东西,那些东西是为了程序运行去做准备的,他们昨晚准备工作后会来调用你的main函数。因此,这个return 0是有意义的,要把这个0返回给调用它的地方,那个地方回来检查这个返回值然后报告给操作系统。在Windows中,一个程序返回0是正确的。返回任何非0的值是有错的。


小测验

1、以下哪句不是正确的原型?
A. int f();
B. int f(int i);
C. int f(int);
D. int f(int i) {}
答案:D

2、以下哪个函数的定义是错误的?
A. void f() {}
B. void f(int i) { return i+1; }
C. void f(int i) {}
D. int f() { return 0; }
答案:B

3、对于不返回值而且只有一个int类型的参数的函数,以下哪些函数原型是正确的?
A. void f(int x);
B. void f();
C. void f(int);
D. void f(x);
答案:A、B、C

4、以下程序的输出是什么?

#include <stdio.h>

void swap(int a, int b);

int main()
{
    
    
	int a = 5;
	int b = 6;

	swap(a,b);
	
	printf("%d-%d\n", a, b);
	
	return 0;
}

void swap(int a, int b)
{
    
    
	int t = a;
	a = b;
	b = t;
}

答案: 5-6

猜你喜欢

转载自blog.csdn.net/InnerPeaceHQ/article/details/121472806
今日推荐