6、函数

1、函数定义和声明

  函数是一个能完成特定功能的代码模块,其程序代码独立,可以给函数传递参数,也可以得到返回值。

  C语言程序一般是由大量的小函数而不是少量的大函数构成,即所谓“小函数构成大程序”。这样做的好处是让各个模块相互独立,并且任务单一。

  C语言程序中的函数在数目上并没有上限。但一个C程序中有且仅有一个main函数,这个函数被称为主函数。整个程序就从这个主函数开始执行,在主函数中还可以调用其它函数来完成所需要的工作。

  1.1、  函数定义

  函数是一个完成特定功能的代码模块,通常有参数,也可以没有参数;通常要求有返回值,也可以是空值,一般形式如下:

<数据类型><函数名称>(<形式参数说明>)
{
    语句;
    return (<表达式>);
}

  函数名称是一个标识符,要求符合标识符的命名规则。

  数据类型是整个函数返回值的类型。这里可以包括存储类型说明符、数据类型说明符以及时间域说明符,如果函数不需要有返回值时,函数类型说明符可以写成void。

  形式参数说明就是形式参数的列表,简称形参。形参可以是任意类型的变量,各参数之间用逗号分隔。在进行函数调用时,调用函数将赋予这些形式参数实际的值。

  函数名后有一对花括弧。“{}”中的内容为函数体。函数体中有若干条语句(大于或等于0个),实现特定的功能。注意:函数体中,表达式语句里使用的变量必须事先已有说明,否则不能使用。

  return <表达式>语句中表达式的值,要求和函数名前的数据类型保持一致。如果数据类型为void,可以省略,也可以写成 “return;”。

  一个函数运行结束后,会销毁在该函数内部定义的局部变量,包括形式参数。

  1.2、  函数声明

  定义是对函数功能的确立,包括函数名、函数返回值类型,形参列表、函数体,是一个完整、独立的函数。

  在这里所说的“函数声明”,是为了把函数名、返回值类型以及形参类型、个数和顺序通知编译系统,以便在调用该函数时,编译系统进行对照检查,包括函数名是否正确、传递参数的类型、个数是否与形参一致。如若出现不对应的情况,编译会有语法错误。

  了解了函数声明的作用后,就不难理解,在编程的过程中,可以简单的照写已定义的函数的首部,再加上一个分号,就成为了对函数的声明。在函数声明中也可以不写形参名,而只写形参的类型。在C语言中,函数声明称为函数原型。用函数原型是ANSIC的一个重要特点。他的作用主要是利用它在程序的编译阶段对调用函数的合法性进行全面检查。

   在C语言中,用户可以通过两种方法来进行函数声明:

  A、 如果函数调用前,没有对函数作声明,且同一源文件的前面出现了该函数的定义,那么编译器就会记住它的参数数量和类型以及函数的返回值类型,即把它作为函数的声明,并将函数返回值的类型默认为int型。

  B、 如果在同一源文件的前面没有该函数的定义,则需要提供该函数的函数原型。用户自定义的函数原型通常可以一起写在头文件中,通过头文件引用的方式来声明。

  函数原型的一般形式如下:

函数类型  函数名(参数类型1,参数类型2……);
函数类型  函数名(参数类型1  参数名1,参数类型2   参数名2……);

 

  第一种形式是基本的形式,同时为了使程序便于阅读,也允许在函数原型中加上参数名,这就成了第二种形式。但是编译器实际上并不检查参数名,参数名可以任意改变。

  实际上,如果在调用函数之前没有对函数进行声明,则编译系统会把第一次遇到的该函数形式(函数定义或函数调用)作为函数的声明,并将函数返回值的类型默认为int型。

  对于函数的声明,也可以使用extern,告诉编译器这个函数是在别的源文件中定义的。

  函数前面加Static是告诉编译器该函数只能在当前文件中调用。

2、函数的调用、参数传递和返回值

  2.1、函数的调用

  函数的使用也叫函数的调用,形式如下:

