34
单单单 C 单单单单单单 单单单 单单

单片机 C 语言开发技术

Embed Size (px)

DESCRIPTION

单片机 C 语言开发技术. 第五章 函数. 内容概述. 函数是 C51 程序的基本组成部分, C51 程序的全部工作都是由各式各样的函数完成的。本章主要介绍函数的定义、调用、参数的传递、变量的作用域等。. 教学目标. 1 .理解函数的概念,能根据需要说明、定义一个函数,确定函数的返回值的类型,函数的形参的数据类型和格式,能正确的调用函数。 2 .理解调用函数在调用函数时的参数传递过程,掌握函数形参传递单个数组元素的方法。 3. 掌握 return 返回一个数值、多个数值的方法。 4 .理解函数递归调用的概念,能利用递归调用解决相关的计算问题。 - PowerPoint PPT Presentation

Citation preview

Page 1: 单片机 C 语言开发技术

单片机 C 语言开发技术

第五章 函数

Page 2: 单片机 C 语言开发技术

内容概述函数是 C51 程序的基本组成部分,

C51程序的全部工作都是由各式各样的函数完成的。本章主要介绍函数的定义、调用、参数的传递、变量的作用域等。

Page 3: 单片机 C 语言开发技术

教学目标1 .理解函数的概念,能根据需要说明、定义一个函

数,确定函数的返回值的类型,函数的形参的数据类型和格式,能正确的调用函数。

2 .理解调用函数在调用函数时的参数传递过程,掌握函数形参传递单个数组元素的方法。

3. 掌握 return 返回一个数值、多个数值的方法。4 .理解函数递归调用的概念,能利用递归调用解决

相关的计算问题。5 .理解函数的作用域和变量的作用域的概念。

Page 4: 单片机 C 语言开发技术

函数是 C51 源程序的基本模块, 通过对函数模块的调用实现特定的功能。

C51 语言中的函数相当于其它高级语言的子程序。

C51 语言不仅提供了极为丰富的库函数,还允许用户建立自己定义的函数。

用户可把自己的算法编成一个个相对独立的函数模块,然后用调用的方法来使用函数。

Page 5: 单片机 C 语言开发技术

5.1  函数的说明与定义 C51 中所有函数与变量一样,在使用之前必须说明。 所谓说明,是指说明函数是什么类型的函数 , 一般

库函数的说明都包含在相应的头文件 <*.h> 中 , 

例如 : 标准输入输出函数包含在“ stdio.h” 中 ,

非标准输入输出函数包含在“ io.h” 中 ,  在使用库函数时必须先知道该函数包含在什么样的

头文件中 , 在程序的开头用 #include <*.h> 或 #include"*.h" 说明。只有这样程序才会编译通过。

Page 6: 单片机 C 语言开发技术

5 . 1.1 函数说明 形式为 :  函数类型  函数名 (数据类型  形式参数 ,  数据类型  形式参数 , ......);

其中 : 函数类型是该函数返回值的数据类型 , 可以是以前介绍的整型 (int), 长整型 (long), 字符型 (char), 单浮点型 (float), 双浮点型 (double) 以及无值型 (void), 也可以是指针 , 包括结构指针。无值型表示函数没有返回值。

Page 7: 单片机 C 语言开发技术

函数名为 C51 的标识符 , 小括号中的内容为该函数的形式参数说明。

可以只有数据类型而没有形式参数 , 也可以两者都有。 对于经典的函数说明没有参数信息。如 :

    int putlll(int x,int y,int z,int color,char *p) /* 说明一个整型函数 */      char *name(void);               /* 说明一个字符串指针函数 */      void student(int n, char *str); /* 说明一个不返回值的函数 */      float calculate(void);            /* 说明一个浮点型函数 */

注意 : 如果一个函数没有说明就被调用 , 编译程序并不认为出错 , 而将此函数默认为整型 (int) 函数。因此当一个函数返回其它类型 , 又没有事先说明 ,  编译时将会出错。

Page 8: 单片机 C 语言开发技术

5.1.2 函数定义 函数定义就是确定该函数完成什么功能以及怎么运行 , 相当于其它语言的一个子程序。

