第2章 Cx51程序设计基础

Keil Cx51是一种专为8051单片机设计的高级语言C编译器,支持符合ANSI标准的C语言进行程序设计,同时针对8051单片机自身特点作了一些特殊扩展。为了帮助以前惯于使用汇编语言编程的单片机用户尽快掌握Cx51编程技术,本章对C语言的一些基本知识结合Cx51特点进行阐述。

2.1 标识符与关键字

C语言的标识符是用来标识源程序中某个对象名字的。这些对象可以是函数、变量、常量、数组、数据类型、存储方式、语句等。一个标识符由字母、数字和下划线等组成,第一个字符必须是字母或下划线。C语言是对大小写字母敏感的,如“max”与“MAX”是两个完全不同的标识符。程序中对于标识符的命名应当简洁明了,含义清晰,便于阅读理解,如用标识符“max”表示最大值,用“TIMER0”表示定时器0等。

关键字是一类具有固定名称和特定含义的特殊标识符,有时又称为保留字。在编写C语言源程序时一般不允许将关键字另作他用,换句话说就是对于标识符的命名不要与关键字相同。与其他计算机语言相比,C语言的关键字是比较少的,ANSI C标准一共规定了32个关键字,表2-1按用途列出了ANSI C标准的关键字。

表2-1 ANSI C标准的关键字

Keil Cx51编译器除了支持ANSI C标准的关键字以外,还根据8051单片机自身特点扩展了如表2-2所示的关键字。

表2-2 Keil Cx51编译器的扩展关键字

2.2 Cx51程序设计的基本语法

虽然C语言对语法的限制不太严格,用户在编写程序时有较大的自由,但它毕竟还是一种程序设计语言,与其他计算机语言一样,采用C语言进行程序设计时,仍需要遵从一定的语法规则。

2.2.1 数据类型

任何程序设计都离不开对于数据的处理,一个程序如果没有数据,它就无法工作。数据在计算机内存中的存放情况由数据结构决定。C语言的数据结构是以数据类型出现的,数据类型可分为基本数据类型和复杂数据类型,复杂数据类型由基本数据类型构造而成。C语言中的基本数据类型有char,int,short,long,float和double。对于Cx51编译器来说,short类型与int类型相同,double类型与float类型相同。

1.char:字符型

有signed char(带符号数)和unsigned char(无符号数)之分,默认值为signed char。它们的长度均为一个字节,用于存放一个单字节的数据。对于singed char类型数据,其字节中的最高位表示该数据的符号,“0”表示正数,“1”表示负数。负数用补码表示,数值的表示范围是-128~+127;对于unsigned char类型数据,其字节中的所有位均用来表示数据的数值,数值的表示范围是0~255。

2.int:整型

有signed int和unsigned int之分,默认值为signed int。它们的长度均为两个字节,用于存放一个双字节的数据。signed int是有符号整型数,字节中的最高位表示数据的符号,“0”表示正数,“1”表示负数。所能表示的数值范围是-32768~+32767。unsigned int是无符号整型数,所能表示的数值范围是0~65535。

3.long:长整型

有signed long和unsigned long之分,默认值为signed long。它们的长度均为四个字节。signed long是有符号的长整型数据,字节中的最高位表示数据的符号,“0”表示正数,“1”表示负数,数值的表示范围是-2147 483648~+2147 483647。unsigned long是无符号长整型数据,数值的表示范围是0~4294 967295。

4.float:浮点型

它是符合IEEE-754标准的单精度浮点型数据,在十进制数中具有7位有效数字。float类型数据占用四个字节(32位二进制数),在内存中的存放格式如下:

其中,S为符号位,“0”表示正,“1”表示负。E为阶码,占用8位二进制数,存放在两个字节中。注意,阶码E值是以2为底的指数再加上偏移量127,这样处理的目的是为了避免出现负的阶码值,而指数是可正可负的。阶码E的正常取值范围是1~254,从而实际指数的取值范围为-126~+127。M为尾数的小数部分,用23位二进制数表示,存放在三个字节中。尾数的整数部分永远为1,因此不予保存,但它是隐含存在的。小数点位于隐含的整数位“1”的后面。一个浮点数的数值范围是(-1)S×2E-127×(1.M)。

例如,浮点数-12.5=0xC1480000,在内存中的存放格式为:

需要指出的是,对于浮点型数据除了有正常数值之外,还可能出现非正常数值。根据IEEE标准,当浮点型数据取以下数值(16进制数)时即为非正常值:

0xFFFFFFFF非数(NaN)

0x7F800000正溢出(+INF)

0xFF800000负溢出(-INF)

另外,由于8051单片机不包括捕获浮点运算错误的中断向量,因此必须由用户自己根据可能出现的错误条件用软件来进行适当的处理。

除了以上四种基本数据类型之外,还有以下一些数据类型。

5.*:指针型

指针型数据不同于以上四种基本数据类型,它本身是一个变量,但在这个变量中存放的不是普通数据而是指向另一个数据的地址。指针变量也要占据一定的内存单元,在C51中指针变量的长度一般为1~3个字节。指针变量也具有类型,其表示方法是在指针符号“*”的前面冠以数据类型符号,如char * point1表示point1是一个字符型的指针变量;float* point2表示point2是一个浮点型的指针变量。指针变量的类型表示该指针所指向地址中数据的类型。使用指针型变量可以方便地对8051单片机各部分物理地址直接进行操作。

6.bit:位类型

这是Keil Cx51编译器的一种扩充数据类型,利用它可定义一个位变量,但不能定义位指针,也不能定义位数组。

7.sfr:特殊功能寄存器

这也是Keil Cx51编译器的一种扩充数据类型,利用它可以定义8051单片机的所有内部8位特殊功能寄存器。sfr型数据占用一个内存单元,其取值范围是0~255。

8.sfr16:16位特殊功能寄存器

它占用两个内存单元,取值范围是0~65535,利用它可以定义8051单片机内部16位特殊功能寄存器。

9.sbit:可寻址位

这也是Keil Cx51编译器的一种扩充数据类型,利用它可以定义8051单片机内部RAM中的可寻址位或特殊功能寄存器中的可寻址位。

例如,采用如下语句:

sfr P0=80H;
sbit FLAG1=P0^1;

可以将8051单片机P0口地址定义为80H,将P0.1位定义为FLAG1。

表2-3列出了Keil Cx51编译器能够识别的数据类型。

表2-3 Keil Cx51编译器能够识别的数据类型

在C语言程序的表达式或变量赋值运算中,有时会出现运算对象的数据不一致的情况,C语言允许任何标准数据类型之间的隐式转换。隐式转换按以下优先级别自动进行:

bit→char→int→long→float

signed→unsigned

其中箭头方向仅表示数据类型级别的高低,转换时由低向高进行,而不是数据转换时的顺序。例如,将一个bit(位类型)变量赋给一个int(整型变量)时,不需要先将bit型变量转换成char型之后再转换成int型,而是将bit型变量直接转换成int型并完成赋值运算。一般来说,如果有几个不同类型的数据同时参加运算,先将低级别类型的数据转换成高级别类型,再作运算处理,并且运算结果为高级别类型数据。C语言除了能对数据类型作自动的隐式转换之外,还可以采用强制类型转换符“()”对数据类型作显式转换,强制类型转换符“()”的应用将在2.2.5节中介绍。

Keil Cx51编译器除了能支持以上这些基本数据之外,还能支持复杂的构造类型数据,如结构类型、联合类型等。这些复杂的数据类型将在本书第5章详细讨论。

2.2.2 常量

常量又称为标量,它的值在程序执行过程中不能改变。常量的数据类型有整型、浮点型、字符型和字符串型等。

1.整型常量