函数名称(实际参数);

 

  其中,参数名称是一个标识符,符合标识符的命名规则;

  实际参数需要确切的数据,也可以是具有确定值的表达式。实参就是在使用函数时,调用函数传递给被调用函数的数据,用来完成所要求的任务。

  函数的参数分为实际参数和形式参数两种:形式参数指的是出现在函数定义中的参数列表,简称形参。实际参数简称实参,出现在主调函数,发生函数调用时,主调函数把实参的值传递给被调函数的形参,从而实现主调函数向被调函数的数据传送。

  需要注意的是,函数调用可以作为一个运算量出现在表达式中,也可以单独形成一个语句。对于无返回值的函数来讲,只能形成一个函数调用语句。

  如果调用无参函数,则实参列表可以没有,但括弧不能省略。如果实参列表包含多个实参,则各参数间用逗号隔开。实参与形参的个数应相等,类型应该一致。实参与形参按类型对应,一一传递数据。这里对实参列表取值的顺序并不是确定的,有的系统按自左至右的顺序求实参的值,有的系统则按自右至左顺序。

  被调函数必须是已经声明了的函数,或者被调函数的位置位于调用函数之前。

  函数的调用形式一般为:

函数名(实参列表);

 

  按照函数在程序中出现的不同位置,有以下三种函数调用方式:

  A、 函数语句:把函数作为一个语句,这是不要求函数有返回值,只要求函数完成一定的操作

  B、 函数表达式:函数出现在一个表达式中,这种表达式称为函数表达式,这时要求函数返回一个确定的值以及参数表达式的运算

  C、 函数参数:函数调用作为一个函数的实参。函数调用作为函数的参数,实质上也是函数表达式形式调用的一种,因为函数的参数本来就要求是表达式形式。

  2.2、函数的参数传递

  在C语言中,传递参数主要有三种方式:

  (1)、值传递方式

  值传递方式只能改变形参,不能改变实参。

  形参只有在函数被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。形参只有在函数内部有效。调用结束返回主调函数后则不能再使用该形参变量。

  形参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,他们都必须具有确定的值,以便把这些值传递给形参。因此应先用赋值、输入等办法使实参获得确定值。

  实参和形参在数量上、类型上、顺序上应严格一致,否则会发生“类型不匹配”的错误。

  参数的传递是单向的,只能从实参传递到形参。

  在函数内部定义的变量都是局部变量,包括形参,只有在调用该函数时才会为该变量分配内存空间,函数运行完就会释放内存单元,实参给形参传值得过程也就是给局部变量赋值的过程。可以在不同的函数中使用相同的变量名,他们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。

  (2)、地址传递方式

  地址传递方式可以改变形参,也可以改变实参。

  如果希望通过对形参的操作来影响实参的值,可以在调用函数时,传递变量的地址。

  地址传递方式和值传递方式正好相反,这种方式是将调用函数的参数本身传递给被调用函数。因此,被调用函数中对形参的操作,将直接改变实参的值。调用函数将实参的地址传递给被调用函数,被调用函数对该地址的目标操作,相当于对实参本身的操作。按地址传递,实参为变量的地址,而形参为同类型的指针。

  (3)、全局变量传参

  全局变量就是在函数体外说明的变量,他们在程序中的每个函数里都是可见的。实际上,全局变量也是一种静态型的变量。将它初始化为0。全局变量一经定义后就会在程序的任何地方可见。使用全局变量传递数据的先后顺序的不同会影响计算结果,应用顺序不当,会导致错误,这种方式尽量少用。

  2.3、函数的返回值

  函数的返回值是指被调用函数返回给调用函数的值。

  (1)、函数的返回值只能通过return语句返回主调函数,return语句的一般形式为:

return 表达式;

 

  函数一旦遇到return语句,不管后面有没有代码,函数立即结束

  (2)、函数返回值的类型和函数定义中函数的类型应保持一致。如果两者不一致,则以函数定义中的类型为准,自动进行类型转换。

  (3)、如函数返回值为整形,在函数定义时可以省去类型说明。

  (4)、没有返回值的函数,可以明确定义为空类型,类型说明符为void。

  函数结束后会销毁它在内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据。但是,这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。