C51 对函数的定义采用 ANSI C 规定的方式。即 :        函数类型  函数名 (数据类型形式参数 ; 数据类型 形式参数 ...)       {           函数体 ;        }

Page 9: 单片机 C 语言开发技术

其中函数类型和形式参数的数据类型为 C51 的基本数据类型

函数体为 C51 提供的库函数和语句以及其它用户自定义函数调用语句的组合 , 并包括在一对花括号“ {”和“ }” 中。

需要指出的是一个程序必须有一个主函数 , 其它用户定义的子函数可以是任意多个 , 这些函数的位置也没有什么限制 , 可以在 main() 函数前 ,  也可以在其后。

C51 将所有函数都被认为是全局性的。而且是外部的 , 即可以被另一个文件中的任何一个函数调用。

Page 10: 单片机 C 语言开发技术

5.2 函数的调用 5.2.1  函数的简单调用 C51 调用函数时直接使用函数名和实参的方

法 , 也就是把要赋给被调用函数的参量 , 按该函数说明的参数形式传递过去 , 然后进入子函数运行 , 运行结束后再按子函数规定的数据类型返回一个值给调用函数。

Page 11: 单片机 C 语言开发技术

例 5-2-1 输入两个整数,输出其中较大的值。#include<stdio.h>int max(int a,int b);/* 说明一个用户自定义函数 */int max(int a,int b){if(a>b) return a; else return b;}void main(){ int x,y,z; printf("input two numbers:\n"); scanf("%d%d",&x,&y); z=max(x,y); printf("maxmum=%d",z);}

Page 12: 单片机 C 语言开发技术

5.2.2 函数的参数传递5.2.2.1 调用函数向被调用函数以形式参数传递 用户编写的函数一般在对其说明和定义时就

规定了形式参数类型 , 因此调用这些函数时参量必须与子函数中形式参数的数据类型、顺序和数量完全相同

Page 13: 单片机 C 语言开发技术

注意 : 当数组作为形式参数向被调用函数传递时 , 只传递数组的地址 , 而不是将整个数组元素都复制到函数中去 , 即用数组名作为实参调用子函数 , 调用时指向该数组第一个元素的指针就被传递给子函数。

Page 14: 单片机 C 语言开发技术

用数组元素作为函数参数传递.当传递数组的某个元素时 , 数组元素作为实参 , 此时按使用其它简单变量

的方法使用数组元素。 #include<stdio.h>

     void disp(int n);      int main()      {           int m[10], i;           for(i=0; i<10; i++){             m[i]=i;             disp(m[i]);  /*逐个传递数组元素 */           }           getch();           return 0;      }      void disp(int n)      {           printf("%3d\t");      }

Page 15: 单片机 C 语言开发技术

5.2.2.2 被调用函数向调用函数返回值 一般使用 return 语句由被调用函数向调

用函数返回值 , 该语句有下列用途 :

1) 它能立即从所在的函数中退出 , 返回到调用它的程序中去。

2) 返回一个值给调用它的函数。

Page 16: 单片机 C 语言开发技术

有两种方法可以终止子函数运行并返回到调用它的函数中 :

1. 一是执行到函数的最后一条语句后返回 ;

2. 一是执行到语句 return 时返回。 前者当子函数执行完后仅返回给调用函数一个 0 。 若要返回一个值 , 就必须用 return 语句。只需在 re

turn 语句中指定返回的值即可。 return 语句可以向调用函数返回值 , 但这种方法只

能返回一个参数

Page 17: 单片机 C 语言开发技术

5.2.2.3 用全程变量实现参数互传 如果将所要传递的参数定义为全程变量 ,

可使变量在整个程序中对所有函数都可见。 全程变量的数目受到限制 , 特别对于较大的

数组更是如此。

Page 18: 单片机 C 语言开发技术

例 5-2-4  以下示例程序中 m[10] 数组是全程变量,数据元素的值在 disp()函数中被改变后,回到主函数中得到的依然是被改变后的值。 #include<stdio.h>

void disp(void); int m[10]; /* 定义全程变量 */ int main() { int i; printf("In main before calling\n"); for(i=0; i<10; i++){ m[i]=i; printf("%3d", m[i]); /* 输出调用子函数前数组的值 */ } disp(); /* 调用子函数 */ printf("\nIn main after calling\n"); for(i=0; i<10; i++) printf("%3d", m[i]); /* 输出调用子函数后数组的值 */ getchar(); return 0; }