整型常量就是整型常数,可表示为以下几种形式。

十进制整数:如1234,-5678,0等。

十六进制整数:ANSI C标准规定十六进制数据以0x开头,数字为0~9,a~f。如0x123表示十六进制数,相当于十进制数291。-0x1a表示十六进制数,相当于十进制数-26。

长整数:在数字后面加一个字母L就构成了长整数,如2048L、0123L、0xff00L等。

2.浮点型常量

浮点型常量有十进制数表示形式和指数表示形式。十进制数表示形式又称定点表示形式,由数字和小数点组成。如0.3141、.3141、314.1、3141.及0.0都是十进制数表示形式的浮点型常量。在这种表示形式中,如果整数或小数部分为0可以省略不写,但必须有小数点。指数表示形式为:

[±]数字[.数字]e[±]数字

其中,[ ]中的内容为可选项,根据具体情况可有可无,但其余部分必须有。如123e4、5e6、-7.0e-8等都是合法的指数形式浮点型常量;而e9、5e4.3和e都是不合法的表示形式。

3.字符型常量

字符型常量是单引号内的字符,如'a','b'等。对于不可显示的控制字符,可以在该字符前面加一个反斜杠“\”组成转义字符。利用转义字符可以完成一些特殊功能和输出时的格式控制。常用转义字符如表2-4所示。

表2-4 常用转义字符表

4.字符串型常量

字符串型常量由双引号""内的字符组成,如"ABCD"、"$1234"等都是字符串常量。当双引号内的字符个数为0时,称为空串常量。需要注意的是,字符串常量首尾的双引号是界限符,当需要表示双引号字符串时,可用双引号转义字符“\"”来表示。另外,C语言将字符串常量作为一个字符类型数组来处理,在存储字符串常量时要在字符串的尾部加一个转义字符\0作为该字符串常量的结束符。因此不要将字符常量与字符串常量混淆,如字符常量'a'与字符串常量"a"是不一样的。

2.2.3 变量及其存储模式

变量是一种在程序执行过程中其值能不断变化的量。使用一个变量之前,必须进行定义,用一个标识符作为变量名并指出它的数据类型和存储模式,以便编译系统为它分配相应的存储单元。在Cx51中对变量进行定义的格式如下:

[存储种类]数据类型 [存储器类型]变量名表;

其中,“存储种类”和“存储器类型”是可选项。变量的存储种类有四种:自动(auto)、外部(extern)、静态(static)和寄存器(register)。定义一个变量时如果省略存储种类选项,则该变量将为自动(auto)变量。定义一个变量时除了需要说明其数据类型之外,Keil Cx51编译器还允许说明变量的存储器类型。Keil Cx51编译器完全支持8051系列单片机的硬件结构和存储器组织,对于每个变量可以准确地赋予其存储器类型,使之能够在单片机系统内准确地定位。表2-5列出了Keil Cx51编译器所能识别的存储器类型。

表2-5 Keil Cx51编译器所能识别的存储器类型

定义变量时如果省略“存储器类型”选项,则按编译时使用的存储器模式SMALL、COMPACT或LARGE来规定默认存储器类型,确定变量的存储器空间,函数中不能采用寄存器传递的参数变量和过程变量也保存在默认的存储器空间。Keil Cx51编译器的三种存储器模式(默认的存储器类型)对变量的影响如下。

1.SMALL

变量被定义在8051单片机的片内数据存储器中,对这种变量的访问速度最快。另外,所有的对象,包括堆栈,都必须位于片内数据存储器中,而堆栈的长度是很重要的,实际栈长取决于不同函数的嵌套深度。

2.COMPACT

变量被定义在分页寻址的片外数据存储器中,每一页片外数据存储器的长度为256字节。这时对变量的访问是通过寄存器间接寻址(MOVX @Ri)进行的,堆栈位于8051单片机片内数据存储器中。采用这种编译模式时,变量的高8位地址由P2口确定,低8位地址由R0或R1的内容决定。采用这种模式的同时,必须适当改变启动配置文件STARTUP.A51中的参数:PDATASTART和PDATALEN;在用BL51进行连接时还必须采用连接控制命令“PDATA”对P2口地址进行定位,这样才能确保P2口为所需要的高8位地址。

3.LARGE

变量被定义在片外数据存储器中(最大可达64KB),使用数据指针DPTR来间接访问变量(MOVX @DPTR)。这种访问数据的方法效率是不高的,尤其是对于2个以上字节的变量,用这种方法相当影响程序的代码长度。

需要特别指出的是,变量的存储种类与存储器类型是完全无关的。例如:

    static unsigned char data x;    /* 在片内数据存储器中定义一个静态无符
                                        号字符型变量x */
     int y;                         /* 定义一个自动整型变量y,它的存储器类型由
                                        编译模式确定 */

8051系列单片机具有多种内部寄存器,其中一些是特殊功能寄存器,如定时器方式控制寄存器TMOD、中断允许控制寄存器IE等。为了能够直接访问这些特殊功能寄存器,Keil Cx51编译器扩充了关键字sfr和sfr16,利用这种扩充关键字可以在C语言源程序中直接对8051单片机的特殊功能寄存器进行定义。定义方法如下:

sfr特殊功能寄存器名=地址常数;

例如:

sfr P0 = 0x80;    /* 定义I/O口P0,其地址为0x80 */

这里需要注意的是,在关键字sfr后面必须跟一个标识符作为寄存器名,名字可任意选取,但应符合一般习惯。等号后面必须是常数,不允许有带运算符的表达式,而且该常数必须在特殊功能寄存器的地址范围之内(0x80~0xFF)。在新一代的8051单片机中,特殊功能寄存器经常组合成16位来使用。采用关键字sfr16可以定义这种16位的特殊功能寄存器。例如,对于8052单片机的定时器T2,可采用如下的方法来定义:

sfr16 T2 = 0xCC;   /* 定义TIMER2,其地址为T2L=0xCC,T2H=0xCD */

这里T2为特殊功能寄存器名,等号后面是它的低字节地址,其高字节地址必须在物理上直接位于低字节之后。这种定义方法适用于所有新一代的8051单片机中新增加的特殊功能寄存器。

在8051单片机应用系统中经常需要访问特殊功能寄存器中的某些位,Keil Cx51编译器为此提供了一个扩充关键字sbit,利用它定义可位寻址对象。定义方法有如下三种。

(1)sbit位变量名=位地址

这种方法将位的绝对地址赋给位变量,位地址必须位于0x80~0xFF之间。例如:

sbit OV = 0xD2;
sbit CY = 0xD7;

(2)sbit位变量名=特殊功能寄存器名^位位置

当可寻址位位于特殊功能寄存器中时可采用这种方法,“位位置”是一个0~7之间的常数。例如:

sfr PSW = 0xD0;
sbit OV = PSW^2;
sbit CY = PSW^7;

(3)sbit位变量名=字节地址^位位置

这种方法以一个常数(字节地址)作为基地址,该常数必须在0x80H~0xFF之间。“位位置”是一个0~7之间的常数。例如:

sbit OV = 0xD0^2;
sbit CY = 0xD0^7;

当位对象位于8051单片机片内存储器中可位寻址区时称之为“可位寻址对象”。Keil Cx51编译器提供了一个bdata存储器类型,允许将具有bdata类型的对象放入8051单片机片内可位寻址区。例如:

int bdata ibase;            /*在位寻址区定义一个整型变量ibase  */
char bdata bary[4];         /*在位寻址区定义一个数组array[4]  */

使用关键字sbit可以独立访问可位寻址对象中的某一位。例如:

sbit mybit0=ibase^0;
sbit mybit15=ibase^15;
sbit Ary07=bary[0]^7;
sbit Ary37=bary[3]^7;

