原创 预处理命令基础和常用预处理命令实例

2011-8-25 10:23 2446 8 9 分类: MCU/ 嵌入式

 

引子:

最近在学习Protothreads--PT是一中轻量级、无堆栈轮询任务简单的实时操作系统。PT采用纯C(就是很多宏(MACRO))代码,可以在任何平台上使用,和硬件结构无关。在调试跟踪代码,一: 12pt">中的宏代码时,发现有些宏代码确实令人费解?!看来自己在预处理这方面功夫不深了。

例如:

struct pt { unsigned short lc; };

#define PT_THREAD(name_args)  char name_args

#define PT_BEGIN(pt)           switch(pt->lc) { case 0:

#define PT_WAIT_UNTIL(pt, c)   pt->lc = __LINE__; case __LINE__: \

                              if(!(c)) return 0

#define PT_END(pt)            } pt->lc = 0; return 2

#define PT_INIT(pt)           pt->lc = 0

其中红色标记的宏就让我“丈二和尚莫不知头脑”,不能理解???!!看来是要对编译器预处理机制和预处理命令做一下深入了解了,提升一下自己的功力了……

预处理命令基础和常用预处理命令使用示例

程序设计语言的预处理的概念:在编译之前进行的处理。 C语言的预处理主要有三个方面的内容:

 1.宏定义; 2.文件包含; 3.条件编译。 预处理命令以符号“#”开头。

.宏定义

1.不带参数的宏定义:

  宏定义又称为宏代换、宏替换,简称

  格式:

  #define 标识符 字符串

  其中的标识符就是所谓的符号常量,也称为宏名

  预处理(预编译)工作也叫做宏展开:将宏名替换为字符串。

  掌握""概念的关键是。一切以换为前提、做任何事情之前先要换,准确理解之前就要

  即在对相关命令或语句的含义和功能作具体分析之前就要换:

  例:

  #define PI 3.1415926

  把程序中出现的PI全部换成3.1415926

说明:

1)宏名一般用大写

2)使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如:数组大小常用宏定义

3)预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。

4)宏定义末尾不加分号;

5)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。

6)可以用#undef命令终止宏定义的作用域

7)宏定义可以嵌套

8)字符串" "中永远不包含宏

9)宏定义不分配内存,变量定义分配内存。

2.带参数的宏

  除了一般的字符串替换,还要做参数代换

  格式:

  #define 宏名(参数表) 字符串

  例如:#define S(a,b) a*b

  area=S(3,2);第一步被换为area=a*b; ,第二步被换为area=3*2;

  类似于函数调用,有一个哑实结合的过程:

1)实参如果是表达式容易出问题

  #define S(r) r*r

  area=S(a+b);第一步换为area=r*r;,第二步被换为area=a+b*a+b;

  正确的宏定义是#define S(r) (r)*(r)

2)宏名和参数的括号间不能有空格

3)宏替换只作替换,不做计算,不做表达式求解

4)函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存

5)宏的哑实结合不存在类型,也没有类型转换。

6)函数只有一个返回值,利用宏则可以设法得到多个值

7)宏展开使源程序变长,函数调用不会

8)宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)

. 文件包含

  一个文件包含另一个文件的内容

  格式:

  #include "文件名"

  或

  #include <文件名>

编译时以包含处理以后的文件为编译单位,被包含的文件是源文件的一部分。编译以后只得到一个目标文件.obj 被包含的文件又被称为标题文件头部文件头文件,并且常用.h作扩展名。修改头文件后所有包含该文件的文件都要重新编译 头文件的内容除了函数原型和宏定义外,还可以有结构体定义,全局变量定义:

1)一个#include命令指定一个头文件;

2)文件1包含文件2,文件2用到文件3,则文件3的包含命令#include应放在文件1的头部第一行;

3)包含可以嵌套;

4<文件名>称为标准方式,系统到头文件目录查找文件,

  "文件名"则先在当前目录查找,而后到头文件目录查找;

5)被包含文件中的静态全局变量不用在包含文件中声明。