Page 19: 单片机 C 语言开发技术

void disp(void) { int j; printf("In subfunc after calling\n");/* 子函数中输出数组的值 */ for (j=0; j<10; j++){ m[j]=m[j]*10; printf("%3d", m[j]); } }

Page 20: 单片机 C 语言开发技术

5.2.3 函数的递归调用     C51 允许函数自己调用自己 , 即函数的递归调用 , 递归调用可以使程序简洁、代码紧凑 , 但要牺牲内存空间作处理时的堆栈。 如要求一个 n!(n 的阶乘 ) 的值可用下面递归调用 :     例 5-2-5  求 n! 实例程序。

#include<stdio.h> unsigned long mul(int n); int main() { int m; puts("Calculate n! n=?\n"); scanf("%d", &m); /*键盘输入数据 */ printf("%d!=%ld\n", m, mul(m));/* 调用子程序计算并输出 */ getchar(); return 0; }

Page 21: 单片机 C 语言开发技术

unsigned long mul(int n) { unsigned long p; if(n>1) p=n*mul(n-1); /* 递归调用计算 n!*/ else p=1L; return(p); /* 返回结果 */ }

运行结果 :           calculate n!  n=? 输入 5 时结果为 :           5!=120

Page 22: 单片机 C 语言开发技术

5.3 函数作用范围与变量作用域 C51 中每个函数都是独立的代码块 , 函数代码归该函数所有 , 除了对函数的

调用以外 , 其它任何函数中的任何语句都不能访问它。 例如使用跳转语句 goto 就不能从一个函数跳进其它函数内部。除非使用全

程变量 , 否则一个函数内部定义的程序代码和数据 , 不会与另一个函数内的程序代码和数据相互影响。

C51 中所有函数的作用域都处于同一嵌套程度 , 即不能在一个函数内再说明或定义另一个函数。

C51 中一个函数对其它子函数的调用是全程的 , 即是函数在不同的文件中 , 也不必附加任何说明语句而被另一函数调用 , 也就是说一个函数对于整个程序都是可见的。

在 C51 中 , 变量是可以在各个层次的子程序中加以说明 , 也就是说 , 在任何函数中 , 变量说明有只允许在一个函数体的开头处说明 , 而且允许变量的说明 ( 包括初始化 )跟在一个复合语句的左花括号的后面 , 直到配对的右花括号为止。它的作用域仅在这对花括号内 , 当程序执行到出花括号时 , 它将不复存在。当然 , 内层中的变量即使与外层中的变量名字相同 , 它们之间也是没有关系的。

Page 23: 单片机 C 语言开发技术

例 5-3-1  全局变量与局部变量示例。  #include<stdio.h> int i=10; int main() { int i=1; printf("%d\t", i); { int i=2; printf("%d\t", i); { extern i; i+=1; printf("%d\t", i); } printf("%d\t", ++i); } printf("%d\n", ++i); return 0; } 运行结果为

1    2    3    4    2

Page 24: 单片机 C 语言开发技术

5. 4 函数的递归调用与再入函数

如果在调用一个函数的过程中又直接或间接地调用该函数本身,称为函数的递归调用。例如计算阶乘函数 f(n)= n!,

C51 编译器采用一个扩展关键字 reentrant ,作为定义函数时的选项,需要将一个函数定义为再入函数时,只要在函数名后面加上关键字 reentrant 即可。定义再入函数的一般形式为:函数类型 函数名 (形式参数表)  [reentrant]再入函数可被递归调用,无论何时,包括中断服务函数在内的任何函数都可调用再入函数。

Page 25: 单片机 C 语言开发技术

