函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,不需要编写大量重复的代码。
函数可以提前保存起来,并给它起一个独一无二的名字,只要知道它的名字就能使用这段代码。函数还可以接收数据,并根据数据的不同做出不同的操作,最后再把处理结果反馈给我们。
函数的概念
一个程序可能会用到很多次,如果每次都写这样一段重复的代码,不但费时费力、容易出错,而且交给别人时也很麻烦,所以C语言提供了一个功能,允许我们将常用的代码以固定的格式封装(包装)成一个独立的模块,只要知道这个模块的名字就可以重复使用它,这个模块就叫做函数(Function)。
函数的本质是一段可以重复使用的代码,这段代码被提前编写好了,放到了指定的文件中,使用时直接调取即可。下面演示如何封装 strcmp() 这个函数。
#include <stdio.h>
//将比较字符串大小的代码封装成函数,并命名为strcmp_alias
int strcmp_alias(char *s1, char *s2){
int i, result;
for(i=0; (result = s1[i] - s2[i]) == 0; i++){
if(s1[i] == '\0' || s2[i] == '\0'){
break;
}
}
return result;
}
int main(){
char str1[] = "http://c.biancheng.net";
char str2[] = "http://www.baidu.com";
char str3[] = "http://data.biancheng.net";
//重复使用strcmp_alias()函数
int result_1_2 = strcmp_alias(str1, str2);
int result_1_3 = strcmp_alias(str1, str3);
printf("str1 - str2 = %d\n", result_1_2);
printf("str1 - str3 = %d\n", result_1_3);
return 0;
}
为了避免与原有的 strcmp 产生命名冲突,我将新函数命名为 strcmp_alias。
这是我们自己编写的函数,放在了当前源文件中(函数封装和函数使用在同一个源文件中),所以不需要引入头文件;而C语言自带的 strcmp() 放在了其它的源文件中(函数封装和函数使用不在同一个源文件中),并在 string.h 头文件中告诉我们如何使用,所以我们必须引入 string.h 头文件。
我们自己编写的 strcmp_alias() 和原有的 strcmp() 在功能和格式上都是一样的,只是存放的位置不同,所以一个需要引入头文件,一个不需要引入。
库函数和自定义函数
C语言在发布时已经为我们封装好了很多函数,它们被分门别类地放到了不同的头文件中(暂时先这样认为),使用函数时引入对应的头文件即可。
C语言自带的函数称为库函数(Library Function)。库(Library)是编程中的一个基本概念,可以简单地认为它是一系列函数的集合,在磁盘上往往是一个文件夹。C语言自带的库称为标准库(Standard Library),其他公司或个人开发的库称为第三方库(Third-Party Library)。
除了库函数,我们还可以编写自己的函数,拓展程序的功能。自己编写的函数称为自定义函数。自定义函数和库函数在编写和使用方式上完全相同,只是由不同的机构来编写。
参数
函数的一个明显特征就是使用时带括号( )
,有必要的话,括号中还要包含数据或变量,称为参数(Parameter)。参数是函数需要处理的数据,例如:
-
strlen(str1)
用来计算字符串的长度,str1
就是参数。 -
puts("C语言中文网")
用来输出字符串,"C语言中文网"
就是参数。
返回值
既然函数可以处理数据,那就有必要将处理结果告诉我们,所以很多函数都有返回值(Return Value)。所谓返回值,就是函数的执行结果。例如:
char str1[] = "C Language";
int len = strlen(str1);
strlen() 的处理结果是字符串 str1 的长度,是一个整数,我们通过 len 变量来接收。
函数返回值有固定的数据类型(int、char、float等),用来接收返回值的变量类型要一致。
函数定义
函数是一段可以重复使用的代码,用来独立地完成某个功能,它可以接收用户传递的数据,也可以不接收。接收用户数据的函数在定义时要指明参数,不接收用户数据的不需要指明,根据这一点可以将函数分为有参函数和无参函数。
将代码段封装成函数的过程叫做函数定义。
无参函数的定义
如果函数不接收用户传递的数据,那么定义时可以不带参数。如下所示:
dataType functionName(){
//body
}
-
dataType 是返回值类型,它可以是C语言中的任意数据类型,例如 int、float、char 等。
-
functionName 是函数名,它是标识符的一种,命名规则和标识符相同。函数名后面的括号
( )
不能少。 -
body 是函数体,它是函数需要执行的代码,是函数的主体部分。即使只有一个语句,函数体也要由
{ }
包围。 -
如果有返回值,在函数体中使用 return 语句返回。return 出来的数据的类型要和 dataType 一样。
例如,定义一个函数,计算从 1 加到 100 的结果:
int sum(){
int i, sum=0;
for(i=1; i<=100; i++){
sum+=i;
}
return sum;
}
累加结果保存在变量sum
中,最后通过return
语句返回。sum 是 int 型,返回值也是 int 类型,它们一一对应。
return
是C语言中的一个关键字,只能用在函数中,用来返回处理结果。
#include <stdio.h>
int sum(){
int i, sum=0;
for(i=1; i<=100; i++){
sum+=i;
}
return sum;
}
int main(){
int a = sum();
printf("The sum is %d\n", a);
return 0;
}
// The sum is 5050
函数不能嵌套定义,main 也是一个函数定义,所以要将 sum 放在 main 外面。函数必须先定义后使用,所以 sum 要放在 main 前面。
main 是函数定义,不是函数调用。当可执行文件加载到内存后,系统从 main 函数开始执行,也就是说,系统会调用我们定义的 main 函数。
无返回值函数
有的函数不需要返回值,或者返回值类型不确定(很少见),那么可以用 void 表示,例如:
void hello(){
printf ("Hello,world \n");
//没有返回值就不需要 return 语句
}
void
是C语言中的一个关键字,表示“空类型”或“无类型”,绝大部分情况下也就意味着没有 return 语句。
有参函数的定义
如果函数需要接收用户传递的数据,那么定义时就要带上参数。如下所示:
dataType functionName( dataType1 param1, dataType2 param2 ... ){
//body
}
dataType1 param1, dataType2 param2 ...
是参数列表。函数可以只有一个参数,也可以有多个,多个参数之间由,
分隔。参数本质上也是变量,定义时要指明类型和名称。与无参函数的定义相比,有参函数的定义仅仅是多了一个参数列表。
数据通过参数传递到函数内部进行处理,处理完成以后再通过返回值告知函数外部。
计算从 m 加到 n 的结果:
int sum(int m, int n){
int i, sum=0;
for(i=m; i<=n; i++){
sum+=i;
}
return sum;
}
参数列表中给出的参数可以在函数体中使用,使用方式和普通变量一样。
调用 sum() 函数时,需要给它传递两份数据,一份传递给 m,一份传递给 n。你可以直接传递整数,例如:
int result = sum(1, 100); //1传递给m,100传递给n
也可以传递变量:
int begin = 4;
int end = 86;
int result = sum(begin, end); //begin传递给m,end传递给n
也可以整数和变量一起传递:
int num = 33;
int result = sum(num, 80); //num传递给m,80传递给n
函数定义时给出的参数称为形式参数,简称形参;函数调用时给出的参数(也就是传递的数据)称为实际参数,简称实参。函数调用时,将实参的值传递给形参,相当于一次赋值操作。
原则上讲,实参的类型和数目要与形参保持一致。如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型,例如将 int 类型的实参传递给 float 类型的形参就会发生自动类型转换。
#include <stdio.h>
int sum(int m, int n){
int i, sum=0;
for(i=m; i<=n; i++){
sum+=i;
}
return sum;
}
int main(){
int begin = 5, end = 86;
int result = sum(begin, end);
printf("The sum from %d to %d is %d\n", begin, end, result);
return 0;
}
//The sum from 5 to 86 is 3731
函数不能嵌套定义
强调一点,C语言不允许函数嵌套定义;也就是说,不能在一个函数中定义另外一个函数,必须在所有函数之外定义另外一个函数。main() 也是一个函数定义,也不能在 main() 函数内部定义新函数。
下面的例子是错误的:
#include <stdio.h>
void func1(){
printf("http://c.biancheng.net");
void func2(){
printf("C语言小白变怪兽");
}
}
int main(){
func1();
return 0;
}
正确的写法应该是这样的:
#include <stdio.h>
void func2(){
printf("C语言小白变怪兽");
}
void func1(){
printf("http://c.biancheng.net");
func2();
}
int main(){
func1();
return 0;
}
func1()、func2()、main() 三个函数是平行的,谁也不能位于谁的内部,要想达到「调用 func1() 时也调用 func2()」的目的,必须将 func2() 定义在 func1() 外面,并在 func1() 内部调用 func2()。
有些编程语言是允许函数嵌套定义的,例如 JavaScript,在 JavaScript 中经常会使用函数的嵌套定义。
形参和实参
如果把函数比喻成一台机器,那么参数就是原材料,返回值就是最终产品;从一定程度上讲,函数的作用就是根据不同的参数产生不同的返回值。
C语言函数的参数会出现在两个地方,分别是函数定义处和函数调用处,这两个地方的参数是有区别的。
形参(形式参数)
在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。
实参(实际参数)
函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。
形参和实参的功能是传递数据,发生函数调用时,实参的值会传递给形参。
形参和实参的区别和联系
-
形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。
-
实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。
-
实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。
-
函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。
#include <stdio.h>
//计算从m加到n的值
int sum(int m, int n) {
int i;
for (i = m+1; i <= n; ++i) {
m += i;
}
return m;
}
int main() {
int a, b, total;
printf("Input two numbers: ");
scanf("%d %d", &a, &b);
total = sum(a, b);
printf("a=%d, b=%d\n", a, b);
printf("total=%d\n", total);
return 0;
}
/*
Input two numbers: 1 100↙
a=1, b=100
total=5050
*/
在这段代码中,函数定义处的 m、n 是形参,函数调用处的 a、b 是实参。通过 scanf() 可以读取用户输入的数据,并赋值给 a、b,在调用 sum() 函数时,这份数据会传递给形参 m、n。
从运行情况看,输入 a 值为 1,即实参 a 的值为 1,把这个值传递给函数 sum() 后,形参 m 的初始值也为 1,在函数执行过程中,形参 m 的值变为 5050。函数运行结束后,输出实参 a 的值仍为 1,可见实参的值不会随形参的变化而变化。
以上调用 sum() 时是将变量作为函数实参,除此以外,也可以将常量、表达式、函数返回值作为实参,如下所示:
total = sum(10, 98); //将常量作为实参
total = sum(a+10, b-3); //将表达式作为实参
total = sum( pow(2,2), abs(-100) ); //将函数返回值作为实参
- 形参和实参虽然可以同名,但它们之间是相互独立的,互不影响,因为实参在函数外部有效,而形参在函数内部有效。
#include <stdio.h>
//计算从m加到n的值
int sum(int m, int n) {
int i;
for (i = m + 1; i <= n; ++i) {
m += i;
}
return m;
}
int main() {
int m, n, total;
printf("Input two numbers: ");
scanf("%d %d", &m, &n);
total = sum(m, n);
printf("m=%d, n=%d\n", m, n);
printf("total=%d\n", total);
return 0;
}
/*
Input two numbers: 1 100↙
a=1, b=100
total=5050
*/
调用 sum() 函数后,函数内部的形参 m 的值已经发生了变化,而函数外部的实参 m 的值依然保持不变,可见它们是相互独立的两个变量,除了传递参数的一瞬间,其它时候是没有瓜葛的。
函数返回值
函数的返回值是指函数被调用之后,执行函数体中的代码所得到的结果,这个结果通过 return 语句返回。
return 语句的一般形式为:
return 表达式;
或者
return (表达式);
有没有( )
都是正确的,为了简明,一般也不写( )
。例如:
return max;
return a+b;
return (100+200);
对返回值的说明
- 没有返回值的函数为空类型,用
void
表示。例如:
void func(){
printf("http://c.biancheng.net\n");
}
一旦函数的返回值类型被定义为 void,就不能再接收它的值了。例如,下面的语句是错误的:
int a = func();
为了使程序有良好的可读性并减少出错, 凡不要求返回值的函数都应定义为 void 类型。
- return 语句可以有多个,可以出现在函数体的任意位置,但是每次调用函数只能有一个 return 语句被执行,所以只有一个返回值。例如:
//返回两个整数中较大的一个
int max(int a, int b){
if(a > b){
return a;
}else{
return b;
}
}
//如果a>b成立,就执行return a,return b不会执行;如果不成立,就执行return b,return a不会执行。
- 函数一旦遇到 return 语句就立即返回,后面的所有语句都不会被执行到了。从这个角度看,return 语句还有强制结束函数执行的作用。例如:
//返回两个整数中较大的一个
int max(int a, int b){
return (a>b) ? a : b;
printf("Function is performed\n");//第 4 行代码就是多余的,永远没有执行的机会。
}
判断素数的函数:
#include <stdio.h>
int prime(int n){
int is_prime = 1, i;
//n一旦小于0就不符合条件,就没必要执行后面的代码了,所以提前结束函数
if(n < 0){ return -1; }
for(i=2; i<n; i++){
if(n % i == 0){
is_prime = 0;
break;
}
}
return is_prime;
}
int main(){
int num, is_prime;
scanf("%d", &num);
is_prime = prime(num);
if(is_prime < 0){
printf("%d is a illegal number.\n", num);
}else if(is_prime > 0){
printf("%d is a prime number.\n", num);
}else{
printf("%d is not a prime number.\n", num);
}
return 0;
}
prime() 是一个用来求素数的函数。素数是自然数,它的值大于等于零,一旦传递给 prime() 的值小于零就没有意义了,就无法判断是否是素数了,所以一旦检测到参数 n 的值小于 0,就使用 return 语句提前结束函数。
return 语句是提前结束函数的唯一办法。return 后面可以跟一份数据,表示将这份数据返回到函数外面;return 后面也可以不跟任何数据,表示什么也不返回,仅仅用来结束函数。
更改上面的代码,使得 return 后面不跟任何数据:
#include <stdio.h>
void prime(int n){
int is_prime = 1, i;
if(n < 0){
printf("%d is a illegal number.\n", n);
return; //return后面不带任何数据
}
for(i=2; i<n; i++){
if(n % i == 0){
is_prime = 0;
break;
}
}
if(is_prime > 0){
printf("%d is a prime number.\n", n);
}else{
printf("%d is not a prime number.\n", n);
}
}
int main(){
int num;
scanf("%d", &num);
prime(num);
return 0;
}
prime() 的返回值是 void,return 后面不能带任何数据,直接写分号即可。
#include <stdio.h>
#include "math.h"
//直接输出是否为素数
void prime(int n) {
int is_prime = 1, i;
if (n < 0) {
printf("%d is a illegal number.\n", n);
return; //return后面不带任何数据
}
//优化的写法,用sqrt减少循环次数
for (i = 2; i <= sqrt(n); i++) {
if (n % i == 0) {
printf("%d/%d=%d\n", n, i, n / i);
is_prime = 0;
break;
}
}
if (is_prime == 1) {
printf("%d is a prime number.\n", n);
} else {
printf("%d is not a prime number.\n", n);
}
}
int main() {
int num;
scanf("%d", &num);
prime(num);
return 0;
}
函数调用
所谓函数调用(Function Call),就是使用已经定义好的函数。函数调用的一般形式为:
functionName(param1, param2, param3 ...);
functionName 是函数名称,param1, param2, param3 ...
是实参列表。实参可以是常数、变量、表达式等,多个实参用逗号,
分隔。
在C语言中,函数调用的方式有多种,例如:
//函数作为表达式中的一项出现在表达式中
z = max(x, y);
m = n + max(x, y);
//函数作为一个单独的语句
printf("%d", a);
scanf("%d", &b);
//函数作为调用另一个函数时的实参
printf( "%d", max(x, y) );
total( max(x, y), min(m, n) );
函数的嵌套调用
函数不能嵌套定义,但可以嵌套调用,也就是在一个函数的定义或调用过程中允许出现对另外一个函数的调用。
计算sum = 1! + 2! + 3! + … + (n-1)! + n!
#include "stdio.h"
// 计算阶乘
int factorial(int n) {
if (n < 0)return -1;
int prod = 1;
for (int i = 0; i < n; ++i) {
prod *= (i + 1);
}
return prod;
}
//计算求和
long sum(long n) {
int sum = 0;
for (int i = 0; i < n; ++i) {
sum += factorial(i + 1);//在定义的过程中出现嵌套调用
}
return sum;
}
int main() {
printf("%ld", sum(10));//在调用的过程中出现嵌套调用
return 0;
}
//4037913
sum() 的定义中出现了对 factorial() 的调用,printf() 的调用过程中出现了对 sum() 的调用,而 printf() 又被 main() 调用,它们整体调用关系为:
main() --> printf() --> sum() --> factorial()
如果一个函数 A() 在定义或调用过程中出现了对另外一个函数 B() 的调用,那么我们就称 A() 为主调函数或主函数,称 B() 为被调函数。
当主调函数遇到被调函数时,主调函数会暂停,CPU 转而执行被调函数的代码;被调函数执行完毕后再返回主调函数,主调函数根据刚才的状态继续往下执行。
一个C语言程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条。这个链条的起点是 main(),终点也是 main()。当 main() 调用完了所有的函数,它会返回一个值(例如return 0;
)来结束自己的生命,从而结束整个程序。
函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码,当遇到函数调用时,CPU 首先要记录下当前代码块中下一条代码的地址(假设地址为 0X1000),然后跳转到另外一个代码块,执行完毕后再回来继续执行 0X1000 处的代码。整个过程相当于 CPU 开了一个小差,暂时放下手中的工作去做点别的事情,做完了再继续刚才的工作。
从上面的分析可以推断出,在所有函数之外进行加减乘除运算、使用 if…else 语句、调用一个函数等都是没有意义的,这些代码位于整个函数调用链条之外,永远都不会被执行到。C语言也禁止出现这种情况,会报语法错误:
#include <stdio.h>
int a = 10, b = 20, c;
//错误:不能出现加减乘除运算
//c = a + b;
//错误:不能出现对其他函数的调用
//printf("c.biancheng.net");
int main(){
return 0;
}
函数声明以及函数原型
C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。
所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
函数声明的格式非常简单,相当于去掉函数定义中的函数体,并在最后加上分号;
,如下所示:
dataType functionName( dataType1 param1, dataType2 param2 ... );
也可以不写形参,只写数据类型:
dataType functionName( dataType1, dataType2 ... );
函数声明给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息,称为函数原型(Function Prototype)。函数原型的作用是告诉编译器与该函数有关的信息,让编译器知道函数的存在,以及存在的形式,即使函数暂时没有定义,编译器也知道如何使用它。
有了函数声明,函数定义就可以出现在任何地方了,甚至是其他文件、静态链接库、动态链接库等。
定义一个函数 sum(),计算从 m 加到 n 的和,并将 sum() 的定义放到 main() 后面:
#include <stdio.h>
//函数声明
int sum(int m, int n); //也可以写作int sum(int, int);
int main(){
int begin = 5, end = 86;
int result = sum(begin, end);
printf("The sum from %d to %d is %d\n", begin, end, result);
return 0;
}
//函数定义
int sum(int m, int n){
int i, sum=0;
for(i=m; i<=n; i++){
sum+=i;
}
return sum;
}
在 main() 函数中调用了 sum() 函数,编译器在它前面虽然没有发现函数定义,但是发现了函数声明,这样编译器就知道函数怎么使用了,至于函数体到底是什么,暂时可以不用操心,后续再把函数体补上就行。
函数原型类似于Java中接口里的抽象方法,没有方法体,只有方法的签名信息,而函数的定义相当于实现了该接口的类对接口中方法的具体实现。
定义两个函数,计算1! + 2! + 3! + ... + (n-1)! + n!
的和:
#include <stdio.h>
// 函数声明部分
long factorial(int n); //也可以写作 long factorial(int);
long sum(long n); //也可以写作 long sum(long);
int main(){
printf("1!+2!+...+9!+10! = %ld\n", sum(10));
return 0;
}
//函数定义部分
//求阶乘
long factorial(int n){
int i;
long result=1;
for(i=1; i<=n; i++){
result *= i;
}
return result;
}
// 求累加的和
long sum(long n){
int i;
long result = 0;
for(i=1; i<=n; i++){
result += factorial(i);
}
return result;
}
简单的代码顶多几百行,完全可以放在一个源文件中。对于单个源文件的程序,通常是将函数定义放到 main() 的后面,将函数声明放到 main() 的前面,这样就使得代码结构清晰明了,主次分明。
使用者往往只关心函数的功能和函数的调用形式,很少关心函数的实现细节,将函数定义放在最后,就是尽量屏蔽不重要的信息,凸显关键的信息。将函数声明放到 main() 的前面,在定义函数时也不用关注它们的调用顺序了,哪个函数先定义,哪个函数后定义,都无所谓了。
然而在实际开发中,往往都是几千行、上万行、百万行的代码,将这些代码都放在一个源文件中简直是灾难,不但检索麻烦,而且打开文件也很慢,所以必须将这些代码分散到多个文件中。对于多个文件的程序,通常是将函数定义放到源文件(.c
文件)中,将函数的声明放到头文件(.h
文件)中,使用函数时引入对应的头文件就可以,编译器会在链接阶段找到函数体。
前面我们在使用 printf()、puts()、scanf() 等函数时引入了 stdio.h 头文件,很多初学者认为 stdio.h 中包含了函数定义(也就是函数体),只要有了头文件就能运行,其实不然,头文件中包含的都是函数声明,而不是函数定义,函数定义都放在了其它的源文件中,这些源文件已经提前编译好了,并以动态链接库或者静态链接库的形式存在,只有头文件没有系统库的话,在链接阶段就会报错,程序根本不能运行。
除了函数,变量也有定义和声明之分。实际开发过程中,变量定义需要放在源文件(.c
文件)中,变量声明需要放在头文件(.h
文件)中,在链接程序时会将它们对应起来。
函数原型给出了使用该函数的所有细节,当我们不知道如何使用某个函数时,需要查找的是它的原型,而不是它的定义,我们往往不关心它的实现。
全局变量和局部变量
形参变量要等到函数被调用时才分配内存,调用结束后立即释放内存。这说明形参变量的作用域非常有限,只能在函数内部使用,离开该函数就无效了。所谓作用域(Scope),就是变量的有效范围。
不仅对于形参变量,C语言中所有的变量都有自己的作用域。决定变量作用域的是变量的定义位置。
局部变量
定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。
int f1(int a){
int b,c; //a,b,c仅在函数f1()内有效
return a+b+c;
}
int main(){
int m,n; //m,n仅在函数main()内有效
return 0;
}
-
在 main 函数中定义的变量也是局部变量,只能在 main 函数中使用;同时,main 函数中也不能使用其它函数中定义的变量。main 函数也是一个函数,与其它函数地位平等。
-
形参变量、在函数体内定义的变量都是局部变量。实参给形参传值的过程也就是给局部变量赋值的过程。
-
可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。
-
在语句块中也可定义变量,它的作用域只限于当前语句块。
全局变量
在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。
int a, b; //全局变量
void func1(){
//TODO:
}
float x,y; //全局变量
int func2(){
//TODO:
}
int main(){
//TODO:
return 0;
}
a、b、x、y 都是在函数外部定义的全局变量。C语言代码是从前往后依次执行的,由于 x、y 定义在函数 func1() 之后,所以在 func1() 内无效;而 a、b 定义在源程序的开头,所以在 func1()、func2() 和 main() 内都有效。
局部变量和全局变量的综合示例
#include <stdio.h>
int n = 10; //全局变量
void func1(){
int n = 20; //局部变量
printf("func1 n: %d\n", n);
}
void func2(int n){
printf("func2 n: %d\n", n);
}
void func3(){
printf("func3 n: %d\n", n);
}
int main(){
int n = 30; //局部变量
func1();
func2(n);
func3();
//代码块由{}包围
{
int n = 40; //局部变量
printf("block n: %d\n", n);
}
printf("main n: %d\n", n);
return 0;
}
代码中虽然定义了多个同名变量 n,但它们的作用域不同,在内存中的位置(地址)也不同,所以是相互独立的变量,互不影响,不会产生重复定义(Redefinition)
错误。
- 对于 func1(),输出结果为 20,显然使用的是函数内部的 n,而不是外部的 n;func2() 也是相同的情况。
当全局变量和局部变量同名时,在局部范围内全局变量被“屏蔽”,不再起作用。或者说,变量的使用遵循就近原则,如果在当前作用域中存在同名变量,就不会向更大的作用域中去寻找变量。
-
func3() 输出 10,使用的是全局变量,因为在 func3() 函数中不存在局部变量 n,所以编译器只能到函数外部,也就是全局作用域中去寻找变量 n。
-
由
{ }
包围的代码块也拥有独立的作用域,printf() 使用它自己内部的变量 n,输出 40。 -
C语言规定,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。对于 main() 函数,即使代码块中的 n 离输出语句更近,但它仍然会使用 main() 函数开头定义的 n,所以输出结果是 30。
根据长方体的长宽高求它的体积以及三个面的面积:
#include <stdio.h>
int s1, s2, s3; //面积
int vs(int a, int b, int c){
int v; //体积
v = a * b * c;
s1 = a * b;
s2 = b * c;
s3 = a * c;
return v;
}
int main(){
int v, length, width, height;
printf("Input length, width and height: ");
scanf("%d %d %d", &length, &width, &height);
v = vs(length, width, height);
printf("v=%d, s1=%d, s2=%d, s3=%d\n", v, s1, s2, s3);
return 0;
}
/*
Input length, width and height: 10 20 30↙
v=6000, s1=200, s2=600, s3=300
*/
根据题意,我们希望借助一个函数得到三个值:体积 v 以及三个面的面积 s1、s2、s3。遗憾的是,C语言中的函数只能有一个返回值,我们只能将其中的一份数据,也就是体积 v 放到返回值中,而将面积 s1、s2、s3 设置为全局变量。全局变量的作用域是整个程序,在函数 vs() 中修改 s1、s2、s3 的值,能够影响到包括 main() 在内的其它函数。
也可以修改为以下代码,将体积也声明为全局变量,vs的作用就是计算并对4个变量进行赋值:
#include <stdio.h>
int v, s1, s2, s3; //面积
int vs(int a, int b, int c) {
v = a * b * c;
s1 = a * b;
s2 = b * c;
s3 = a * c;
}
int main() {
int length, width, height;
printf("Input length, width and height: ");
scanf("%d %d %d", &length, &width, &height);
vs(length, width, height);
printf("v=%d, s1=%d, s2=%d, s3=%d\n", v, s1, s2, s3);
return 0;
}
变量的作用域
所谓作用域(Scope),就是变量的有效范围,就是变量可以在哪个范围以内使用。有些变量可以在所有代码文件中使用,有些变量只能在当前的文件中使用,有些变量只能在函数内部使用,有些变量只能在 for 循环内部使用。
变量的作用域由变量的定义位置决定,在不同位置定义的变量,它的作用域是不一样的。
在函数内部定义的变量(局部变量)
在函数内部定义的变量,它的作用域也仅限于函数内部,出了函数就不能使用了,我们将这样的变量称为局部变量(Local Variable)。函数的形参也是局部变量,也只能在函数内部使用:
#include <stdio.h>
int sum(int m, int n){
int i, sum=0;
//m、n、i、sum 都是局部变量,只能在 sum() 内部使用
for(i=m; i<=n; i++){
sum+=i;
}
return sum;
}
int main(){
int begin = 5, end = 86;
int result = sum(begin, end);
//begin、end、result 也都是局部变量,只能在 main() 内部使用
printf("The sum from %d to %d is %d\n", begin, end, result);
return 0;
}
对局部变量的两点说明:
-
main() 也是一个函数,在 main() 内部定义的变量也是局部变量,只能在 main() 函数内部使用。
-
形参也是局部变量,将实参传递给形参的过程,就是用实参给局部变量赋值的过程,它和
a=b; sum=m+n;
这样的赋值没有什么区别。
在所有函数外部定义的变量(全局变量)
C语言允许在所有函数的外部定义变量,这样的变量称为全局变量(Global Variable)。
全局变量的默认作用域是整个程序,也就是所有的代码文件,包括源文件(.c
文件)和头文件(.h
文件)。如果给全局变量加上 static 关键字,它的作用域就变成了当前文件,在其它文件中就无效了。
定义一个函数,根据长方体的长宽高求它的体积以及三个面的面积:
#include <stdio.h>
//定义三个全局变量,分别表示三个面的面积
int s1 = 0, s2 = 0, s3 = 0;
int vs(int length, int width, int height){
int v; //体积
v = length * width * height;
s1 = length * width;
s2 = width * height;
s3 = length * height;
return v;
}
int main(){
int v = 0;
v = vs(15, 20, 30);
printf("v=%d, s1=%d, s2=%d, s3=%d\n", v, s1, s2, s3);
v = vs(5, 17, 8);
printf("v=%d, s1=%d, s2=%d, s3=%d\n", v, s1, s2, s3);
return 0;
}
/*
v=9000, s1=300, s2=600, s3=450
v=680, s1=85, s2=136, s3=40
*/
根据题意,我们希望借助一个函数得到四份数据:体积 v 以及三个面的面积 s1、s2、s3。遗憾的是,C语言中的函数只能有一个返回值,我们只能将其中的一份数据(也就是体积 v)放到返回值中,其它三份数据(也就是面积 s1、s2、s3)只能保存到全局变量中。
C语言代码从前往后依次执行,变量在使用之前必须定义或者声明,全局变量 s1、s2、s3 定义在程序开头,所以在 vs() 和 main() 中都有效。
在 vs() 中将求得的面积放到 s1、s2、s3 中,在 main() 中能够顺利取得它们的值,这说明:在一个函数内部修改全局变量的值会影响其它函数,全局变量的值在函数内部被修改后并不会自动恢复,它会一直保留该值,直到下次被修改。
全局变量也是变量,变量只能保存一份数据,一旦数据被修改了,原来的数据就被冲刷掉了,再也无法恢复了,所以不管是全局变量还是局部变量,一旦它的值被修改,这种影响都会一直持续下去,直到再次被修改。
关于变量的命名
每一段可运行的C语言代码都包含了多个作用域,即使最简单的C语言代码也是如此:
int main(){
return 0;
}
这就是最简单的、可运行的C语言代码,它包含了两个作用域,一个是 main() 函数内部的局部作用域,一个是 main() 函数外部的全局作用域。
C语言规定,在同一个作用域中不能出现两个名字相同的变量,否则会产生命名冲突;但是在不同的作用域中,允许出现名字相同的变量,它们的作用范围不同,彼此之间不会产生冲突。这句话有两层含义:
-
不同函数内部可以出现同名的变量,不同函数是不同的局部作用域;
-
函数内部和外部可以出现同名的变量,函数内部是局部作用域,函数外部是全局作用域。
- 不同函数内部的同名变量是两个完全独立的变量,它们之间没有任何关联,也不会相互影响:
#include <stdio.h>
void func_a(){
int n = 100;
printf("func_a: n = %d\n", n);
n = 86;
printf("func_a: n = %d\n", n);
}
void func_b(){
int n = 29;
printf("func_b: n = %d\n", n);
func_a(); //调用func_a()
printf("func_b: n = %d\n", n);
}
int main(){
func_b();
return 0;
}
/*
func_b: n = 29
func_a: n = 100
func_a: n = 86
func_b: n = 29
*/
func_a() 和 func_b() 内部都定义了一个变量 n,在 func_b() 中,n 的初始值是 29,调用 func_a() 后,n 值还是 29,这说明 func_b() 内部的 n 并没有影响 func_a() 内部的 n。这两个 n 是完全不同的变量,彼此之间根本“不认识”,只是起了个相同的名字而已。
- 函数内部的局部变量和函数外部的全局变量同名时,在当前函数这个局部作用域中,全局变量会被“屏蔽”,不再起作用。也就是说,在函数内部使用的是局部变量,而不是全局变量。
变量的使用遵循就近原则,如果在当前的局部作用域中找到了同名变量,就不会再去更大的全局作用域中查找。另外,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。
举例说明:
#include <stdio.h>
int n = 10; //全局变量
void func1(){
int n = 20; //局部变量
printf("func1 n: %d\n", n);
}
void func2(int n){
printf("func2 n: %d\n", n);
}
void func3(){
printf("func3 n: %d\n", n);
}
int main(){
int n = 30; //局部变量
func1();
func2(n);
func3();
printf("main n: %d\n", n);
return 0;
}
/*
func1 n: 20
func2 n: 30
func3 n: 10
main n: 30
*/
代码中虽然定义了多个同名变量 n,但它们的作用域不同,所有不会产生命名冲突。
下面是对输出结果的分析:
-
对于 func1(),输出结果为 20,显然使用的是 func1() 内部的 n,而不是外部的 n。
-
调用 func2() 时,会把 main() 中的实参 n 传递给 func2() 中的形参 n,此时形参 n 的值变为 30。形参 n 也是局部变量,所以就使用它了。
-
func3() 输出 10,使用的是全局变量,因为在 func3() 中不存在局部变量 n,所以编译器只能到函数外部,也就是全局作用域中去寻找变量 n。
-
main() 中 printf() 语句输出 30,说明使用的是 main() 中的 n,而不是外部的 n。
块级变量
所谓代码块,就是由{ }
包围起来的代码。代码块在C语言中随处可见,例如函数体、选择结构、循环结构等。不包含代码块的C语言程序根本不能运行,即使最简单的C语言程序也要包含代码块。
C语言允许在代码块内部定义变量,这样的变量具有块级作用域;换句话说,在代码块内部定义的变量只能在代码块内部使用,出了代码块就无效了。
定义一个函数 gcd(),求两个整数的最大公约数:
#include <stdio.h>
//函数声明
int gcd(int a, int b); //也可以写作 int gcd(int, int);
int main() {
printf("The greatest common divisor is %d\n", gcd(100, 60));
return 0;
}
//函数定义
int gcd(int a, int b) {
//若a<b,那么交换两变量的值
if (a < b) {
int temp1 = a; //块级变量
a = b;
b = temp1;
}
//求最大公约数
while (b != 0) {
int temp2 = b; //块级变量
b = a % b;
a = temp2;
}
return a;
}
//The greatest common divisor is 20
关注 temp1 和 temp2 这两个变量,它们都是在代码块内部定义的块级变量,temp1 的作用域是 if 内部,temp2 的作用域是 while 内部。
在 for 循环条件里面定义变量
遵循 C99 标准的编译器允许在 for 循环条件里面定义新变量,这样的变量也是块级变量,它的作用域仅限于 for 循环内部。例如,计算从 m 累加到 n 的和:
#include <stdio.h>
int sum(int m, int n);
int main(){
printf("The sum from 1 to 100 is %d\n", sum(1, 100));
return 0;
}
int sum(int m, int n){
int sum = 0;
for(int i=m; i<=n; i++){ //i是块级变量
sum += i;
}
return sum;
}
变量 i 定义在循环条件里面,所以是一个块级变量,它的作用域就是当前 for 循环,出了 for 循环就无效了。
如果一个变量只在 for 循环内部使用,就可以将它定义在循环条件里面,这样做可以避免在函数开头定义过多的变量,使得代码结构更加清晰。
定义一个函数 strchar(),查看给定的字符是否位于某个字符串中:
#include <stdio.h>
#include <string.h>
int strchar(char *str, char c);
int main(){
char url[] = "http://c.biancheng.net";
char letter = 'c';
if(strchar(url, letter) >= 0){
printf("The letter is in the string.\n");
}else{
printf("The letter is not in the string.\n");
}
return 0;
}
int strchar(char *str, char c){
for(int i=0,len=strlen(str); i<len; i++){ //i和len都是块级变量
if(str[i] == c){
return i;
}
}
return -1;
}
循环条件里面可以定义一个或者多个变量,这段代码我们就定义了两个变量,分别是 i 和 len,它们都是块级变量,作用域都是当前 for 循环。
单独的代码块
C语言还允许出现单独的代码块,它也是一个作用域:
#include <stdio.h>
int main(){
int n = 22; //编号①
//由{ }包围的代码块
{
int n = 40; //编号②
printf("block n: %d\n", n);
}
printf("main n: %d\n", n);
return 0;
}
/*
block n: 40
main n: 22
*/
这里有两个 n,它们位于不同的作用域,不会产生命名冲突。{ } 的作用域比 main() 更小,{ } 内部的 printf() 使用的是编号为②的 n,main() 内部的 printf() 使用的是编号为①的 n。
再谈作用域
每个C语言程序都包含了多个作用域,不同的作用域中可以出现同名的变量,C语言会按照从小到大的顺序、一层一层地去父级作用域中查找变量,如果在最顶层的全局作用域中还未找到这个变量,那么就会报错。
#include <stdio.h>
int m = 13;
int n = 10;
void func1(){
int n = 20;
{
int n = 822;
printf("block1 n: %d\n", n);
}
printf("func1 n: %d\n", n);
}
void func2(int n){
for(int i=0; i<10; i++){
if(i % 5 == 0){
printf("if m: %d\n", m);
}else{
int n = i % 4;
if(n<2 && n>0){
printf("else m: %d\n", m);
}
}
}
printf("func2 n: %d\n", n);
}
void func3(){
printf("func3 n: %d\n", n);
}
int main(){
int n = 30;
func1();
func2(n);
func3();
printf("main n: %d\n", n);
return 0;
}
/*
block1 n: 822
func1 n: 20
if m: 13
else m: 13
if m: 13
else m: 13
func2 n: 30
func3 n: 10
main n: 30
*/
下图展示了这段代码的作用域:
蓝色表示作用域的名称,红色表示作用域中的变量,global 表示全局作用域。在灰色背景的作用域中,我们使用到了 m 变量,而该变量位于全局作用域中,所以得穿越好几层作用域才能找到 m。
递归函数
一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层,当最内层的函数执行完毕后,再一层一层地由里到外退出。
一个求阶乘的例子:
#include <stdio.h>
//求n的阶乘
long factorial(int n) {
if (n == 0 || n == 1) {
return 1;
}
else {
return factorial(n - 1) * n; // 递归调用
}
}
int main() {
int a;
printf("Input a number: ");
scanf("%d", &a);
printf("Factorial(%d) = %ld\n", a, factorial(a));
return 0;
}
/*
Input a number: 5↙
Factorial(5) = 120
*/
factorial() 就是一个典型的递归函数。调用 factorial() 后即进入函数体,只有当 n0 或 n1 时函数才会执行结束,否则就一直调用它自身。
由于每次调用的实参为 n-1,即把 n-1 的值赋给形参 n,所以每次递归实参的值都减 1,直到最后 n-1 的值为 1 时再作递归调用,形参 n 的值也为1,递归就终止了,会逐层退出。
要想理解递归函数,重点是理解它是如何逐层进入,又是如何逐层退出的。
递归的进入
-
求 5!,即调用 factorial(5)。当进入 factorial() 函数体后,由于形参 n 的值为 5,不等于 0 或 1,所以执行
factorial(n-1) * n
,也即执行factorial(4) * 5
。为了求得这个表达式的结果,必须先调用 factorial(4),并暂停其他操作。换句话说,在得到 factorial(4) 的结果之前,不能进行其他操作。这就是第一次递归。 -
调用 factorial(4) 时,实参为 4,形参 n 也为 4,不等于 0 或 1,会继续执行
factorial(n-1) * n
,也即执行factorial(3) * 4
。为了求得这个表达式的结果,又必须先调用 factorial(3)。这就是第二次递归。 -
以此类推,进行四次递归调用后,实参的值为 1,会调用 factorial(1)。此时能够直接得到常量 1 的值,并把结果 return,就不需要再次调用 factorial() 函数了,递归就结束了。
层次/层数 | 实参/形参 | 调用形式 | 需要计算的表达式 | 需要等待的结果 |
---|---|---|---|---|
1 | n=5 | factorial(5) | factorial(4) * 5 | factorial(4) 的结果 |
2 | n=4 | factorial(4) | factorial(3) * 4 | factorial(3) 的结果 |
3 | n=3 | factorial(3) | factorial(2) * 3 | factorial(2) 的结果 |
4 | n=2 | factorial(2) | factorial(1) * 2 | factorial(1) 的结果 |
5 | n=1 | factorial(1) | 1 | 无 |
递归的退出
当递归进入到最内层的时候,递归就结束了,就开始逐层退出了,也就是逐层执行 return 语句。
-
n 的值为 1 时达到最内层,此时 return 出去的结果为 1,也即 factorial(1) 的调用结果为 1。
-
有了 factorial(1) 的结果,就可以返回上一层计算
factorial(1) * 2
的值了。此时得到的值为 2,return 出去的结果也为 2,也即 factorial(2) 的调用结果为 2。 -
以此类推,当得到 factorial(4) 的调用结果后,就可以返回最顶层。经计算,factorial(4) 的结果为 24,那么表达式
factorial(4) * 5
的结果为 120,此时 return 得到的结果也为 120,也即 factorial(5) 的调用结果为 120,这样就得到了 5! 的值。
层次/层数 | 调用形式 | 需要计算的表达式 | 从内层递归得到的结果 (内层函数的返回值) | 表达式的值 (当次调用的结果) |
---|---|---|---|---|
5 | factorial(1) | 1 | 无 | 1 |
4 | factorial(2) | factorial(1) * 2 | factorial(1) 的返回值,也就是 1 | 2 |
3 | factorial(3) | factorial(2) * 3 | factorial(2) 的返回值,也就是 2 | 6 |
2 | factorial(4) | factorial(3) * 4 | factorial(3) 的返回值,也就是 6 | 24 |
1 | factorial(5) | factorial(4) * 5 | factorial(4) 的返回值,也就是 24 | 120 |
至此,我们已经对递归函数 factorial() 的进入和退出流程做了深入的讲解,把看似复杂的调用细节逐一呈献给大家,即使你是初学者,相信你也能解开谜团。
递归的条件
每一个递归函数都应该只进行有限次的递归调用,否则它就会进入死胡同,永远也不能退出了,这样的程序是没有意义的。
要想让递归函数逐层进入再逐层退出,需要解决两个方面的问题:
-
存在限制条件,当符合这个条件时递归便不再继续。对于 factorial(),当形参 n 等于 0 或 1 时,递归就结束了。
-
每次递归调用之后越来越接近这个限制条件。对于 factorial(),每次递归调用的实参为 n - 1,这会使得形参 n 的值逐渐减小,越来越趋近于 1 或 0。
关于递归函数
factorial() 是最简单的一种递归形式——尾递归,也就是递归调用位于函数体的结尾处。除了尾递归,还有更加烧脑的两种递归形式,分别是中间递归和多层递归:
-
中间递归:发生递归调用的位置在函数体的中间;
-
多层递归:在一个函数里面多次调用自己。
递归函数也只是一种解决问题的技巧,它和其它技巧一样,也存在某些缺陷,具体来说就是:递归函数的时间开销和内存开销都非常大,极端情况下会导致程序崩溃。
整体理解
从整体上看,C语言代码是由一个一个的函数构成的,除了定义和说明类的语句(例如变量定义、宏定义、类型定义等)可以放在函数外面,所有具有运算或逻辑处理能力的语句(例如加减乘除、if else、for、函数调用等)都要放在函数内部。
例如,下面的代码就是错误的:
#include <stdio.h>
int a = 10;
int b = a + 20;
int main(){
return 0;
}
但是下面的代码就是正确的:
#include <stdio.h>
int a = 10;
int b = 10 + 20;
int main(){
return 0;
}
int b = 10 + 20;
在编译时会被优化成int b = 30;
,消除加法运算。
在所有的函数中,main() 是入口函数,有且只能有一个,C语言程序就是从这里开始运行的。
C语言不但提供了丰富的库函数,还允许用户定义自己的函数。每个函数都是一个可以重复使用的模块,通过模块间的相互调用,有条不紊地实现复杂的功能。可以说,C程序的全部工作都是由各式各样的函数完成的,函数就好比一个一个的零件,组合在一起构成一台强大的机器。
标准C语言(ANSI C)共定义了15 个头文件,称为“C标准库”,所有的编译器都必须支持,如何正确并熟练的使用这些标准库,可以反映出一个程序员的水平。
-
合格程序员:<stdio.h>、<ctype.h>、<stdlib.h>、<string.h>
-
熟练程序员:<assert.h>、<limits.h>、<stddef.h>、<time.h>
-
优秀程序员:<float.h>、<math.h>、<error.h>、<locale.h>、<setjmp.h>、<signal.h>、<stdarg.h>
C语言中所有的函数定义,包括主函数 main() 在内,都是平行的。也就是说,在一个函数的函数体内,不能再定义另一个函数,即不能嵌套定义。但是函数之间允许相互调用,也允许嵌套调用。习惯上把调用者称为主调函数,被调用者称为被调函数。函数还可以自己调用自己,称为递归调用。
main() 函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从 main() 函数开始,完成对其它函数的调用后再返回到 main() 函数,最后由 main() 函数结束整个程序。