第3章 PIC单片机的C语言基础知识
C语言是国际上广泛流行的计算机高级语言,是一种源于编写UNIX操作系统的语言,也是一种结构化语言,可产生紧凑代码。C语言结构是以括号“{}”而不是以字和特殊符号表示的语言。在许多硬件平台中可以不使用汇编语言,而采用C语言来编写相关控制代码,以进行硬件系统的控制。由于C语言程序本身并不依赖机器硬件系统,如果在系统中更改单片机的型号或性能时,对源程序稍加修改就可根据单片机的不同较快地进行程序移植,而移植程序时不一定要求用户 (程序开发人员)掌握MCU的指令系统,因此,现在许多硬件开发人员都使用C语言进行单片机系统的开发。
PIC单片机有很多第三方厂商为其开发了用于程序开发的C编译器,这些C编译器的语法结构基本相同,但在某些细节上还是有所区别的,因此用户选择了某个C编译器后,应掌握相应的C语言语法。本书以PIC C编译器为例,讲述PIC单片机的开发及应用。
3.1 数据运算
1. C语言程序结构
PIC单片机的C语言程序结构与一般C语言有很大的区别,每个PIC单片机C语言程序至少有一个main()函数 (即主函数)且只能有一个,它是C语言程序的基础,是程序代码执行的起点,而其他的函数都是通过main()函数直接或间接调用的。
PIC单片机C语言的程序结构具有以下特点。
一个C语言源程序由一个或多个源文件组成,主要包括一些C源文件(后缀名为“.c”的文件)和头文件(后缀名为“.h”),对于一些支持C语言的汇编语言混合编程的编译器而言,还可包括一些汇编源程序(后缀名为“.asm”)。
每个源文件至少包含一个main()函数,也可以包含一个main()函数和其他多个函数。头文件中声明一些函数、变量或预定义一些特定值,而函数的实现是在C源文件中。
一个C语言程序总是从main()函数开始执行的,而不论main()函数在整个程序中的位置如何。
源程序中可以有预处理命令(如include命令),这些命令通常放在源文件或源程序的最前面。
每个声明或语句都以分号结尾,但预处理命令、函数头和花括号“{}”之后不能加分号。
标识符、关键字之间必须加一个空格以示间隔。若已有明显的间隔符,也可不再加空格来间隔。
源程序中所用到的变量都必须先声明然后才能使用,否则编译时会报错。
C源程序的书写格式自由度较高,灵活性很强,有较大的任意性,但是这并不表示C源程序可以随意乱写。为了书写清晰,并便于阅读、理解、维护,在书写程序时最好遵循以下规则进行。
通常情况下,一个声明或一个语句占用一行。在语句的后面可适量添加一些注释,以增强程序的可读性。
不同结构层次的语句,从不同的起始位置开始,即在同一结构层次中的语句,缩进同样的字数。
用“{}”括起来的部分表示程序的某一层次结构。“{}”通常写在层次结构语句第一个字母的下方,与结构化语句对齐,并占用一行。
在此以下面的程序为例,进一步说明PIC单片机C语言程序的结构特点及书写规则,程序清单如下:
/**************************************************** //第1行 File name: LED_test.c //第2行 Chip type: PIC16F877A //第3行 Clock frequency:4.0MHz //第4行 ****************************************************/ //第5行 #include <pic.h> //第6行 #define uchar unsigned char //第7行 #define uint unsigned int //第8行 #define key RB1 //第9行 #define beep RC7 //第10行 #define LED PORTD //第11行 _CONFIG(0x3B31); //第12行 void delayms(uint ms) //第13行 { //第14行 uchar i; //第15行 while(ms--) //第16行 { //第17行 for(i=0;i<33;i++); //第18行 } //第19行 } //第20行 void main(void) //第21行 { //第22行 uint i; //第23行 TRISB1=0; //第24行 TRISC=0x00; //第25行 PORTC=0xFF; //第26行 TRISD=0x00; //第27行 PORTD=0xFF; //第28行 while(1) //第29行 { //第30行 if(key==0) //第31行 { //第32行 for(i=200;i>0;i--) //第33行 { //第34行 beep=~beep; //第35行 delayms(1); //第36行 } //第37行 for(i=200;i>0;i--) //第38行 { //第39行 beep=~beep; //第40行 delayms(3); //第41行 } //第42行 } //第43行 else //第44行 { //第45行 LED=~LED; //第46行 delayms(2000); //第47行 } //第48行 } //第49行 } //第50行
这是PIC16F877A单片机端口测试程序,PIC16F877A的RB1端口外接按键K1,RC7端口外接蜂鸣器,8只发光二极管与RD0~RD7端口连接。PIC16F877A采用外部4MHz晶振。当K1按下 (RB1为低电平)时,蜂鸣器发出警报声;当K1未按下时,蜂鸣器不发出警报声,8只LED发光二极管进行秒闪显示。下面分析这个C程序源代码。
第1行~第5行为注释部分。传统的注释定界符使用斜杠 -星号 (即“/*”)和星号 -斜杠 (即“*/”),斜杠 -星号用于注释的开始。编译器一旦遇到斜杠 - 星号 (即“/*”),就忽略后面的文本 (即使是多行文本),直到遇到星号 -斜杠 (即“*/”)。简言之,在此程序中第1行至第5行的内容不参与编译。在程序中还可使用双斜杠 (即“//”)来作为注释定界符。当使用双斜杠时,编译器忽略该行语句中双斜杠 (即“//”)后面的一些文本。
第6行~第11行为预处理命令。在程序中,凡是以“#”开头的均表示这是一条预处理命令语句。第6行为文件包含预处理命令,其意义是把双引号 (即“”)或尖括号 ( < >)内指定的文件包含到本程序,成为本程序的一部分。第7行~第11行为宏定义预处理命令语句,其中第7行表示uchar为无符号字符类型;第8行表示uint为无符号整数类型;第9行表示key代替RB1端口;第10行表示beep代替RC7端口;第11行表示LED代替PORTD (RD0~ RD7)端口。
被包含的文件通常是由系统提供的,也可以由程序员自己编写,其后缀名为“.h”。C语言的头文件中包括了各个标准库函数的函数原型。因此,在程序中调用一个库函数时,必须包含函数原型所在的头文件。
第12行定义工作配置字,即熔丝配置位的设定。
第13行定义了一个延时函数,函数名为“delayms”,函数的参数为“uint ms”。该函数采用了两个层次结构和两重循环语句。第14行~第20行表示外部层次结构,其中第14行表示延时函数从此处开始执行;第20行表示延时函数结束。第17行~第19行表示内部层次;第15行~第19行为数据说明和执行语句部分。第16行为外循环部分;第18行为内循环部分。
第21行定义了main主函数,函数的参数为“void”,意思是函数的参数为空,即不用传递参数给函数,函数即可运行。该函数也采用了多层次结构,第23行~第49行为第1层结构;第32行~第43行、第45行~第48行为第2层结构;第34行~第37行、第39行~第42行为第3层结构。
2. 标识符与关键字
C语言的标识符用来标识源程序中的变量、函数、标号和各种用户定义的对象的名字。PIC C语言中的标识符只能是由字母 (A ~ Z、a ~ z)、数字 (0~9)、下画线组成的字符串。其中第1个字符必须是字母或下画线,其后只能取字母、数字或下画线。标识符区分大、小写,其长度不能超过32个字符。
注意:
标识符不能用中文。
关键字是由C语言规定的具有特定意义的特殊标识符,有时又称为保留字,这些关键字应当以小写形式输入。在编写C语言源程序时,用户定义的标识符不能与关键字相同。PIC C中的关键字主要分为三类,其中数据类的关键字主要有auto、enum、char、const、typedef、void、volatile、short、int、extern、float、double、static、union、unsigned、long、register、struct、signed、sizeof;程序控制类的关键字主要有for、break、case、goto、do、else、continue、while、if、return、switch、default;预处理类的关键字主要有endi、ifdef、ifndef、include、undef、line、define、elif。
3. 数据类型
具有一定格式的数字或数值称为数据,数据是计算机操作的对象。数据的不同格式称为数据类型。
PIC C支持的数据类型有位变量型 (bit)、字符型 (char)、无符号字符型 (unsigned char)、有符号字符型 (signed char)、整型 (int)、短整型 (short int)、无符号整型 (unsigned int)、有符号整型 (signed int)、长整型 (long int)、无符号长整型 (unsigned long int)、有符号长整型 (signed long int)、单精度浮点型 (float)、双精度浮点型 (double)等,如图3-1所示。
图3-1 PIC C支持的数据类型
基本类型就是使用频率最高的数据类型,其值不可以再分解为其他类型。PIC C基本数据类型的长度和范围如表3-1所示。若在PIC编译器中执行菜单命令“Project”→“Configure”,并在“Configure Project”对话框的“Code Generation”选项卡中将“char is unsigned”复选框选中,或在源程序中使用了“#pragma uchar+”指令,则char的范围为0~255。
表3-1 PIC C基本数据类型的长度和范围
在PIC C中,若一个表达式中有两个操作数的类型不同,则编译器会自动按以下原则将其转换为同一类型的数据。
如果两个数有一个为浮点型(即单精度或双精度浮点型),则另一个操作数将转换成浮点型。
如果两个数有一个是长整型或无符号长整型,则另一个操作数将转换成相同的类型。
如果两个数有一个是整型或无符号整型,则另一个操作数将转换成相同的类型。
字符型和无符号字符型优先级最低。
4. 常量、变量及存储器管理
1)常量 所谓常量就是在程序运行过程中其值不能改变的数据。根据数据类型的不同,常量可分为整型常量、字符常量、字符串常量和实数常量等。
整型常量可以用二进制、八进制、十进制和十六进制数表示。表示二进制数时,在数字的前面加上“0b”的标志,其数码取值只能是“0”和“1”,如“0b10110010”表示二进制数的“10110010”,其值为十制数的1×27 +1×25 +1×24 +1×21 =178;表示八进制数时,在数字的前面加上“O”的标志,其数码取值只能是“0”~“7”,如“O517”表示八进制数的“517”,其值为十进制数的5×82 +1×81 +7×80 =335;表示十六进制数时,在数字的前面加上“0x”或“0X”的标志,其数码取值是数字“0~9”、字母“a”~“f”或字母“A”~“F”,如“0x3a”和“0X3A”均表示相同的十六进制数值,其值为十进制数的3×161 +10×160 =58。
无符号整数常量在一个数字后面加上“u”或“U”,如6325U;长整型整数常量在一个数字后面加上“l”或“L”,如97L。无符号长整型整数常量在一个数字后面加上“ul”或“UL”,如25UL;实数型常量在一个数字后面加上“f”或“F”,如3.146F。字符常量是用单引号将字符括起来,如 ‘a’;字符串常量是用引号将字符括起来,如“PIC”。
注意:
① 常量可以被定义成数组,最多8维;② 常量存在程序存储器中,所以必须使用关键词const;③常量表达式在编译时自动求解;④常量可以在函数内部声明。
以下为一些常量的定义:
int integer_constant=2134+87; //定义一个“2134+87”的整数 char char_constant=a; //定义一个字符‘a’ long long_int_constant1=98L; //定义一个长整数98 long long_int_constant2=0x1234; //定义一个十六进制的长整数0x1234 long long_int_constant3=0b10011000; //定义一个二进制的长整数0b10011000 int integer_array1[] ={3,2,4}; //定义一个一维数组,其元素分别为3、2、4 int integer_array2[8] ={3,2}; //定义一个含有8个元素的一维数组,其中 //前2个元素为3、2,其余的元素为0 int integer_array3[2,3] ={{3,2,1},{4,5,6}}; //定义一个2行3列共6个元素的二维数组 char string_constant1[] =This is a string constant; //定义一个字符串数组 const char string_constant2[]=This is also a string constant; //定义一个字符串数组
2)变量 所谓变量就是在程序运行过程中其值可以改变的数据。
(1)局部变量与全局变量: 根据实际程序的需求,变量可被声明为局部变量或全局变量。
局部变量是在创建函数时由函数分配的存储器空间,这些变量只能在所声明的函数内使用,而不能被其他的函数访问。但是,在多个函数中可以声明变量名相同的局部变量,而不会引起冲突,因为编译器会将这些变量视为每个函数的一部分。局部变量分为自动变量和静态变量两种。自动变量通常分布在函数的自动变量区;静态变量分配在一个固定的存储单元内。
全局变量是由编译器分配的存储器空间,可被程序内所有的函数访问。全局变量能够被任何函数修改,并且会保持全局变量的值,以便其他函数可以使用。
【自动变量】 在默认情况下,PIC C将所有函数内部定义的aotu(自动)型局部变量都分配在寄存器页0(bank0)。为了节约宝贵的存储空间,PIC C编译器采用一种“静态覆盖”技术以实现对C语言函数中的局部变量分配固定的地址空间。其大致原理是:在编译器编译源代码时扫描整个程序中函数调用的嵌套关系的层次,算出每个函数中的局部变量字节数,然后为每个局部变量分配一个固定的地址,且按调用嵌套的层次关系,各变量的地址可以相互重叠。使用这一技术后,所有的动态局部变量都可以按已知的固定地址进行直接寻址,用PIC汇编指令实现的效率最高,但这时不能出现函数递归调用。
由于所有的局部变量将占用bank0的存储空间,因此用户定义在bank0中的变量字节数将受到一定的限制,在使用时一定要注意。
【静态变量】 静态变量(static)是一种只在声明它的函数范围内有效的局部变量,它占用固定的存储单元,而且这个存储单元不会被别的函数使用,但其他函数可以通过指针访问它。未初始化的静态变量分配在rbss_n程序块中,占用1个固定的存储单元,而且该存储单元不会被其他函数调用。静态变量在程序开始只初始化一次,因此若只在某函数内部使用一个变量,而又希望其值在2次函数调用期间保持不变,为实现程序模块化,可将其声明为静态变量。例如,以下声明中,有些是合法的,而有些是非法的。
voidmin(void) { unsigned char var1; //合法声明 unsigned char bank1 var2; //非法声明 static unsigned char bank1 ver3; //合法声明,定义静态变量ver3 unsigned char var4=0x02; //合法声明,每次调用都初始化 static unsigned char bank1 var5=0x02;//合法声明,但只初始化一次 … }
对于一些需要被不同的函数调用,并且必须保存其值的局部变量而言,最好将这些局部变量声明为静态变量static。如果静态变量没有赋初值,在程序开始时会被自动赋值为0。例如:
int alfa(void) { static int bank1 n=1; //声明为静态变量 return n++; } void main(void) { int i; i=alfa(); //返回值为1 i=alfa(); //返回值为2 … }
【全局变量】 全局变量是由编译器分配的存储器空间,可被程序内所有的函数访问。全局变量能够被任何函数修改,并且会保持全局变量的值,以便其他函数可以使用。
全局变量定义后只能在同一个源文件中使用。如果要在其他文件中使用该全局变量,必须在使用前用extern关键字对该变量进行外部声明。例如,用户先在main.cpp文件里定义了一个整型变量:int count=1;然后又想在sub.cpp源文件里对变量count进行操作,则必须在sub.cpp源文件内使用该变量之前写上extern int count;进行声明。
注意:
声明时不能进行赋值,如果写成“externintcount=10;” 是错误的。
(2)变量修饰关键词: 在定义变量时,可以使用相应的变量修改关键词,除了上述的auto、static、extern外,还可以使用其他的变量修饰关键词,如volatile、const、persistent等。
【volatile】 volatile为易变型变量声明关键词,它说明了一个变量的值是会随机变化的,即使程序并没有刻意对它进行任何赋值操作。在单片机中,作为输入的I/O端口其内容是随意变化的;在中断内被修改的变量相对主程序流程来讲也是随意变化的;很多特殊功能寄存器的值也将随着指令的运行而动态改变。所有这种类型的变量必须被明确定义成“volatile”类型,例如:
volatile unsigned char STATUS @ 0x03; volatile bit commFlag; volatile int abc;
“volatile”类型定义PIC C编程时很重要,它可以告诉编译器的优化处理器这些变量是实实在在存在的,在优化过程中不能无故消除。假定用户在程序中定义了一个变量并对其进行了一次赋值,但随后就再也没有对其进行任何读写操作,如果是非volatile型变量,优化后的结果是该变量有可能被彻底删除以节约存储空间。另外一种情形是在使用某一个变量进行连续的运算操作时,该变量的值将在第一次操作时被复制到中间临时变量中,如果它是非volatile型变量,则紧接其后的其他操作将有可能直接从临时变量中取数以提高运行效率,显然这样做之后,对于那些随机变化的参数就会出问题。只要用户将其定义成volatile类型,程序编译后的代码就可以保证每次操作时直接从变量地址处取数。
【const】 const为常数型变量声明关键词。如果变量定义前冠以“const”类型修饰,那么所有这些变量就成为常数,程序运行过程中不能对其修改。除了位变量,其他所有基本类型的变量或高级组合变量都将被存放在程序空间(ROM区)以节约数据存储空间。显然,被定义在ROM区的变量是不能再在程序中对其进行赋值修改的,这也是“const”的本来意义。实际上这些数据最终都将以“retlw”的指令形式存放在程序空间,但PIC C会自动编译生成相关的附加代码从程序空间读取这些常数,编程人员无须操心太多。例如:
const char string_constant2[] ="This is also a string constant"; //定义一个字符串常量数组
如果定义了“const”类型的位变量,那么这些位变量还是被放置在RAM中,但程序不能对其进行赋值修改。其实,不能修改的位变量没有太大的实际意义,相信大家在实际编程时不会大量用到。
【persistent】 persistent为非初始化型变量声明关键词。按照标准C语言的做法,程序在开始运行前首先要把所有定义的但没有预置初值的变量全部清零。PIC C会在最后生成的机器码中加入一小段初始化代码来实现这一变量清零操作,且这一操作将在main函数被调用之前执行。问题是作为一个单片机的控制系统有很多变量是不允许在程序复位后被清零的。为了达到这一目的,PIC C提供了“persistent”修饰词以声明此类变量无须在复位时自动清零。用户应该自己决定程序中哪些变量必须声明成“persisten”类型,而且要自己判断什么时候需要对其进行初始化赋值。例如:
persistent unsigned char hour,minute,second; //定义时、 分、 秒变量
如果程序经上电复位后开始运行,则需要将persistent型的变量初始化,如果是其他形式的复位,如看门狗引发的复位,则无须对persistent型变量作任何修改。PIC单片机内提供了各种复位的判别标志,用户程序可依具体设计灵活处理不同的复位情形。
(3)变量的绝对定位: 在程序中可以使用“@”操作符指定全局变量或静态变量固定于某一绝对地址中。例如:
int a @0x80; //指定整型变量a保存在0x80数据存储器中 struct b { int a; char c; } alfa @0x100; //结构体alfa保存在0x100数据存储器中
注意:
PIC C编译器对绝对定位的变量不保留地址空间。也就是说,虽然上面变量a的地址为0x80,变量alfa的地址为0x100,但在程序执行过程中,地址0x80和0x100完全有可能又被分配给其他变量使用,这样就发生了地址冲突。所以,针对变量的绝对定位要注意,需要用户自己确保绝对地址的正确性。
实际上,只有单片机中的特殊功能寄存器需要绝对定位,而这些寄存器的地址定位又在PIC C编译环境所提供的头文件中已经实现,无须用户考虑。
(4)位变量: 由于在PIC的汇编指令中有专用的位操作指令,所在PIC C中为了有效地利用这些汇编指令提高编译效率,就在PIC C中引入了位型数据,即位变量。位变量只能是全局或静态的,用关键词bit声明,其语法格式如下:
bit <标识符>;
例如:
/*声明和赋初值*/ bit alfa=1; //定义位变量alfa,其初值等于1 bit beta; //定义位变量beta void main(void) if (alfa)beta=!beta; /*…*/
PIC C将定位在同一bank内的8个位变量合并成一字节存放在一个固定的地址。所以,所有针对位变量的操作都将直接使用PIC单片机的位操作汇编指令高效实现。基于此,位变量不能是局部自动型变量,而且无法将其组合成复合型高级变量。
PIC C对整个数据存储空间实现位编址,0x000单元的第0位是位地址0x0000,以此后推,每字节有8个位地址,所以整个数据存储空间都可以按位寻址。编制位地址纯粹是为了编译器最后产生汇编级位操作指令而用,对用户来说基本可以不考虑。但若能了解位变量的位地址编址方式,则可在最后程序调试时很方便地查找自己所定义的位变量,如果一个位变量alfa为0x14,那么其实际的存储空间位于:
字节地址=0x14/8=0x02
位偏移=0x14% 8=4
即alfa位变量位于地址0x02字节的第4位。在程序调试时,如果要观察alfa的变化,就必须观察地址为0x02的字节,而不是地址为0x12的。
如果位变量没有赋初值,在程序开始时会自动赋值为0,如上述变量beta定义时,其初值为0。在表达式赋值中位变量自动转变为无符号字符型。
3)存储器管理 PIC单片机采用Harvard (哈佛)结构,其存储器比较复杂,ROM和RAM都采用分页方式。由于C语言是针对冯·诺依曼结构的处理器开发的,并不适合描述PIC单片机的存储空间,所以在使用PIC C时对此做了相应的扩充,并引入bank和const以实现存储器的管理。
(1)数据存储器的管理: PIC单片机的数据存储器RAM较为特殊,是按bank来使用的。不同型号的PIC单片机,其内部RAM的数量各不相同。例如,PIC16F877A单片机的所有RAM空间被分为4个bank,称为bank0~ bank3。在每个bank中,最前面一部分均为特殊功能寄存器,其中有一些地址虽然没有定义具体的寄存器但仍被保留,以备将来扩展之用。可以自由使用的RAM位于每个bank的后部,其字节长度在4个bank中不完全相同。
位于bank0~ bank3最后的16个特殊地址单元叫做快速存取区。对于bank1~ bank3来说,实际并没有真正的物理空间存在,所有对此空间的操作都被映射到bank0对应的0x70~0x7F这16个地址单元中。也就是说,寻址bank的最高16字节不需要考虑当前bank的设定。
为了使编译器中产生最高的机器码,PIC C将单片机中数据寄存器的bank问题交由用户管理,因此在定义用户变量时,用户必须自己决定这些变量具体放在哪一个bank中。如果没有特别指明,所定义的变量将被定位在bank0中,例如:
unsigned char buffer[12]; bit flag1,flag2; float tmp[8];
除了bank0内的变量声明不需要做特殊处理外,定义在其他bank内的变量前必须加上相应的bank序号,例如:
bank1 unsigned char buffer[12]; //变量buffer[12]定位在bank1 中 bank2 bit flag1,flag2; //变量flag1、 flag2 定位在bank2 中 bank3 float tmp[8]; //变量tmp[8]定位在bank3 中
中档系列PIC单片机数据寄存器的一个bank大小为128字节,其中包括前面若干字节的特殊功能寄存器区域。在PIC C中,某一bank内定义的变量字节总数不能超过可用RAM的字节数。如果超过bank容量,在最后编译链接时会报错,报错信息为“Error[000]:Can’t find 0x230 words for psect rbss_0 in segment Bank0”。
链接器显示出总共有0x230 (560)字节准备放到bank0中,但bank0的容量不够。显然,编译器无法在bank0中找到这么多RAM,只能将一部分原本定位在bank0中的变量改放到其他bank中才能解决此问题。
虽然变量所在的bank定位必须由用户自己决定,但在编写源程序时,在进行变量存储操作之前无须再特意编写设定bank的指令。PIC C编译器会根据所操作的对象自动生成对应bank设定的汇编指令。为了避免频繁的bank切换以提高代码效率,应尽量把实现同一任务的变量定位在同一个bank内;在对不同bank内的变量进行读/写操作时,也应尽量把位于相同bank内的变量归并在一起连续操作。
(2)程序存储器的管理: 程序存储器只能读,不能写,程序存储器中除了代码外,往往还存储固定的表格、字形码等不需要在程序中进行修改的数据。
在PIC C中,使用const关键词来说明存储于程序存储器中的数据。例如:
const int abc=100;
其中,变量abc的值 (100)将被存储于程序存储器中,这个值是不可以被改变的。
5. C语言的运算符及表达式
运算符是告诉编译程序执行特定算术或逻辑操作的符号。C语言的运算符和表达式相当丰富,在高级语言中是比较少见的。C语言常用的运算符如表3-2所示。
表3-2 C语言常用的运算符
C语言规定了一些运算符的优先级和结合性。优先级是指当运算对象两侧都有运算符时执行运算的先后次序;结合性是指当一个运算两侧运算符的优先级别相同时的运算顺序。C语言运算符的优先级及结合性如表3-3所示。
表3-3 C语言运算符的优先级及结合性
1)算术运算符 算术运算符可用于各类数值运算,它包括加 ( +)、减 ( -)、乘(*)、除 (/)、求余 (又称为取模运算,%)、自增 ( ++)和自减 ( --)共7种。
用算术运算符和括号将运算对象连接起来的式子称为算术表达式,其运算对象包括常量、变量、函数和结构等。例如:
a+b; a+b-c; a*(b+c)-(d-e)/f; a+b/c-3.6 +′ b′;
算术运算符的优先级规定为: 先乘除求模,后加减,括号最优先。即在算术运算符中,乘、除、求余运算符的优先级相同,并高于加、减运算符。在表达式中若出现括号,其中的内容优先级最高。例如:
a-b/c; //在这个表达式中,除号的优先级高于减号,因此先运算b/c求得商, //再用a减去该商 (a+b)*(c-d% e)-f;//在这个表达式中,括号的优先级最高,因此先运算(a+b)和(c-d% e), //然后再将这两者相乘,最后再减去f。注意,执行(c-d% e)时, //先将d除以e所得的余数作为被减数,然后用c减去该被减数即可
算术运算符的结合性规定为自左至右方向,又称为“左结合性”,即当一个运算对象两侧的算术运算符优先级别相同时,运算对象与左面的运算符结合。例如:
a-b+c; //式中b两侧的“ -”、 “ +”运算符的优先级别相同, //则按左结合性,先执行a-b,再与c相加 a*b/c; //式中b两侧的“*”、 “/”运算符的优先级别相同, //则按左结合性,先执行a*b,再除以c
自增 ( ++)和自减 ( --)运算符的作用是使变量的值增加或减少1。例如:
++a; //先使a的值加上1,然后再使用a的值 a++; //先使用a的当前值进行运算,然后再使a加上1 --a; //先使a的值减少1,然后再使用a的值 a--; //先使用a的当前值进行运算,然后再使a减少1
2)赋值运算符和赋值表达式
(1)一般赋值运算符: 在C语言中,最常见的赋值运算符为“=”,它的作用是计算表达式的值,再将数据赋值给左边的变量。赋值运算符具有右结合性,即当一个运算对象两侧的运算符优先级别相同时,运算对象与右面的运算符结合,其一般形式为: 变量=表达式。例如:
x=a+b; //变量x输出的内容为a加上b s=sqrt(a)+sin(b); //变量s输出的内容为a的平方根加上b的正弦值 y=i++; //变量y输出的内容为i,然后i的内容加1 y=z=x=3; //可理解为y=(z=(x=3))
如果赋值运算符两边的数据类型不相同,系统将自动进行类型转换,即把赋值号右边的类型转换成左边的类型。具体规定如下: ①实数型转换为整型时,舍去小数部分;②整型转换为实数型时,数值不变,但将以实数型形式存放,即增加小数部分 (小数部分的值为0);③字符型转换为整型时,由于字符型为一字节,而整型为两字节,因此将字符的ASCII码值放到整型量的低8位中,高8位为0;④整型转换为字符型时,只把低8位转换给字符变量。
(2)复合赋值运算符: 在赋值运算符“=”的前面加上其他运算符,就可以构成复合赋值运算符。C语言中的复合赋值运算符包括加法赋值运算符 (+=)、减法赋值运算符 (-=)、乘法赋值运算符 (*=)、除法赋值运算符 (/=)、求余 (取模)赋值运算符 (%=)、逻辑“与”赋值运算符 (&=)、逻辑“或”赋值运算符 (| =)、逻辑“异或”赋值运算符 (^=)、逻辑“取反”赋值运算符 (~ =)、逻辑“左移”赋值运算符 ( <<=)、逻辑“右移”赋值运算符 ( >>=)共11种。
复合赋值运算首先对变量进行某种运算,然后再将运算的结果赋给该变量。采用复合赋值运算可以简化程序,同时提高C程序的编译效率。复合赋值运算表达式的一般格式为:
变量 复合赋值运算符 表达式 例如: a+=b; //相当于a=a+b a- =b; //相当于a=a-b a*=b; //相当于a=a*b a/=b; //相当于a=a/b a% =b; //相当于a=a% b
3)关系运算符 在程序中有时需要对某些量的大小进行比较,然后根据比较的结果进行相应的操作。在C语言中,关系运算符专用于两个量的大小比较,比较运算的结果只有“真”和“假”两个值。
C语言中的关系运算符包括大于 ( >)、小于 ( <)、等于 ( ==)、大于或等于( >=)、小于或等于 ( <=)、不等于 (!=)共6种。
用关系运算符将两个表达式连接起来的式子称为关系表达式。关系运算符两边的运算对象可以是C语言中任意合法的表达式或变量。关系表达式的一般格式为:
表达式 关系运算符 表达式
关系运算符的优先级别如下: ①大于 (>)、小于 ( <)、大于或等于 ( >=)、小于或等于(<=)属于同一优先级,等于 (==)、不等于 (!=)属于同一优先级,其中前4种运算符的优先级高于后2种运算符;②关系运算符的优先级别低于算术运算符,但高于赋值运算符。
例如:
x>y; //判断x是否大于y a+b<c; //判断a加上b的和是否小于c a+b-c==m ×n; //判断a加上b的和再减去c的差值是否等于m乘以n的积
关系运算符的结合性为左结合。C语言不像其他高级语言一样有专门的“逻辑值”,它用整数“0”和“1”来描述关系表达式的运算结果,规定用“0”表示逻辑“假”,即当表达式不成立时,运算结果为“0”;用“1”表示逻辑“真”,即当表达式成立时,运算结果为“1”。例如:
unsigned charx=8,y=9,z=18; //定义无符号字符x、 y、 z,其初始值分别为8、9、18 x>y; //x=8,y=9,x小于y,因此表达式不成立,运算结果为“0” x+y<z; //x加y等于17,小于z(z=18),因此表达式成立,运算结果为“1” (y=18)==z; //y重新赋值为18后等于z(z=18),因此表达式成立,运算结果为“1” x+6! =z; //x加6等于14,是不等于z(z=18),因此表达式成立,运算结果为“1” a==x<y<z //由于关系运算符的结合性为左结合,因此x<y的值为1,而1<z的值 //为1,所以a的值为1
4)逻辑运算符 逻辑关系主要包括逻辑“与”、逻辑“或”、逻辑“非”3种基本运算。在C语言中,用“&&”表示逻辑“与”运算;用“||”表示逻辑“或”运算;用“!”表示逻辑“非”运算。其中,“&&”和“||”是双目运算符,它要求有两个操作数,而“!”是单目运算符,只要求一个操作数。
注意:
“&”和“|”是位运算符,不要将逻辑运算符与位运算符混淆。
用逻辑运算符将关系表达式或逻辑量连接起来的式子称为逻辑表达式。逻辑表达式的一般格式为:
表达式 逻辑运算符 表达式
逻辑表达式的值是一个逻辑量,为“真”(即“1”)和“假”(即“0”)。对于逻辑“与”运算 (&&)而言,参与运算的两个量都为“真”时,结果才为“真”,否则为“假”;对于逻辑“或”运算 ( ||)而言,参与运算的两个量中只要有一个量为“真”,结果即为“真”,否则为“假”;对于逻辑“非”运算 (!)而言,参与运算的量为“真”时,结果为“假”,参与运算的量为“假”时,结果为“真”。
逻辑运算符的优先级别如下: ①在3个逻辑运算符中,逻辑“非”运算符 (!)的优先级最高,其次是逻辑“与”运算符 (&&),逻辑“或”运算符 ( ||)的优先级最低;②与算术运算符、关系运算符及赋值运算符的优先级相比,逻辑“非”运算符 (!)的优先级高于算术运算符,算术运算符的优先级高于关系运算符,关系运算符的优先级高于逻辑“与”运算符 (&&)和逻辑“或”运算符 ( ||),而赋值运算符的优先级最低。
假如:
unsigned chara=5,b=8,y; //定义无符号字符a、 b、 y,a的初始值为5,b的初始值为8 y=! a; //y的值为逻辑 “假”,因为a=5为逻辑 “真”,所以 “! a” 为逻辑 “假” y=a|| b; //y的值为逻辑 “真”,因为a、 b为逻辑 “真”,所以 “a|| b” 为逻辑 “真” y=a&&b; //y的值为逻辑 “真”,因为a、 b为逻辑 “真”,所以 “a&&b” 为逻辑 “真” y=! a&&b; //y的值为逻辑 “假”,因为 “!” 的优先级高于 “&&”,需先执行 “! a”, //其值为逻辑 “假” (即 “0”);而 “0&&b” 的运算为逻辑 “假”,所以结果为 //逻辑 “假”
5)位操作运算符 能对运算对象进行位操作是C语言的一大特点,正是由于这一特点使C语言具有了汇编语言的一些功能,从而使它能对计算机的硬件直接进行操作。
位操作运算符是按位对变量进行运算的,并不改变参与运算的变量的值。如果希望按位改变运算变量的值,则应利用相应的赋值运算。另外,位运算符只能对整型或字符型数据进行操作,不能用来对浮点型数据进行操作。
C语言中的位操作运算符包括按位“与”(&)、按位“或”( |)、按位“异或”(^)、按位“取反”(~)、按位“左移”( <<)、按位“右移”( >>)共6种运算。除了按位“取反”运算符外,其余5种位操作运算符都是两目运算符,即要求运算符两侧各有一个运算对象。
【按位“与”(&)】 按位“与”的运算规则是,参加运算的两个运算对象,若两者相应的位都为“1”,则该位的结果为“1”,否则为“0”。
例如: 若a =0x62 =0b01100010,b =0x3c =0b00111100,则表达式c = a &b的值为0x20,即
【按位“或”(| )】 按位“或”的运算规则是,参加运算的两个运算对象,若两者相应的位中只要有一位为“1”,则该位的结果为“1”,否则为“0”。
例如: 若a=0xa5 =0b10100101,b =0x29 =0b00101001,则表达式c = a | b的值为0xad,即
【按位“异或”(^)】 按位“异或”的运算规则是,参加运算的两个运算对象,若两者相应的位值相同,则该位的结果为“0”;若两者相应的位值相异,则该位的结果为“1”。
例如: 若a = 0xb6 = 0b10110110,b = 0x58 = 0b01011000,则表达式c = a^b的值为0xee,即
【按位“取反”()】 按位“取反”(~)是单目运算,用来对一个二进制数按位进行“取反”操作,即“0”变“1”,“1”变“0”。
例如: 若a=0x72=0b01110010,则表达式a=~ a的值为0x8d,即
【按位“左移”(<<)】 【按位“右移”(>>)】 按位“左移”( <<)是用来将一个操作数的各二进制位全部左移若干位,移位后,空白位补“0”,而溢出的位舍弃。
例如: 若a =0x8b =0b10001011,则表达式a =a <<2,将a值左移2位后,其结果为0x2c,即
按位“右移”( >>)是用来将一个操作数的各二进制位全部右移若干位,移位后,空白位补“0”,而溢出的位舍弃。
例如: 若a =0x8b =0b10001011,则表达式a =a >>2,将a值右移2位后,其结果为0x22,即
注意:
在PICC中不能直接访问寄存器的某一位,所以对寄存器的位操作时需使用C语言中的位运算功能。在PIC中可以直接或间接访问寄存器的某一位,它可以直接对位进行置“1”或清“0”操作。
(1)将寄存器的某一位清零:在PICC中可使用按位“与”(&)来实现;在PIC中除了可以使用按位“与”(&)外,还可以直接将该位赋为“0”。
例如:将RC2清零,而其他位不变,以下3条指令均可实现该要求。
PORTC &=0xfb; //相当于PORTC=PORTC &0xfb,由于0xfb的二进制代码 //为 “11111011”,因此执行该指令后,RC2被清零了 PORTC&=~(1<<2); //相当于PORTC=PORTC &~(1<<2),该指令先执行“(1<<2)” //将 “1”左移2 位,再执行 “~”取 “反”操作,然后将取“反”的 //结果与PORTC按位进行 “与” 操作 RC2 =0; //此指令直接将RC2 这一位清零
(2)将寄存器的某一位置“1”:在PIC中可使用按位“或”(|)来实现;在PIC中除了可以使用按位“或”(|)外,还可以直接将该位赋为“1”。
例如:将RC4置“1”,而其他位不变,以下3条指令均可实现该要求。
PORTC |=0x10; //相当于PORTC =PORTC | 0x10,由于0x10 的二进制代码为 // “00010000”,因此执行该指令后,RC4 被置1 PORTC |=(1 <<4); //相当于PORTC =PORTC & (1 <<4),该指令先执行“(1 <<4)” //将“1”左移4 位,再与PORTC按位进行“或”操作 RC4 =1; //此指令直接将RC4 这一位置“1”
(3)将寄存器的某一位“取反”:在ICC PIC中可使用按位“异或”(^)来实现;在PIC中除了可以使用按位“异或”(^)外,还可以直接将该位“取反”(~)。
例如:将RC6位“取反”,而其他位不变,以下3条指令均可实现该要求。
PORTC ^=0x40; //相当于PORTC=PORTC ^0x40,由于0x40的二进制代码为 “01000000”, //因此执行该指令后,RC6 被 “取反” PORTC ^=(1<<6);//相当于PORTC =PORTC ^ (1 <<6),该指令先执行 “(1 <<6)” //将 “1”左移6 位,再与PORTC按位进行 “异或”操作 RC6 =~RC6 //此指令直接将RC6 这一位 “取反”
6)条件运算符 条件运算符是C语言中唯一的一个三目运算符,它要求有3个运算对象,用它可以将3个表达式连接构成一个条件表达式。条件表达式的一般格式如下:
表达式1 ? 表达式2 : 表达式3
条件表达式的功能是首先计算表达式1的逻辑值,当逻辑值为“真”时,将表达式2的值作为整个条件表达式的值;当逻辑值为“假”时,将表达式3的值作为整个条件表达式的值。例如:
min=(a<b)? a : b //当a小于b时,min=a;当a小于b不成立时,min=b
7)逗号运算符 逗号运算符又称为顺序示值运算符。在C语言中,逗号运算符将两个或多个表达式连接起来。逗号表达式的一般格式如下:
表达式1,表达式2,…,表达式n
逗号表达式的运算过程是,选求解表达式1,再求解表达式2,…依次求解到表达式n。例如:
a=2+3,a*8 //先求解a=2+3,得a的值为5,然后求解a*8得40,整个逗号表达式 //的值为40 a=4*5,a+10,a/6 //先求解a=4*5,得20,再求解a+10得30,最后求解a/6得5,整个逗 //号表达式的值为5
8)求字节运算符 在C语言中提供了一种用于求取数据类型、变量及表达式的字节数的运算符sizeof。求字节运算符的一般形式如下:
sizeof(表达式)或sizeof(数据类型)
注意:
sizeof是一种特殊的运算符,它不是一个函数。通常,字节数的计算在程序编译时就完成了,而不是在程序执行的过程中计算出来的。
3.2 流程控制
C语言是一种结构化编程语言,用户可采用结构化方式编写相关源程序。采用结构化设计的程序具有结构清晰、层次分明、易于阅读修改和维护等特点。
结构化程序由若干个模块组成,每个模块中包含若干个基本结构,而每个基本结构中可有若干条语句。在C语言中,有3种基本结构: 顺序结构、选择结构和循环结构。
【顺序结构】 顺序结构是一种最基本、最简单的编程结构。在这种结构中,程序由低地址向高地址顺序执行指令代码。如图3-2所示,程序要先执行A,然后再执行B,两者是顺序执行的关系。
图3-2 顺序结构
【选择结构】 选择结构是对给定的条件进行判断,再根据判断的结果决定执行哪一个分支。如图3-3所示,图中P代表一个条件,当P条件成立(或称为“真”)时,执行A,否则执行B。
图3-3 选择结构
注意:
只能执行A或B之一。两条路径汇合在一起,然后从一个出口退出。
【循环结构】 循环结构是在给定条件成立时,反复执行某段程序。在C语言中,循环结构又分为“当”(while)型循环结构和“直到”(do while)型循环结构。
“while”型循环结构如图3-4(a)所示,当P条件成立(或称为“真”)时,反复执行A操作,直到P为“假”时,才停止循环。
图3-4 循环结构
“do while”型循环结构如图3-4(b)所示,先执行A操作,再判断P是否为“假”,若P为“假”,再执行A,如此反复,直到P为“真”为止。
1. 条件语句与控制结构
编程解决实际问题时,通常需要根据某些条件进行判断,然后决定执行哪些语句,这就是条件选择语句。在C语言中提供了3种形式的if条件选择语句和switch多分支选择语句。
1)if语句
(1)if语句的结构形式: if语句是C语言中的一个基本判断语句,它的3种结构形式语句如下。
【形式1】
if(表达式) {语句};
在这种结构形式中,如果括号中的表达式成立,则程序执行“{}”中的语句;否则程序将跳过“{}”中的语句部分,顺序执行其他语句。例如:
if (RB0==0) { //如果RB0端口为低电平,则执行下述语句 RB4=~PORTB.4; //RB4 端口输出相反的状态 RB5 =0; //RB5 端口输出为低电平 }
【形式2】
if (表达式) {语句1;} else {语句2;}
在这种结构形式中,如果括号中的表达式成立,则程序执行“{语句1;}”中的语句;否则程序执行“{语句2;}”中的语句。例如:
if (RB0 ==0) { //如果RB0 端口为低电平,则执行下述语句 RB4 =~RB4; //RB4 端口输出相反的状态 RB5 =0; //RB5 端口输出为低电平 } else { //如果RB0 端口不是低电平,则执行下述语句 RB7 =~RB7; //RB7 端口输出相反的状态 RB5 =1; //RB5 端口输出为高电平
【形式3】
} if (表达式1) {语句1;} else if (表达式2) {语句2;} else if (表达式3) {语句3;} … else if (表达式m) {语句m;} else {语句n;}
在这种结构形式中,如果括号中的表达式1成立,则程序执行“{语句1;}”中的语句,然后退出if选择语句,不执行下面的语句;否则,如果表达式2成立,则程序执行“{语句2;}”中的语句,然后退出if选择语句,不执行下面的语句;否则,如果表达式3成立,则程序执行“{语句3;}”中的语句,然后退出if选择语句,不执行下面的语句;……;否则,如果表达式m成立,则程序执行“{语句m;}”中的语句,然后退出if选择语句,不执行下面的语句;否则,上述表达式均不成立,则程序执行“{语句n;}”中的语句。
例如,根据a值的大小决定numb系数,编写的程序段如下:
if (a>6500) {numb=1;} else if (a>6000) {numb=0.8;} else if (a>5800) {numb=0.6;} else if (a>5600) {numb=0.4} else {numb=0;}
(2)if语句的嵌套: 如果if语句中又包含1个或多个if语句,这种情况称为if语句的嵌套。if语句嵌套的基本形式如下:
下面以PIC16F877A单片机为例说明if条件语句,其电路原理如图3-5所示。在PIC16F877A单片机的RD0和RD4端口分别接D1和D2两个发光二极管,该实例的控制任务是当按钮K2闭合时,发光二极管D1点亮,D2熄灭;当开关K1断开时,发光二极管D1熄灭,而D2点亮。编写的程序如下:
图3-5 发光二极管控制电路图
/**************************************************** File name: ex_if.c Chip type: PIC16F877A Clock frequency: 4.0MHz ****************************************************/ #include "pic.h" #define uchar unsigned char //定义开关及LED与端口的连接 #define redLED RD0 #define greenLED RD4 _CONFIG(0x3B31); //配置字设置 void main(void) { TRISB = 0x02; //定义RB1端口按键输入 RBPU= 0; //RB端口内部弱上拉 PORTB=0xFF; TRISD=0x00; //定义PORTD为输出方式 PORTD=0xFF; while(1) { if(PORTB&0x02) //检测RB1端口上的开关为断开状态 { redLED=0x1; //发光二极管D1熄灭(低电平有效) greenLED=0x0; //发光二极管D2点亮 } else //检测RB1端口上的开关为闭合状态 { redLED=0x0; //发光二极管D1点亮 greenLED=0x1; //发光二极管D2熄灭 } } }
2)switch语句 在实际使用中,通常会碰到多分支选择问题,此时可以使用if嵌套语句来实现,但是如果分支很多的话,if语句的层数太多、程序冗长,可读性降低,而且很容易出错。基于此,在C语言中使用switch语句可以很好地解决多重if嵌套容易出现的问题。switch语句是另一种多分支选择语句,是用来实现多方向条件分支的语句。
(1)switch语句格式
switch (表达式) case常量表达式1: {语句1;} break; case常量表达式2: {语句2;} break; case常量表达式3: {语句3;} break; … case常量表达式m: {语句m;} break; default: {语句n;} break;
(2)switch语句使用说明
switch后面括号内的“表达式”可以是整型表达式或字符型表达式,也可以是枚举型数据。
当switch后面表达式的值与某一“case”后面的常量表达式相等时,就执行该“case”后面的语句,然后遇到break语句退出switch语句。若所有“case”中常量表达式的值都不与表达式的值相匹配,则执行default后面的语句。
每一个case的常量表达式的值必须互不相同,否则就会出现互相矛盾的现象(对同一个值,有两种或者多种解决方案提供)。
每个case和default的出现次序不影响执行结果,可先出现“default”再出现其他的“case”。
假如在case语句的最后没有“break;”,则流程控制转移到下一个case继续执行。所以,在执行一个case分支后,使流程跳出switch结构,即终止switch语句的执行,可用一个break语句完成。
下面仍以图3-5所示电路原理图为例,说明switch的用法。该实例的任务是当按下按钮K2时,发光二极管D1亮;当按下按钮K3时,发光二极管D2亮。编写的程序如下:
/**************************************************** File name: ex_switch.c Chip type: PIC16F877A Clock frequency: 4.0MHz ****************************************************/ #include "pic.h" #define uchar unsigned char //定义开关及LED与端口的连接 #define redLED RD0 #define greenLED RD4 _CONFIG(0x3B31); //配置字设置 void main(void) { TRISB = 0x06; //定义RB1、 RB2端口按键输入 RBPU = 0; //RB端口内部弱上拉 PORTB=0xFF; TRISD=0x00; //定义PORTD为输出方式 PORTD=0xFF; while(1) { //该表达式判断K1和K2是否闭合,如果闭合则与之相连的引脚为低电平 switch (PORTB &0x06) { case 0x04: //表达式的值等于0x04,表示K2开关处于闭合状态 redLED=0x0; //发光二极管D1亮 break; //跳出switch结构,不执行下面的语句 case 0x02: //表达式的值等于0x02,表示K3开关处于闭合状态 greenLED =0x0; //发光二极管D2亮 break; default: //表达式的值既不等于0x01,又不等于0x02, //表示两个开关均未闭合 redLED=0x1; //两个发光二极管均不亮 greenLED=0x1; break; } } }
2. 循环语句与控制结构
在许多实际问题中,需要程序进行有规律的重复执行,此时可以采用一些循环语句来实现。在C语言中,用来实现循环的语句有goto语句、while语句、do-while语句和for语句等。
1)goto语句 goto语句为无条件转向语句,该语句可以实现循环。goto语句的一般形式如下:
goto语句标号;
其中,语句标号不必特殊加以定义,它是一个任意合法的标识符,其命名规则与变量名相同,由字母、数字和下画线组成,并且第一个字符必须为字母或下画线,不能用整数作为标号。这个标识符加上一个“:”一起出现在函数内某处时,执行goto语句后,程序将跳转到该标号处并执行其后的语句。标号必须与goto语句同处于一个函数中,但可以不在一个循环层中。
结构化程序设计主张限制使用goto语句,主要是因为它会使程序层次不清,且不易读,但也并不是绝对禁止使用goto语句,在多层嵌套退出时,用goto语句比较合理。一般来说,使用goto语句可以有以下两种用途: 与if语句一起构成循环结构,从循环体中跳转到循环体外。
【与if语句一起构成循环结构】 例如,用if语句和goto语句构成循环结构,求编写的程序如下:
/**************************************************** File name: ex_goto.c Chip type: PIC16F877A Clock frequency: 4.0MHz ****************************************************/ #include "pic.h" _CONFIG(0x3B31); //配置字设置 void main(void) { int i=0,sum=0; loop: if(i<=50) { sum=sum+i; i++; goto loop; } }
【从循环体中跳转到循环体外】 在C语言中,如果要跳出本层循环和结束本次循环,可以使用break语句和continue语句。goto语句的使用机会已大大减少,只是需从多层循环的内层跳到多层循环体外时才用到goto语句。但是,这种用法不符合结构化原则,一般不宜采用,只有在特殊情况(如需要大大提高生成代码的效率)时才使用。
2)while语句 while语句很早就出现在C语言编程的描述中,它是最基本的控制元素之一,用来实现“当型”循环结构。while语句的一般格式如下:
while (表达式) {语句;}
若程序的执行进入while循环的顶部时,将对表达式求值。如果该表达式为“真”(非零),则执行while循环内的语句。当执行到循环底端时,马上返回while循环的顶部,再次对表达式求值。如果值仍为“真”,则继续循环,否则完全绕过该循环,而继续执行紧跟在while循环之后的语句,其流程如图3-6所示。
图3-6 while语句的流程图
例如,用while语句求,编写的程序如下:
/**************************************************** File name: ex_while.c Chip type: PIC16F877A Clock frequency: 4.0MHz ****************************************************/ #include "pic.h" _CONFIG(0x3B31); //配置字设置 void main(void) { int n=0,sum=0; while (n<=50) { sum=sum+n; n++; } }
3)do-while语句 do-while循环与while循环十分相似,二者的区别在于: do-while语句是先执行循环后判断,即循环内的语句至少执行一次,然后再判断是否继续循环,其流程如图3-7所示;while语句是在每次执行的指令前先判断。do-while语句的一般格式如下:
图3-7 do-while语句流程图
do {语句;} While (条件表达式);
例如,用do-while语句求,编写的程序如下:
/ ************************************************ File name: ex dowhile. c Chip type: PIC16F877A Clock frequency: 4. 0MHz ****************************************************/ #include "pic. h" _CONFIG(0x3B31); //配置字设置 void main(void) { int n=0,sum=0; do { } sum=sum+n; n++; while (n<=50); }
4)for语句 在C语言中,for语句的使用最为灵活,完全可以取代while语句或者do-while语句。它不仅可以用于循环次数已经确定的情况,而且可以用于循环次数不确定而只给出循环结束条件的情况。for语句的一般格式如下:
for (表达式1;表达式2;表达式3) {语句;}
for循环语句的流程如图3-8所示,其执行过程如下。
图3-8 for语句流程图
(1)先对表达式1赋初值,进行初始化。
(2)判断表达式2是否满足给定的循环条件,若满足循环条件,则执行循环体内语句,然后执行第③步;若不满足循环条件,则结束循环,转到第⑤步。
(3)若表达式2为“真”,则在执行指定的循环语句后,求解表达式3。
(4)回到第②步继续执行
(5)退出for循环,执行后面的下一条语句。
for语句最简单的应用形式也是最易理解的形式,如下所示:
for (循环变量赋初值;循环条件;循环变量增值) {语句;}
例如,用for语句求,编写的程序如下:
/**************************************************** File name:ex_for.c Chip type:PIC16F877A Clock frequency:4.0MHz ****************************************************/ #include "pic.h" _CONFIG(0x3B31); //配置字设置 void main(void) { int n,sum=0; for (n=0;n<=50;n++) { sum=sum+n; } }
显然,用for语句简单、方便。对于以上for语句的一般形式,也可以用相应的while循环形式来表示:
表达式1; while (表达式2) { 语句; 表达式3; }
同样,for语句的一般形式还可以用相应的do-while循环形式来表示:
表达式1; do { 语句; 表达式3; } while (表达式2)
for语句使用最为灵活,除了可以取代while语句或者do-while语句外,在结构形式上也体现了其灵活性,下面对for循环语句的几种特例进行说明。
【for语句中小括号内的表达式1缺省】 for语句中小括号内的表达式1缺省时,应在for语句之前给循环变量赋初值。
注意:
虽然表达式1省略了,但是表达式1后面的分号不能省略。例如:
int n,sum=0; for (;n<=50;n++) { sum=sum+n; }
该程序段执行时,不对n设置初值,直接跳过“求解表达式1”这一步,而其他不变。
【for语句中小括号内的表达式2缺省】 for语句中小括号内的表达式2缺省时,不判断循环条件,默认循环条件始终为“真”,使循环无终止地进行下去。例如:
int n,sum=0; for (n=0;;n++) { sum=sum+n; } 它相当于: int n,sum=0; while (1) { sum=sum+n; n++; }
【for语句中小括号内的表达式3缺省】 for语句中小括号内的表达式3缺省时,在程序中应书写相关语句以保证循环能正常结束。例如:
int n,sum=0; for (n=0;n<=50;) { sum=sum+n; n++; }
在此程序段中,将n++的操作不放在for语句的表达式3的位置处,而作为循环体的一部分,效果是一样的,都能使循环正常结束。
【for语句中小括号内的表达式1和表达式3缺省】 for语句中小括号内的表达式1和表达式3缺省,而只给出循环条件,在此种情况下,完全等效于while语句。例如:
int n,sum=0; for (;n<=50;) { sum=sum+n; n++; }
它相当于:
int n,sum=0; while (n<=50) { sum=sum+n; n++; }
【for语句中小括号内的3个表达式都缺省】 for语句中小括号内的3个表达式都缺省,既不设置初值,也不判断条件,而循环变量也不增值,使程序无终止地执行循环体。例如:
for (;;) {… /*循环体*/}
它相当于:
while (1) {… /*循环体*/}
【for语句中没有循环体】 例如:
int n; for(n=0;n<1000;n++) {;}
此例在程序段中起延时作用。
5)break语句和continue语句
【break语句】 break语句通常可以用在switch语句或者循环语句中。当break语句用于switch语句中时,可使程序跳出switch而执行switch以后的语句;当break语句用于while、do-while、for循环语句中时,可使程序提前终止循环而执行循环后面的语句,通常break语句总是与if语句连在一起的,即满足条件时便跳出循环。break语句的一般格式如下:
break;
注意:
① break语句不能用于循环语句和switch语句之外的任何其他语句中。②break语句只能跳出它所处的那一层循环,而不像goto语句可以直接从最内层循环中跳出来。因此,要退出多重循环时,采用goto语句比较方便。
【continue语句】 continue语句一般用在while、do-while、for循环语句中,其功能是跳过循环体中剩余的语句而强行执行下一次循环。通常continue语句总是与if语句连在一起的,用来加速循环。continue语句的一般格式如下:
continue;
continue语句和break语句的区别: break语句结束循环,不再进行条件判断;continue语句只能结束本次循环,不终止整个循环。
3.3 数组
数组是一组具有固定数目的相同类型成分分量的有序数据集合。数组是C语言提供的一种最简单的构造类型,其成分分量的类型为该数组的基本类型。如整型变量的有序集合称为整型数组,字符型变量的有序集合称为字符型数组。数组中的每个元素都属于同一个数据类型,在同一数组中不允许出现不同类型的变量。
在数组中,可以用一个统一的数组名和下标来唯一地确定数组中的元素。数组中的下标放在方括号中,是从0开始 (0,1,2,3,4,…,n)的一组有序整数。如数组a[i],当i=0,1,2,3,…,n时,a[0],a[1],a[2],……,a[n]分别是数组a[i]的元素。数组中有一维、二维、三维和多维数组之分,常用的有一维、二维和字符数组。
1. 一维数组
1)一维数组的定义 数组只有一个下标,称为一维数组。在C语言中,使用数组之前需先对其进行定义。一维数组的定义方式如下:
类型说明符 数组名[常量表达式];
其中,类型说明符是任一种基本数据类型或构造数据类型 (如int,char等);数组名是用户定义的数组标识符,即合法的标识符;方括号中的常量表达式表示数据元素的个数,也称为数组的长度。例如:
unsigned int a[8]; //定义了含有8个元素的无符号整型数组a float b[10],c[16]; //定义了含有10个元素的实型数组b,含有16个元素的实型数组c unsigned char ch[20]; //定义了含有20个元素的字符数组ch
注意:
对于数组类型的定义,应注意以下几点。
(1)数组名的定义规则和变量名相同,应遵循标识符命名规则。在同一程序中,数组名不能重名,即不能与其他变量名相同。
(2)数组名后是用方括号括起来的常量表达式,不能用圆括号。
(3)方括号中常量表达式表示数组元素的个数,如a[10]表示数组a有10个元素。每个元素由不同的下标表示,数组中的下标是从0开始计算的,而不是从1开始计算。因此,a的10个元素分别为a[0],a[1],…,a[9]。注意,a[10]数组中并没有a[10]这个数组元素。
(4)常量表达式中可以包括常量和符号常量,不能包含变量。即C语言中数组元素个数不能在程序运行过程中根据变量值的不同而随机修改,数组的元素个数在程序编译阶段就已经确定了。
2)一维数组元素的引用 定义了一维数组之后,就可以引用这个一维数组中的任何元素,且只能逐个引用而不能一次引用整个数组的元素。引用数组元素的一般形式如下:
数组名[下标]
这种引用数组元素的方法称为“下标法”。C语言规定,以下标法使用数组元素时,下标可以越界,即下标可以不在0~ (长度-1)的范围内。如定义数组为a[3],能合法使用的数组元素是a[0]、a[1]、a[2],而a[3]、a[4]虽然也能使用,但由于下标越界,超出数组元素的范围,程序运行时可能会出现不可预料的结果。
例如,对10个元素的数组进行赋值时,必须使用循环语句逐个输出各个变量:
int i,a[10]; //定义变量i及含10 个元素的一维数组a for (i=0;i<10;i++) { a[i] =0; }
而不能采用类似于下面的方法用一个语句输出整个数组变量:
int i,a[10]; a=0;
3)一维数组的初始化 给数组赋值,除了用赋值语句对数组元素赋值外,还可以采用初始化赋值和动态赋值的方法。
数组初始化是指在定义数组的同时给数组元素赋值。虽然数组赋值可以在程序运行期间用赋值语句进行,但是这样将耗费大量的运行时间,尤其是对大型数组而言,这种情况更加突出。采用数组初始化的方式赋值时,由于数组初始化是在编译阶段进行的,这样将减少运行时间,提高效率。
一维数组初始化赋值的一般形式如下:
类型说明符 数组名[常量表达式] ={值,值,值,…,值};
其中,在“{}”中的各数据值即为各元素的初值,各值之间用逗号间隔。例如:
const unsigned char tab[8] ={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};
经过上述定义的初始化后,各个变量值为: tab[0] =0xfe;tab[1] =0xfd;tab[2] =0xfb;tab[3] =0xf7;tab[4] =0xef;tab[5] =0xdf;tab[6] =0xbf;tab[7] =0x7f。
C语言对一维数组元素的初始化赋值还有以下特例。
(1)只给一部分元素赋初值: 当“{}”中值的个数少于元素个数时,可以只给前面部分元素赋值。例如:
const unsigned char tab[10] ={0x00,0x00,0x07,0x02,0x02,0x02,0x7F};
在此语句中,定义了tab数组有10个元素,但“{}”内只提供了7个初值,这表示只给前面7个元素赋值,后面3个元素的初值为0。
(2)给全部元素赋相同值: 给全部元素赋相同值时,应在“{}”内将每个值都写上。例如:
int a[10] ={2,2,2,2,2,2,2,2,2,2};
而不能写为:
int a[10] =2;
(3)给全部元素赋值,但不给出数组元素的个数: 如果给全部元素赋值,则在数组说明中进行,可以不给出数组元素的个数。例如:
const unsigned char tab1[24] ={0x00,0x00,0x7F,0x1E,0x12,0x02,0x7F,0x00, 0x00,0x00,0x07,0x02,0x02,0x02,0x7F,0x00, 0x00,0x00,0x7F,0x1E,0x12,0x02,0x7F,0x00};
可以写为:
const unsigned char tab1[] ={0x00,0x00,0x7F,0x1E,0x12,0x02,0x7F,0x00, 0x00,0x00,0x07,0x02,0x02,0x02,0x7F,0x00, 0x00,0x00,0x7F,0x1E,0x12,0x02,0x7F,0x00};
由于数组tab1初始化时“{}”内有24个数,因此,系统自定义tab1的数组个数为24,并将这24个字符分配给24个数组元素。
2. 二维数组
1)二维数组的定义 C语言允许使用多维数组,最简单的多维数组就是二维数组。实际上,二维数组是以一维数组为元素构成的数组。二维数组的定义方式如下:
类型说明符 数组名[常量表达式1][常量表达式2];
其中,常量表达式1表示第1维下标的长度,常量表达式2表示第2维下标的长度。二维数组的存取顺序是: 按行存取,先存取第1行元素的第0列,1列,2列,…,直到第1行的最后一列;然后返回到第2行开始,再取第2行的第0列,1列,2列,…,直到第1行的最后一列。按此顺序下去,直到最后一行的最后一列。例如:
int a[4][6]
该语句定义了4行6列共24个元素的二维数组a[][],其存取顺序如下:
2)二维数组元素的引用 二维数组元素引用的一般形式为:
数组名[下标][下标]
其中,下标可以是整数,也可以是整数表达式。例如:
a[2][4] //表示a数组第2行第4列的元素 b[3-1][2*2-1] //不要写成a[2,3],也不要写成a[3-1,2*2-1] 的形式
在使用数组时,下标值应在已定义的数组大小范围之内,以避免越界错误。例如:
int a[3][4]; … a[3][4] =4; //定义a为3 ×4的数组,其行下标值最大为2,列坐标值最大为3, //而a[3][4]超过数组范围
3)二维数组的初始化 二维数组初始化也是在类型说明时给各下标变量赋以初值。对二维数组赋值时可以按以下方法进行。
(1)按行分段赋值: 按行分段赋值是将第1个“{}”内的数值赋给第1行的元素,第2个“{}”内的数值赋给第2行的元素,依次类推。采用这种方法比较直观,例如:
const unsigned char tab[3][4]={{0x00,0x00,0x7F,0x1E},{0x12,0x02,0x7F,0x00},{0x02,0x02,0x7F,0x00}};
(2)按行连续赋值: 按行连续赋值是将所有数据写在1个“{}”内,按数组排列的顺序对各个元素赋初值。例如:
const unsigned char tab[3][4] ={0x00,0x00,0x7F,0x1E,0x12,0x02,0x7F,0x00,0x02,0x02,0x7F,0x00};
从这段赋值可以看出,第 (2)种方法与第 (1)种方法完成相同的任务,都是定义同一个二维数组tab且赋相同的初始值,但是第 (2)种方法没有第 (1)种方法直观,如果二维数组需要赋的初始值比较多时,采用第 (2)种方法将会在“{}”内写一大片,容易遗漏,也不容易检查。
(3)对部分元素赋初值: 可以对二维数组的部分元素赋初值,未赋值的元素自动取“0”值。例如:
int a[3][4] ={{1},{3},{6}}; //二维数组a各元素的值为 { {1,0,0,0}, // {3,0,0,0},{6,0,0,0}} int b[3][4]={{2},{1,3},{2,4,3}}; //二维数组b各元素的值为 { {2,0, //0,0},{1,3,0,0},{2,4,3,0}} int c[3][4] ={{2},{3,5}}; //二维数组c各元素的值为 { {2,0,0,0}, // {3,5,0,0},{0,0,0,0}} int d[3][4] ={{1},{},{2,3,4}}; //二维数组d各元素的值为 { {1,0, //0,0},{0,0,0,0},{2,3,4,0}}
(4)元素赋初值时,可以不指定第1维的长度: 如果对全部元素都赋初始值,则定义数组时对第1维的长度可以不指定,但第2维的长度不能省略。例如:
int a[3][4] ={{1,2,3,4}{5,6,7,8}{9,10,11,12}};
与下面的定义等价:
int a[][4] ={{1,2,3,4}{5,6,7,8}{9,10,11,12}};
如果只对部分元素赋初始值,则定义数组时对第1维的长度可以不指定,但第2维的长度不能省略,且应分行赋初始值。例如:
int a[][4] ={{1,2,3},{},{5}};
该程序段定义了3行4列的二维数组,元素各初始值分别为{{1,2,3,0},{0,0,0,0},{5,0,0,0}}。
3. 字符数组
用来存放字符数据的数组称为字符数组。字符数组中一个元素存放一个字符,所以可以用字符数组来存放长度不同的字符串。
1)字符数组的定义 字符数组的定义与前面介绍的类似,即
(unsigned)char数组名[常量表达式];
例如:
char a[10]; //定义了包含10个元素的字符数组a
字符数组也可以是二维或多维数组,和数值型多维数组相同。例如:
char b[3][5]; //定义了3行5列共15个元素的二维字符数组b
2)字符数组的引用 字符数组的引用与数值型数组一样,只能按元素引用。
3)字符数组的初始化 字符数组和数值型数组一样,也允许在定义时进行初始化赋值。例如:
如果“{}”中提供的初值个数 (即字符个数)大于数组长度,C语言将作为语法错误处理。如果初值个数小于数组长度,则只将这些字符赋给数组中前面那些元素,其余的元素自动定义为空字符 (即“\ 0”)。对全体元素赋初值时,也可以省去长度。例如:
也可以写成:
4)字符串和字符串结束标志 字符串常量是由双引号括起来的一串字符。在C语言中,将字符串常量作为字符数组来处理。例如,在上例中就是用一个一维字符型数组来存放一个字符串常量“PIC”,这个字符串的实际长度与数组长度相等。如果字符串的实际长度与数组长度不相等,为了测定字符串的实际长度,C语言规定以字符“\ 0”作为字符串结束标志,也就是说,在遇到第1个字符“\ 0”时,表示字符串结束,由它前面的字符组成字符串。
在C语言中没有专门的字符串变量,通常用一个字符数组来存放一个字符串。将一个字符串存入一个数组时,也将结束符“\ 0”存入数组,并以此作为该字符串是否结束的标志。
如果将字符串直接给字符数组赋初值,可采用以下的两种方法:
unsigned char a[] ={"PIC"}; unsigned char a[] ="PIC";
3.4 指针
所谓指针就是在内存中的地址,它可能是变量的地址,也可能是函数的入口地址。如果指针变量存储的地址是变量的地址,称该指针为变量的指针 (或变量指针);如果指针变量存储的地址是函数的入口地址,称该指针为函数的指针 (或函数指针)。
1. 变量的指针和指向变量的指针变量
指针变量与变量指针的含义不同: 指针变量简称为指针,是指它是一个变量,且该变量是指针类型的;而变量指针是指它是一个变量的地址,该变量是指针类型的,且它存放另一个变量的地址。
1)指针变量的定义 在C语言中,所有的变量在使用之前必须定义,以确定其类型。指针变量也一样,由于它是用来专门存放地址的,因此必须将它定义为“指针类型”。指针定义的一般形式如下:
类型标识符 *指针变量名
其中,类型标识符就是本指针变量所指向的变量的数据类型;“*”表示这是一个指针变量;指针变量名就是指针变量的名称。例如:
int *ap1 //定义整型指针变量ap1 char *ap2,*ap3 //定义了两个字符型指针变量ap2和ap3 float *ap4 //定义了实数型指针变量ap4
注意:
在定义指针变量时要注意以下两点。
(1)指针变量名前的“*”表示该变量为指针变量,在上例中的指针变量名为ap1、ap2、ap3、ap4,而不是*ap1、*ap2、*ap3、*ap4,这与定义变量有所不同。
(2)一个指针变量只能指向同一个类型的变量,在上例中的ap1只能指向整型变量,而不能指向字符型或实数型指针变量。
2)指针变量的引用 指针变量在使用之前也要先定义说明,然后赋予具体的值。指针变量的赋值只能赋予地址,而不能赋予任何其他数据,否则将引起错误。在C语言中,变量的地址由编译系统分配,用户不知道变量的具体地址。
有两个有关的运算符“&”和“*”,其中,“&”为取地址运算符;“*”为指针运算符 (或称“间接访问”运算符)。例如:
在C语言中,指针变量的引用是通过取地址运算符“&”来实现的。使用取地址运算符“&”和赋值运算符“=”就可以使一个指针变量指向一个变量。
例如,指针变量p所对应的内存地址单元中装入了变量x所对应的内存单元地址,可使用以下程序段实现:
int x; //定义整型变量x int *p=&x; //指针变量声明的时候初始化
还可以采用以下程序段实现:
int x; //定义整型变量x int *p; //定义整型指针变量p p=&x; //用赋值语句对指针赋值
2. 数组指针和指向数组的指针变量
指针既然可以指向变量,当然也可以指向数组。所谓数组的指针是指数组的起始地址,数组元素的指针是指数组元素的地址。若有一个变量用来存放一个数组的起始地址 (指针),则称它为指向数组的指针变量。
1)指向数组元素的指针变量定义与赋值 定义一个指向数组元素的指针变量的方法与指针变量的定义相同。例如:
int x[6]; //定义含有6个整型数据的数组 int *p; //定义指向整型数据的指针p p=&x[0]; //对指针p赋值,此时数组x[5]的第1个元素x[0]的地址就赋给了指针变量p p=x; //对指针p赋值,此种引用方法与“p=&x[0];”的作用完全相同,但形式上更简单
在C语言中,数组名代表数组的首地址,也就是第0号元素的地址。因此语句“P=&x [0];”和“P=x;”是等价的。还可以在定义指针变量时赋给初值:
int *p=&x[0];//或者int *p=x;
等价于:
int * p; p=&x[0];
2)通过指针引用数组元素 如果p指向一个一维数组x[6],并且p已给它赋予了一个初值&x[0],可以使用以下3种方法引用数组元素。
【下标法】 C语言规定,如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素。P +i和x+i,就是a[i],或者说它们都指向x数组的第i个元素。
【地址法】 *(p+i)和*(x+i)也就是x[i]。实际上,编译器对数组元素x[i]就是处理成*(x+i),即按数组的首地址加上相对位移量得到要找元素的地址,然后找出该单元中的内容。
【指针法】 用间接访问的方法来访问数组元素,指向数组的指针变量也可以带下标,如p[i]与*(p+i)等价。
3)关于指针变量的运算 如果先使指针变量p指向数组x[] (即p=x;),则
(1)p++ (或p+=1): 该操作将使指针变量p指向下一个数组元素,即x[1]。若再执行x=*p,则将取出x[1]的值,将其赋给变量x。
(2)*p++: 由于++与*运算符优先级相同,而结合方向为自右向左,因此*p++等价于*(p++),其作用是先得到p指向的变量的值 (即*p),然后再执行p自加运算。
(3)*p++与*++p作用不同: *p++是先取*p值,然后使p自加1;而*++p是先使p自加1,再取*p值。如果p的初值为&x[0],则执行a=*p++时,a的值为x[0]的值;而执行*++p后,a的值等于x[1]的值。
(4)(*p)++: (*p)++表示p所指向的元素值加1。
注意:
是元素值加1,而不是指针变量值加1。如果指针变量p指向&x[0],且x[0] =4,则(*p)++等价于(a[0])++。此时,x[0]的值增为5。
(5)如果p当前指向数组中第i个元素,那么存在以下3种关系:
(p--)与x [i--] 等价,相当于先执行*p,然后再使p自减;
(++p)与x [ ++i] 等价,相当于先执行自加,然后再执行*p运算;
(--p)与x [ --i] 等价,相当于先执行自减,然后再执行*p运算;
3. 字符串指针和指向字符串的指针变量
1)字符串指针和指向字符串指针的表示形式 在C语言中有两种方法实现一个字符串运算: 一种是使用字符数组来实现;另一种是用字符串指针来实现。例如:
//使用字符 //数组定义 char *b="PIC"; //使用字符 //串指针定义
字符串指针变量的定义说明与指向字符变量的指针变量说明是相同的。在上述程序段中,a[]是一个字符数组,字符数组是以“\ 0”常量结尾的;b是指向字符串的指针,它没有定义字符数组,由于C语言对字符串常量是按字符数组处理的,实际在使用字符串指针时,C编译器也在内存中开辟了一个字符数组用来存放字符串常量。
2)使用字符串指针变量与字符数组的区别 用字符数组和字符串指针变量都可实现字符串的存储和运算,但两者是有区别的,在使用时应注意以下几个问题。
字符串指针变量本身是一个变量,用于存放字符串的首地址。而字符串本身是存放在以该首地址为首的一块连续的内存空间中并以“\ 0”作为结束的串。字符数组是由若干个数组元素组成的,它可用来存放整个字符串。
定义一个字符数组时,在编译中即已分配内存单元,有确定的地址。而定义一个字符指针变量时,给指针变量分配内存单元,但该指针变量具体指向哪个字符串并不知道,即指针变量存放的地址不确定。
赋值方式不同。对字符数组不能整体赋值,只能转化成分量,对单个元素进行。而字符串指针变量赋值可整体进行,直接将其指向字符串首地址即可。
字符串指针变量的值在程序运行过程中可以改变,而字符数组名是一个常量,不能改变。
3.5 结构体
在一些复杂的系统程序中,仅有一些基本类型 (如字符型、整型和浮点型等)的数据是不够的,有时需要将各种类型的变量放在一起,形成一个组合形变量,即结构体变量(structure,又称为结构或结构体)。
结构体 (structure)是C语言应用比较多的一种数据结构,它可以有效地将各种数据(包括各种不同类型的数据)整合到一个数据体中,可以更好地实现程序的结构化,更方便地管理数据和对数据进行操作。
在嵌入式系统开发中,一方面由于系统资源的严重不足,另一方面各种变量相互通信、相互作用,正确合理使用结构体不仅可以为系统节约一部分宝贵的资源,而且还可以简化程序的设计,使软件设计的可读性和可维护性都大大增强。
1. 结构体的定义和引用
结构体的定义和引用主要有以下三个步骤。
1)定义结构体的一般形式 结构体是一种构造类型,它由若干成员组成,每一成员可以是一个基本数据类型或者又是一个构造类型。定义一个结构类型的一般形式为:
struct 结构体名 { 结构体成员说明 };
结构体成员说明用来描述结构体中由哪些成员组成,且每个成员必须作类型说明。结构体成员说明的格式为:
类型标识符 成员名;
成员名的命名应符合标识符的命名规则,在同一结构体中不同分量不允许使用相同的名字。例如,定义一个名为stu的结构类型:
struct stu { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 };
在上述定义中,“struct stu”表示这是一个结构体类型,结构体名为“stu”。在该结构体中包含7个结构体成员: int num、char name[30]、int age、long number、char sex、float secore[7]和char address[50]。这7个结构体成员中,第1个和第3个成员为整型变量;第2个和最后一个成员为字符数组;第4个为长整型变量;第5个为字符变量;第6个为浮点型数组。
注意:
struct stu是程序开发人员自己定义的结构类型,它和系统定义的标准类型(如int、char和float等)一样可以用来定义变量的类型。
2)定义结构体类型变量 上面定义的struct stu只是结构体的类型名,而不是结构体的变量名。为了正常执行结构体的操作,除了定义结构体的类型名外,还需要进一步定义该结构类型的变量名。定义一个结构体的变量名时,可采用以下3种方法进行。
【方法1】 先定义结构体,再声明结构体变量。这种形式的定义格式如下:
struct结构体名 { 结构体成员说明 };
定义好一个结构体后,就可以用它来定义结构体变量。一般格式如下:
struct结构体名 变量名1,变量名2,变量名3,…,变量名n;
例如:
struct stu { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 }; struct stu student1,student2;//定义结构体类型变量student1和student2 struct stu student3,student4;//定义结构体类型变量student3和student4
上例中,在定义了结构的类型struct stu之后,使用“struct stu student1,student2”和“struct stu student3,student4”定义了student1、student2、student3和student4为struct stu类型的结构体变量。
【方法2】 在定义结构体类型的同时定义该结构体变量。这种形式的定义格式如下:
struct结构体名 { 结构体成员说明 }变量名1,变量名2,变量名3,…,变量名n;
例如:
struct stu { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 } student1,student2,student3,student4;
也可以再定义更多的该结构体类型变量:
struct stu student5,student6;
【方法3】 直接定义结构体类型变量。这种形式的定义格式如下:
struct { 结构体成员说明 }变量名1,变量名2,变量名3,…,变量名n;
例如:
struct { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 } student1,student2,student3,student4;
在上述3种方法中,都声明了student1、student2、student3和student4这4种变量,这些变量的类型完全相同,其中方法3与方法2的区别在于: 方法3中省去了结构体名,而直接给出了结构体变量。
说明
(1)结构体类型和结构体变量是两个不同的概念,对于一个结构体变量而言,在定义时一般先定义一个结构体类型,然后再定义该结构体变量为该种结构体类型。
(2)在定义一个结构体类型时,结构体名不占用任何存储空间,也不能对结构体名进行赋值、存取和运算,只是给出该结构的组织形式。结构体变量是一个结构体中的具体组织成员,编译器会给该结构体变量分配确定的存储空间,所以可以对结构体变量名进行赋值、存取和运算。
(3)结构体的成员也可以是一个结构变量,它可以单独使用,其作用与地位相当于普通变量。
(4)结构体成员可以与程序中的其他变量名相同,但两者表示不同的含义。
(5)结构体可以嵌套使用,一个结构体中允许包含另一个结构体。
(6)如果在程序中使用的结构体数目较多、规模较大,可以将它们集中定义在一个头文件里,然后用宏指令“#include”将该头文件包含在需要它们的源文件中。
3)结构体类型变量的引用 定义了一个结构体变量后,就可以对它进行引用,即对其进行赋值、存取和运算。一般情况下,结构体变量的引用是通过对其成员的引用来实现的。结构体变量成员引用的一般格式如下:
结构体变量名. 成员名;
其中,“.” 是存取成员的运算符。例如:
student1.num=2010003; //学生1的学号 student1.age=12; //学生1的年龄
对结构体变量进行引用时,还应遵循以下规则。
(1)结构体不能作为一个整体参加赋值、存取和运算,也不能整体作为函数参数或函数的返回值。对结构体所执行的操作,只能用“&”运算符取结构体的地址,或对结构体变量的成员分别加以引用。
(2)如果一个结构体变量中的成员又是另一个结构体变量,即出现结构体嵌套时,则需要采用若干个成员运算符,一级一级地找到最低一级的成员,而且只能对这个最低级的结构元素进行存取访问。例如:
student1.birthday.month=12; //学生1的生日月份(嵌套结构)
注意:
在此例中不能用student1.birthday. 来访问student1变量的成员birthday,因为birthday本身也是一个结构体类型变量。
(3)结构体类型变量的成员可以像普通变量一样进行各种运算。例如:
student1.age++;
2. 结构体的初始化
和其他类型的变量一样,对结构体类型的变量也可以在定义时赋初值进行初始化。例如:
struct { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 }; struct stu student1={2010003,"LiPing",12,43010119981203126′,M′,{89,86,95,90,77,94,68}, "湖南长沙"}; struct stu student2 = {2010004,"WangQian",11,43010119990906123′,W′,{80,86,95,87,79,96, 77},"湖南长沙"}; struct stu student3 ={2010005,"TianMinQin",14,43010119960725122′,M′,{79,80,65,83,77,94, 78},"湖南长沙"}; struct stu student4 = {2010006,"ChenLei",13,43010119971116127′,W′,{89,84,59,87,68,91, 65},"湖南长沙"};
3. 结构体数组
如果数组中每个元素都具有相同结构类型的结构体变量,则称该数组为结构体数组。结构体数组与变量数组的不同在于: 结构体数组的每一个元素都是具有同一个结构体类型的结构体变量;它们都具有同一个结构体类型,都含有相同的成员项。
1)结构体数组的定义 结构体数组的定义和结构体变量的定义方法类似,只需说明其为数组即可。例如:
struct stu { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 }; struct stu student[4];
以上定义了一个数组student,其元素为struct stu类型数据,数组有4个元素。也可直接定义一个结构体数组,如:
struct stu { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 } student[4]; 或 struct { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 } student[4];
2)结构体数组的初始化 结构体数组也可以在定义时赋初值进行初始化。例如:
struct stu { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 }student[4] ={{2010003,"LiPing",12,43010119981203126′,M′,{89,86,95,90,77,94,68},"湖 南长沙"},{2010004,"WangQian",11,43010119990906123′,W′,{80,86,95,87,79,96,77}""湖南 长沙"},struct stu student3={2010005,"TianMinQin",14,43010119960725122′,M′,{79,80,65,83, 77,94,78},"湖南长沙"},struct stu student4 ={2010006,"ChenLei",13,43010119971116127′,W′, {89,84,59,87,68,91,65},"湖南长沙"}};
4. 指向结构体类型数据的指针
一个结构体变量的指针就是该变量所占据的内存中的起始地址。可以设一个指针变量,用来指向一个结构体数组,此时该指针变量的值就是结构体数组的起始地址。
1)指向结构体变量的指针 当一个指针变量用来指向一个结构体变量时,称之为结构体指针变量。结构体指针与数组指针、函数指针的情况相同,它的值是所指向的结构变量的首地址。通过结构体指针可以访问该结构体变量。指向结构体变量的指针变量的一般形式为:
struct 结构体类型名 *指针变量名;
或者
struct { 结构体成员说明 }*指针变量名;
例如:
struct stu { int num; //定义学生的学号 char name[30]; //定义学生的姓名 int age; //定义学生的年龄 long number; //定义学生的身份证号码 char sex; //定义学生性别 float secore[7]; //定义学生7科考试成绩 char address[50]; //定义学生家庭地址 }; struct stu *person;
在上述例子中定义了一个stu结构体的指针变量person。结构体指针变量在使用之前必须先对其赋初值。赋值是将结构体变量的首地址赋予该指针变量,不能将结构体名赋予该指针变量。
2)指向结构体数组的指针 指针变量可以指向数组,同样指针变量也可以指向结构体数组及其元素。指向结构体数组的指针变量的一般形式如下:
struct结构体数组名 *结构体数组指针变量名;
或者
struct { 结构体成员说明; }*结构体数组指针变量名[];
3.6 共用体
所谓共用体 (或称为联合,union)是指将不同的数据项组织成一个整体,它们在内存中占用同一段存储单元。共用体类型也是用来描述类型不同的数据,但与结构体类型不同,共用体数据成员存储时采用覆盖技术,共享 (部分)存储空间。
1. 共用体类型变量的定义
共用体类型变量的定义与结构体类型变量的定义类似,也可采用3种方法。
【方法1】 先定义共用体类型,再定义变量名。这种形式的定义格式如下:
union结构体名 { 共用体成员说明; };
定义好一个共用体类型后,就可以用它来定义共用体变量。一般格式如下:
union共用体名 变量名1,变量名2,变量名3,…,变量名n;
例如:
union data { int i; char ch; float f; }; union a,b,c; //定义共用体变量a、b、c
【方法2】 在定义共用体类型的同时定义共用体变量名。这种形式的定义格式如下:
union结构体名 { 共用体成员说明; }变量名1,变量名2,变量名3,…,变量名n;
例如:
union data { int i; char ch; float f; } a,b,c; //定义共用体变量a、b、c
【方法3】 直接定义共用体变量。这种形式的定义格式如下:
union { 共用体成员说明; }变量名1,变量名2,变量名3,…,变量名n;
例如:
union { int i; char ch; float f; } a,b,c; //定义共用体变量a、 b、 c
说明
(1)同一个内存中可以用来存放几种不同类型的成员,但是在每一瞬间只能存放其中的一种,而不是同时存放几种。换句话说,每一瞬间只有一个成员起作用,其他的成员不起作用,即不是同时都存在和起作用。
(2)共用体变量中起作用的成员是最后一次存放的成员,在存入一个新成员后,原有成员就失去作用。
(3)共用体变量的地址和其各成员的地址都是同一地址。
(4)不能对共用体变量名赋值,也不能企图引用变量名来得到一个值,并且,不能在定义共用体变量时对它进行初始化。
(5)不能把共用体变量作为函数参数,也不能使函数带回共用体变量,但可以使用指向共用体变量的指针。
(6)共用体类型可以出现在结构体类型的定义中,也可以定义共用体数组。反之,结构体也可以出现在共用体类型的定义中,数组也可以作为共用体的成员。
2. 共用体变量的引用
只有先定义了共用体变量才能在后续程序中引用它。
注意:
不能引用共用体变量,而只能引用共用体变量中的成员。
共用体变量成员引用的一般格式如下:
共用体变量名. 成员名;
例如:
a.i=15; //引用共用体变量a中的整型变量i a.f=1.35; //引用共用体变量a中的实数型变量f
3.7 函数
C语言是由函数构成的,函数是C语言中的一种基本模块。一个较大的程序通常由多个程序模块组成,每个模块用来实现一个特定的功能,在程序设计中模块的功能是用子程序来实现的。在C语言中,子程序的作用是由函数来完成的。一个C程序由一个主函数和若干个其他函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次,同一工程中的函数也可以分放在不同文件中一起编译。
从使用者的角度来看,有两种函数: 标准库函数和用户自定义函数。标准库函数是由C编译系统的函数库提供的,用户不需要自己定义这些函数,可以直接使用它们;用户自定义函数是由用户根据自己的需要编写的函数,用来解决用户的专门需要。
从函数的形式看,有三种函数: 无参函数、有参函数和空函数。无参函数被调用时,主调函数并不将数据传送给被调用函数,一般用来执行指定的一组操作。无参函数可以带回或不带回函数值,但一般以不带回函数值的居多。有参函数被调用时,在主调函数和被调用函数之间有参数传递,即主调函数可以将数据传给被调用函数使用,被调用函数中的数据也可以带回来供主调函数使用。空函数的函数体内无语句,为空白的。调用空函数时,什么工作都不做,不起任何作用。定义空函数的目的不是为了执行某种操作,而是为了以后程序功能的扩充。
1. 函数定义的一般形式
1)无参函数的定义形式 无参函数的定义形式如下:
返回值类型标识符 函数名() { 函数体语句 }
其中,返回值类型标识符指明本函数返回值的类型;函数名是由用户定义的标识符;“()”内没有参数,但该括号不能少,或者括号里加“void”关键字;“{}”中的内容称为函数体语句。在很多情况下,无参函数没有返回值,所以函数返回值类型标识符可以省略,此时函数类返回值标识符可以写为“void”。例如:
void Timer0_Iint(void) //Timer0初始化函数 { T0CS=1; PSA=1; TMR0=0; T0IF=0; T0IE=1; GIE=1; TRISD=0; }
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类型。例如:
viod abc(); //函数abc()为不带返回值的函数
3. 函数的调用
在C语言程序中,函数可以相互调用,所谓函数调用就是在一个函数体中引用另外一个已经定义了的函数,前者称为主调函数,后者称为被调函数。
1)函数调用的一般形式 在C语言中,主调函数通过函数调用语句来使用函数。函数调用的一般形式如下:
函数名 (实参列表);
对于有参数型的函数,如果包含了多个实参,应将各参数之间用逗号分隔开。主调用函数的实参数目与被调用函数的形参数目应该相等,且类型保持一致。实参与形参按顺序对应,一一传递数据。
如果调用的是无参函数,则实参表可以省略,但是函数名后面必须有一对空括号。
2)函数调用的方式 在C语言中,主调用函数调用被调函数可以采用以下3种调用方式。
【函数语句调用】 在主调用函数中将函数调用作为一条语句,并不要求被调用函数返回结果数值,只要求函数完成某种操作,例如:
disp LED(); //无参调用,不要求被调函数返回一个确定的值,只要求此函数完成LED显 //示操作
【函数表达式调用】 函数作为表达式的一项出现在表达式中,要求被调用函数带有return语句,以便返回一个明确的数值参加表达式的运算。例如:
a=3*min(x,y); //被调用函数min作为表达式的一部分,它的返回值乘以3再赋给a
【作为函数参数调用】 在主调函数中将函数调用作为另一个函数调用的实参。例如:
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)函数的嵌套调用与递归调用
【函数的嵌套调用】 在C语言中,函数的定义都是相互独立的,不允许在定义函数时,一个函数内部包含另一个函数。虽然在C语言中函数不能嵌套定义,但可以嵌套调用函数。嵌套调用函数是指在一个函数内调用另一个函数,即在被调用函数中又调用其他函数。
在PIC编译器中,函数间的调用及数据保存与恢复是通过硬件堆栈和软件堆栈来实现的。当没有使用外部数据存储器时,硬件堆栈和软件堆栈均在内部数据存储器中;当有外部存储器时,硬件堆栈在内部数据存储器中,软件堆栈则在外部数据存储器中。在PIC编译器中,嵌套层数只受到硬件堆栈和软件堆栈的限制,如果嵌套层数太深,有可能导致硬件或软件堆栈溢出。
【函数的递归调用】 在调用一个函数的过程中又出现直接或间接调用该函数本身,称为函数的递归调用。在C语言中,允许函数递归调用。函数的递归调用通常用于问题的求解,可以将一种解法逐次地用于问题的子集表示的场合。PIC编译器能够自动处理函数递归调用的问题,在递归调用时不必作任何声明,调用深度仅受到堆栈大小的限制。
4. 数组、指针作为函数的参数
C语言规定,数组、指针均可作为函数的参数使用,进行数据传递。
1)数组作为函数参数 在C语言中,可以用数组元素或者整个数组作为函数的参数。
注意:
用数组作为函数的参数时,需要注意以下几点。
(1)当用数组名作函数的参数时,应该在调用函数和被调用函数中分别定义数组。
(2)实参数组与形参数组类型应一致,如果不一致会出错。
(3)实参数组和形参数组大小可以一致,也可以不一致。C编译器对形参数组大小不作检查,只是将实参数组的首地址传给形参数组。如果要求形参数组得到实参数组全部的元素值,则应当指定形参数组与实参数组大小一致。
2)用指向函数的指针变量作为函数的参数 函数在编译时分配一个入口地址 (函数首地址),这个入口地址赋予一个指针变量,使该指针变量指向该函数,然后通过指针变量就可以调用这个函数,这种指向函数的指针变量称为函数指针变量。
指针变量可以指向变量、字符串和数组,同样指针变量也可以指向一个函数,即可以用函数的指针变量来调用函数。函数指针变量常用的功能之一是将指针作为参数传递给其他函数。函数指针变量的一般形式如下:
函数值返回类型 (*指针变量名)(函数形参列表);
其中,函数值返回类型表示被指函数的返回值类型;(*指针变量名)表示定义的指针变量名;(函数形参列表)表示该指针是一个指向函数的指针。
调用函数的一般形式为:
(*指针变量名)(函数形参列表)
注意:
函数指针变量不能进行算术运算,即不能移动函数指针变量。
3)用指向结构的指针变量作为函数的参数 C语言不允许整体引用结构体变量名,如果要将一个结构体变量的值从一个函数传递给另一个函数,可采用以下3种方法。
(1)像用变量作为函数的参数一样,直接引用结构体变量的成员作参数。
(2)用结构体作为函数的参数,采用这种方式必须保证实参与形参的类型相同,属于“值传递”。在单片机中,这些操作是通过入栈和出栈来实现的,会增加系统的处理时间,影响程序的执行效率,并且还需要较大的数据存储空间。
(3)用指向结构体变量的成员作参数,将实参值传给形参,其用法和普通变量作实参一样,也属于“值传递”方式。
4)返回指针值的函数 一个函数可以返回一个整型值、字符值和浮点值,同样也可以返回指针类型的数据,即返回一个数据的地址。返回指针值的函数的一般定义形式为:
返回值类型 *函数名 (参数表)
3.8 编译预处理
编译预处理是C语言编译系统的一个重要组成部分。很好地利用C语言的预处理命令可以增强代码的可读性、灵活性和易于修改等特点,便于程序的结构化。在C语言程序中,凡是以“#”开头的均表示这是一条预处理命令语句,如包含命令#include、宏定义命令#define等。C语言提供的预处理功能有3种: 宏定义、文件包含和条件编译。
1. 宏定义
宏定义命令为#define,它的作用是实现用一个简单易读的字符串来代替另一个字符串。宏定义可以增强程序的可读性和维护性。宏定义分为不带参数的宏定义和带参数的宏定义。
1)不带参数的宏定义 不带参数的宏定义,其宏名后不带参数。不带参数宏定义的一般形式为:
#define 标识符 字符串
其中,#表示这是一条预处理命令;define表示为宏定义命令;标识符为所定义的宏名;字符串可以是常数、表达式等。例如:
#define PI 3.1415926
它的作用是指定用标识符 (即宏名)PI代替“3.1415926”字符串,这种方法使用户能以一个简单的标识符代替一个长的字符串。当程序中出现3.1415926这个常数时,就可以用PI这个字符代替,如果想修改这个常数,只需修改这个宏定义中的常数即可,这就是增加程序的维护性的体现。
说明
(1)宏定义是用宏名代替一个字符串,在宏展开时又以该字符串取代宏名,它是一种简单的替换。通过这种宏定义的方法,可以减少程序中重复书写某些字符串的工作量。字符串中可以包含任何字符、常数或表达式,预处理程序对它不作任何检查。
(2)宏名可以用大写或小写字母表示,但为了区别于一般的变量名,通常采用大写字母。
(3)宏定义不是C语句,不用加分号;如果加分号,则在编译时连同分号一起转换。
(4)当宏定义在一行中书写不下,需要在下一行继续写时,应该在最后一个字符后紧跟着加一个反斜线“\”,并在新的一行的起始位置继续书写,起始位置不能插入空格。
(5)可以用#undef终止一个宏定义的作用域。
(6)一个宏命令只能定义一个宏名。
2)带参数的宏定义 带参数的宏在预编译时不但要进行字符串替换,还要进行参数替换。带参数宏定义的一般形式为:
#define 宏名 (形参表)字符串
带参数的宏调用的一般形式为:
宏名 (实参表);
例如:
#define MIN(x,y) ((x)<(y))? (x):(y)) //宏定义 a=MIN(3,7) //宏调用
说明
(1)带参数的宏定义中,宏名和形参表之间不能有空格出现,否则将空格以后的字符都作为替换字符串的一部分。
(2)在宏定义中,字符串内的形参最好用“()”括起来以避免出错。
(3)带参数的宏与函数是不同的:①函数调用时,先求出表达式的值,然后代入形参,而使用带参数的宏只是进行简单的字符替换,在宏展开时并不求解表达式的值;②函数调用是在程序运行时处理的,分配临时的内存单元,而使用带参数的宏只是在编译时进行,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念;③对函数中的实参和形参都要定义类型,二者的类型要求一致,如果不一致,应进行类型转换,而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号而已,展开时代入指定的字符即可;④调用函数只能得到一个返回值,而用宏可以设法得到若干个结果。
2. 文件包含
所谓文件包含处理是指一个源文件可以将另外一个源文件的全部内容包含进来,即将另外的文件包含到本文件中。C语言中#include为文件包含命令,其一般形式为:
#include <文件名>
或
#include "文件名"
例如:
#include <mega16.h> #include <stdio.h> #include <delay.h>
上述程序文件包含命令的功能是将“mega16.h”文件插入到#include <mega16.h>命令行位置;“stdio.h”文件插入到#include <stdio.h>命令行位置;“delay.h”文件插入到#include <delay.h>命令行位置。即在编译预处理时,源程序将“mega16.h”、“stdio.h”和“delay.h”这3个文件的全部内容复制并分别插入到#include < mega16.h >、#include<stdio.h>、#include <delay.h>命令行位置。
在程序设计中,文件包含是很有用的。它可以节省程序设计人员的重复工作,或者可以将一个大的程序分为多个源文件,由多个编程人员分别编写程序,然后再用文件包含命令把源文件包含到主文件中。
注意:
(1)在#include命令中,文件名可以用双引号或尖括号的形式括起来,但这两种形式有所区别:采用双引号将文件名括起来时,系统首先在引用被包含文件的源文件所在的C文件目录中寻找要包含的文件,如果找不到,再按系统指定的标准方式搜索\inc目录;使用尖括号将文件名括起来时,不检查源文件所在的文件目录而直接按系统指定的标准方式搜索\inc目录。
(2)一个#include命令只能指定一个被包含文件,如果要包含多个文件,则需要用多个#include命令。
(3)#include命令行包含的文件称为头文件。头文件名可以由用户指定,也可以是系统头文件,其后缀名为.h。
(4)在一个被包含的文件中同时又可以包含另一个文件,即文件包含可以嵌套。通常,嵌套有深度的限制,这种限制根据编译器的不同而不同。在PIC C编译器中,最多允许16层文件的嵌套。
(5)当被包含文件修改后,对包含该文件的源程序必须重新编译。
(6)#include语句可以位于代码的任何位置,但它通常设置在程序模块的顶部,以提高程序的可读性。
3. 条件编译
通常情况下,在编译器中单击文件编译时,将会对源程序中所有的行都进行编译 (注释行除外)。如果程序员只想源程序中的部分内容在满足一定条件时才进行编译,可通过条件编译对一部分内容指定编译的条件来实现相应操作。条件编译命令有以下3种形式。
【形式1】
#ifdef 标识符 程序段1 #else 程序段2 #endif
作用: 当标识符已经被定义过 (通常是用#define命令定义)时,对程序段1编译,否则编译程序段2;如果没有程序段2,本格式中的“#else”可以没有。此程序段1可以是语句组,也可以是命令行。
【形式2】
#ifndef 标识符 程序段1 #else 程序段2 #endif
作用: 当标识符没有被定义时,对程序段1编译,否则编译程序段2。这种形式的作用与第1种形式的作用正好相反,在书写上也只是将第1种形式中的“#ifdef”改为“#ifndef”。
【形式3】
#if 常量表达式 程序段1 #else 程序段2 #endif
作用: 如果常量表达式的值为逻辑“真”,则对程序段1进行编译,否则编译程序段2。可以事先给定一定的条件,使程序在不同的条件下执行不同的功能。