. 条件编译

  有些语句行希望在条件满足时才编译。

  格式:(1

  #ifdef 标识符

  程序段1

  #else

  程序段2

  #endif

  或

  #ifdef

  程序段1

  #endif

  当标识符已经定义时,程序段1才参加编译。

  格式:(2

  #ifndef 标识符

  格式:(3

  #if 表达式1

  程序段1

  #else

  程序段2

  #endif

  当表达式1成立时,编译程序段1,当不成立时,编译程序段2

  使用条件编译可以使目标程序变小,运行时间变短。预编译使问题或算法的解决方案增多,有助于我们选择合适的解决方案。此外,还有布局控制:#pragma,这也是我们应用预处理的一个重要方面,主要功能是为编译程序提供非常规的控制流信息。

 

 

常用的预处理命令如下:

l#define        定义一个预处理宏

l#undef         取消宏的定义

l#include       包含文件命令

l#include_next  #include相似, 但它有着特殊的用途

l#if            编译预处理中的条件命令, 相当于C语法中的if语句

l#ifdef         判断某个宏是否被定义, 若已定义, 执行随后的语句

l#ifndef        #ifdef相反, 判断某个宏是否未被定义

l#elif          #if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if

l #else          #if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else

l #endif         #if, #ifdef, #ifndef这些条件命令的结束标志.

l #line          标志该语句所在的行号

l              将宏参数替代为以参数值为内容的字符窜常量

l##              将两个相邻的标记(token)连接为一个单独的标记

 

常用的预处理命令中编程技巧:

1)宏定义语句中可以利用"\"来换行.

e.g.

#  define  ONE 1

等价于: #define ONE 1

#define err(flag, msg) if(flag) \

    printf(msg)

等价于: #define err(flag, msg) if(flag) printf(msg)

 

2)函数宏之后的参数要用括号括起来

e.g.

#define mul(x,y) ((x)*(y))

看看下面这个例子:
e.g.
#define mul(x,y) x*y
"mul(1, 2+2);" 将被扩展为: 1*2 + 2
同样, 整个标记串也应该用括号引用起来:
e.g.
#define mul(x,y) (x)*(y)
sizeof mul(1,2.0) 将被扩展为 sizeof 1 * 2.0

 

3)函数宏的参数问题

e.g.
#define insert(stmt) stmt
insert ( a=1; b=2;) 相当于在代码中加入 a=1; b=2 .
insert ( a=1, b=2;)
 就有问题了: 预处理器会提示出错: 函数宏的参数个数不匹配. 预处理器把","视为参数间的分隔符.  
insert ((a=1, b=2;))
可解决上述问题.

 

4do-while(0)语句

#define swap(x,y) \
      do { unsigned long _temp=x; x=y; y=_tmp} while (0)

swap(1,2); 将被替换为: do { unsigned long _temp=1; 1=2; 2=_tmp} while (0);

Linux内核源代码中对这种do-while(0)语句有这广泛的应用.

 

5) 宏可以被多次定义, 前提是这些定义必须是相同的.

这里的"相同"要求先后定义中空白符出现的位置相同, 但具体的空白符类型或数量可不同, 比如原先的空格可替换为多个其他类型的空白符: 可为tab

e.g.
#define NULL 0
#define NULL
      0

上面的重定义是相同的, 但下面的重定义不同:

#define fun(x) x+1
#define fun(x) x + 1
: #define fun(y) y+1
如果多次定义时, 再次定义的宏内容是不同的, gcc会给出"NAME redefined"警告信息.

 

6) gcc, 可在命令行中指定对象宏的定义:
e.g.
$ gcc -Wall -DMAX=100 -o tmp tmp.c   //-D
是前缀

相当于在tmp.c中添加" #define MAX 100".

那么, 如果原先tmp.c中含有MAX宏的定义, 那么再在gcc调用命令中使用-DMAX, 会出现什么情况呢?
----DMAX=1, 则正确编译.
---
-DMAX的值被指定为不为1的值, 那么gcc会给出MAX宏被重定义的警告, MAX的值仍为1.

 

注意: 若在调用gcc的命令行中不显示地给出对象宏的值, 那么gcc赋予该宏默认值(1), : -DVAL == -DVAL=1

 

7) #define所定义的宏的作用域
宏在定义之后才生效, 若宏定义被#undef取消, #undef之后该宏无效. 并且
字符串中的宏不会被识别
e.g.
#define TWO 2      //   sum = ONE + TWO     

char c[] = "TWO"  

 

8) 使用#if可以提升代码的可移植性---针对不同的平台使用执行不同的语句. 也经常用于大段代码注释.
e.g.
#if 0
{
    
一大段代码;
}
#endif

 

9)如果常量表达式为一个未定义的宏, 那么它的值被视为0.
#if MACRO_NON_DEFINED
  //相当于 #if 0
在判断某个宏是否被定义时, 应当避免使用#if, 因为该宏的值可能就是被定义为0. 而应当使用#ifdef#ifndef.

 

10#ifdef#ifndef经常用于避免头文件的重复引用:
#ifndef __FILE_H__
  #define __FILE_H__
  #include "file.h"
#endif

代表如果还未定义__FILE_H__,就执行下面两行代码,也就宏定义__FILE_H__引用头文件。

 

11defined(name):它有返回值,若宏被定义,则返回1, 否则返回0.
它与#if, #elif, #else结合使用来判断宏是否被定义, 乍一看好像它显得多余, 因为已经有了#ifdef#ifndef. defined用于在一条判断语句中声明多个判别条件:

#if defined(VAX) && defined(UNIX) && !defined(DEBUG)

 

