第3章 单片机C语言开发基础 1.7节已经介绍了C语言的结构、数据类型、运算符、函数,本章将主要通过C语言编程控制学习板上的流水灯,学习如何灵活运用C语言中的运算符、控制语句、数组、指针、预处理。本章内容可以说是对C51知识点的一个完整总结,内容较多,初学者全面掌握有一定难度。初学者对其中一些知识点可做简单了解,在后续章节的学习中再结合具体应用,以加深理解。 3.1运算符的应用 C语言中的运算符主要包括算术运算符、关系运算符、逻辑运算符、赋值运算符等。以下就是几个应用运算符来编程的实例。 【例31】用单片机实现乘法78×18的运算,并通过P2口的发光二极管分时显示结果的高八位和低八位状态。 分析: 先设置两个字符型变量i和j,将它们分别赋值为78和18,可以先计算它们相乘的结果为1404,等于十六进制数0x057C,在程序中用变量s保存它们相乘的这个结果。因为i和j的值小于255,所以用字符型变量保存即可; 变量s的值大于255并小于65535,所以必须保存为整型变量。相乘的十六位结果在八位并口P2上显示,只能把它拆成高八位和低八位分别显示,显示时,为区别高八位和低八位,它们中间让发光二极管全灭,并停顿1s。变量s高八位的二进制数是0000 0101B,因为发光二极管的状态是并口为高时熄灭,所以高八位送显示时,将有最低位、倒数第二位的灯熄灭,其他灯亮; 变量s低八位的二进制数是0111 1100B,当高八位送显示时,将有最高位、最低位两位灯亮,其他灯熄灭。我们可以把以下程序下载到学习板,观察显示状态是否正确。 #include<reg51.h> #define uint unsigned int //宏定义 #define uchar unsigned char delay() { uint m,n; for(m=1000;m>0;m--) for(n=110;n>0;n--); } void main() { uint s;//保存乘法结果 uchar i,j; //保存相乘的因数 i=78; j=18; s=i*j; while(1) { P2=s/256; //取乘积的高八位送P2口显示 delay(); P2=0xff; delay(); P2=s%256; //取乘积的低八位送P2口显示 delay(); } } 程序中用指令“P2=s/256”取变量s的高八位送显示,指令右面的算式变量s除以256后取整,所以P2得到的是乘积的高八位。而用指令P2=s%256取变量s的低八位送显示,符号“%”表示取s和256相除的余数,即变量s的低八位。通过这个例子 可以练习除法和取余运算的用法。如果修改程序中i和j所赋的初值,还可以得到其他情况下乘法运算的结果。 这个程序里用到了宏定义,宏定义的格式为: #define 新名称 原内容 #define命令的作用是: 用“新名称”代替后面的“原内容”,一般用于“原内容”比较长,又在程序里反复用到的情况。这样如果在程序中出现“原内容”,就可以用一个比较简短的“新名称”代替,使程序的书写更加简化。例如,本例在程序开始已经做了宏定义“#define uint unsigned int”,在此宏定义的后面,所有应该写unsigned int的地方,都用uint代替了。同一个程序中,宏定义对一个内容只能定义一次。 【例32】用16个发光二极管显示除法运算结果。 在学习板上除了P2口接的8个发光二极管以外,P3口利用串并转换接口芯片74HC595也扩展了8个发光二极管。 发光二极管硬件驱动电路如图31所示。 其中,芯片74HC595是八位串行输入转并行输出移位寄存器。引脚SER(14)是串行移位输入引脚,串行数据从低位到高位在该引脚输入; 引脚SRCLK(11)移位时钟输入引脚,该引脚的上升沿可以使14脚的数据输入芯片内,即该引脚的上升沿控制数据串行移入; 引脚RCLK(12)并行输出时钟端,通常情况下该引脚保持低电平,当串行数据移入完成时, 图31发光二极管硬件驱动电路图接地符号与软件界面截图保持一致,全书同。 该引脚产生一个上升沿,将刚才移入的数据在QA~QH端并行输出。由74HC595的工作原理可知,仅用这一个芯片就可以只占用单片机的3个I/O口(即P3.4~P3.6)来驱动8个发光二极管,大大节约了硬件资源。感兴趣的读者可以把例31中乘法运算的程序修改一下,结果在图31的16位发光二极管上显示出来。 假设本例中除法运算为“10÷6”结果只保留一位小数,结果中整数部分可以在P2口的发光二极管上显示,小数部分在74HC595扩展的发光二极管上显示。程序如下: #include<reg51.h> #define uint unsigned int #define uchar unsigned char sbit data1=P3^4; //定义74HC595中用到的几个口 sbit iclk=P3^6; sbit oclk=P3^5; uchar i,j,k,m; void delay() { uint a,b; for(a=10;a>0;a--) for(b=110;b>0;b--); } void xianshi(uchar m) //74HC595显示子程序 { uchar n; n=m; //要显示的数存放在n里 oclk=0; iclk=0; for(j=8;j>0;j--) //要显示的数左移串行输入 { n=n<<1; data1=CY; iclk=1; delay(); iclk=0; } oclk=1;//移位完成并行输出 delay(); oclk=0; } void main() { i=10; //将被除数和除数分别赋给i和j j=6; P2=i/j; k=((i%j)*10)/j; //小数位保存在k中 xianshi(k);//调用显示子程序 while(1); } 这个程序主要用到了74HC595的显示驱动函数,命名为“xianshi()”,它的形参为m,即要送D9~D16显示的数据。在这个子函数里,首先定义了一个无符号字符型变量n,用来存放要显示的数,然后将74HC595的串行输入时钟和并行输出时钟都置为低电平。再开始串行输入: n左移一位,移出的位移到了CY里,再把CY的值(第一次移位是显示数据最高位)给74HC595串行数据输入端,再将串行输入时钟置高电平,该时钟原来为低电平,因此在该引脚形成了一个上升沿,将串行数据输入端的当前值移入74HC595,再延时将时钟端置低。串行数据输入的过程要反复移位8次,才能将要显示的八位二进制数全部移入,这里用了一个for循环,控制移位一共进行8次。当跳出循环时移位完成,此时需要将八位二进制数并行输出,输出通过并行输出时钟控制: 并行输出时钟原值为低电平,现在置为高电平,形成一个上升沿,这个上升沿控制74HC595将刚才移入的数据并行由QA~QH输出。输出完成后,再将输出时钟端置为低电平,为下一次显示做准备。 主程序里先将被除数和除数分别赋值给i和j,然后将i和j相除的结果取整后直接赋值给P2口,这是商的整数部分。小数部分的计算方法为: 先将i和j相除后取余数,再扩大10倍,再用除数除后取整,这样计算得到的小数部分只有小数点后的第一位,再调用74HC595的显示子函数,在发光二极管D9~D16显示这个小数位。发光二极管熄灭表示1,点亮表示0,通过观察学习板上发光二极管的亮灭状态,即可知道除数运算的结果。 上述方法在单片机控制小数点的显示时非常有用。也可以把本例中被除数和除数的值变化一下,观察发光二极管的状态有什么变化。另外,读者也可以考虑: 如果想显示更多的小数位时,程序应如何编写。 【例33】用自增、自减运算控制P2口的流水灯。 如果对变量i执行自增运算时写成“i++”,执行完i的值被加1; 对变量i执行自减运算时写成“i--”,执行完变量i的值被减1。这两个运算符主要用在for循环的表达式里,用来修改循环指针。 #include<reg51.h> void delay() //延时1s子程序 { unsigned int i,j; for(i=1000;i>0;i--) for(j=110;j>0;j--); } void main() { unsigned char m,n; while(1) { for(m=0;m<10;m++) //m从0到9自增1,状态从P2输出 { P2=m; delay(); } for(n=10;n>0;n--) //n从10到1自减1,状态从P2输出 { P2=n; delay(); } } } 这个程序可以实现P2口的流水灯按一定规律变化。端口P2驱动的发光二极管在端口数据为1时,灯灭,在数据为0时,灯亮,亮灭规律都是按照二进制数的形式变化。程序中两个for循环使P2口输出数据按从0到10,再从10到0的规则循环变化,相应的发光二极管也按这个规律点亮或熄灭。从观察P2口灯亮灭状态的变化,就可以知道自增和自减指令的功能了。 3.2C语言的语句 一个完整的C程序是由若干条C语句按一定的方式组合而成的。按C语句执行方式的不同,C程序可分为顺序结构、选择结构和循环结构。 顺序结构: 指程序按语句的顺序逐条执行。 选择结构: 指程序根据条件选择相应的执行顺序。 循环结构: 指程序根据某条件的存在重复执行一段程序,直到这个条件不满足为止。如果这个条件永远存在,就会形成死循环。 一般的C程序都是由上述三种结构混合而成的。但要保证C程序能够按照预期的意图运行,还要用到以下5类语句来对程序进行控制。 1. 控制语句 控制语句完成一定的控制功能,C语言中有9种控制语句。 if…else: 条件语句; for: 循环语句; while: 循环语句; do…while: 循环语句; continue: 结束本次循环语句; break: 终止执行循环语句; switch: 多分支选择语句; goto: 跳转语句; return: 从函数返回语句。 2. 函数调用语句 调用已定义过的函数,如延时函数。 3. 表达式语句 由一个表达式和一个分号构成,示例如下: z=x+y; 4. 空语句 空语句什么也不做,常用于消耗若干机器周期,延时等待。 5. 复合语句 用“{ }”把一些语句括起来就构成了复合语句。 以下重点学习一些控制语句的编程方法。 3.2.1if语句 if语句 用来判定所给条件是否满足,根据判定的结果(真或假)选择执行给出的两种操作之一。if语句有3种基本形式: 当表达式成立时,执行表达式后面的语句,不成立跳过该语句; 当表达式不成立时,执行else后面的语句,当表达式成立时,执行if后面的语句; 当表达式有多个时,哪个表达式成立,就执行相应表达式后面的语句,都不成立时,就执行最后一个else后面的语句。它们的格式如下: (1) if(表达式) (2) if(表达式) 语句1 else 语句2 (3) if(表达式1) 语句1 else if(表达式2) 语句2 else if(表达式3) 语句3 … else 语句n 例34详解 【例34】用if语句控制P2口一个流水灯从低位到高位循环移位点亮。 #include<reg51.h> void delay() { unsigned int i,j; for(i=1000;i>0;i--) for(j=110;j>0;j--); } void main() { unsigned char m=0xfe; //赋初值最低位的灯亮 while(1) //无限循环 { P2=m; delay(); m=(m<<1)|0x01; //m的值左移一位并且在最低位填1 if(m==0xff)m=0xfe; //当点亮的灯移出最高位时,恢复初值0xfe } } 主程序中的指令“m=(m<<1)|0x01;”是让m的值左移一位,此时最高位移出到CY中,最低位移入零补位。但我们这里想让移入的位是一,所以后面加了一个“|”(位或)运算,让移位后的结果与“0x01”相或,使最低位保持为一。指令“if(m==0xff)m=0xfe;”的表达式中用到了关系运算符“==”(等于),即当变量m和0xff相等的条件成立时,执行语句“m=0xfe”。当变量m和0xff相等时,是点亮灯的位从最高位移出时,为了让灯接着循环点亮,后面的语句让点亮灯的状态回到初始状态。这里的if语句形式属于3种中的第一种。 3.2.2switch…case多分支选择语句 if语句比较适合于从两者之间选择。当要实现从多种选择一种时,采用switch…case多分支选择语句,可使程序变得更为简洁。一般格式如下: switch(表达式) { case 常量表达式1: //如果常量表达式1满足,则执行语句1 语句1; break; //执行语句1后,用此指令跳出switch结构 case 常量表达式2: //如果常量表达式2满足,则执行语句2 语句2; break; //执行语句2后,用此指令跳出switch结构 … case 常量表达式n: 语句n; break; default: //上述表达式都不满足时,执行语句(n+1) 语句n+1; } 用到switch语句时要注意,常量表达式的值必须是整型或字符型; 当满足某个常量表达式,并执行完它后面的语句时,一定不要忘了写break语句,否则程序就会出错。 例35详解 【例35】用多分支选择语句switch…case实现P2口流水灯的控制。 #include<reg51.h> #define uchar unsigned char #define uint unsigned int void delay() { uint i,j; for(i=1000;i>0;i--) for(j=110;j>0;j--); } void main() { char m=3; //m赋初值为3 while(1) { switch(m--) //表达式为m--,第一次执行为m的初值 { case 0: //如果表达式的值为0,执行这句后面的语句 P2=0x01; break; //执行完前一条指令,跳出switch case 1: //如果表达式的值为1 ,执行这句后面的语句 P2=0x02; break; case 2: //如果表达式的值为2,执行这句后面的语句 P2=0x04; break; default: //当表达式的值不等于0~2,执行这句后面的语句 P2=0x08; } delay(); if(m<0)m=3;//如果m自减1后的结果小于零,重新赋为初值3 } } 本例先定义了一个有符号字符数m,并赋初值为3。用“m--”作为switch语句的表达式,注意这里第一次执行switch语句时,表达式的值为m的初值3。把表达式的值与case语句后面的常量表达式比较,看是否相等。因为第一次执行时,表达式的值为3,不等于0、1、2,所以执行default后面的语句“P2=0x08”,此时P2口倒数第4个灯熄灭。延时后,在循环语句while(1)的控制下,再次进入switch语句,这次表达式为m的初值减1,等于2,则程序跳到case后面值为2的下一条语句执行“P2=0x04”,此时P2口倒数第3个灯熄灭。再进入switch时表达式值为1,转去执行“case 1:”的下一条语句“P2=0x02; ”。再进入时,表达式值为0,转去执行“case 0:”后面的语句“P2=0x01; ”。当m的值减为负值时,“if(m<0)m=3;”语句控制m的值返回初值3。这样,在switch语句的控制下,我们会看到学习板上P2口低四位的有一个灯,在从高位到低位循环移位被熄灭。 通过这个程序的编写,我们学习了switch指令的用法: 先把switch后面的表达式和case后面的常量表达式比较,如果相等,执行此case指令下面的一条指令,碰到break时,跳出switch语句。如果switch后面的表达式和case后面的常量表达式都不相等,则执行default后面的语句。可见,switch语句根据表达式的值,形成了多分支选择的结构。 3.2.3do…while循环语句 该循环语句先执行循环体一次,再判断表达式的值。若为真值,则继续执行循环,否则退出循环。一般格式如下: do循环体语句 while(表达式); do…while循环语句的执行过程如下: 先执行一次指定的循环体语句,然后判断表达式; 当表达式的值为非零时,返回到第一步重新执行循环体语句; 如此反复,直到表达式的值等于0时,循环结束。 使用时要注意while(表达式)后的分号“;”不能丢,它表示整个循环语句的结束。 例36详解 【例36】用do…while循环语句实现流水灯3次左移循环点亮。 #include<reg51.h> #include<intrins.h> //包含循环左移指令的头文件 void delay() { unsigned int i,j; for(i=1000;i>0;i--) for(j=110;j>0;j--); } void main() { unsigned char m,n; n=2; do//do…while语句先执行循环体,再判断循环条件 { P2=0xfe; //P2口的初始状态,最低位的灯亮 delay(); for(m=7;m>0;m--) //循环左移7次 { P2=_crol_(P2,1); //循环左移语句 delay(); } }while(n--); //n减1不为0时,返回执行循环体 while(1); //n等于0时,跳出循环,原地踏步 } 上述程序中,我们控制一个流水灯从最低位开始,间隔1s左移点亮一次,直到最高位,共循环移位3次,最后停在最高位上。这里用变量n控制循环移位次数,n的初值取为2,是因为当do…while第一次执行完循环体时,n取值为2; 每执行完一次循环体,n取值减1,直到第3次执行完,n的值减为0,跳出循环。 3.3C语言的数组 数组是同类型的一组变量,引用这些变量时可用同一个标志符,借助于下标来区分各个变量。数组中的每一个变量称为数组元素。数组由连续的存储区域组成,最低地址对应于数组的第一个元素,最高地址对应于最后一个元素。数组可以是一维的,也可以是多维的。 3.3.1一维数组 一维数组的表达式如下: 类型说明符数组名[常量]; 方括号中的常量称为下标。C语言中,下标是从0开始的。示例如下: inta[10];//定义整型数组a,它有a[0]~a[9]共10个元素,每个元素都是整型变量 一维数组的赋值方法有以下几种。 (1) 在数组定义时赋值,示例如下: int a[10]={0,1,2,3,4,5,6,7,8,9}; 数组元素的下标从0开始,赋值后,a[0]=0,a[1]=1,依次类推,直至a[9]=9。 (2) 对于一个数组也可以部分赋值,示例如下: int b[10]={0,1,2,3,4,5}; 这里只对前6个元素赋值。对于没有赋值的b[6]~b[9],默认的初始值为0。 (3) 如果一个数组的全部元素都已赋值,可以省去方括号中的下标,示例如下: inta[ ]={0,1,2,3,4,5,6,7,8,9}; 数组元素的赋值与普通变量相同,可以把数组元素像普通变量一样使用。 3.3.2二维数组 C语言允许使用多维数组,最简单的多维数组是二维数组。其一般表达式形式如下: 类型说明符数组名[下标1][下标2]; 示例如下: unsigned char x[3][4];//定义无符号字符型二维数组,有3×4=12个元素 二维数组以行列矩阵的形式存储。第一个下标代表行,第二个下标代表列。上一数组中各数组元素的顺序排列如下: x[0][0]、x[0][1]、x[0][2]、x[0][3] x[1][0]、x[1][1]、x[1][2]、x[1][3] x[2][0]、x[2][1]、x[2][2]、x[2][3] 二维数组的赋值方法可以采用以下两种方式。 (1) 按存储顺序整体赋值,这是一种比较直观的赋值方式,示例如下: int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11}; 如果是全部元素赋值,可以不指定行数,即 int a[ ][4]={0,1,2,3,4,5,6,7,8,9,10,11}; (2) 按每行分别赋值。为了更直观地给二维数组赋值,可以按每行分别赋值,这时要用{ }标明,没有说明的部分默认为0,示例如下: int a[3][4]={ {0,1,2,3}, {4,5,6,7}, {8}}; //最后3个元素,没有赋值的被默认为0 3.3.3字符数组 用来存放字符型数据的数组称为字符数组。与整型数组一样,字符数组也可以在定义时进行初始化赋值。示例如下: char a[8]={'B','e','i','-','j','x','l','d'}; 上述语句定义了字符型数组,它有a[0]~a[7]共8个元素,每个元素都是字符型变量。还可以用字符串的形式来对全体字符数组元素进行赋值,示例如下: char str[]={"Now, Temperature is:"}; 或者写成更简洁的形式: char str[ ]="Now, Temperature is:"; 要特别注意的是: 字符串是以'\0'作为结束标志的。所以,当把一个字符串存入数组时,也把结束标志'\0'存入了数组。因此,上面定义的字符数组“str[20]”最后一个元素不是“:”,而是'\0'。数组必须先定义,才能使用。 3.3.4数组的应用 流水灯的控制方法有许多种,一种比较常用的方法是: 把流水灯的控制代码按顺序存入数组,再依次引用数组元素,并送到发光二极管的接口显示。无论流水灯的控制逻辑多么复杂,用这种方法都可以很容易地通过调用控制代码实现。 例如,定义一个无符号字符型数组如下: unsigned char code Tab[ ]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe}; 上述数组定义中用到了关键字“code”,因为数组的各个元素在使用过程中不发生变化,所以可以用这个关键字定义数组元素的存放方式,减小数组数据的存储空间。假设这个数组中的元素是要送给P2口的控制代码,程序如果把它们按顺序送给P2口,并间隔一定的延时,就可以实现一个流水灯的右移点亮。 例37详解 【例37】用数组控制P2口一个流水灯间隔1s右移点亮。 #include<reg51.h> #define uchar unsigned char #define uint unsigned int uchar code tab[]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe}; //定义一个数组 uchar i=0; void delay() { uint m,n; for(m=1000;m>0;m--) for(n=110;n>0;n--); } void main() { for(;i<8;i++) //循环给P2口送数8次 { P2=tab[i]; //引用数组元素,并送P2口显示 delay(); //调用延时1s子程序 } while(1); //显示结束,程序停在这里 } 本例程序中我们可以看到数组元素的用法: 在使用数组之前要先定义一个数组,包含数组元素类型、数组名和具体的数组元素,这里定义的数组是无符号字符型的,数组中的元素都是要给P2口的控制代码,并且是按顺序存放的。在程序中引用数组时用到语句“P2=tab[i];”,把数组中的第i个元素取出,直接赋值给P2显示。并且这个语句又是for循环内部语句,for循环控制取数组元素从tab[0]直到tab[7]。其中,for循环中第一个表达式省略,因为程序前面在定义变量i时,已经给i赋了初值0。这个程序里,我们可以试着把数组元素改成其他形式,就可以形成不同逻辑的流水灯控制,所以数组的应用在花式流水灯控制上,是一种非常高效的编程控制方法。 另外,通过这个例子我们还可以明确局部变量和全局变量的概念。在函数内部定义的变量称为局部变量,如本例中延时函数内部定义的变量m、n,就是延时函数的局部变量。局部变量只在该函数内有效。例如,一个函数定义了变量“x”为整型数据,另一个函数则把变量“x”定义为字型数据,两者之间互不影响。全局变量也称为外部变量,它定义在函数的外部,最好在程序的顶部。它的有效范围为从定义开始的位置到源文件结束。例如,本例中在程序最开始定义的语句“uchar i=0;”,就定义了一个全局变量i,从这条语句开始以下的程序中都可以使用此变量。全局变量可以被函数内的任何表达式访问。如果全局变量和某一函数的局部变量同名时,在该函数内,只有局部变量被引用,全局变量被自动“屏蔽”。例如,我们在主函数中用同样的语句再定义一个变量i时,此时在主函数中只有主函数定义的内部变量i是有效的。 3.3.5数组作为函数参数 一个数组的名字表示该数组的首地址,所以用数组名作为函数的参数时,被传递的就是数组的首地址,被调用的函数的形式参数必须定义为指针型变量。 用数组名作为函数的参数时,应该在主调函数和被调函数中分别进行数组定义,而不能只在一方定义数组。并且,两个函数中定义的数组类型必须一致。如果不一致,将导致编译出错。实参数组和形参数组的长度可以一致,也可以不一致,编译器不检查形参数组的长度,只是将实参数组的首地址传递给形参数组。为保证两者长度一致,最好在定义形参数组时,不指定长度,只在数组名后面跟一个空的方括号[ ]。编译时,系统会根据实参数组的长度为形参数组自动分配长度。 例38详解 【例38】使用数组作参数控制P2口八位流水灯点亮。 先定义流水灯控制码数组,再定义流水灯点亮函数,使其形参为数组,数据类型要和实参数组的类型一致。 #include<reg51.h> void delay() { unsigned int i,j; for(i=1000;i>0;i--) for(j=110;j>0;j--); } void xianshi(unsigned char a[]) //定义显示子函数,形参为字符型数组首地址 { unsigned char m; for(m=0;m<8;m++) { P2=a[m]; //取数组的第m个元素送P2口显示 delay(); } } void main() { unsigned char code tab[]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe}; //定义流水灯控制码 while(1) { xianshi(tab); //调用显示子函数 } } 本例中先定义了一个显示子函数,子函数的形参是无符号字符型数组tab的首地址,该子函数的功能是顺序地取数组中的元素,送P2口显示。在主函数中,先定义一个流水灯控制码数组,该数组类型要和显示子函数形参类型相同,这里都是无符号字符型。数组中的元素是要送到P2口的显示代码,顺序把它们送显示,可以实现P2口一个灯右移循环点亮。然后在主函数中调用刚才定义的显示函数,这时要注意,调用时函数的实参是刚才定义的控制码数组名。 3.4C语言的指针 指针是C语言中的一个重要概念,也是C语言的一个重要特色。正确而灵活地运用指针,可以有效地表示复杂的数据结构,动态地分配内存,方便地使用字符串,有效地使用数组。利用指针引用数组元素速度更快,占用内存更少。 3.4.1指针的定义和引用 1. 指针的概念 一个数据的“指针”就是它的地址。通过变量的地址能找到该变量在内存中的存储单元,从而能得到它的值。指针是一种特殊类型的变量。它具有一般变量的三要素: 名字、类型和值。指针的命名与一般变量是相同的,它与一般变量的区别在于值和类型上。 1) 指针的值 指针存放的是某个变量在内存中的地址值。被定义过的变量都有一个内存地址。如果一个指针存放了某个变量的地址值,就称这个指针指向该变量。由此可见,指针本身具有一个内存地址。另外,它还存放了它所指向的变量的地址值。 2) 指针的类型 指针的类型就是该指针所指向的变量的类型。例如,一个指针指向int型变量,该指针就是int型指针。 3) 指针的定义格式 指针变量不同于整型或字符型等其他类型的数据,使用前必须将其定义为“指针类型”。指针定义的一般形式如下: 类型说明符*指针名字 示例: int i; //定义一个整型变量i int *pointer; //定义整型指针,名字为pointer 可以用取地址运算符“&”使一个指针变量指向一个变量,例如: pointer=&i; //"&i"表示取i的地址,将i的地址存放在指针变量pointer中 在定义指针时要注意两点: (1) 指针名字前的“*”表示该变量为指针变量。 (2) 一个指针变量只能指向同一个类型的变量,如整型指针不能指向字符型变量。 2. 指针的初始化 在使用指针前必须进行初始化,一般格式如下: 类型说明符指针变量=初始地址值; 示例: unsigned char *p; //定义无符号字符型指针变量p unsigned char m; //定义无符号字符型数据m p=&m; //将m的地址存在p中(指针变量p被初始化了) 严禁使用未经初始化的指针变量,否则将引起严重后果。 3. 指针数组 指针可以指向某类变量,也可以指向数组。以指针变量为元素的数组称为指针数组。这些指针变量应具有相同的存储类型,并且指向的数据类型也必须相同。 指针数组定义的一般格式如下: 类型说明符*指针数组名[元素个数]; 示例: int *p[2]; //p[2]是含有p[0]和p[1]两个指针的指针数组,指向int型数据 指针数组的初始化可以在定义时同时进行,示例如下: unsigned char a[ ]={0,1,2,3}; unsigned char *p[4]={&a[0],&a[1],&a[2],&a[3]}; //存放的元素必须为地址 4. 指向数组的指针 一个变量有地址,一个数组元素也有地址,所以可以用一个指针指向一个数组元素。如果一个指针存放了某数组的第一个元素的地址,就说该指针是指向这一数组的指针。数组的指针即数组的起始地址。示例如下: unsigned char a[ ]={0,1,2,3}; unsigned char *p; p=&a[0]; //将数组a的首地址存放在指针变量p中 经上述定义后,指针p就是数组a的指针。 C语言规定: 数组名代表数组的首地址,也就是第一个元素的地址。例如,下面两个语句等价: p=&a[0]; p=a; C语言规定: p指向数组a的首地址后,p+1就指向数组的第二个元素a[1],p+2指向a[2],依次类推,p+i指向a[i]。 引用数组元素可以用下标(如a[3]),但使用指针速度更快,且占用内存少。这正是使用指针的优点和C语言的精华所在。 对于二维数组,C语言规定: 如果指针p指向该二维数组的首地址(可以用a表示,也可以用&a[0][0]表示),那么p[i]+j指向的元素就是a[i][j]。这里i、j分别表示二维数组的第i行和第j列。 3.4.2指针的应用 【例39】用指针数组控制P2口八位流水灯点亮。 指针数组中元素是变量的地址,在本例中就应该是流水灯控制码的地址。可先定义流水灯的控制码数组为: unsigned char code Tab[ ]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f}; 然后将元素的地址依次存入如下指针数组: unsigned char *p[ ]={&Tab[0],&Tab[1],&Tab[2],&Tab[3],&Tab[4],&Tab[5],&Tab[6],&Tab[7]}; 最后利用指针运算符“*”取得各指针所指元素的值,并送入P2口显示即可。 程序如下: #include<reg51.h> #define uchar unsigned char #define uint unsigned int void delay() { uint i,j; for(i=0;i<1000;i++) for(j=0;j<110;j++); } void main() { uchar code tab[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f}; uchar *p[]={&tab[0],&tab[1],&tab[2],&tab[3],&tab[4],&tab[5],&tab[6],&tab[7]}; uchar m=0; for(;m<8;m++) //循环控制取数组元素8次 { P2=*p[m]; //将指针数组中第i个元素送P2口 delay(); } while(1); //取数完毕程序停在这里 } 本例采用指针数组来控制P2口流水灯,以下再看一个用指向数组的指针来控制流水灯的例子。这里同样要定义流水灯控制码数组,再将数组名(数组的首地址)赋给指针。然后即可通过指针引用数组的元素,从而控制八位流水灯点亮。引用指针时要注意和上例的区别。 【例310】用指向数组的指针来控制流水灯。 #include<reg51.h> void delay() { unsigned int i,j; for(i=1000;i>0;i--) for(j=110;j>0;j--); } void main() { unsigned char code Tab[]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe};//流水灯控制码数组 unsigned char *p,m; //定义无符号字符型指针和控制循环的变量 p=Tab; //指针指向数组首地址 for(m=0;m<8;m++) //循环显示8个状态 { P2=*(p+m); //通过指针引用数组元素,送到P2显示 delay(); } while(1); //显示完毕,程序停在这里 } 我们可以从以上两个例子比较一下两种用法的区别。指针数组需要先将指针数组元素都赋值(初始化)再使用,通过指针取数组元素的方法是“*p[m]”(其中m是数组元素下标)。而指向数组的指针使用前要先定义一个指针,并将数组的首地址给指针,取数组元素的方法是“*(p+m)”。因此,这两种方法使用时是有一定的区别的,一定要注意区分。 3.4.3指针作函数参数的应用 函数的参数不仅可以是数据,也可以是指针,它的作用是将一个变量的地址传送到另一个函数中。 【例311】用指针作函数参数控制P2口八位流水灯点亮。 首先,定义一个指针指向存储流水控制码的数组的首地址,然后以这个指针作为实际参数传递给被调函数的形参,因为该形参也是一个指针,该指针也指向流水控制码的数组,所以只要用指针引用数组元素的方法就可以控制P2口八位流水灯点亮。 #include<reg51.h> void delay() { unsigned int i,j; for(i=1000;i>0;i--) for(j=110;j>0;j--); } void xianshi(unsigned char *p) //显示子程序,形参为无符号字符型指针 { unsigned char i; while(1)//无限循环 { i=0; while(*(p+i)!='\0') //当数组中元素未取完时,接着取数送显示 { P2=*(p+i); //取数组中第i+1个元素送P2口显示 delay(); i++; //修改循环计数变量 } } } void main() { unsigned char code tab[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f}; //定义显示码数组 unsigned char *pin; pin=tab; //指针指向数组首地址 xianshi(pin); //调用显示子程序 } 此程序中首先定义了一个显示子程序,它的形参为无符号字符型指针。子程序的功能为: 循环取数组中的元素,并送P2口显示。如果把指针p指向数组的首地址,则p+i指向的是数组中的第i+1个元素,*(p+i)则表示取数组中第i+1个元素的值。程序中“'\0'”表示数组元素的结束标志,数组元素在存储的时候,不仅要存储各个元素值,在最后还要存储结束标志。如果读数组元素时,读到结束标志就表示全部元素已经读完。所以指令行while(*(p+i)!='\0')的功能是: 当读数组元素不是结束标志时,循环继续,即接着取数并送显示。在本例主程序中,首先定义了显示控制码数组,该数组元素按顺序取数时,可以实现一个流水灯向左移位循环点亮。主程序中又定义了和数组同类型的指针pin,并让该指针指向数组首地址,再调用刚定义的显示子程序时,就可以用指针pin作为它的实参。 3.4.4函数型指针的应用 在C语言中,指针变量除了能指向数据对象外,也可以指向函数。一个函数在编译时,分配了一个入口地址,这个入口地址就称为函数的指针。可以用一个指针变量指向函数的入口地址,然后通过该指针变量调用此函数。 定义指向函数的指针变量的一般形式如下: 类型说明符(*指针变量名)(形参列表) 函数的调用可以通过函数名调用,也可以通过函数指针来调用。要通过函数指针调用函数,只要把函数的名字赋给该指针就可以了。 【例312】用函数型指针控制P2口八位流水灯点亮。 先定义流水灯点亮函数,再定义函数型指针,然后将流水灯点亮函数的名字(入口地址)赋给函数型指针,就可以通过该函数型指针调用流水灯点亮函数。注意: 函数型指针的类型说明符必须和函数的类型说明符一致。 #include<reg51.h> unsigned char code tab[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f}; //定义显示码数组 void delay() { unsigned int i,j; for(i=1000;i>0;i--) for(j=110;j>0;j--); } void xianshi() //定义显示子程序 { unsigned char i; for(i=0;i<8;i++) { P2=tab[i]; //取数组中的第i+1个元素送P2口 delay(); } } void main() { void(*p)(void); //定义函数型指针p p=xianshi; //将函数入口地址赋给指针p while(1)(*p)(); //通过指针p调用显示函数,并循环 } 本例中定义的显示子程序没有什么特别的地方,就是顺序取数组中的元素,并送P2口显示。在主程序中,需要先定义一个函数型指针,因为该指针所指向的函数没有参数和返回值,所以该指针的参数和类型说明符都为空。再用指令“p=xianshi;”把显示子程序名赋给指针,使指针指向函数入口地址。最后用指令“while(1)(*p)();”循环调用显示子程序,其中,“(*p)();”是循环内部语句,功能是通过指针p调用所指向的函数。 3.5C语言的编译预处理 编译预处理是C语言编译器的一个组成部分。在C语言中,通过一些预处理命令可以在很大程度上为C语言本身提供许多功能和符号等方面的扩充,增强C语言的灵活性和方便性。预处理命令可以在编写程序时加在需要的地方,但它只在程序编译时起作用,并且通常是按行进行处理的,因此又称为编译控制行。编译器在对整个程序进行编译之前,先对程序中的编译控制进行预处理,然后在将预处理的结果与整个C语言源程序一起进行编译,以产生目标代码。常用的预处理命令有宏定义、文件包含和条件编译。为了与一般C语言语句区别,预处理命令以“#”开头。 1. 宏定义 C语言允许用一个标志符来表示一个字符串,称为宏。被定义为宏的标志符为宏名。在编译预处理时,程序中的所有宏名都用宏定义中的字符串代替,这个过程称为宏代换。宏定义分为不带参数的宏定义和带参数的宏定义。 (1) 不带参数的宏定义的一般形式如下: #define 标志符 字符串 这种用法在前面的程序里已经用过了。如: #define uchar unsigned char 对于不带参数的宏定义说明如下: ① 宏定义不是C语句,不能在行末加分号,如果加了会连分号一起替代。 ② 宏名的有效范围为定义命令之后到本源文件结束。通常,#define命令写在文件开头,在函数之前,作为文件的一部分,在此文件范围内有效。 ③ 可以用#undef命令终止宏定义的作用域,在该语句之后,原来定义的宏将不起作用。 (2) 带参数的宏定义不是进行简单的字符串替换,还要进行参数替换,其一般形式如下: #define 宏名(参数表) 字符串 字符串中包含在括号中所指定的参数,示例如下: #define PI 3.1415926 #define S(r) PI*(r)*(r) main() { float a,area; a=5.6; area=S(a); } 经预处理后,程序在编译时如果遇到带参数的宏,则按照指定的字符串从左到右进行置换。关于带参数的宏定义说明如下: 宏定义如果写成 #define S(r) PI*r*r 可能引发歧义。如果参数不是r而是a+b时,S(a+b)将被替换为PI*a+b*a+b,这显然与编程的意图不一致。为此,应当在定义时在字符串中的形参外面加上一个括号,即#define S(r) PI*(r)*(r)。宏名与参数表之间不能有空格,否则将空格以后的字符都作为替代字符串的一部分。 2. 文件包含 文件包含是指一个程序将另一个指定的文件的全部内容包含进来。文件包含的命令一般格式如下: #include<文件名> 文件包含命令的功能是用指定文件的全部内容替换该预处理行。例如,在每个程序的最开始都要写上一行“#include<reg51.h>”,就是在此行用reg51.h头文件替换该行,因为在后面的程序中要用到该头文件中定义的内容。在进行较大规模程序设计时,文件包含命令十分有用。为了使用模块化编程,可以将组成C语言程序的各个功能函数分散到多个程序文件中,分别由若干人员完成,最后再用#include命令将它们嵌入一个总的程序文件中去。需要注意的是: 一个文件包含命令只能指定一个被包含文件。如果程序中要包含多个文件,则需要使用多个包含命令。当程序中需要调用C51编译器提供的各种库函数时,必须在程序的开头使用#include命令将相应的函数说明文件包含进来。 3. 条件编译 一般情况下,对C语言程序进行编译时,所有的程序都参加编译,但有时希望对其中一部分内容只在满足一定条件时才进行编译,这就是所谓的条件编译。条件编译可以选择不同的编译范围,从而产生不同的代码。C51编译器的预处理提供的条件编译命令可以分为以下3种形式。 1) 形式一 #ifdef 标志符 程序段1 #else 程序段2 #end if 如果指定的标志符已被定义,则程序段1参加编译,并产生有效代码,而忽略掉程序段2; 否则,程序段2参加编译并产生有效代码,而忽略掉程序段1。 2) 形式二 #if 常量表达式 程序段1 #else 程序段2 #end if 如果常量表达式为“真”,那么就编译该语句后的程序段。 3) 形式三 #ifndef 标志符 程序段1 #else 程序段2 #end if 该形式编译命令的格式与第一种命令格式只有第一行不同,它的作用与第一种编译命令的作用刚好相反,即如果标志符还没有被定义,那么就编译该语句后的程序段。 【例313】用带参数的宏定义完成运算a*b/(a+b),将结果送P2口显示。 本例可以用带参数的宏定义如下: #define F(a,b) (a)*(b)/((a)+(b)) 注意: 在字符串中的形式参数外面加上一个括号,可以避免编译时的歧义; 宏名F与带参数的括号之间不应加空格; 带参数的宏和函数不同,函数是先求出实参数表达式的值,然后代入形参。而带参数的宏只是进行简单的字符替换。 #include<reg51.h> #define F(a,b) (a)*(b)/((a)+(b)) void main() { int i,j,k; i=34; j=45; k=30; P2=F(i+j,k); while(1); } 本例中定义的宏F其中的形参a、b分别被实参i+j、k代替。将i、j、k的值代入,公式的计算结果为21.743,计算结果自动舍去小数点后的值,送到P2口的值为十进制的21,展成二进制数为0001 0101B,这个数送P2口,我们可以看到: 送“1”的位二极管熄灭,送“0”的位二极管点亮。 【例314】使用条件编译控制P2口点亮灯的状态。 通过本例掌握条件编译的使用方法。要求某条件满足时,P2口低四位灯点亮; 若不满足,则高四位灯点亮。 #include<reg51.h> #define max 100 void main() { #if max>80 //当max>80时,0xf0送P2口 P2=0xf0; #else P2=0x0f; //当max≤80时,0x0f送P2口 #endif } 本例根据常量表达式“max>80”的值是否为真来控制编译,因为前面已经用“#define max 100”定义了宏名max来表示100,所以常量表达式的值为真,执行“P2=0xf0;”,再用“#endif”命令结束本次条件编译。使用这种格式,需要事先给定一个条件,使程序在不同的条件下完成不同的功能。 习题 (1) 用C51编程实现除法“90÷8”的运算,并通过P2口的发光二极管分时显示结果的商和余数。 (2) 编程实现: 设初始状态为P2口8个灯全亮,用if语句控制P2口流水灯从高位到低位顺序熄灭。 (3) 编程实现: 用多分支选择语句switch…case实现P2口流水灯从高位到低位点亮。 本章小结 本章内容是对C51主要知识点(包括运算符、控制语句、数组、指针、预处理)的完整总结归纳,因为涉及的内容较多,读者全面掌握有一定难度。但实际上,学习单片机C语言是为了灵活应用,不需要把这些知识点都一一牢记,只需对它们都有初步的印象,在需要时知道怎么用就可以了。另外,也可以把本章内容作为参考,在后面具体软件设计中遇到问题时,及时查阅本章知识,以对知识点内容融会贯通。本章采用边学边练的方法,所有程序都是针对学习板上流水灯的控制。