预处理命令在我之前看过的C语言基础教程中好像并没有详细说到,在现在的一些项目中预处理命令的出现频率却越来越多。事物的存在必有其存在的理由,于是就花时间去琢磨了一下,以及查阅相关资料,发现使用预处理命令去优化代码可以达到很好的效果。预处理命令在某资料中是这样描述的"C程序的源代码中可包括各种编译指令,这些指令称为预处理命令。虽然它们实际上不是C语言的一部分,但却扩展了C程序设计的环境"。预处理命令实质上是运行在编译器编译过程的指令。所以说在嵌入式程序设计中使用预处理命令不会占用最终目标运行系统的存储空间。
在ANSI标准定义的C语言预处理程序中包括下列命令:
#include,#define,#if,#else,#elif,#endif,#ifdef,#ifndef,#error,#undef,#line,#pragma等。从以上可以看出预处理命令的共同特点就是以"#"开头。下面就分别介绍几个在项目中使用比较多的预处理命令。
这个预处理命令算是使用的最多而又最重要的一个预处理命令了。它的作用你是否还记得?include就是"包含"的意思,在程序编译的时候预处理器看到#include就会把<>尖括号或者" "中的那个文件找到,然后用该文件的内容替换掉#include <>这一行。
使用方法或者格式:
#include <xxx.h>
#define叫做宏定义,标识符为所定义的宏名,简称宏。标识符的命名规则与前面讲的变量的命名规则是一样的。#define 的功能是将标识符定义为其后的常量。一经定义,程序中就可以直接用标识符来表示这个常量。是不是与定义变量类似?但是要区分开!变量名表示的是一个变量,但宏名表示的是一个常量。可以给变量赋值,但绝不能给常量赋值。宏所表示的常量可以是数字、字符、字符串、表达式。使用宏定义可以用宏代替一个在程序中经常使用的常量。注意,是"经常"使用的。这样,当需要改变这个常量的值时,就不需要对整个程序一个一个进行修改,只需修改宏定义中的常量即可。且当常量比较长时,使用宏就可以用较短的有意义的标识符来代替它,这样编程的时候就会更方便,不容易出错。因此,宏定义的优点就是方便和易于维护。
需要注意的是,预处理指令不是语句,所以后面不能加分号。
使用方法或者格式:
#define xxx 1 //定义常量
#define xxx 'a' //定义字符
#define xxx "abcb" //定义字符串
#define xxx abc() //定义函数
还可以这样:
#define XX_ABS(x) ((x)>=0? (x):-(x)) //返回绝对值
#define XX_SIGN(x) ((x<0)?-1:((x>0)?1:0)) //正数返回 1,负数返回 -1 零返回 0
#ifndef是"if not defined"的简写,是宏定义的一种,它是可以根据是否已经定义了一个变量来进行分支选择,一般用于调试等。实际上确切地说这应该是预处理功能中三种(宏定义,文件包含和条件编译)中的第三种——条件编译。
条件指示符#ifndef的最主要目的是防止头文件的重复包含和编译。条件编译当然也可以用条件语句来实现, 但是用条件语句将会对整个源程序进行编译,生成的目标代码程序很长,而采用条件编译,则根据条件只编译其中的程序段1或程序段2,生成的目标程序较短。如果条件选择的程序段很长,采用条件编译的方法是十分必要的。
假如你有一个C源文件,它包含了多个头文件,比如头文件A和头文件B,而头文件B又包含了头文件A,则最终的效果是,该源文件包含了两次头文件A。如果你在头文件A里定义了结构体或者类类型(这是最常见的情况),那么问题来了,编译时会报大量的重复定义错误。
使用方法或者格式:
//经常在H文件(头文件)看到是格式:
#ifndef 标识符
#define 标识符
/*************
程序段
***************/
#endif
其他格式:
#ifndef表达式
/***********
程序段 1
***********/
#else
/***********
程序段 2
***********/
#endif
#if,#else,#elif,#endif通常是组合使用,#if 叫条件预处理命令主要作用跟if判断语句相似,格式上的区别在在于使用#if需要使用#endif以表示结束。否则编译器会报错。主要作用就是通过判断条件是否满足去选择编译程序段。
使用方法或者格式:
格式1
#if XXX
/*****
程序段
*****/
#endif
格式2
#if XXX
/*****
程序段1
*****/
#else
/*****
程序段2
*****/
#endif
格式3
#ifndef XXX 0 //
#if (XXX==1)
/*****
程序段1
*****/
#elif (XXX==2)
/*****
程序段2
*****/
#else
/*****
程序段3
*****/
#endif
在以上的格式基础上如果再加上#define宏定义就更好地去控制了。比如下面的这个例子:
在一个嵌入式的小系统中有两种调试方式,一种是串口调试,一种是USB转串口调试。串口调试一般使用在系统程序开发阶段调试。而USB转串口调试一般使用在系统开发完成后的功能调试,因为USB还可以实现文件系统的挂载,在后期使用USB进行固件升级是非常方便的。现在的问题就是用什么方法可以更简单地实现串口调试与USB转串口调试的快速转换。
其中的方法之一就是使用条件预处理命令实现切换,
有四个函数:
USB_Init();
Usb_printf(char *p); //向usb转串口打印调试信息
Usart_Init();
Usart_printf(char *p); //向串口打印调试信息
实例过程如下:
#define PRMOS 0 //0 使用串口打印 1 使用usb打印信息
#if PRMOS
#define DEBUG(s) Usb_printf(s)
#else
#define DEBUG(s) Usart_printf(s)
#endif
int main(void)
{
#if PRMOS
USB_Init();
#else
Usart_Init();
#endif
While(1)
{
DEBUG("Hello, World! n");
Delay_ms(1000);
}
}
大致的思路就是使用#define定义一个常量和使用同一个标识符分别定义串口打印函数和usb打印函数,然后使用条件判断预处理#if通过判断PRMOS标识符选择编译哪一部分,在程序设计的时候也需要使用#if把USB和串口相关的代码区分开。这样在编译的时候始终都是编译一部分,所以整体的编译速度或者占用空间都是比较少的。在功能切换时只需要更改PRMOS的值就完成了全部代码的修改,这样是不是很方便。
这种程序设计方法在一些实时操作系统的配置中使用的比较多,例如FreeRTOS中的功能配置
总结。预处理命令主要是使用在程序的优化上。可以使程序设计更灵活更好看等等。