再入函数有如下规定:1 .再入函数不能传送 bit 类型的参数,也不能定义一个局部位标量,再入函数不能包括位操作以及 8051系列单片机的可位寻址区。2 .与 PL/M51兼容的函数不能具有 reentrant属性,也不能调用再入函数。3 .在编译时存储器模式的基础上为再入函数在内部或外部存储器中建立一个模拟堆栈区,称为再入栈。再入函数的局部变量及参数被放在再入栈中,从而使再入函数可以进行递归调用。面非再入函数的局部变量被放在再入栈之外的暂存区内,如果对非再入函数进行递归调用,则上次调用时使用的局部变量数据将被覆盖。4 .在同一个程序中可以定义和使用不同存储器模式的再入函数,任意模式的再入函数不能调用不同模式的再入函数,但可任意调用非再入函数。5 .在参数的传递上,实际参数可以传递给间接调用的再入函数。无再入属性的间接调用函数不能包含调用参数,但是可以使用定义的全局变量来进行参数传递。

Page 26: 单片机 C 语言开发技术

5. 5 中断服务函数与寄存器组定义

C51 编译器支持在 C 语言源程序中直接编写 805l 单片机的中断服务函数程序,从而减轻了采用汇编语言编写中断服务程序的繁琐程度。为了在 C 语言源程序中直接编写中断服务函数的需要,C51 编译器对函数的定义进行了扩展,增加了—个扩展关键字interrupt 。关键字 interrupt 是函数定义时的一个选项,加上这个选项即可以将—个函数定义成中断服务函数。定义中断服务函数的一般形式为:函数类型 函数名(形式参数表)  [interrupt n][using n]关键字 interrupt 后面的 n 为中断号,取值范围为 0~ 31 。

Page 27: 单片机 C 语言开发技术

805l 单片机的常用中断源和中断向量如表 5 . 1 所示: 表 5.1常用中断号与中断向量

中断号 n 中断源 中断向量 8n+3

O 外部中断 0 0003H

1 定时器 0 000BH

2 外部中断 1 0013H

3 定时器 1 001BH

4 串行口 0023H

Page 28: 单片机 C 语言开发技术

8051系列单片机可以在内部 RAM 中使用 4 个不同的工作寄存器组,每个寄存器组中包含 8 个工作寄存器(R0—R 7) 。 C51 编译器扩展了一个关键字 using ,专门用来选择 805l 单片机中不同的工作寄存器组。 Using 后面的 n 是一个 0~ 3 的常整数,分别选中 4 个不同的工作寄存器组。在定义一个函数时 using 是一个选项,如果不用该选项,则由编译器选择一个寄存器组作绝对寄存器组访问。需要注意的是,关键字 using 和 interrupt 的后面都不允许跟一个带运算符的表达式。 关键字 using 对函数目标代码的影响如下: 在函数的入口处将当前工作寄存器组保护到堆栈中;指定的工作寄存器内容不会改变;函数返回之前将被保护的工作寄存器组从堆栈中恢复。

Page 29: 单片机 C 语言开发技术

使用关键字 using 在函数中确定一个工作寄存器组时必须十分小心,要保证任何寄存器组的切换都只在仔细控制的区域内发生,如果不做到这一点将产生不正确的函数结果。另外还要注意,带 using属性的函数原则上不能返回 bit 类型的值。并且关键字using 不允许用于外部函数。 关键字 interrupt 也不允许用于外部函数,它对中断函数目标代码的影响如下: 在进入中断函数时,特殊功能寄存器 ACC 、 B 、 DPH 、 DPL 、PSW 将被保存入栈;如果不使用寄存组切换,则将中断函数中所用到的全部工作寄存器都入栈;函数返回之前存器内容出栈;中断函数由 8051 单片机指令 RETI 结束。注意:1 、如果中断函数中用到浮点运算,必须保存浮点寄存器的状态。(在 math.h 中保存浮点寄存器函数为 pfsave ,恢复浮点寄存器的状态函数为 fprestore)。2 、如果在中断函数中调用了其他函数,则被调函数所使用的工作寄存器组与中断函数的要保持一致。

Page 30: 单片机 C 语言开发技术

编写 8051 单片机中断函数时应遵循以下规则:1 .中断函数不能进行参数传递,如果中断函数中包含任何参数声明都将导致编译出错。2 .中断函数没有返回值,如果企图定义一个返回值将得到不正确的结果。因此建议在定义中断函数时将其定义为 void 类型,以明确说明没有返回值。3 .在任何情况下都不能直接调用中断函数,否则会产生编译错误。因为中断函数的退出是由 8051 单片机指令 RETI 完成的, RETI 指令影响 8051 单片机的硬件中断系统。如果在没有实际中断请求的情况下直接调用中断函数, RETI 指令的操作结果会产生一个致命的错误。4 .如果中断函数中用到浮点运算,必须保存浮点寄存器的状态,当没有其它程序执行浮点运算时可以不保存。C51 编译器的数学函数库 math . h 中,提供了保存浮点寄存器状态的库函数 pfsave 和恢复浮点寄存器状态的库函数 fprestore 。

