C语言学习-函数
1 函数的概念
1.1为什么要引入函数
在C语言中一段功能代码可能会用到很多次,如果每次都写这样一段重复的代码,不但费时费力、容易出错,而且交给别人时也很麻烦。所以为了避免使代码变得庞杂、逻辑混乱、程序冗余,C语言提供了一个功能,允许我们将常用的代码以固定的格式封装(包装)成一个独立的模块,只要知道这个模块的名字就可以重复使用它,这个模块就叫做函数(Function)。
1.2什么是函数
函数的本质是一段可以复用的完成特定功能的代码模块。可以给函数传递参数,也可以得到返回值。函数被提前编写好,放到指定的文件中,使用时直接在需要的位置,直接调用即可。
2.函数的定义和声明
2.1函数的定义
与变量类似,函数必须要先定义再使用。一般形式如下:
<数据类型> <函数名称> (<形式参数说明>)
{
函数体;
return (<表达式>);
}
- 函数名:函数的标识符,使用者可以根据函数名调用该函数。函数名应符合C语言的标识符命名规范,且不得重名,不得与系统函数(如printf()等库函数或main()函数)重名。命名应做到“见文知意”而避免起简单的函数名。
- 数据类型:函数返回值的类型,函数的返回值可理解为函数的计算结果。函数可以没有返回值,但如果有返回值,返回值类型需要与函数的数据类型匹配。若该函数不需要返回值,则应将函数类型定义为void(空类型)。
- 形式参数说明:函数的形式参数列表,简称形参。形参可以是任意类型的变量,各参数之间用逗号分隔。在进行函数调用时,调用函数将赋予这些形参实际的值。如果不需要传递参数,可以省略形参。
- 函数体:实现函数功能的代码。
- 函数返回值:return (<表达式>);语句中表达式的值即函数的返回值,要求必须与函数的数据类型保持一致。如果数据类型为void 表示无返回值,可以省略return 表达式也可以写成”return ;”。
2.2函数的声明
C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。
所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
函数声明的格式非常简单,相当于去掉函数定义中的函数体,并在最后加上分号";"如下形式:
<数据类型> <函数名称> (参数类型1 , 参数类型 2,……);
或 <数据类型> <函数名称> (参数类型1 参数名称1, 参数类型 2 参数名称2,……);
例如:
double Power(double x, int n) ;
double Power(double, int);
注意:
- 第一种形式是基本形式。
- 同时为了使程序便于阅读,也允许在函数原型中加入参数名,这样就成了第二种形式。编译器实际上并不检查函数声明种的参数名,因此参数名称可以任意改变。
- 函数声明给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息,所以又称为函数原型(Function Prototype)。函数原型的作用是告诉编译器与该函数有关的信息,让编译器知道函数的存在,以及存在的形式,即使函数暂时没有定义,编译器也知道如何使用它。
- 编译时加 -Wall选项,编译器从上到下检查主调函数中调用的每一个函数,根据声明检查函数格式。如果没有函数声明则告警。
3.函数的调用、参数传递和返回值
3.1函数调用
调用函数的一般形式是:
函数名(实际参数列表);
其中:
“实际参数列表”需要确切的数据,也可以是具有确定值的表达式。实际参数就是在使用函数时,调用函数传递给被调用函数的数据,用以完成所要求的任务。
注意:
- 函数调用时可能没有实参列表(具体实参情况需要依据形参决定),但是绝对不可以省略括号。
- 如果实参列表中包含多个实参,则参数间用逗号隔开。实参与形参的个数相等,类型一致且一一对应。
- 实参可以是常量、变量或表达式等,但必须有确定的值。在调用时将实参的值赋值给形参。
- 在函数调用过程中,我们把调用函数的函数称为“主调函数”,相应地,被主调函数调用访问的函数称为“被调函数”。
- 在定义函数时出现的形参,并不占用存储单元。在调用过程中,形参会被临时开辟内存单元,实参将值传递给形参,形参拿到值开始函数的运算。在函数调用结束后,形参单元会被释放。
总结:函数的参数分为形式参数和实际参数两种
形式参数:被调函数在参数列表中定义的参数称为“形式参数”(简称形参)
实际参数:在主调函数调用被调函数的时候真正传递的数据称为“实际参数”(简称实参)
3.2函数调用的方式
按照函数在程序中出现的不同位置,有以下3中函数调用的方式:
- 函数语句:把函数printf作为一个语句。这时不要求函数带返回值,只要求完成一定的操作,如:
printf(“hello world\n”);
- 函数表达式:函数出现在一个表达式中,这种表达式称为函数表达式。这时要求函数返回一个确定的值以参加表达式的运算如:
i = sum(a, b)
- 函数参数:函数调用作为一个函数的实参。如:
printf(“The sum of a and b is %d\n”, sum(a, b));
3.3函数的参数传递
在C语言中,传递参数的方式主要有2种:
- 值传递方式:将需运算的变量的值传递给函数形参的方式称之为“值传递”。形参和实参是两个不同的变量,占用不同的空间,因此,当形参的值发生变化时,并不影响实参的值。
- 地址传递方式:将需运算的变量的地址传递给函数形参的方式称之为“地址传递”。这种方式是将实参本身的地址传递给被调用的函数。因此,被调用的函数种对形参的操作,将直接改变实参的值。
3.4全局变量与局部变量
3.4.1 局部变量
定义在函数内部或代码块的变量称为局部变量(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;
}
注意:
1) 在 main 函数中定义的变量也是局部变量,只能在 main 函数中使用;同时,main 函数中也不能使用其它函数中定义的变量。main 函数也是一个函数,与其它函数地位平等。
2) 形参变量、在函数体内定义的变量都是局部变量。实参给形参传值的过程也就是给局部变量赋值的过程。
3) 可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。
4) 在代码块中也可定义变量,它的作用域只限于当前代码块(代码块由{}包围)。
3.4.2全局变量
在所有函数外部定义的变量称为全局变量(Global Variable),全局变量的作用域是从定义变量的位置开始直到本文件结束。
定义全局变量的方法:在所有函数的外部定义变量即可。
注意:
代码中虽然定义了多个同名变量 n,但它们的作用域不同,在内存中的位置(地址)也不同,所以是相互独立的变量,互不影响,不会产生重复定义(Redefinition)错误。
1) 对于 func1(),输出结果为 20,显然使用的是函数内部的 n,而不是外部的 n;func2() 也是相同的情况。
当全局变量和局部变量同名时,在局部范围内全局变量被“屏蔽”,不再起作用。或者说,变量的使用遵循就近原则,如果在当前作用域中存在同名变量,就不会向更大的作用域中去寻找变量。
2) func3() 输出 10,使用的是全局变量,因为在 func3() 函数中不存在局部变量 n,所以编译器只能到函数外部,也就是全局作用域中去寻找变量 n。
3) 由{ }包围的代码块也拥有独立的作用域,printf() 使用它自己内部的变量 n,输出 40。
4) C语言规定,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。对于 main() 函数,即使代码块中的 n 离输出语句更近,但它仍然会使用 main() 函数开头定义的 n,所以输出结果是 30
3.5函数的返回值
函数的返回值是指被调用函数返回给调用函数的值。
- 函数的返回值只能通过return 语句返回给主调函数,return语句的一般形式为:
return 表达式;
或
return (表达式);
- 在函数中允许有多个return语句,但return语句表示函数结束,因此只能有一个return语句被执行,只能返回一个值。
- 函数的返回值类型和函数定义中函数的类型应保持一致。如果两者不一致,则以函数定义中的类型为准,自动执行类型准换。
- 如果函数返回值为整型,在函数定义时可以省去类型说明。
- 没有返回值的函数,可以明确定义为void类型。
4.函数与数组
前面介绍了函数之间的参数传递都是基本数据类型的数据,本节将介绍数组在函数与函数间的传递。
4.1数组作参数
当形参是数组形式时,其本质也是一个指针。数组作为实参传递时,并没有传递数组所有的内容,而是该数组的首地址。
由于数组的特殊性,只要知道了数组的首地址,就可以依次访问数组中的所有元素。
注意:
- 实参数组和形参数组类型保持一致。
- 形参数组大小不起任何作用,因为C语言把数组类型的形参当做同级别的指针去处理,不对形参数组大小作检查,不会检查形参的数组是否有越界。在调用时,主调函数的实参将首地址传递给被调函数的形参数组名。
- 形参数组可以不指定大小,定义时保留空方括号即可。
float avgrage(float array[]);
- 除了传递数组名以外,还要传递数组中元素的个数。
4.2多维数组作参数
如果形参是多维数组,以二维数组为例,在定义函数时可以省略第一个下标,绝对不可以省略第二个下标。例如,若我们想编写一个函数,求出一个4*3的二维数组内最大值,则可以写成:
int max_value(int array[4][3])
或
int max_value(int array[][3])
但是绝对不可以写成:
int max_value(int array[][])//省略了第二个下标,错误
这点要格外注意。
4.3传递指针
前面介绍了函数参数是数组形式的用法,下面介绍另一种很常见的写法即通过指针传递数组。
- 若需要给子函数传递一维数组
例如:
float avgrage(float array[]);
利用指针传递一维数组,可以改写成下面形式:
float avgrage(float *array);
- 若需要给子函数传递二维数组
例如:
int max_value(int array[][3]);
利用指针传递二维数组,可以改写成下面形式:
int max_value(int (*array)[3]);
- 若需要给子函数传递指针数组
例如:之前学过的main函数
int main(int argc, const char *argv[])
可以改写成
int main(int argc, const char **argv)
总结:当形参是数组形式时,本质是同级别的指针
5.指针函数与函数指针
5.1指针函数
一个函数既可以返回int类型、char类型等,也可以返回一个指针类型数据,即返回一个地址。所以若一个函数的返回值是指针,则称该函数为“指针函数”。
指针函数的定义形式为:
<数据类型> *<函数名称> (<参数说明>) {...}
在函数名称之前的*符号,说明该函数返回的是一个地址量。
注意:当指针作为函数的返回值的时候,主调函数需要考虑指针指向的数据是否已经被回收。(函数调用是通过栈实现的。在调用函数时,系统会将被调函数所需的程序空间安排在一个栈中。每当调用一个函数时,就在栈顶为它分配一个存储区。每当从一个函数退出时就释放它的存储区。)由于被调函数的局部变量存储在栈区,因此当被调函数执行完毕后系统会回收该段内存空间,这样被调函数内数据就会丢失。
5.2函数指针
5.2.1函数指针的定义
在C语言程序中定义的函数在编译时系统也会对函数代码分配一段存储空间,这段存储空间的起始地址称“函数的入口地址”(函数名代表了函数的入口地址)。
函数指针是专门用来存放函数地址的指针。
函数指针变量说明的一般形式如下:
<数据类型> (*<函数指针名称>)(<参数列表>);
其中
- 数据类型:是函数指针所指向的函数的返回值类型;
- 函数指针名称:符合标识符命名规则
- 参数说明列表:应该与函数指针所指向的函数的形参保持一致;参数列表只写形参类型即可,不必写形参名
- (*<函数指针名称>)中,*说明为指针,()不可缺省,表明为指向函数的指针。
例如:
int (*p) (int,int);
这个指针变量p的类型是int (*)(int,int),表示这是一个指向有两个int型参数、返回值为int型的函数的指针。
5.2.2函数指针的赋值
p = max; //直接给出函数名,不用给出参数。(因为是把函数地址赋值给p,与参数无关)。
p = max(a,b); //错误
5.2.3通过函数指针调用函数
当一个函数指针指向了一个函数,就可以通过这个指针来调用该函数
调用时,用(*函数指针名)代替函数名即可,且要在(*函数指针名)后加上实参。
eg:
c = max(a, b); 等价于 p = max; c = (*p)(a, b);
注意:
- 定义了一个函数指针,但不代表该函数指针可以指向任何类型的函数。示例程序中的指针p只能指向参数是2个int型、返回值是int型的函数。不能指向非这种类型的函数。
- 使用函数指针调用函数前,一定要先将该指针变量指向该函数。
- 函数指针不能进行算数运算。如p+1、p-1、p++、p--等都是非法的
5.3 函数指针数组
函数指针数组是一个包含若干个函数指针变量(函数地址)的数组;
函数指针数组定义形式如下:
<数据类型> (*<函数指针数组名> [<大小>]) (<参数列表>);
其中<大小>表示数组元素个数。
eg :
int (*pFunc[2]) (int a, int b);
示例: