3.2 C51函数
3.2.1 函数概述
从C51程序的结构上划分,C51函数分为主函数main()和普通函数两种。而普通函数又分为两种,一种是标准库函数;另一种是用户自定义函数。
1. 标准库函数
标准库函数是由C51编译器提供的,供使用者在设计应用程序时使用。C51具有功能强大、资源丰富的标准函数库,在进行程序设计时,应该善于充分利用这些功能强大、内容丰富的标准库函数资源,以提高效率,节省时间。
在调用库函数时,用户在源程序include命令中应该包含的头文件名。例如,调用左移位函数_crol_时,要求程序在调用输出库函数前包含以下的include命令:
#include<intrins.h>
include命令必须以#号开头,系统提供的头文件以“.h”作为文件的后缀,文件名用一对尖括号括起来。注意:include命令不是C51语句,因此不能在最后加分号。
2. 用户自定义函数
用户自定义函数,顾名思义,是用户根据自己的需要编写的函数。
从函数定义的形式上划分可以有三种形式:无参数函数、有参数函数和空函数。
(1)无参数函数
此种函数在被调用时无参数输入,无参数函数一般用来执行指定的一组操作。无参数函数的定义形式为:
类型标识符 函数名() { 类型说明 函数体语句 }
类型标识符是指函数值的类型,若不写类型说明符,默认为int类型;若函数类型标识符为void,表示不需要返回函数值。{}中的内容称为函数体,在函数体中也有类型说明,这是对函数体内部所用到变量的类型说明。例如:
void Delay_1ms() //延时1s程序 { uint i, j; for(i=1000;i>0;i--) for(j=115;j>0;j--); //此处分号不可少 }
Delay_1ms为函数名,Delay_1ms函数是一个无参函数,当这个函数被调用时,延时1s时间。函数前面的void表示这个函数执行完后不返回任何数据。
(2)有参数函数
在调用此种函数时,必须输入实际参数,以传递给函数内部的形式参数。在函数结束时返回结果,供调用它的函数使用。有参数函数的定义方式为:
类型标识符 函数名(形式参数表) 形式参数类型说明 { 类型说明 函数体语句 }
有参数函数比无参数函数多了形式参数表,各参数之间用逗号间隔。在进行函数调用时,主调用函数将赋予这些形式参数实际的值。
例:单片机控制P2口8只LED灯以间隔1s亮灭闪烁。
源程序如下。
#include<reg52.h> void Delay_ms(unsigned int xms) //被调用函数定义,xms是形式参数 { unsigned int i, j; for(i=xms;i>0;i--) //i=xms,即延时x毫秒, xms由实际参数传入一个值 for(j=115;j>0;j--); //此处分号不可少 } void main() { while(1) { P2=0xff; Delay_ms(1000); //主调用函数 P2=0; Delay_ms(1000); //主调用函数 } }
该程序在ch3/ch3_4文件夹中。
Delay_ms(unsigned int xms)函数括号中的变量xms是这个函数的形式参数,其类型为unsigned int,当这个函数被调用时,主调用函数Delay_ms(1000)将实际参数1000传递给形式参数xms,从而达到延时1s的效果。Delay_ms函数前面的void表示这个函数执行完后不返回任何数据。
重点提示:如何能知道Delay_ms(1000)就是延时1s呢?下面通过Keil进行模拟调试,具体方法如下。
① 在Keil的工程设置对话框中,将晶振频率设置为11.0592MHz,将仿真功能设置为软件仿真(Use Simulator)。
② 对源程序进行编译、链接,按Ctrl+F5组合键,进入模拟仿真状态。
③ 将光标移到P2=0xff行,按按钮或使用快捷键Ctrl+F10,使程序运行至光标所在处,此时,观察左侧的寄存器窗口,可看到sec显示的时间为0.0004(单位为s),这是定时程序的起始时间,如图3-3所示。
图3-3 延时程序起始时间
④ 再将光标移到P2=0行,按按钮或Ctrl+F10组合键,使程序运行到该行,此时,观察左侧的寄存器窗口,可看到sec显示的时间为1.0128(单位为s),这是定时程序的结束时间,如图3-4所示。
图3-4 延时程序结束时间
⑤ 将结束时间减去起始时间,即为定时程序的延时时间,即定时时间为:
1.0128-0.0004≈1000ms=1s
上面的例子中,Delay_ms函数是一个void函数,没有返回值,下面再举一个带返回值的例子,这个例子的功能是求两个数中的大数,函数定义可写为:
int max(int a,int b) { if(a>b) return a; else return b; }
该例中,max函数是一个整型函数,其返回的函数值是一个整数,形参a、b均为整型量,a、b的具体值是由主调用函数在调用时传输过来的。在max函数体中的return语句是把a(或b)的值作为函数的值返回给主调用函数。
(3)空函数
此种函数体内无语句,是空白的。调用此种空函数时,什么工作也不做,不起任何作用。而定义这种函数的目的并不是为了执行某种操作,而是为了以后程序功能的扩充。空函数的定义形式为:
返回值类型说明符 函数名() {}
例如:
int add() { }
应该指出的是,在C51中,程序总是从main函数开始,完成对其他函数的调用后再返回到main函数,最后由main函数结束整个程序。
3.2.2 函数的参数和返回值
在上面的几个例子中,我们对函数的参数已有所了解,下面再简要进行归纳总结。
1. 函数的参数
定义一个函数时,位于函数名后面圆括号中的变量名为“形式参数”(简称形参),而在调用函数时,函数名后面括号中的表达式为“实际参数”(简称实参)。形式参数在未发生函数调用之前,不占用内存单元,因而也是没有值的。只有在发生函数调用时它才被分配内存单元,同时获得从主调用函数中实际参数传递过来的值。函数调用结束后,它所占用的内存单元也被释放。
进行函数调用时,主调用函数将实际参数的值传递给被调用函数中的形式参数。为了完成正确的参数传递,实际参数的类型必须与形式参数的类型一致,如果两者不一致,则会发生“类型不匹配”错误。
函数调用中发生的数据传输是单向的。即只能把实参的值传输给形参,而不能把形参的值反向地传输给实参。
2. 函数的返回值
函数的返回值是指函数被调用之后,执行函数体中的程序段所取得的并返回给主调用函数的值。对函数返回值说明如下。
(1)函数的返回值只能通过return语句返回。
return语句的一般形式为:
return 表达式;
或者为:
return (表达式);
该语句的功能是计算表达式的值,并返回给主调用函数。在函数中允许有多个return语句,但每次调用只能有一个return语句被执行,因此只能返回一个函数值。
(2)函数体内可以没有return语句,程序的流程就一直执行到函数末尾的“}”,然后返回到函数,这时也没有确定的函数值带回。在定义此类函数时,可以明确定义为“空类型”(void)。一旦函数被定义为空类型后,就不能在主调用函数中使用被调用函数的函数值了。为了使程序有良好的可读性并减少出错,凡不要求返回值的函数都应定义为空类型。
3.2.3 函数的调用
函数调用的一般形式为:
函数名(实际参数表);
对于有参数型函数,若包含多个实际参数,则应将各参数之间用逗号分隔开。主调用函数的数目与被调用函数的形式参数的数目应该相等。实际参数与形式参数按实际顺序一一对应传递数据。
如果调用的是无参数函数,则实际参数表可以省略,但函数名后面必须有一对空括号。
1. 函数调用的方式
主调用函数对被调用函数的调用主要有以下两种方式。
(1)函数调用语句
即把被调用函数名作为主调用函数中的一个语句。例如:
Delay_1ms(); Delay_ms(1000);
此时并不要求被调用函数返回结果数值,只要求函数完成某种操作。
(2)函数结果作为表达式的一个运算对象
例如:
result=2*max(a,b);
被调用函数max为表达式的一部分,它的返回值乘以2再赋给变量result。
2. 对被调用函数的说明
在一个函数中调用另一个函数必须具有以下条件。
(1)被调用函数必须是已经存在的函数(库函数或用户自定义函数)。
(2)如果程序中使用了库函数,或使用了不在同一文件中的自定义函数,则应该在程序的开头处使用#include包含语句,将所用的函数信息包含到程序中来。
例如:
#include <stdio.h> //将标准输入、输出头文件(在函数库中)包含到程序中来 #include <math.h> //将函数库中专用数学库的函数包含到程序中来
这样,程序编译时,系统就会自动将函数库中的有关函数调入到程序中去,编译出完整的程序代码。
(3)如果被调用函数出现在主调用函数之后,在调用之前应对被调用函数进行声明,例如:
#include<reg52.h> void main() { void Delay_ms(unsigned int xms); //被调用函数声明,xms是形式参数 while(1) { P2=0xff; Delay_ms(1000); //主调用函数 P2=0; Delay_ms(1000); //主调用函数 } } void Delay_ms(unsigned int xms) //函数定义,xms是形式参数 { unsigned int i, j; for(i=xms;i>0;i--) //i=xms,即延时x毫秒,xms由实际参数传入一个值 for(j=115;j>0;j--); //此处分号不可少 }
(4)如果被调用函数出现在主调用函数之前,不用对被调用函数加以说明。因为C51编译器在编译主调用函数之前,已经预先知道已定义了被调用函数的类型,并自动加以处理。例如,ch3/ch3_4实例中采用的就是这种方式。这种函数调用方式存在的问题是,当程序中编写的函数较多时,若被调用函数位置放置不正确,容易引起编译错误。
3.2.4 局部变量和全局变量
1. 局部变量
局部变量被声明在一个函数之中,局部变量只在函数内部有效。另外,在主函数定义的变量,也只在主函数中有效,而不会因为在主函数中定义而在整个文件中有效,并且主函数也不能使用其他函数中定义的变量。例如:
2. 全局变量
一个源文件可能包含多个函数,在函数内定义的变量是局部变量,而在函数外定义的变量是外部变量,也称全局变量。全局变量可以为本文件中其他函数所共有,它的有效范围为从定义变量的位置开始到本文件结束,例如:
注意事项:若全局变量与局部变量同名,则在局部变量的作用范围内,全局变量将被屏蔽,即它不起作用。另外,全局变量在程序的全部执行过程中都占用存储单元,而局部变量则是在需要时才开辟存储单元。
3.2.5 变量的存储种类
变量的存储种类有四种:自动变量(auto)、外部变量(extern)、静态变量(static)和寄存器变量(register)。
(1)自动变量
在定义变量时,如果未写变量的存储种类,则缺省状态下为auto变量,自动变量由系统为其自动分配存储空间。例如:
unsigned char a ; //a是一个无符号字符型自动变量 unsigned int b ; //b是一个无符号整型自动变量
为了书写方便,经常使用简化形式来定义变量的数据类型,其方法是在源程序的开头使用#define语句。例如:
#define uchar unsigned char
#define uint unsigned int
经以上宏定义后,在后面就可以用uchar、uint定义变量了。例如:
uchar a ; //a是一个无符号字符型自动变量 uint b ; //b是一个无符号整型自动变量
(2)静态变量
若变量前加有static,则该变量为静态变量。例如:
static unsigned char a ; //a是一个无符号字符型静态变量
静态变量既可以在函数外定义,也可以在函数内定义,一般情况下,应在函数内部进行定义,这种静态变量称为静态局部变量。
静态局部变量的值在函数调用结束后不消失而保留原值,即占用的存储单元不释放,在下一次调用函数时,该变量的值是上次已有的值。这一点与自动变量不同,自动变量在调用结束后其值消失,即占用的存储单元将被释放。
重点提示:在定义变量时,如果不赋初值的话,对于静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符型变量)。而对于自动变量来说,如果不赋初值,它的值是一个不确定的值。这是由于每次函数调用结束后自动变量的存储单元已释放,下次调用时又重新分配新的存储单元,而分配的单元中的值是不确定的。
(3)外部变量
如果一个程序包括两个文件,两个文件都要用到同一个变量(例如a),不能分别在两个文件中各自定义一个变量a,否则,在进行程序编译连接时会出现“重复定义”的错误。正确的做法是:在第一个文件中定义全局变量a,在第二个文件中用extern对全局变量a进行“外部声明”,这样,在编译连接时,系统会由此知道a是一个已在别处定义的外部全局变量,在本文件中就可以合法地引用变量a了,具体定义如下。
第一个文件对变量a的定义:
unsigned char a ; //a是一个无符号字符型变量,注意,要在函数外部定义,即定义成全局变量
第二个文件对变量a的定义:
extern a; //a在另一个文件中已进行定义
另外,需要说明的是,在C51中,除了外部变量外,还有外部函数,如果有一个函数前面有关键字extern,表示此函数是在其他文件中定义过的外部函数。
(4)寄存器变量
如果有一些变量使用频繁,为了提高执行效率,可以将变量放在CPU的寄存器中,需要时从寄存器取出,不必再从内存中存取,由于对寄存器的存取速度远高于对内存的存取速度,因此,这样做可提高执行效率。这种变量叫作寄存器变量,用关键字register进行声明。