4.2.2 C51语言源程序的编辑和编译
1.C51语言源程序的编辑
C51语言源程序由一个或多个函数构成,其中至少应包含一个主函数main()。其他功能函数可以是C51语言编译器提供的库函数,也可以是用户按需要自行编写的函数。不管main()函数处于程序中什么位置,程序总是从main()开始执行。main()函数可以调用其他功能函数,但不能被其他函数调用。被调用函数如果位于主调函数前面则可直接调用,否则要先说明后才能调用。C51语言程序的一般结构如下:
编写C51语言程序要注意以下几点:
(1)函数以“{”开始,以“}”结束,二者必须成对出现,它们之间的部分为函数体。
(2)C51语言程序没有行号,一行内可以书写多条语句,一条语句也可以分写在多行上。
(3)每条语句最后必须以一个分号“;”结尾,分号是C51语言程序的必要组成部分。
(4)每个变量必须先定义后引用。函数内部定义的变量为局部变量,又称内部变量,只有在定义它的那个函数之内才能够使用。在函数外部定义的变量为全局变量,又称外部变量,在定义它的那个程序文件中的函数都可以使用它。
(5)对程序语句的注释必须放在双斜杠“//”之后,或者放在“/*……*/”之内。
2.C51语言源程序的编译
编译就是把文本形式源代码翻译成机器语言形式的目标文件的过程。C51语言源程序的编译过程就是要把我们编写的一个C51语言源程序转换成可以在硬件上运行的程序(可执行代码),这个过程又可以分为预编译、编译、汇编和链接等几个阶段。
(1)预编译
预编译指的是在正式的编译阶段之前进行的预处理阶段。这一阶段将根据已放置在文件中的预处理指令来修改源文件的内容。例如,#include指令就是一个预处理指令,它可以把头文件的内容添加到预处理后的文件中。因为编程时可用的硬件或操作系统是不同的,所以不同的环境所需的代码可能有所不同,这种在编译之前修改源文件的方式为编程提供了很大的灵活性,可以适应不同的计算机和操作系统环境。在许多情况下,可以把用于不同环境的代码放在同一个文件中,在预处理阶段自动修改代码,使之适应当前的环境。
预编译程序所完成的是对源程序的“替代”工作。经过此种“替代”,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。
(2)编译
第二个阶段是编译、优化阶段。经过预编译得到的输出文件中,只有常量,如数字、字符串、变量等的定义,以及C语言的关键字,如main、if、else、for、while、{、}、+、—、*、/等。编译程序所要做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码或汇编代码。
优化处理是编译系统中一项比较艰深的技术,它涉及的问题不仅同编译技术本身有关,而且同机器的硬件环境有很大的关系。在优化处理过程中,一部分优化是对中间代码的优化,这种优化不依赖于具体的计算机,其主要工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播以及删除无用赋值等;另一部分优化则主要针对目标代码的生成而进行的,这种优化同机器的硬件结构密切相关,最主要的是考虑如何充分利用机器的各个硬件寄存器存放有关变量的值,以减少对于内存的访问次数。
(3)汇编
汇编是指把汇编代码翻译成目标机器指令的过程。对于被汇编系统处理的每一个C语言源程序,最终都将经过这一处理而得到相应的目标文件。目标文件中存放的就是与源程序等效的机器语言代码。
(4)链接
由汇编所生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题,如某个源文件中可能引用了另一个源文件中定义的变量或者函数,在程序中可能调用了某个库文件中的函数等。所有的这些问题都需要经链接程序的处理方能得以解决。链接的主要工作就是将有关的目标文件彼此相连接,即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个单片机可以识别的目标文件(hex文件)。
3.C51语言源程序常用预处理指令
在C51语言编程的过程中,如果希望在编译时包含其他的源文件,定义宏或者根据条件决定编译时是否包含某些代码,就需要使用预处理指令。
预处理指令是以“#”号开头的代码行,除了空白字符外,“#”号必须是该行的第一个字符,“#”后是指令关键字。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码进行预处理。
预处理过程会读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。
C51语言源程序常用的预处理指令如表4-2所示。
表4-2 C51语言源程序常用预处理指令
(1)文件包含指令
文件包含指令#include的作用是在指令处展开被包含的文件。包含可以是多重的,一个被包含的文件中还可以包含其他文件,标准C语言编译器至少支持八重嵌套包含。在程序中使用#include指令有两种方式,分别是:
第一种方式是用尖括号把头文件括起来,这种方式告诉预处理程序该头文件是在编译器自带的头文件库或者外部头文件库中;第二种方式是用双引号把头文件括起来,这种方式告诉预处理程序该头文件是在当前被编译的应用程序的源代码文件中,如果找不到,再去编译器自带的头文件库中搜索。
一个应用程序既可以包含编译器提供的公共头文件,也可以包含用户自定义的头文件。编译器是安装在公共子目录下的,而被编译的应用程序是安装在用户自定义的子目录下的,所以,通过两种不同的命令格式就可以区分头文件的位置。
(2)宏定义指令
宏定义指令#define的作用是定义一个代表特定内容的标识符。在预处理过程中,会把源代码中出现的宏标识符替换成宏定义时的值。最常见的一种宏定义用法是定义代表某个值的全局符号,另一种宏定义用法是定义带参数的宏,这样的宏可以像函数一样被调用,在调用宏时,用实际参数代替定义中的形参。
该指令的一般格式为:首先声明一个标识符,然后给出这个标识符代表的代码。在后面的源程序中,就用这些标识符来替代该代码。习惯上,为了把程序中的宏标识符和变量标识符区别开来,我们总是用大写字母来定义宏,如
在上述例子中,使用宏定义指令将数值20赋予符号NUM,这样,符号NUM就代表20,在后面的程序中,如果想要使用20这个数值,则直接引用NUM就可以了。例如,在程序中定义了一个数组array,该数组所能容纳的最大元素数目为20,如果想要改变数组的大小,只需要更改宏定义并重新编译程序即可。程序中可以多次使用这个宏定义,因此,当宏定义中的数值改变了,所有引用该宏定义的地方都会改变。
宏表示的值既可以是一个数值,也可以是一个字符串常量,还可以是一个常量表达式,并且允许包括前面已经定义的宏标识符,如
有时还可以使用带参数的#define指令,带参数的#define指令和函数调用看起来有些相似,如
预处理过程把上面的一行代码转换成“dat=5×5×5;”。
(3)条件编译指令
条件编译指令的作用是决定哪些代码被编译,哪些不被编译。在使用过程中,可以根据表达式的值或者某个特定的宏是否被定义来确定下面的语句是否被编译。
①#if和#endif指令
#if指令用来检测关键字后的常量表达式是否为真。如果表达式为真,则编译后面的代码,直到出现#else、#elif或#endif为止;如果表达式为假,则不编译后面的代码。#endif用于终止#if预处理指令。例如,
在上述程序中,由于宏定义RUN代表0,所以#if条件为假,不编译后面的代码,直到#endif,程序直接输出End。如果将#define语句去掉,效果是一样的。
②#ifdef和#ifndef
#ifdef指令的作用是检测关键字后的常量是否被声明过。如果常量被声明过,则编译后面的代码,直到出现#else、#elif或#endif为止;如果常量没有被声明过,则不编译后面的代码。#ifndef用法与#ifdef用法相反。例如,
在上述程序中,由于声明了RUN宏,所以#ifdef条件为真,编译后面的代码,直到#endif,程序直接输出Start。若#ifndef条件为假,则不编译后面的代码。
③#else和#elif指令
#else指令的作用是用于某个#if指令之后,当前面的#if指令的条件不为真时,就编译#else后面的代码,#endif指令将用来终止上面的条件块。#elif预处理指令综合了#else和#if指令的作用。
在上述程序中,由于声明了宏RED,所以#elif条件为真,编译后面的代码,直到#else,程序直接输出Red。若其他条件为假,则不编译后面的代码。
4.MCS-51单片机头文件详解
我们在用C语言编程时往往第一行就是文件包含指令,MCS-51单片机的头文件为reg51.h或reg52.h,在这些文件中包含了很多对寄存器地址的声明。
(1)文件包含的意义
文件包含是指在一个文件内将另外一个文件的内容全部包含进来。在编程的时候,如果某些定义和命令使用的频率很高,几乎每个程序中都可能要用到,那么为了提高编程效率,减少编程人员的重复劳动,将这些定义和命令单独组成一个文件(如MCS-51单片机的头文件reg51.h),在使用这些定义和命令时,只需要在程序开头将这个文件包含进来就可以了(如#include<reg51.h>)。
(2)寄存器地址的声明
MCS-51单片机有21个SFR,每个SFR都有自己的字节地址,部分SFR还有独立的位地址,如果在编程的时候要使用这些寄存器,就需要事先声明这些寄存器的名称和地址。为此,C51语言采用了两种专属的变量说明指令sfr和sbit来声明寄存器名称和地址,它们并非标准C51语言的关键字,而是Keil软件为了能直接访问MCS-51单片机中的SFR而提供的新关键字。
sfr指令用来声明特殊功能寄存器的名称和字节地址,其一般用法是:
例如,sfr P1=0x90是声明P1口所对应的特殊功能寄存器的变量名为P1,它在内存中对应的地址为0x90。
sbit指令用来声明那些可位寻址的特殊功能寄存器的名称和位地址,其一般用法有三种。
第一种:
例如,sbit IT0=0x88是声明外部中断0触发控制位的变量名为IT0,它的位地址为0x88。
第二种:
例如,sbit EA=0x A8^7是声明中断总允许位的变量名为EA,它的位地址是IE寄存器(地址为0x A8)的第7位。
第三种:
例如,sbit P1_2=P1^2是声明I/O口P1.2的变量名为P1_2,它的位地址是P1寄存器位置的第2位。
在声明特殊功能寄存器时,寄存器的名称可以由用户随便定义,但必须满足C语言对变量名的定义规则。只有对寄存器及相关位进行声明后,我们才能对其进行赋值,Keil软件才能编译通过。在头文件reg51.h中包含了对绝大多数特殊寄存器的声明(没有针对I/O口的位声明),如果要使用它们,在程序开头用文件包含指令并将头文件reg51.h包含进去就可以了。
(3)头文件reg51.h原文及解释
reg51.h文件一般在“C:\KEIL\C51\INC”下,在INC文件夹内还有一些其他的头文件。下面附出头文件reg51.h的原文,并把注释一并附后。