分类角度

分类

说明

函数定义的角度

库函数

由C语言系统提供,用户无需定义,也不必再程序中作类型说明,只需在程序中包含有该函数原型的头文件即可在程序中直接调用,如printf等

用户定义函数

不仅要在程序中定义函数本身,而且在主调函数模块中还必须对该被调函数进行类型说明,然后才能使用

有无返回值

有返回值函数

被调用执行完后将向调用者返回一个值

无返回值函数

此类函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数值

主调函数和被调函数之间数据传送的角度

无参函数

函数定义、函数说明以及函数调用中均不带参数。主调函数和被调函数之间不进行参数传送。此类函数通常用来完成一组指定的功能,可以返回或不返回函数值

有参函数

在函数定义以及函数说明时都有参数,称为形式参数。在函数调用时也必须给出形参,称为实际参数。进行函数调用时,主调函数将把实参的值传送给形参,供被调函数使用。

库函数功能

字符类型分类函数

用于对字符按ASCII码分类:字母、数字、控制字符、分隔符、大小写字母等

转换函数

用于字符或字符串的转换;在字符量和各类数字量之间进行转换;在大、小写之间进行转换

目录路径函数

用于文件目录和路劲操作

诊断函数

用于内部错误检测

图形函数

用于屏幕管理和各种图形功能

输入输出函数

用于完成输入输出功能

接口函数

用于与DOS、BIOS和硬件的接口

字符串函数

用于字符串操作和处理

内存管理函数

用于内存管理

数学函数

用于数学函数计算

日期和时间函数

用于日期、时间转换操作

进程控制函数

用于进程管理和控制

 

3、函数和数组

  3.1、传递数组

  当形参是数组形式时,其本质上是同级别的指针。数组作为实参传递时,形参并没有复制实参所有的内容,而是复制了实参数组的首地址。

  数组作为函数参数时,实际上传的是该数组的首地址。

  当形参是数组形式时,本质上是同级别的指针,数组名作为实参传递时,形参并没有复制实参所有的内容,而是复制了实参数组的首地址。

  全局数组传递方式:

  复制传递方式:实参为数组的指针,形参为数组名(本质是一个指针变量)。

  地址传递方式:实参为数组的指针,形参为同类型的指针变量。

  Void sun(int a[]);    //int a[] 表示指向数组的指针,实际上是int *a;

  3.2、传递指针

  前面介绍了函数形参是数组形式的用法,下面介绍另一种很常见的写法即传递指针。若需要给予函数传递一维数组,可以写成下面的形式:

  Int test_arry(int *a,int m,int (*a)[m],int *p);

4、main函数的参数

  普通函数可以带参数,其实,main函数也可以带参数。当执行程序时,也可以在命令行上给main函数传参。完整的main函数原型如下:

int main(int argc,char *argv[])

 

  上面是数组的形式,也可以写成指针的形式:

int main(int argc,char **argc);

 

  Argc是传给main函数的参数的个数,argv指向具体的参数。

5、指针函数

  5.1、指针函数的定义和使用

    通常一个函数都有返回值。如果一个函数没有返回值,则该函数是一个无值型函数。若一个函数的返回值是指针,则称函数为指针函数。

指针函数的定义的一般形式如下:
<数据类型> *<函数名称>(<参数说明>)
{
    语句序列;
} 

  其中,<数据类型>、<函数名称>、<形式参数说明>等与一般函数定义相同。在<函数名称>之前的*符号,说明该函数返回一个地址量。

  在实现一个指针函数时,读者应该特别注意,指针函数返回的地址,在主调函数中,必须是有效的,是可以访问的内存。如果返回的是函数内的局部变量的地址,局部变量分配在堆栈中,当函数执行完后,局部变量自动释放,在主调函数中,不能再访问,因此会有警告。访问一段释放的内存,是非法操作,显示的是乱码,若修改非法内存的值,程序的后果可能更严重,是不可预料的。

  可以将局部数组加上static修饰,静态局部变量,当程序结束时才回收内存。

