3.7 函数
一个较大的程序通常由多个程序模块组成,每个模块用来实现一个特定的功能,在程序设计中模块的功能是用子程序来实现的。在C语言中,子程序的作用是由函数来完成的。因此,C语言是由函数构成的,函数是C语言中的一种基本模块。一个C程序由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次,同一工程中的函数也可以分放在不同文件中一起编译。
从使用者的角度来看,有两种函数:标准库函数和用户自定义函数。标准库函数是由C编译系统的函数库提供的,用户不需自己定义这些函数,可以直接使用它们。用户自定义函数是由用户根据自己的需要编写的函数,用来解决用户的专门需要。
从函数的形式看,有三种函数:无参函数、有参函数和空函数。无参函数被调用时,主调函数并不将数据传送给被调用函数,一般用来执行指定的一组操作。无参函数可以带回或不带回函数值,但一般以不带回函数值的居多。有参函数被调用时,在主调函数的被调用函数之间有参数传递,即主调函数可以将数据传给被调用函数使用,被调用函数中的数据也可以带回来供主调函数使用。空函数的函数体内无语句,为空白的。调用空函数时,什么工作都不做,不起任何作用。定义空函数的目的并不是为了执行某种操作,而是为了以后程序功能的扩充做准备。
1.函数定义的一般形式
1)无参函数的定义形式无参函数的定义形式如下:
返回值类型标识符 函数名() { 函数体语句 }
其中,返回值类型标识符指明本函数返回值的类型;函数名是由用户定义的标识符;“()”内没有参数,但该括号不能少,或者括号里加“void”关键字;“{}”中的内容称为函数体语句。在很多情况下,无参函数没有返回值,因此函数返回值类型标识符可以省略。此时,函数返回值类型标识符也可以写为“void”。例如:
void Timer0_Iint(void) //Timer0初始化函数 {TCCR0=0x0D; TCNT0=0x00; OCR0 =78; TIMSK|=0x02; TIFR |=0x02; }
2)有参函数的定义形式有参函数的定义形式如下:
返回值类型标识符 函数名(形式参数列表) 形式参数说明 { 函数体语句 }
有参函数比无参函数多了一个内容,即形式参数列表。在形式参数列表中给出的参数称为形式参数,它们可以是各种类型的变量,各参数之间用逗号间隔。在进行函数调用时,主调函数将赋予这些形式参数实际的值。例如:
in min(int j,k) {int n; if(j>k) { n=k; } else { n=j; } return n; }
在此定义了一个min函数,返回值为一个整型(int)变量,形式参数为j和k,也都是整型变量。int n语句定义n为一个整型变量,通过if条件语句,将最小的值传送给n变量。Return n的作用是将n的值作为函数值带回到主调函数中,即n的返回值。
3)空函数的定义形式空函数的定义形式如下:
返回值类型标识符 函数名() { }
调用该函数时,实际上什么工作都不用做,它没有任何实际用途。例如:
float min() { }
2.函数的参数和函数返回值
C语言通过函数间的参数传递方式,可以使一个函数对不同的变量进行功能相同的处理。函数间的参数传递,由函数调用时主调用函数的实际参数与被调用函数的形式参数之间进行数据传递来实现。
1)形式参数和实际参数在定义函数时,函数名后面括号内的变量名称为“形式参数”,简称“形参”;在调用函数时,函数名后面括号内的表达式称为“实际参数”,简称“实参”。
形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。实参出现在主调函数中,进入被调函数后,实参变量也不能使用。形参和实参都可以进行数据传送,发生函数调用时,主调函数把实参的值传送给被调函数的形参从而实现主调函数向被调函数的数据传送。
在使用形参和实参时应注意以下几点。
(1)在被定义的函数中,必须指定形参的类型。
(2)实参和形参的类型必须一致,否则将会产生错误。
(3)在定义函数中指定的形参变量,没进行函数调用时,它们并不占用内存中的存储单元,只有在发生函数调用时它们才占用内存中的存储单元,且在调用结束后,形参所占用的存储单元也会立即被释放。
(4)实参可以是常量、变量或表达式。无论实参是哪种类型的量,在进行函数调用时,它们必须都具有确定的值,以便在调用时将实参的值赋给形参变量。如果形参是数组名,则传递的是数组首地址而不是变量的值。
(5)在C语言中进行函数调用时,实参与形参间的数据传递是单向进行的,只能由实参传递给形参,而不能由形参传递给实参。
2)函数的返回值在函数调用时,通过主调函数的实参与被调函数的形参之间进行数据传递来实现函数间的参数传递。在被调函数的最后,通过return语句返回函数将被调函数中的确定值返回给主调函数。return语句的一般形式如下:
return (表达式);
例如:
int x,y; //定义两个整型变量x,y { return(x<y? x:y); //如果x小于y,则返回x,否则返回y }
函数返回值的类型一般在定义函数时用返回类型标识符来指定。在C语言中,凡不加类型说明的函数,都按整型来处理。如果函数值类型的return语句中表达式的值不一致,则以函数类型为准,自动进行类型转换。
对于不需要有返回值的函数,可以将该函数定义为“void”类型(或称“空类型”)。这样,编译器会保证在函数调用结束时不使用函数返回任何值。为了使程序减少出错,保证函数的正确调用,凡是不要求有返回值的函数,都应该将其定义为void类型。例如:
void abc(); //函数abc()为不带返回值的函数
3.函数的调用
在C语言程序中,函数可以相互调用。所谓函数调用就是指在一个函数体中引用另外一个已经定义了的函数,前者称为主调函数,后者称为被调函数。
1)函数调用的一般形式在C语言中,主调函数通过函数调用语句来使用函数。函数调用的一般形式如下:
函数名 (实参列表);
对于有参数型的函数,如果包含了多个实参,则应将各参数之间用逗号分隔开。主调用函数的实参的数目与被调用函数的形参的数目应该相等,且类型保持一致。实参与形参按顺序对应,一一传递数据。
如果调用的是无参函数,则实参表可以省略,但是函数名后面必须有一对空括号。
2)函数调用的方式在C语言中,主调用函数调用被调函数可以采用以下3种调用方式。
(1)函数语句调用:在主调用函数中将函数调用作为一条语句,并不要求被调用函数返回结果数值,只要求函数完成某种操作,例如:
disp_LED(); //无参调用,不要求被调函数返回一个确定的值,只要求此函数完成 //LED显示操作
(2)函数表达式调用:函数作为表达式的一项出现在表达式中,要求被调用函数带有return语句,以便返回一个明确的数值参加表达式的运算。例如:
a=3*min(x,y); //被调用函数min作为表达式的一部分,它的返回值乘2再赋给a。
(3)作为函数参数调用:在主调函数中将函数调用作为另一个函数调用的实参。例如:
a=min(b,min(c,d)) //min(c,d)是一次函数调用,它的值作为另一次调用的实参。a为b、 //c和d中的最小值
3)对被调用函数的说明在一个函数中调用另一函数(即被调用函数)时,需具备以下条件。
(1)被调用函数必须是已经存在的函数(是库函数或用户自己定义的函数)。
(2)如果程序中使用了库函数,或使用了不在同一文件中的另外的自定义函数,则应该在程序的开头处使用#include包含语句,以将所有的函数值包括到程序中来。
(3)对于自定义函数,如果该函数与调用它的函数在同一文件中,则应根据主调用函数与被调用函数在文件中的位置,决定是否对被调用函数的类型做出说明。这种类型说明的一般形式为
返回值类型说明符 被调用函数的函数名();
在C语言中,在以下3种情况下可以不在调用函数前对被调用函数做类型说明。
(1)如果函数的值(函数的返回值)为整型或字符型,可以不必进行说明,系统会对它们自动按整型进行说明。
(2)如果被调用函数的定义出现在主调用函数之前,可以不对被调用函数加以说明。因为C编译器在编译主调用函数之前,已经预先知道已定义了被调用函数的类型,并会自动对其加以处理。
(3)如果已在所有函数定义之前,在文件的开头、函数的外部说明了函数类型,则在各个主调函数中不必对所调用的函数再做类型说明。
4)函数的嵌套调用与递归调用
(1)函数的嵌套调用:在C语言中,函数的定义都是相互独立的,不允许在定义函数时,一个函数内部包含另一个函数。虽然在C语言中函数不能嵌套定义,但可以嵌套调用函数。嵌套调用函数是指在一个函数内调用另一个函数,即在被调用函数中又调用其他函数。
在CodeVisionAVR编译器中,函数间的调用及数据保存与恢复是通过硬件堆栈和软件堆栈来实现的。当没有使用外部数据存储器时,硬件堆栈和软件堆栈均在内部数据存储器中;当有外部数据存储器时,则硬件堆栈在内部数据存储器中,软件堆栈在外部数据存储器中。在CodeVisionAVR编译器中,嵌套层数只受到硬件堆栈和软件堆栈的限制,如果嵌套层数太深,有可能导致硬件或软件堆栈溢出。
(2)函数的递归调用:在调用一个函数的过程中又出现直接或间接调用该函数本身,称为函数的递归调用。在C语言中,允许函数递归调用。函数的递归调用通常用于问题的求解,可以将一种解法逐次地用于问题的子集表示的场合。CodeVisionAVR编译器能够自动处理函数递归调用的问题,在递归调用时不必做任何声明,其调用深度仅受到堆栈大小的限制。
4.数组、指针作为函数的参数
C语言规定,数组、指针均可作为函数的参数使用,用于进行数据传递。
1)数组作为函数的参数在C语言中,可以用数组元素或者整个数组作为函数的参数。用数组作为函数的参数时,需要注意以下几点。
(1)当用数组名作为函数的参数时,应该在调用函数和被调用函数中分别定义数组。
(2)实参数组与形参数组的类型应一致,如果不一致则会出错。
(3)实参数组和形参数组的大小可以一致,也可以不一致。C编译器对形参数组的大小不做检查,只是将实参数组的首地址传给形参数组。如果要求形参数组得到实参数组全部的元素值,则应当指定形参数组与实参数组的大小一致。
2)指向函数的指针变量作为函数的参数函数在编译时分配一个入口地址(函数首地址),先赋予这个入口地址一个指针变量,使该指针变量指向该函数,然后通过指针变量就可以调用这个函数了,这种指向函数的指针变量称为函数指针变量。
指针变量可以指向变量、字符串和数组,同样也可以指向一个函数,即可以用函数的指针变量来调用函数。函数指针变量常用的功能之一是将指针作为参数传递给其他函数。函数指针变量的一般形式为
函数值返回类型 (*指针变量名)(函数形参列表);
其中,“函数值返回类型”表示被指函数的返回值类型;“(*指针变量名)”表示定义的指针变量名;“(函数形参列表)”表示该指针是一个指向函数的指针。
调用函数的一般形式为
(*指针变量名)(函数形参列表)
使用函数指针变量时应注意:函数指针变量不能进行算术运算,即不能移动函数指针变量。
3)指向结构的指针变量作为函数的参数 C语言不允许整体引用结构体变量名,如果要将一个结构体变量的值从一个函数传递给一个函数,可采用以下3种方法。
(1)像用变量作为函数的参数一样,直接引用结构体变量的成员作为参数。
(2)用结构体作为函数的参数,采用这种方式必须保证实参与形参的类型相同,属于“值传递”。把一个完整的结构体变量作为参数传递,并一一对应传递给各成员的数据。在单片机中,这些操作是通过入栈和出栈来实现的,会增加系统的处理时间,影响程序的执行效率,并且还需要较大的数据存储空间。
(3)用指向结构体变量的成员作为参数,将实参值传给形参,其用法和普通变量作为实参一样,也属于“值传递”方式。
4)返回指针值的函数一个函数可以返回一个整型值、字符值和浮点值,同样也可以返回指针类型的数据,即返回一个数据的地址。返回指针值的函数的一般定义形式为
返回值类型 *函数名(参数表)