12)我们对#include_next不太熟悉. #include_next仅用于特殊的场合. 它被用于头文件中(#include既可用于头文件中, 又可用于.c文件中)来包含其他的头文件. 而且包含头文件的路径比较特殊: 从当前头文件所在目录之后的目录来搜索头文件.
比如: 头文件的搜索路径一次为A,B,C,D,E. #include_next所在的当前头文件位于B目录, 那么#include_next使得预处理器从C,D,E目录来搜索#include_next所指定的头文件.

 

13)下面列出一些标准C中常见的预定义对象宏(其中也包含gcc自己定义的一些预定义宏:
__LINE__ 当前语句所在的行号, 10进制整数标注.

__FILE__ 当前源文件的文件名, 以字符串常量标注.

__DATE__ 程序被编译的日期, "Mmm dd yyyy"格式的字符串标注.

__TIME__ 程序被编译的时间, "hh:mm:ss"格式的字符串标注, 该时间由asctime返回.

__STDC__ 如果当前编译器符合ISO标准, 那么该宏的值为1

__STDC_VERSION__ 如果当前编译器符合C89, 那么它被定义为199409L, 如果符合C99, 那么被定义为199901L.
__STDC_HOSTED__  如果当前系统是"本地系统(hosted)", 那么它被定义为1. 本地系统表示当前系统拥有完整的标准C.

 

gcc定义的预定义宏:

__OPTMIZE__      如果编译过程中使用了优化, 那么该宏被定义为1.
__OPTMIZE_SIZE__   同上, 但仅在优化是针对代码大小而非速度时才被定义为1.
__VERSION__      显示所用gcc的版本号.
要想看到gcc所定义的所有预定义宏, 可以运行: $ cpp -dM /dev/null

 

14 #, ##的使用

#用于在宏扩展之后将宏定义参数转换为以此参数为内容的字符串常量.
e.g.
#define TEST(a,b) printf( #a "<" #b "=%d\n", (a)<(b));  

那么调用TEST(10,9)的效果是printf( 10<9 =%d\n", (10)<(9));

注意: #只针对紧随其后的token有效!

##用于将它前后的两个token组合在一起转换成以这两个token为内容的字符串常量. 注意##前后必须要有token.
e.g.
#define TYPE(type, n) type## n
之后调用: 
TYPE(int, a) = 1;
TYPE(long, b) = 1999;
将被替换为:
int a = 1;
long b = 1999;

 

结语:

#define PT_WAIT_UNTIL(pt, c)   pt->lc = __LINE__; case __LINE__: \

                              if(!(c)) return 0

“修炼上面秘技”以后,“功力”大增,再看这个先前令人费解的MACRO,就是小菜一碟了。原来这个宏使用的方法是挺综合的是一个带参数的双重宏。

#define  __LINE__两个宏同时使用。

PT的任务中如果使用该宏:

 PT_WAIT_UNTIL(pt, counter == 1000); //假设改行在文件中的行号为39

把该语句展开后就是:

 

 

 

   pt->lc =39

   case 39

                    if((Counter ==1000))

           return 0;  //PT_WAITING切换到其他任务执行

         

 

 

PARTNER CONTENT

文章评论1条评论)

登录后参与讨论

xucun915_925777961 2011-8-25 13:07

来学习一下^_^
相关推荐阅读
用户967659 2011-10-06 13:50
FAT16格式SD卡磁盘存储结构分析
本文根据“file allocation table - 16bit”(16位文件分配表)一文,使用WinHex工具分析FAT16格式的SD卡存储结构。 一.格式化后,放入文件和文件夹SD卡抓图: ...
用户967659 2011-10-06 11:16
FAT16文件系统磁盘存储结构
file allocation table - 16bit clark@hushmail.com | updates closed forewordsthis article is about f...
用户967659 2011-10-06 10:43
电容滤波要注意的两个要点
电容在EMC设计中非常重要,也是我们常用的滤波元件!但在实际过程中,大家对电容的使用并不是很明确,存在像:滤波电容值越大越好等理解误区!这里我把电容滤波的两个要点介绍一下: 1、电容滤波是有频段的,...
用户967659 2011-09-03 16:35
CRC校验C语言实现
一般标准的通讯协议中都需要有校验方法来校验通讯过程中的数据的正确性,如我们工程中用的标准MODBUS串行通讯网络采用两种错误校验方法,奇偶校验可以用于检验每一个字符,信息帧校验(LRC或CRC)使用于...
用户967659 2011-08-25 10:22
Protothreads简介
2008-10-18 20:44 951人阅读 评论(0) 收藏 举报     原来在用RTOS时感觉特别方便的就是任务函数中可以调用系统提供的延时,等待等函数,切换到其它任务上执行,这样写程序又...
EE直播间
更多
我要评论
1
8
关闭 站长推荐上一条 /3 下一条