6、函数指针

  6.1、函数指针的声明

  函数指针是专门用来存放函数地址的指针。函数地址是一个函数的入口地址,函数名代表了函数的入口地址。

  当一个函数指针指向了一个函数,就可以通过这个指针来调用该函数,可以将函数作为参数传递给函数指针。

  函数指针变量说明的一般形式如下:

<数据类型>(*<函数指针名称>)(<参数说明列表>);

 

  例如:int (*p) (int,int);    //p就表示指向函数的指针

  其中,

  <数据类型>是函数指针所指向的函数的返回值类型;

  <函数指针名称>符合标识符命名规则;

  <参数说明列表>应该与函数指针所指向的函数的形参说明保持一致;

  (*<函数指针名称>)中,*说明为指针,()不可缺省,表明为指向函数的指针。

  6.2、定义函数指针类型

  有时为了书写方便,可以声明一个函数指针数据类型。

  函数指针类型说明的一般形式如下:

Typedef<数据类型>(*<函数指针类型名称>)(<参数说明列表>);

 

  例如:typedef  int (*MFunc) (int ,int);    //MFunc表示函数指针数据类型

    MFunc  p;    //定义一个指针

  6.3、函数指针数组

  函数指针数组是一个包含若干个函数指针变量的数组。

  定义形式如下:

<数据类型>(*<函数指针数组名称> [<大小>])(<参数说明列表>);

 

  其中,<大小>是指函数指针数组元素的个数

7、递归函数

  7.1、递归函数的定义

  所谓递归函数是指一个函数的函数体中直接调用或间接调用了该函数自身的函数。

  递归函数调用的执行过程分为两个阶段。

  递推阶段:从原问题出发,按递归公式递推从未知到已知,最终达到递归终止条件。

  回归阶段:按递归终止条件求出结果,逆向逐步代入递归公式,回归到原问题求解。

  7.2、函数调用机制说明

  任何函数之间不能嵌套定义,调用函数与被调用函数之间相互独立(彼此可以调用)。发生函数调用时,被调函数中保护了调用函数的运行环境和返回地址,使得调用的状态可以在被调函数运行返回后完全恢复,而且该状态与被调函数无关。

  被调函数运行的代码虽是同一个函数的代码体,但由于调用点,调用时状态,返回点的不同。可以看做是函数的一个副本,与调用函数的代码无关,所以函数的代码是独立的。被调函数运行的栈空间独立于调用函数的栈空间,所以与调用函数之间的数据也是无关的。函数之间靠参数传递和返回值来联系,函数看作为黑盒。

  7.3、递归调用的形式

  递归调用有直接递归调用和间接递归调用两种形式。

  直接递归即在函数中出现调用函数本身。

  间接递归调用是指函数中调用了其他函数,而该其他函数却又调用了本函数。

  7.4、递归的条件

  一个问题能否用递归实现,看其是否具有以下特点:

  A、 须有完成函数任务的语句

  B、 一个确定是否能避免递归调用的测试

  C、 一个递归调用语句

  D、 先测试,后递归调用

8、回调函数

  8.1、回调函数的定义

  回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

回调函数的定义一般形式如下:
<数据类型><函数名称>(<参数说明列表>)
{
    语句序列;
}
其中,<数据类型><函数名称>与一般函数定义相同。<参数说明列表>中的某个参数是一个函数指针。

  8.2、回调函数实现机制

  A、定义一个回调函数

  B、提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者

  C、当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理

  8.3、自定义回调函数

  回调函数定义主要有以下两种方式:

  A、 通过命名方式

  B、 直接通过函数指针

 

posted @ 2020-02-18 17:12  孤情剑客  阅读(415)  评论(0)    收藏  举报