采用这种方法定义可位寻址变量时要求基址对象的存储器类型为bdata,操作符“^”后面“位位置”的最大值取决于指定的基地址类型,对于char类型来说是0~7;对于int类型来说是0~15;对于long类型来说是0~31。

需要注意的是,sbit是一个独立的关键字,不要将它与关键字bit相混淆。关键字bit是Keil Cx51编译器的一种扩充数据类型,用来定义一个普通位变量,它的值是二进制数的0或1。一个函数中可以包含bit类型的参数,函数的返回值也可为bit类型。例如:

static bit direction_bit        /* 定义一个静态位变量direction_bit */
extern bit lock_prt_port        /* 定义一个外部位变量lock_prt_port */
bit bfunc(bit b0,bit b1);       /* 定义一个返回位型值的函数bfunc,函
    {...                             数中包含有两个位型参数b0 和b1 */
    return(b1)                  /* 返回一个位型值b1 */
}

如果在函数中禁止使用中断(#pragma disable)或者函数中包含有明确的寄存器组切换(using n),则该函数不能返回位型值,否则在编译时会产生编译错误。另外,不能定义位指针,也不能定义位数组。

上面介绍了变量及其定义方法,这在编写C语言程序时是十分重要的。从变量的作用范围来看,还有全局变量和局部变量之分。全局变量是指在程序开始处或各个功能函数的外面定义的变量,在程序开始处定义的全局变量对于整个程序都有效,可供程序中所有函数共同使用;而在各功能函数外面定义的全局变量只对从定义处开始往后的各个函数有效,只有从定义处往后的那些功能函数才可以使用该变量,定义处前面的函数则不能使用。

局部变量是指在函数内部或以花括号{ }围起来的功能块内部所定义的变量,局部变量只在定义它的函数或功能块以内有效,在该函数或功能块以外则不能使用。局部变量可以与全局变量同名,但在这种情况下局部变量的优先级较高,而同名的全局变量在该功能块内被暂时屏蔽。

从变量的存在时间来看又可分为静态存储变量和动态存储变量。

静态存储变量是指该变量在程序运行期间其存储空间固定不变;动态存储变量是指该变量的存储空间不确定,在程序运行期间根据需要动态地为该变量分配存储空间。一般来说,全局变量为静态存储变量,局部变量为动态存储变量。

在进行程序设计的时候经常需要给一些变量赋以初值,C语言允许在定义变量的同时给变量赋初值。下面是一些变量定义的例子。

char data var1;                          /* 在data区定义字符型变量var1 */
int  idata var2;                         /* 在idata区定义整型变量var2 */
int a=5;                                 /* 定义变量a,同时赋以初值5,变量a位于
                                            由编译模式确定的默认存储区 */
char code text[ ]="ENTER PARAMETER:";    /* 在code区定义字符串数组 */
unsigned char xdata vecter [10][4][4];   /* 在xdata区定义无符号字符型三维数组
                                            变量vecter[10][4][4] */
static unsigned long xdata array [100];  /* 在xdata区定义静态无符号长整型数组
                                            变量array[100]*/
extern float idata x,y,z;                /* 在idata区定义外部浮点型变量x,y,z*/
char xdata * px;                         /* 在xdata区定义一个指向对象类型为
                                            char的指针px,指针px自身在默认存
                                            储区(由编译模式确定),长度为2字节
                                           (0~0xFFFF)*/
char xdata * data pdx;                   /* 除了指针明确定位于内部数据存储器区
                                            (data)之外,与上例完全相同,由于指定
                                            了存储器类型,所以与编译模式无关 */
extern bit data lock_prt_port;           /* 在data区定义一个外部位变量 */
char bdata flags;                        /* 在bdata区定义字符型变量 */
sbit flag0=flags^0;                      /* 在bdata区定义可位寻址变量 */
sfr P0=ox80;                             /* 定义特殊功能寄存器P0 */
sfr16 T2=0xCC;                           /* 定义特殊功能寄存器T2 */

2.2.4 用typedef重新定义数据类型

在C语言程序中除了可以采用上面所介绍的数据类型之外,用户还可以根据自己的需要对数据类型重新定义。重新定义时需用到关键字typedef,定义方法如下:

typedef  已有的数据类型  新的数据类型名;

其中“已有的数据类型”是指上面所介绍的C语言中所有的数据类型,包括结构、指针和数组等,“新的数据类型名”可按用户自己的习惯或根据任务需要决定。关键字typedef的作用只是将C语言中已有的数据类型作了置换,因此可用置换后的新数据类型名来进行变量的定义。例如:

typedef int word;       /* 定义word为新的整型数据类型名 */
word i,j;               /* 将I,j定义为int型变量 */

在这个例子中,先用关键字typedef将word定义为新的整型数据类型,定义的过程实际上是用word置换了int,因此下面就可以直接用word对变量i,j进行定义,而此时word等效于int,所以i,j被定义成整型变量。例如:

typedef int NUM[100]; /* 将NUM定义为整型数组类型 */
NUM  n;                 /* 将n定义为整型数组变量 */
typedef char * POINTER; /* 将POINTER定义为字符指针类型 */
POINTER point;          /* 将point定义为字符指针变量 */

用typedef还可以定义结构类型:

typedef struct          /* 定义结构体 */
    { int month;
      int day;
      int year;
    } DATE;

这里DATE为一个新的数据类型(结构类型)名,可以直接用它来定义变量:

DATE birthday;          /* 定义birthday为结构类型变量 */
DATE * point;           /* 定义指向这个结构类型数据的指针 */

关于结构类型数据在本章后面还要详细讨论。一般而言,用typedef定义的新数据类型用大写字母表示,以便与C语言中原有的数据类型相区别。另外还要注意,用typedef可以定义各种新的数据类型名,但不能直接用来定义变量。typedef只是对已有的数据类型作了一个名字上的置换,并没有创造出一个新的数据类型,例如前面例子中的word,它只是int类型的一个新名字而已。

采用typedef来重新定义数据类型有利于程序的移植,同时还可以简化较长的数据类型定义(如结构数据类型等)。在采用多模块程序设计时,如果不同的模块程序源文件中用到同一类型的数据时(尤其是像数组、指针、结构、联合等复杂数据类型),经常用typedef将这些数据重新定义并放到一个单独的文件中,需要时再用预处理命令#include将它们包含进来。

2.2.5 运算符与表达式

C语言对数据有很强的表达能力,具有十分丰富的运算符。运算符就是完成某种特定运算的符号,表达式则是由运算符及运算对象所组成的具有特定含义的一个式子。C语言是一种表达式语言,在任意一个表达式的后面加一个分号“;”就构成了一个表达式语句。由运算符和表达式可以组成C语言程序的各种语句。

运算符按其在表达式中所起的作用,可分为赋值运算符、算术运算符、增量与减量运算符、关系运算符、逻辑运算符、位运算符、复合赋值运算符、逗号运算符、条件运算符、指针和地址运算符、强制类型转换运算符和sizeof运算符等。运算符按其在表达式中与运算对象的关系,又可分为单目运算符、双目运算符和三目运算符等。单目运算符只需要有一个运算对象,双目运算符要求有两个运算对象,三目运算符要求有三个运算对象。掌握各种运算符的意义和使用规则,对于编写正确的C语言程序是十分重要的。

1.赋值运算符

在C语言中,符号“=”是一个特殊的运算符,称之为赋值运算符。赋值运算符的作用是将一个数据的值赋给一个变量,利用赋值运算符将一个变量与一个表达式连接起来的式子称为赋值表达式,在赋值表达式的后面加一个分号“;”便构成了赋值语句。赋值语句的格式如下:

变量 = 表达式;

该语句的意义是先计算出右边表达式的值,然后将该值赋给左边的变量。上式中的“表达式”还可以是另一个赋值表达式,即C语言允许进行多重赋值。例如:

x=9;         /* 将常数 9 赋给变量x */
x=y=8;       /* 将常数 8 同时赋给变量x和y */

这些都是合法的赋值语句。在使用赋值运算符“=”时应注意不要与关系运算符“= =”相混淆,运算符“= =”用来进行相等关系运算。

2.算术运算符

C语言中的算术运算符有:

+ 加或取正值运算符

- 减或取负值运算符

* 乘运算符

/ 除运算符

% 取余运算符

上面这些运算符中加、减、乘、除为双目运算符,它们要求有两个运算对象。对于加、减和乘法符合一般的算术运算规则。除法运算有所不同,如果是两个整数相除,其结果为整数,舍去小数部分,例如:5/3的结果为1,5/10的结果为0。如果是两个浮点数相除,其结果为浮点数,例如:5.0/10.0的结果为0.5。取余运算要求两个运算对象均为整型数据,例如:7%4的结果为3。取正值和取负值为单目运算符,它们的运算对象只有一个,分别是取运算对象的正值和负值。

用算术运算符将运算对象连接起来的式子即为算术表达式。算术运算的一般形式为:

表达式1  算术运算符  表达式2

例如:x+y/(a-b),(a+b)*(x-y)都是合法的算术表达式。C语言中规定了运算符的优先级和结合性。在求一个表达式的值时,要按运算符的优先级别进行。算术运算符中取负值(-)的优先级最高,其次是乘法(*)、除法(/)和取余(%)运算符,加法(+)和减法(-)运算符的优先级最低。

需要时可在算术表达式中采用圆括号来改变运算符的优先级,例如在计算表达式x+y/(a-b)的值时,首先计算(a-b),然后再计算y/(a-b),最后计算x+y/(a-b)。如果在一个表达式中各个运算符的优先级别相同,则计算时按规定的结合方向进行。例如计算表达式x+y-z的值,由于+和-优先级别相同,计算时按“从左至右”的结合方向,先计算x+y,再计算(x+y)-z。这种“从左至右”的结合方向称为“左结合性”,此外还有“右结合性”。

3.增量和减量运算符

C语言中除了基本的加、减、乘、除运算符之外,还提供一种特殊的运算符:

++ 增量运算符

-- 减量运算符

增量和减量是C语言中特有的一种运算符,它们的作用分别是对运算对象作加1和减1运算。例如:++i,i++,--j,j--等。

看起来++i和i++的作用都是使变量i的值加1,但是由于运算符++所处的位置不同,使变量i加1的运算过程也不同。++i(或--i)是先执行i+1(或i-1)操作,再使用i的值,而i++(或i--)则是先使用i的值,再执行i+1(或i-1)操作。

增量运算符++和减量运算符--只能用于变量,不能用于常数或表达式。

例2.1:使用增量“++”和减量“- -”运算符的例子。

    #include <stdio.h>
    main(){
        int  x,y,z;
        x = y = 8;  z = ++x;
        printf("\n %d %d %d",y,z,x);
        x = y = 8;  z = x++;
        printf("\n %d %d %d",y,z,x);
        x = y = 8;  z = --x;
        printf("\n %d %d %d",y,z,x);
        x = y = 8;  z = x--;
        printf("\n %d %d %d",y,z,x);
        printf("\n");
        while(1);
    }

程序执行结果:

    8 9 9
    8 8 9
    8 7 7
    8 8 7

在这个程序例子中使用了Keil Cx51编译器提供的输出库函数printf,在C语言程序中凡是使用了库函数的,都必须在程序开始处将该库函数的预定义文件包含进来,才能使程序得到正确的编译和执行。本程序在开始处使用了预处理命令#include将声明库函数printf原型的头文件stdio.h包含到程序中去。另外,为了使库函数printf能够在μVision2仿真调试状态下正确工作,应在C语言源程序中增加对8051单片机串行口初始化的语句,或者将C语言源程序与修改后(加入了8051单片机串口初始化指令)的启动程序STARTUP.A51连接在一起。关于输入输出库函数的详细介绍请参见本书第9章。

4.关系运算符

C语言中有6种关系运算符:

> 大于

< 小于

>= 大于等于

<= 小于等于

= = 等于

!= 不等于

前4种关系运算符具有相同的优先级,后两种关系运算符也具有相同的优先级;但前4种的优先级高于后2种。用关系运算符将两个表达式连接起来即成为关系表达式。关系表达式的一般形式为:

表达式1  关系运算符  表达式2

例如:x>y,x+y>z,(x=3)>(y=4)都是合法的关系表达式。

关系运算符通常用来判别某个条件是否满足,关系运算的结果只有0和1两种值。当所指定的条件满足时结果为1,条件不满足时结果为0。

例2.2:使用关系运算符的例子。

#include <stdio.h>
main() {
    int  x,y,z;
    printf("input data x,y ? \n");
    scanf("%d %d",&x,&y);
    printf("\n   x   y   x<y  x<=y x>y  x>=y x!=y x==y");
    printf("\n%5d%5d",x,y);
    z = x <  y; printf("%5d",z);
    z = x <= y; printf("%5d",z);
    z = x >  y; printf("%5d",z);
    z = x >= y; printf("%5d",z);
    z = x != y; printf("%5d",z);
    z = x == y; printf("%5d",z);
    printf("\n");
    while(1);
}

程序执行结果(1):

input data x,y ?
5  3  回车
  x   y   x<y   x<=y   x>y   x>=y   x!=y   x==y
  5   3   0     0      1     1      1      0

程序执行结果(2):

input data x,y ?
-5  -3 回车
  x   y   x<y   x<=y   x>y   x>=y   x!=y   x==y
  -5  -3  1     1      0     0      1      0

程序执行结果(3):

input data x,y ?
4  4 回车
  x   y   x<y   x<=y   x>y   x>=y   x!=y   x==y
  4   4   0     1      0     1      0      1

在本例中使用了Keil Cx51编译器提供的输入库函数scanf,与printf函数一样,scanf也是通过8051单片机的串行口实现数据输入的,它的使用方法与printf函数类似。

5.逻辑运算符

C语言中有3种逻辑运算符:

|| 逻辑或

&& 逻辑与

! 逻辑非

逻辑运算符用来求某个条件式的逻辑值,用逻辑运算符将关系表达式或逻辑量连接起来就是逻辑表达式。逻辑运算的一般形式为:

逻辑与   条件式1 && 条件式2
逻辑或   条件式1 || 条件式2
逻辑非   ! 条件式

例如:x&&y,a||b,!z都是合法的逻辑表达式。

进行逻辑与运算时,首先对条件式1进行判断,如果结果为真(非0值),则继续对条件式2进行判断,当结果也为真时,表示逻辑运算的结果为真(值为1);反之,如果条件式1的结果为假,则不再判断条件式2,而直接给出逻辑运算的结果为假(值为0)。

进行逻辑或运算时,只要两个条件式中有一个为真,逻辑运算的结果便为真(值为1),只有当条件式1和条件式2均不成立时,逻辑运算的结果才为假(值为0)。

进行逻辑非运算时,对条件式的逻辑值直接取反。

逻辑运算符的优先级为(由高至低):!(非)→&&(与)→||(或),即逻辑非的优先级最高。

例2.3:使用逻辑运算符的例子。

#include <stdio.h>
main() {
    int  x,y,z;
    printf("input data x,y ? \n");
    scanf("%d %d",&x,&y);
    printf("\n   x   y     !x     x||y   x&&y");
    printf("\n%5d%5d",x,y);
    z = !x;    printf("%8d",z);
    z = x || y; printf("%8d",z);
    z = x && y; printf("%8d",z);
    printf("\n");
    while(1);
}

程序执行结果(1):

input data x,y ?
12  8  回车
x   y   !x   x||y   x&&y
12   8    0     1      1

程序执行结果(2):

input data x,y ?
9  -3  回车
x   y   !x   x||y   x&&y
9   -3   0     1      1

程序执行结果(3):

input data x,y ?
0  81  回车
x   y   !x   x||y   x&&y
0   81   1     1      0

程序执行结果(4):

input data x,y ?
-23  0  回车
x   y   !x   x||y   x&&y
-23  0    0     1      0

程序执行结果(5):

input data x,y ?
0   0  回车
x   y   !x   x||y   x&&y
0   0    1     0      0

6.位运算符

能对运算对象进行按位操作是C语言的一大特点,正是由于这一特点使C语言具有了汇编语言的一些功能,从而使之能对计算机的硬件直接进行操作。C语言中共有6种位运算符:

~ 按位取反

<< 左移

>> 右移

& 按位与

^ 按位异或

| 按位或

位运算符的作用是按位对变量进行运算,并不改变参与运算的变量的值。若希望按位改变运算变量的值,则应利用相应的赋值运算。另外位运算符不能用来对浮点型数据进行操作。位运算符的优先级从高到低依次是:按位取反(~)→左移(<<)和右移(>>)→按位与(&)→按位异或(^)→按位或(|)。位运算的一般形式如下:

变量1  位运算符  变量2

表2-6列出了按位取反、按位与、按位或和按位异或的逻辑真值。

表2-6 按位取反、按位与、按位或和按位异或的逻辑真值

例2.4:位逻辑运算。

#include <stdio.h>
main() {
    unsigned int  x = 0x57db,y = 0xb0f3;
    printf("\n   x    y   x&y   x^y   x|y   ~x");
    printf("\n%6x%6x%6x%6x%6x%6x",x,y,x&y,x^y,x|y,~x);
    printf("\n");
    while(1);
}
    x      y    x&y    x^y    x|y    ~x
    57db  b0f3   10d3   e728   f7fb   a824

程序执行结果:

位运算符中的移位操作比较复杂。左移(<<)运算符是用来将变量1的二进制位值向左移动由变量2所指定的位数。例如:a=0x8f(即二进制数10001111),进行左移运算a<<2,就是将a的全部二进制位值一起向左移动2位,其左端移出的位值被丢弃,并在其右端补以相应位数的“0”。因此,移位的结果是a=0x3c(即二进制数(00111100)。

右移(>>)运算符是用来将变量1的二进制位值向右移动由变量2指定的位数。进行右移运算时,如果变量1属于无符号类型数据,则总是在其左端补“0”;如果变量1属于有符号类型数据,则在其左端补入原来数据的符号位(即保持原来的符号不变),其右端的移出位被丢弃。对于a= 0x8f,如果a是无符号数,则执行a>>2之后结果为a=0x23(即二进制数00100011);如果a是有符号数,则执行a>>2之后结果为a=0xe3(即二进制数11100011)。

例2.5:移位运算。

    #include <stdio.h>
    main() {
    int a,b;
    unsigned int  x,y;
    a = b = 0xaa55;  x = y = 0xaa55;
    printf("\n a=%4x  b=%4x  x=%4x  y=%4x",a,b,x,y);
        a = a << 1;  b = b >> 1;
        x = x << 1;  y = y >> 1;
        printf("\n a=%4x  b=%4x  x=%4x  y=%4x",a,b,x,y);
        printf("\n");
        while(1);
    }

程序执行结果:

a=aa55   b=aa55   x=aa55   y=aa55
a=54aa   b=d52a   x=54aa   y=552a

7.复合赋值运算符

在赋值运算符“=”的前面加上其他运算符,就构成了所谓复合赋值运算符:

+= 加法赋值

-= 减法赋值

*= 乘法赋值

/= 除法赋值

%= 取模赋值

<<= 左移位赋值

>>= 右移位赋值

&= 逻辑与赋值

|= 逻辑或赋值

^= 逻辑异或赋值

~= 逻辑非赋值

复合赋值运算首先对变量进行某种运算,然后将运算的结果再赋给该变量。复合运算的一般形式为:

变量  复合赋值运算符  表达式

例如:a+=3等价于a=a+3;x*=y+8等价于x=x*(y+8)。凡是二目运算符,都可以和赋值运算符一起组合成复合赋值运算符。采用复合赋值运算符,可以使程序简化,同时还可以提高程序的编译效率。

例2.6:利用复合赋值运算符实现算术运算。

#include <stdio.h>
main() {
    int a,b,c,d,x,y,z;
    x = 634; y = 19; z = 28;
    a = 3 * (b = x/(y-4)) - z/2;
    printf("\n%10d%10d",a,b);
    a = 100; b = 45; c = -19; d = 94; x = -2; y = 5;
    a += 6;
    b -= x;
    c *= 10;
    d /= x+y;
    z %= 8;
    printf("\n%10d%10d%10d%10d%10d",a,b,c,d,z);
    printf("\n");
    while(1);
}

程序执行结果:

112   42
106   47   -190   31   4

例2.7:利用复合赋值运算符实现位逻辑运算。

#include <stdio.h>
main() {
    int  x,y;
    x = 2; y = 3;
    x <<= 2; printf("\n%3d",x);
    x >>= 1; printf("\n%3d",x);
    x <<= y; printf("\n%3d",x);
    x = 2;
    x &= y;  printf("\n%3d",x);
    x |= y;  printf("\n%3d",x);
    x ^= y;  printf("\n%3d",x);
    printf("\n");
    while(1);
}

程序执行结果:

8
4
32
2
3
0

8.逗号运算符

在C语言中逗号“,”是一个特殊的运算符,可以用它将两个(或多个)表达式连接起来,称为逗号表达式。逗号表达式的一般形式为:

表达式1,表达式2,…,表达式n

程序运行时对于逗号表达式的处理,是从左至右依次计算出各个表达式的值,而整个逗号表达式的值是最右边表达式(即表达式n)的值。

例2.8:逗号运算符的使用。

#include <stdio.h>
main() {
    int  a,b,c,w,x,y,z;
    w = ( x=5,y=-11,z=43,3);
    printf("\n%d %d %d %d",w,x,y,z);
    a = 3 * (b = w + x,c = y * ( z -10)) -6;
    printf("\n%d %d %d",a,b,c);
    printf("\n");
    while(1);
}

程序执行结果:

3  5  -11  43
-1095  8  -363

在许多情况下,使用逗号表达式的目的只是为了分别得到各个表达式的值,而并不一定要得到和使用整个逗号表达式的值。另外还要注意,并不是在程序的任何地方出现的逗号,都可以认为是逗号运算符。例如函数中的参数也是用逗号来间隔的,上例中库输出函数printf("\n%d %d %d",a,b,c)中的“a,b,c”是函数的三个参数,而不是一个逗号表达式。

9.条件运算符

条件运算符“?:”是C语言中唯一的一个三目运算符,它要求有三个运算对象,用它可以将三个表达式连接构成一个条件表达式。条件表达式的一般形式如下:

逻辑表达式  ?  表达式1:表达式2

其功能是首先计算逻辑表达式,当值为真(非0值)时,将表达式1的值作为整个条件表达式的值;当逻辑表达式的值为假(0值)时,将表达式2的值作为整个条件表达式的值。例如:条件表达式max=(a>b) ? a:b的执行结果是将a和b中较大者赋值给变量max。另外,条件表达式中逻辑表达式的类型可以与表达式1和表达式2的类型不一样。

10.指针和地址运算符

指针是C语言中的一个十分重要的概念,在C语言的数据类型中专门有一种指针类型。变量的指针就是该变量的地址,还可以定义一个指向某个变量的指针变量。为了表示指针变量和它所指向的变量地址之间的关系,C语言提供了两个专门的运算符:

* 取内容

& 取地址

取内容和取地址运算的一般形式分别为:

变量 = * 指针变量
指针变量 = & 目标变量

取内容运算的含义是将指针变量所指向的目标变量的值赋给左边的变量;取地址运算的含义是将目标变量的地址赋给左边的变量。需要注意的是,指针变量中只能存放地址(即指针型数据),不要将一个非指针类型的数据赋值给一个指针变量。

例2.9:指针及地址运算符的使用。

#include <stdio.h>
main() {
    int i;
    int *int_ptr;
    int_ptr = &i;
    *int_ptr = 5;
    printf("\n i = %d",i);
    while(1);
}

程序执行结果:

i=5

11.强制类型转换运算符

C语言中的圆括号“()”也可作为一种运算符使用,这就是强制类型转换运算符,它的作用是将表达式或变量的类型强制转换成为所指定的类型。在C语言程序中进行算术运算时,需要注意数据类型的转换。有两种数据类型转换方式,即隐式转换和显式转换。隐式转换是在对程序进行编译时由编译器自动处理的。隐式转换遵循以下规则。

① 所有char型的操作数转换成int型。

② 用运算符连接的两个操作数如果具有不同的数据类型,按以下次序进行转换:如果一个操作数是float类型,则另一个操作数也转换成float类型;如果一个操作数是long类型,则另一个操作数也转换成long类型;如果一个操作数是unsigned类型,则另一个操作数也转换成unsigned类型。

③ 在对变量赋值时发生的隐式转换,将赋值号“=”右边的表达式类型转换成赋值号左边变量的类型。例如,把整型数赋值给字符型变量,则整型数的高8位将丧失;把浮点数赋值给整型变量,则小数部分将丧失。在C语言中只有基本数据类型(即char、int、long和float)可以进行隐式转换。其余的数据类型不能进行隐式转换,例如,我们不能把一个整型数利用隐式转换赋值给一个指针变量,在这种情况下就必须利用强制类型转换运算符来进行显式转换。强制类型转换运算符的一般使用形式为:

(类型)=表达式

显式类型转换在给指针变量赋值时特别有用。例如,预先在8051单片机的片外数据存储器(xdata)中定义了一个字符型指针变量px,如果想给这个指针变量赋一初值0xB000,可以写成:px=(char xdata *)0xB000;这种方法特别适合于用标识符来存取绝对地址。

例2.10:强制类型转换运算符的使用。

#include <stdio.h>
main() {
    char xdata * px;
    char q;
    int   x = 0xf32a;
    long  y = 0x901af364;
    float z = 3.14159;
    px=(char xdata *)0xB000;
    * px='A';
    q=*((char xdata *)0xB000);
    printf("\n%bx  %x  %d  %c",(char)x,(int)y,(int)z,q) ;
    while(1);
}

程序执行结果:

2a  f364  3  A

12.sizeof运算符

C语言中提供了一种用于求取数据类型、变量以及表达式的字节数的运算符:sizeof,该运算符的一般使用形式为:

sizeof(表达式)或sizeof(数据类型)

应该注意的是,sizeof是一种特殊的运算符,不要错误地认为它是一个函数。实际上,字节数的计算在程序编译时就完成了,而不是在程序执行的过程中才计算出来的。

例2.11:sizeof运算符的使用。

#include <stdio.h>
main() {
    printf("\n    char: %bd byte",sizeof(char));
    printf("\n     int: %bd bytes",sizeof(int));
    printf("\n    long: %bd bytes",sizeof(long));
    printf("\n   float: %bd bytes",sizeof(float));
    while(1);
}

程序执行结果:

char: 1 byte
int:  2 bytes
long: 4 bytes
float:4 bytes

前面对C语言中的各种运算符分别作了介绍,此外还有三个运算符:数组下标运算符“[ ]”、存取结构或联合中变量的运算符“->”或“.”,它们将在第5章予以介绍。表2-7给出了这些运算符在使用过程中的优先级和结合性。

表2-7 运算符的优先级和结合性

2.3 Cx51程序的基本语句

2.3.1 表达式语句

C语言是一种结构化的程序设计语言,它提供了十分丰富的程序控制语句。表达式语句是最基本的一种语句。在表达式的后边加一个分号“;”就构成了表达式语句。下面的语句都是合法的表达式语句:

a=++b*9;
x=8; y=7;
z=(x+y)/a;
++i;

表达式语句也可以仅由一个分号“;”组成,这种语句称为空语句。空语句是表达式语句的一个特例。空语句在程序设计中有时是很有用的,当程序在语法上需要有一个语句,但在语义上并不要求有具体的动作时,便可以采用空语句。空语句通常有以下两种用法。

① 在程序中为有关语句提供标号,用以标记程序执行的位置。例如,采用下面的语句可以构成一个循环。

repeat:;
        ..
      goto repeat ;

② 在用while语句构成的循环语句后面加一个分号,形成一个不执行其他操作的空循环体。这种空语句在等待某个事件发生时特别有用。例如,下面这段程序是读取8051单片机串行口数据的函数,其中就用了一个空语句while (!RI);来等待单片机串行口接收结束。

#include <reg51.h>      /* 插入8051单片机的预定义文件 */
char _getkey ()         /* 函数定义 */
{                       /* 函数体开始 */
char c;                 /* 定义变量 */
while (!RI);            /* 空语句,等待8051单片机串行口接收结束 */
c = SBUF;               /* 读串行口内容 */
RI = 0;                 /* 清除串行口接收标志 */
return (c);             /* 返回 */
}                       /* 函数体结束 */

采用分号“;”作为空语句使用时,要注意与简单语句中有效组成部分的分号相区别。不能滥用空语句,以免引起程序的误操作,甚至造成程序语法上的错误。

2.3.2 复合语句

复合语句是由若干条语句组合而成的一种语句,它是用一个大括号“{}”将若干条语句组合在一起而形成的一种功能块。复合语句不需要以分号“;”结束,但它内部的各条单语句仍需以分号“;”结束。复合语句的一般形式为:

{
  局部变量定义;
  语句1;
  语句2;
  …
  语句n;
}

复合语句在执行时,其中的各条单语句依次顺序执行。整个复合语句在语法上等价于一条单语句,因此在C语言程序中可以将复合语句视为一条单语句。复合语句允许嵌套,即在复合语句内部还可以包含别的复合语句。通常复合语句都出现在函数中,实际上,函数的执行部分(即函数体)就是一个复合语句。复合语句中的单语句一般是可执行语句,此外还可以是变量的定义语句(说明变量的数据类型)。

在复合语句内所定义的变量,称为该复合语句中的局部变量,它仅在当前这个复合语句中有效。利用复合语句将多条单语句组合在一起,以及在复合语句中进行局部变量定义是C语言的一个重要特征。

例2.12:复合语句及其局部变量的使用。

#include <stdio.h>
main() {                 /* 主函数体开始 */
    int a,b,c,d;         /* 定义变量a,b,c,d,它们在整个主函数中有效 */
    a = 1; b = 2; c = 3; d = 4;
    printf("\nX: %d %d %d %d",a,b,c,d);
    {                    /* 复合语句1 */
      int b,m;           /* 定义局部变量b,m,它们仅在复合语句1 中有效 */
      b = 8; m = 100;
      printf("\nY: %d %d %d %d | %d",a,b,c,d,m);
      {                  /* 复合语句2 */
      int c,n;           /* 定义局部变量c,n,它们仅在复合语句2中有效 */
      c = 9; n = 150;
      printf("\nZ: %d %d %d %d | %d %d",a,b,c,d,m,n);
      }                  /* 复合语句2 结束 */
      printf("\nY: %d %d %d %d | %d",a,b,c,d,m);
  }                      /* 复合语句1 结束 */
  printf("\nX: %d %d %d %d",a,b,c,d);
  printf("\n");
  while(1);
}                        /* 主函数体结束 */

程序执行结果:

X: 1 2 3 4
Y: 1 8 3 4 | 100
Z: 1 8 9 4 | 100150
Y: 1 8 3 4 | 100
X: 1 2 3 4

在这个程序的主函数体开始处,定义了变量a,b,c,d,它们在整个主函数体中都是有效的,在主函数体中的复合语句1和复合语句2中都可以使用它们。另外,在复合语句1中又定义了局部变量m和与主函数体中定义的变量同名的局部变量b,这种局部变量m和b仅在定义它的复合语句1中有效,而且局部变量b的优先级高于在主函数体中定义的同名变量b。因此在复合语句1中执行printf函数输出的b值为8,而不是2。

同样,在复合语句2中定义了一个与主函数体中同名的局部变量c,在复合语句2中执行printf函数输出的c值为9,而不是3。一旦出了复合语句,则其中的局部变量立即失效。如果有同名的局部变量,则恢复该变量在上一层位置所定义的初值。读者可以通过仔细分析本程序的执行结果来弄清各个变量的作用范围。

2.3.3 条件语句

条件语句又称为分支语句,它是用关键字if构成的。C语言提供了三种形式的条件语句。

(1)if(条件表达式)语句

其含义为:若条件表达式的结果为真(非0值),就执行后面的语句;反之若条件表达式的结果为假(0值),就不执行后面的语句。这里的语句也可以是复合语句。这种条件语句的执行过程如图2.1(a)所示。

(2)if(条件表达式)语句1

else语句2

其含义为:若条件表达式的结果为真(非0值),就执行语句1;反之若条件表达式的结果为假(0值),就执行语句2。这里的语句1和语句2均可以是复合语句。这种条件语句的执行过程如图2.1(b)所示。

图2.1 条件语句的执行过程

(3)if(条件表达式1)语句1

else if(条件式表达2)语句2

else if(条件式表达3)语句3

… …

else if(条件表达式n)语句m

else语句n

这种条件语句常用来实现多方向条件分支,其执行过程如图2.2所示。

图2.2 多分支条件语句的执行过程

例2.13:条件语句的使用——求一元二次方程的根。

#include <stdio.h>
#include <math.h>
main() {
  float a,b,c,x1,x2;
  float r,s;
  a = 2.0; b = 3.0; c = 4.0;
  r = b * b -4.0 * a * c;
  if( r > 0.0 )
    {
      s = sqrt(r);
      x1 = (-b + s) / (2.0 * a);
      x2 = (-b - s) / (2.0 * a);
      printf("real: x1 =%15.7f,x2 =%15.7f\n",x1,x2);
    }
  else if( r == 0.0 )
      printf("double: x1,x2 =%15.7f\n",-b/(2.0*a));
  else
    {
      x1 = -b / (2.0 * a);
      x2 = sqrt(-r) / (2.0 * a);
      printf("complex: Re=%15.7f,Im=%15.7f\n",x1,x2);
    }
    while(1);
}

程序执行结果:

complex: Re= -0.7500000,Im= 1.1989580

在这个程序中使用了库函数sqrt(r)来求方程的根,sqrt是一个算术库函数。为了使程序能得到正确的编译和执行,在本程序的开始处使用了预处理命令#include将库函数sqrt所在的预处理文件math.h包含到程序中去。

2.3.4 开关语句

开关语句也是一种用来实现多方向条件分支的语句。虽然采用条件语句也可以实现多方向条件分支,但是当分支较多时会使条件语句的嵌套层次太多,程序冗长,可读性降低。开关语句直接处理多分支选择,使程序结构清晰,使用方便。开关语句是用关键字switch构成的,它的一般形式如下:

switch (表达式)
  {
    case  常量表达式1:语句1
                    break;
    case  常量表达式2:语句2
                    break;
  …              …
    case  常量表达式n:语句n
                    break;
    default:语句d
    }

开关语句的执行过程是:将switch后面表达式的值与case后面各个常量表达式的值逐个进行比较,若遇到匹配时,就执行相应case后面的语句,然后执行break语句,break语句又称间断语句,它的功能是中止当前语句的执行,使程序跳出switch语句。若无匹配的情况,则只执行语句d。开关语句的执行过程如图2.3所示。

图2.3 开关语句的执行过程

例2.14:开关语句的使用。

本程序按照输入的年份year和月份month,计算该月有多少天。程序需要判断该年是否为闰年。闰年的2月有29天,平年的2月只有28天。闰年的条件是:年份数year能被4整除,但不能被100整除;或者年份数year能被400整除。这个条件可以用一个逻辑关系式来表达:

year%4= =0 && year%100 != 0 || year%400 = = 0

当这个表达式的值为真(非0值)时,year为闰年,否则为平年。

#include <stdio.h>
main() {
    int year,month,len;
    while(1){
      printf("Enter year & month: \n");
          scanf("%d%d",&year,&month);
      switch(month) {
          case 1:  len=31; break;
          case 3:  len=31; break;
          case 5:  len=31; break;
          case 7:  len=31; break;
          case 8:  len=31; break;
          case 10: len=31; break;
          case 12: len=31; break;
          case 4:  len=30; break;
          case 6:  len=30; break;
          case 9:  len=30; break;
          case 11: len=30; break;
          case 2:  if(year%4==0&&year%100 != 0||year%400==0) len=29;
                  else len=28;
                      break;
              default: printf("Input error \n");
                      len=0;
                      break;
      }
          if(len != 0)
              printf("The lenth of%d,%dis %d \n",year,month,len);
    }
}

程序执行结果:

Enter year & month:
1996  2 回车
The lenth of 1996,2 is 29

2.3.5 循环语句

实际应用中很多地方需要用到循环控制,如需要反复进行某种操作等,这时可以用循环语句来实现。在C语言程序中用来构成循环控制的语句有:while语句、do-while语句、for语句以及goto语句,分述如下。

① 采用while语句构成循环结构的一般形式如下:

while(条件表达式) 语句;

其意义为,当条件表达式的结果为真(非0值)时,程序就重复执行后面的语句,一直执行到条件表达式的结果变为假(0值)时为止。这种循环结构是先检查条件表达式所给出的条件,再根据检查的结果决定是否执行后面的语句。如果条件表达式的结果一开始就为假,则后面的语句一次也不会被执行。这里的语句可以是复合语句。图2.4所示为while语句的执行过程。

图2.4 while语句的执行过程

例2.15:使用while语句计算自然数1~100的累加和。

#include<stdio.h>
main() {
    int i,s=0;
    i=1;
    while (i<=100)
    {                    /* 复合语句循环体 */
      s=s+i;
      i++;
    }                    /* 循环体结束 */
    printf("1+2+ … +100 = %d\n",s);
    while(1);
}

程序执行结果:

1+2+ … +100 = 5050

② 采用do-while语句构成循环结构的一般形式如下:

do语句while(条件表达式);

这种循环结构的特点是先执行给定的循环体语句,然后再检查条件表达式的结果。当条件表达式的值为真(非0值)时,则重复执行循环体语句,直到条件表达式的值变为假(0值)时为止。因此,用do-while语句构成的循环结构在任何条件下,循环体语句至少会被执行一次。图2.5给出了这种循环结构的流程图。

图2.5 do-while循环结构的流程图

例2.16:用do-while语句构成的循环计算自然数1~100的累加和。

#include<stdio.h>
main() {
    int i,s=0;
    i=1;
    do
      {                 /* 复合语句循环体 */
      s=s+i;
      i++;
    }                   /* 循环体结束 */
  while(i<=100);
  printf("1+2+ … +100 = %d\n",s);
  while(1);
}

程序执行结果:

1+2+ ··· +100 = 5050

例2.16的程序与例2.15的程序十分相似。它们的区别仅仅是执行循环体语句和判断条件表达式的结果的顺序不同。另外,用do-while语句构成的循环结构中,while(条件表达式)的后面必须有一个分号,而用while语句构成的循环结构中while(条件表达式)后面是没有分号的。这一点在写程序时一定要注意。

③ 采用for语句构成循环结构的一般形式如下:

for([初值设定表达式];[循环条件表达式];[更新表达式])语句

for语句的执行过程是:先计算出初值设定表达式的值,以此作为循环控制变量的初值,再检查循环条件表达式的结果,当满足条件时就执行循环体语句并计算更新表达式,然后再根据更新表达式的计算结果来判断循环条件是否满足,一直进行到循环条件表达式的结果为假(0值)时退出循环体。for语句的执行过程如图2.6所示。

图2.6 for语句的执行过程

例2.17:用for语句构成的循环计算自然数1~100的累加和。

#include<stdio.h>
main() {
    int i,s=0;
    for (i=1; i<=100; i++)
      s=s+i;                  /* 循环体语句 */
    printf("1+2+ … +100 = %d\n",s);
    while(1);
}

程序执行结果:

1+2+…+100 = 5050

在C语言程序的循环结构中,for语句的使用最为灵活,它不仅可以用于循环次数已经确定的情况,而且可以用于循环次数不确定而只给出循环结束条件的情况。另外,for语句中的三个表达式是相互独立的,并不一定要求三个表达式之间有依赖关系。并且for语句中的三个表达式都可能缺省,但无论缺省哪一个表达式,其中的两个分号都不能缺省。一般不要缺省循环条件表达式,以免形成死循环。

例2.18:for语句中缺省表达式的例子——计算自然数1~100的累加和。

#include<stdio.h>
main() {
    int i,s=0;
    i=1;                    /* 设置循环初值 */
    for ( ;i<=100 ;) {    /* 缺省初值设定表达式和更新表达式 */
      s=s+i;                /* 循环体语句 */
      i++;                  /* 循环控制变量更新 */
      }
    printf("1+2+ … +100 = %d\n",s);
    while(1);
}

程序执行结果:

1+2+…+100 = 5050

④ goto语句是一个无条件转向语句,它的一般形式为:

goto语句标号;

其中语句标号是一个带冒号“:”的标识符。将goto语句和if语句一起使用,可以构成一个循环结构。但更常见的是在C语言程序中采用goto语句来跳出多重循环,需要注意的是只能用goto语句从内层循环跳到外层循环,而不允许从外层循环跳到内层循环。

例2.19:使用goto语句跳出循环结构。

本程序采用循环结构来求一整数的等差数列,该数列满足条件:头四个数的和值为26,积值为880。该数列的公差应为正整数,否则将产生负的项,此外该数列的首项数必须小于5,且其公差也应小于5,否则头四项的和值将大于26。

#include<stdio.h>
main() {
    int a,b,c,d,i;
    for(a=1; a<5; ++a) {
      for(d=1; d<5; ++d) {
        b=a+(a+d)+(a+2*d)+(a+3*d);
        c=a*(a+d)*(a+2*d)*(a+3*d);
        if(b==26 && c==880)
          goto pt;
      }
    }
    pt: for(i=0; i<=10; ++i)
          printf("%d,",a+i*d);
    printf("…\n");
    while(1);
}

程序执行结果:

2,5,8,11,14,17,20,23,26,29,…

在这个程序中采用for语句构成了两重循环嵌套,即在第一个for语句的循环体中又出现了另一个for语句的循环体,需要时还可以构成多重循环结构。程序在最内层循环体中采用了一个goto语句,它的作用是直接跳出两层循环,即跳到第一层循环体外边由标号pt:所指出的地方。前面在介绍开关语句时提到采用break语句可以跳出开关语句,break语句还可以用于跳出循环语句。对于上面的例子,也可以采用break语句来终止循环。

例2.20:用break语句终止循环。

#include<stdio.h>
main() {
    int a,b,c,d,i;
    for(a=1; a<5; ++a) {
      for(d=1; d<5; ++d) {
        b=a+(a+d)+(a+2*d)+(a+3*d);
        c=a*(a+d)*(a+2*d)*(a+3*d);
        if(b==26 && c==880)
          break;
        }
        if(b==26 && c==880)
          break;
      }
    for(i=0; i<=10; ++i)
      printf("%d,",a+i*d);
    printf("…\n");
    while(1);
}

程序执行结果:

2,5,8,11,14,17,20,23,26,29,…

从例2.20可以看到,对于多重循环的情况,break语句只能跳出它所处的那一层循环,而不像goto语句可以直接从最内层循环中跳出来。由此可见,要退出多重循环时,采用goto语句比较方便。需要指出的是,break语句只能用于开关语句和循环语句之中,它是一种具有特殊功能的无条件转移语句。另外还要注意,在进行实际程序设计时,为了保证程序具有良好的结构,应当尽可能少地采用goto语句,以使程序结构清晰易读。

在循环结构中还可以使用一种中断语句continue,它的功能是结束本次循环,即跳过循环体中下面尚未执行的语句,把程序流程转移到当前循环语句的下一个循环周期,并根据循环控制条件决定是否重复执行该循环体。continue语句的一般形式为:

continue;

continue语句通常和条件语句一起用在由while、do-while和for语句构成的循环结构中,它也是一种具有特殊功能的无条件转移语句,但与break语句不同,continue语句并不跳出循环体,而只是根据循环控制条件确定是否继续执行循环语句。

例2.21:利用continue语句把10~20之间不能被3整除的数输出。

#include<stdio.h>
main() {
    int n;
    for(n=10; n<=20; n++) {
      if(n%3==0)
        continue;
      printf("%d ",n);
      }
    while(1);
}

程序执行结果:

10  11  13  14  16  17  19  20

2.3.6 返回语句

返回语句用于终止函数的执行,并控制程序返回到调用该函数时所处的位置。返回语句有两种形式:

return(表达式);
或者return;

如果return语句后边带有表达式,则要计算表达式的值,并将表达式的值作为该函数的返回值。若使用不带表达式的第2种形式,则被调用函数返回主调用函数时,函数值不确定。一个函数的内部可以含有多个return语句,但程序仅执行其中的一个return语句而返回主调用函数。一个函数的内部也可以没有return语句,在这种情况下,当程序执行到最后一个界限符“}”处时,就自动返回主调用函数。

例2.22:return语句的使用。

#include <stdio.h>
main() {                                 /* 主函数体 */
    int x,n,p,power(int x,int n);
    printf("calculate X to the power of N\n please input X and N ? ");
    scanf("%d%d",&x,&n);
    p = power(x,n);                      /* 在此处调用函数 */
    printf("%d to the power of %d = %d\n",x,n,p);
    while(1);
}
int power(int x,int n)                   /* 被调用函数 */
int x,n;
{
    int i,p = 1;
    for( i=0; i<n; ++i ) p *= x;
    return( p );                         /* 带值返回到调用处 */
}

程序执行结果:

calculate X to the power of N
please input X and N ?  5 3  回车
5 to the power of 3 = 125