Page 31: 单片机 C 语言开发技术

5 .如果在中断函数中调用了其它函数,则被调用函数所使用的寄存器组必须与中断函数相同。用户必须保证按要求使用相同的寄存器组,否则会产生不正确的结果,这一点必须引起足够的注意。如果定义中断函数时没行使用 using选项,则由编译器选择一个寄存器组作绝对寄存器组访问。另外,由于中断的产生不可预测,中断函数对其它函数的调用可能形成递规调用,需要时可将被中断函数所调用的其它函数定义成再入函数。6 . C51 编译器从绝对地址 8n十 3处产生一个中断向量,其中 n 为中断号。该向量包含一个到中断函数入口地址的绝对跳转。在对源程序编译时,可用编译控制指令NOINTVECTOR抑制中断向量的产生,从而使用户能够从独立的汇编程序模块中提供中断向量。

Page 32: 单片机 C 语言开发技术

5 . 5 常用 C51库函数

C5l 编译器的运行库中包含有丰富的库函数,使用库函数可大大简化用户的程序设计工作,提高编程效率。由于 805l系列单片机本身的特点,某些库函数的参数和调用格式与 ANSIC 标准有所不同,例如函数 isdigit 的返回值类型为 bit 而不是 char 。每个库函数都在相应的头文件中给出了函数原型声明,用户如果需要使用库函数,必须在源程序的开始处采用预处理器指令 #include将有关的头文件包含进来。如果省略了头文件,将不能保证函数的正确运行。 C51 库函数中类型的选择考虑到了 805l系列单片机的结构特性,用户在由己的应用程序中应尽可能地使用最小的数据类型,以最大限度地发挥 805l系列单片机的性能,同时可减少应用程序的代码长度。下面将常用的 C51 库函数分别作以解释。

Page 33: 单片机 C 语言开发技术

1、一般 I/ 0函数 STDIO.H2、字符函数 CTYPE.H3、字符串函数 STRING .H4、访问 SFR和 SFR_bit地址 REGxxx. H

1、一般 I/ 0函数 STDIO.HC51 库中包含有字符 I/ O 函数,它们通过 8051系列单片机的串行接口工作、如果希望支持其他 I/ O 接口,只需要改动 getkey()和 putchar()函数,库中所有其他 I/ O支持函数部依赖于这两个函数模块,不需要改动。另外需要注意,在使用 8051系列单片机的串行口之前,应先对其进行初始化。例如以 2400波特率(12MHz 时钟频率 )初始化串行口如下: SCON= 0x52; / * SCON 置初值 */ TMOD= 0x20; / * T MOD 量初值 */ THl= 0xf3; / * T1 量初值 */ TRl= 1; / *启动 T1*/当然也可以采用其他波特率来对串行口进行初始化。

Page 34: 单片机 C 语言开发技术

5. 6 预处理器

C 语言与其它高级程序设计语言的一个主要区别就是对程序的编译预处理功能,编译预处理器是 C 语言编译器的一个组成部分。在 C 语言中,通过一些预处理命令可以在很大程度上为 C语言本身提供许多功能和符号等方面的扩充,增强了 C 语言的灵活性和方便性。预处理命令可以在编写程序时加在需要的地方,但它只在程序编译时起作用,且通常是按行进行处理的,因此又称为编译控制行。 C 语言的预处理命令类似于汇编语言中的伪指令。编译器在对整个程序进行编译之前,先对程序中的编译控制行进行预处理,然后再将预处理的结果与整个 C 语言源程序一起进行编译,以产生目标代码。 C51 编译器的预处理器支持所有满足 ANSI 标准 X3J11细则的预处理命令。常用的预处理命令有:宏定义、文件包含和条件编译。为了与一般C 语言语句相区别,预处理命令由符号“ #” 开头。