预处理命令

 
将一个 C 源程序转换为可执行程序的过程中 , 编译预处理是最初的步骤 . 这一步骤是由预处理器 (preprocessor) 来完成的 . 在源流程序被编译器处理之前 , 预处理器首先对源程序中的 " (macro)" 进行处理 .

 
一般的 C 编译器都将预处理 , 汇编 , 编译 , 连接过程集成到一起了 . 编译预处理往往在后台运行 . 在有的 C 编译器中 , 这些过程统统由一个单独的程序来完成 , 编译的不同阶段实现这些不同的功能 . 可以指定相应的命令选项来执行这些功能 . 有的 C 编译器使用分别的程序来完成这些步骤 . 可单独调用这些程序来完成 . gcc , 进行编译预处理的程序被称为 CPP, 它的可执行文件名为 cpp.

 
编译预处理命令的语法与 C 语言的语法是完全独立的 . 比如 : 可以将一个宏扩展为与 C 语法格格不入的内容 , 但该内容与后面的语句结合在一个若能生成合法的 C 语句 , 也是可以正确编译的 .

(
) 预处理命令简介
预处理命令由 #(hash 字符 ) 开头 , 它独占一行 , # 之前只能是空白符 . # 开头的语句就是预处理命令 , 不以 # 开头的语句为 C 中的代码行 . 常用的预处理命令如下 :
#define
             
定义一个预处理宏
#undef              
取消宏的定义

#include           
包含文件命令
#include_next    
#include 相似 , 但它有着特殊的用途

#if                     
编译预处理中的条件命令 , 相当于 C 语法中的 if 语句
#ifdef               
判断某个宏是否被定义 , 若已定义 , 执行随后的语句
#ifndef              
#ifdef 相反 , 判断某个宏是否未被定义
#elif                 
#if, #ifdef, #ifndef 或前面的 #elif 条件不满足 , 则执行 #elif 之后的语句 , 相当于 C 语法中的 else-if
#else               
#if, #ifdef, #ifndef 对应 , 若这些条件不满足 , 则执行 #else 之后的语句 , 相当于 C 语法中的 else
#endif              #if, #ifdef, #ifndef
这些条件命令的结束标志 .
defined           
#if, #elif 配合使用 , 判断某个宏是否被定义

#line               
标志该语句所在的行号
#                     
将宏参数替代为以参数值为内容的字符窜常量
##                  
将两个相邻的标记 (token) 连接为一个单独的标记
#pragma         
说明编译器信息

#warning      
显示编译警告信息
#error           
显示编译错误信息

(
) 预处理的文法
 
预处理并不分析整个源代码文件 , 它只是将源代码分割成一些标记 (token), 识别语句中哪些是 C 语句 , 哪些是预处理语句 . 预处理器能够识别 C 标记 , 文件名 , 空白符 , 文件结尾标志 .

预处理语句格式 :    #command name(...) token(s)
1, command
预处理命令的名称 , 它之前以 # 开头 , # 之后紧随预处理命令 , 标准 C 允许 # 两边可以有空白符 , 但比较老的编译器可能不允许这样 . 若某行中只包含 #( 以及空白符 ), 那么在标准 C 中该行被理解为空白 . 整个预处理语句之后只能有空白符或者注释 , 不能有其它内容 .
2, name
代表宏名称 , 它可带参数 . 参数可以是可变参数列表 (C99).
3,
语句中可以利用 "/" 来换行 .
e.g.
#  define  ONE 1 /* ONE == 1 */
等价于 : #define ONE  1
#define err(flag, msg) if(flag) /
    printf(msg)

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

(
) 预处理命令详述
1, #define
#define
命令定义一个宏 :
#define MACRO_NAME(args) tokens(opt)
之后出现的 MACRO_NAME 将被替代为所定义的标记 (tokens). 宏可带参数 , 而后面的标记也是可选的 .

对象宏
 
不带参数的宏被称为 " 对象宏 (objectlike macro)"
#define
经常用来定义常量 , 此时的宏名称一般为大写的字符串 . 这样利于修改这些常量 .
e.g.
#define MAX 100
int a[MAX];
#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif

#define __FILE_H__
中的宏就不带任何参数 , 也不扩展为任何标记 . 这经常用于包含头文件 .
要调用该宏 , 只需在代码中指定宏名称 , 该宏将被替代为它被定义的内容 .

函数宏
 
带参数的宏也被称为 " 函数宏 ". 利用宏可以提高代码的运行效率 : 子程序的调用需要压栈出栈 , 这一过程如果过于频繁会耗费掉大量的 CPU 运算资源 . 所以一些代码量小但运行频繁的代码如果采用带参数宏来实现会提高代码的运行效率 .
 
函数宏的参数是固定的情况
 
函数宏的定义采用这样的方式 : #define name( args ) tokens
 
其中的 args tokens 都是可选的 . 它和对象宏定义上的区别在于宏名称之后不带括号 .
注意 , name 之后的左括号 ( 必须紧跟 name, 之间不能有空格 , 否则这就定义了一个对象宏 , 它将被替换为以 ( 开始的字符串 . 但在调用函数宏时 , name ( 之间可以有空格 .

e.g.
#define mul(x,y) ((x)*(y))

注意 , 函数宏之后的参数要用括号括起来 , 看看这个例子 :
#define mul(x,y) x*y
"mul(1, 2+2);"
将被扩展为 : 1*2 + 2
同样 , 整个标记串也应该用括号引用起来 :
#define mul(x,y) (x)*(y)
sizeof mul(1,2.0)
将被扩展为 sizeof 1 * 2.0

调用函数宏时候 , 传递给它的参数可以是函数的返回值 , 也可以是任何有意义的语句 :
e.g.
mul (f(a,b), g(c,d));

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

在定义和调用函数宏时候 , 要注意一些问题 :
1,
我们经常用 {} 来引用函数宏被定义的内容 , 这就要注意调用这个函数宏时的 ";" 问题 .
eg:
#define swap(x,y) { unsigned long _temp=x; x=y; y=_tmp}
如果这样调用它 : "swap(1,2);" 将被扩展为 : { unsigned long _temp=1; 1=2; 2=_tmp};
明显后面的 ; 是多余的 , 我们应该这样调用 : swap(1,2)
虽然这样的调用是正确的 , 但它和 C 语法相悖 , 可采用下面的方法来处理被 {} 括起来的内容 :
#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) 语句有这广泛的应用 .

2,
有的函数宏是无法用 do-while(0) 来实现的 , 所以在调用时不能带上 ";", 最好在调用后添加注释说明 .
eg:
#define incr(v, low, high) /
    for ((v) = (low),; (v) <= (high),; (v)++)
只能以这样的形式被调用 : incr(a, 1, 10)  /* increase a form 1 to 10 */

函数宏中的参数包括可变参数列表的情况
C99 标准中新增了可变参数列表的内容 . 不光是函数 , 函数宏中也可以使用可变参数列表 .
#define name(args, ...) tokens
#define name(...) tokens
 "..."
代表可变参数列表 , 如果它不是仅有的参数 , 那么它只能出现在参数列表的最后 . 调用这样的函数宏时 , 传递给它的参数个数要不少于参数列表中参数的个数 ( 多余的参数被丢弃 ).
通过 __VA_ARGS__ 来替换函数宏中的可变参数列表 . 注意 __VA_ARGS__ 只能用于函数宏中参数中包含有 "..." 的情况 .
e.g.
#ifdef DEBUG
#define my_printf(...) fprintf(stderr, __VA_ARGS__)
#else
#define my_printf(...) printf(__VA_ARGS__)
#endif


tokens
中的 __VA_ARGS__ 被替换为函数宏定义中的 "..." 可变参数列表 .

注意在使用 #define 时候的一些常见错误 :
#define MAX = 100
#define MAX 100;
=, ;
的使用要值得注意 . 再就是调用函数宏是要注意 , 不要多给出 ";".
注意 : 函数宏对参数类型是不敏感的 , 不必考虑将何种数据类型传递给宏 .

关于定义宏的另外一些问题
(1)
宏可以被多次定义 , 前提是这些定义必须是相同的 . 这里的 " 相同 " 要求先后定义中空白符出现的位置相同 , 但具体的空白符类型或数量可不同 , 比如原先的空格可替换为多个其他类型的空白符 : 可为 tab, 注释 ...
e.g.
#define NULL 0
#define NULL
  /* null pointer */     0
上面的重定义是相同的 , 但下面的重定义不同 :
#define fun(x) x+1
#define fun(x) x + 1
: #define fun(y) y+1
如果多次定义时 , 再次定义的宏内容是不同的 , gcc 会给出 "NAME redefined" 警告信息 .

应该避免重新定义函数宏 , 不管是在预处理命令中还是 C 语句中 , 最好对某个对象只有单一的定义 . gcc , 若宏出现了重定义 , gcc 会给出警告 .

(2)
gcc , 可在命令行中指定对象宏的定义 :
e.g.
$ gcc -Wall -DMAX=100 -o tmp tmp.c
相当于在 tmp.c 中添加 " #define MAX 100".
 
那么 , 如果原先 tmp.c 中含有 MAX 宏的定义 , 那么再在 gcc 调用命令中使用 -DMAX, 会出现什么情况呢 ?
---
-DMAX=1, 则正确编译 .
---
-DMAX 的值被指定为不为 1 的值 , 那么 gcc 会给出 MAX 宏被重定义的警告 , MAX 的值仍为 1.
注意 : 若在调用 gcc 的命令行中不显示地给出对象宏的值 , 那么 gcc 赋予该宏默认值 (1), : -DVAL == -DVAL=1

(3) #define
所定义的宏的作用域
宏在定义之后才生效 , 若宏定义被 #undef 取消 , #undef 之后该宏无效 . 并且字符串中的宏不会被识别
e.g.
#define ONE 1
sum = ONE + TWO    /* sum = 1 + TWO  */
#define TWO 2
sum = ONE + TWO    /* sum = 1 + 2    */  
#undef ONE
sum = ONE + TWO    /* sum = ONE + 2  */
char c[] = "TWO"   /* c[] = "TWO", NOT "2"! */

(4)
宏的替换可以是递归的 , 所以可以嵌套定义宏 .
e.g.
# define ONE NUMBER_1
# define NUMBER_1 1
int a = ONE  /* a = 1 */


2, #undef
#undef
用来取消宏定义 , 它与 #define 对立 :
#undef name
如够被取消的宏实际上没有被 #define 所定义 , 针对它的 #undef 并不会产生错误 .
当一个宏定义被取消后 , 可以再度定义它 .

3, #if, #elif, #else, #endif
#if, #elif, #else, #endif
用于条件编译 :

#if
常量表达式 1
    
语句 ...
#elif
常量表达式 2
    
语句 ...
#elif
常量表达式 3
    
语句 ...
...
#else
    
语句 ...
#endif

#if
#else 分别相当于 C 语句中的 if, else. 它们根据常量表达式的值来判别是否执行后面的语句 . #elif 相当于 C 中的 else-if. 使用这些条件编译命令可以方便地实现对源代码内容的控制 .
else
之后不带常量表达式 , 但若包含了常量表达式 , gcc 只是给出警告信息 .

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


常量表达式可以是包含宏 , 算术运算 , 逻辑运算等等的合法 C 常量表达式 , 如果常量表达式为一个未定义的宏 , 那么它的值被视为 0.
#if MACRO_NON_DEFINED  == #if 0
在判断某个宏是否被定义时 , 应当避免使用 #if, 因为该宏的值可能就是被定义为 0. 而应当使用下面介绍的 #ifdef #ifndef.

注意 : #if, #elif, #else 之后的宏只能是对象宏 . 如果 name 为名的宏未定义 , 或者该宏是函数宏 . 那么在 gcc 中使用 "-Wundef" 选项会显示宏未定义的警告信息 .

4, #ifdef, #ifndef, defined.
#ifdef, #ifndef, defined
用来测试某个宏是否被定义
#ifdef name 
#ifndef name

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


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

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

#if, #elif, #else 不同 , #indef, #ifndef, defined 测试的宏可以是对象宏 , 也可以是函数宏 . gcc 中使用 "-Wundef" 选项不会显示宏未定义的警告信息 .

5, #include , #include_next
#include
用于文件包含 . #include 命令所在的行不能含有除注释和空白符之外的其他任何内容 .
#include "headfile"
#include <headfile>
#include
预处理标记
前面两种形式大家都很熟悉 , "#include 预处理标记 " , 预处理标记会被预处理器进行替换 , 替换的结果必须符合前两种形式中的某一种 .

实际上 , 真正被添加的头文件并不一定就是 #include 中所指定的文件 . #include"headfile" 包含的头文件当然是同一个文件 , #include <headfile> 包包含的 " 系统头文件 " 可能是另外的文件 . 但这不值得被注意 . 感兴趣的话可以查看宏扩展后到底引入了哪些系统头文件 .

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

6,
预定义宏
标准 C 中定义了一些对象宏 , 这些宏的名称以 "__" 开头和结尾 , 并且都是大写字符 . 这些预定义宏可以被 #undef, 也可以被重定义 .

下面列出一些标准 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 的版本号 .

7, #line
#line
用来修改 __LINE__ __FILE__.
e.g.
  printf("line: %d, file: %s/n", __LINE__, __FILE__);
 #line 100 "haha"
  printf("line: %d, file: %s/n", __LINE__, __FILE__);
  printf("line: %d, file: %s/n", __LINE__, __FILE__);


显示 :
line: 34, file: 1.c
line: 100, file: haha
line: 101, file: haha


8, #pragma, _Pragma
#pragma
用编译器用来添加新的预处理功能或者显示一些编译信息 . #pragma 的格式是各编译器特定的 , gcc 的如下 :
#pragma GCC name token(s)

#pragma
之后有两个部分 : GCC 和特定的 pragma name. 下面分别介绍 gcc 中常用的 .

(1) #pragma GCC dependency
dependency
测试当前文件 ( 既该语句所在的程序代码 ) 与指定文件 ( #pragma 语句最后列出的文件 ) 的时间戳 . 如果指定文件比当前文件新 , 则给出警告信息 .
e.g.
demo.c 中给出这样一句 :
#pragma GCC dependency "temp-file"
然后在 demo.c 所在的目录新建一个更新的文件 : $ touch temp-file, 编译 : $ gcc demo.c 会给出这样的警告信息 warning: current file is older than temp-file
如果当前文件比指定的文件新 , 则不给出任何警告信息 .

还可以在在 #pragma 中给添加自定义的警告信息 .
e.g.
#pragma GCC dependency "temp-file" "demo.c needs to be updated!"
1.c:27:38: warning: extra tokens at end of #pragma directive
1.c:27:38: warning: current file is older than temp-file

注意 : 后面新增的警告信息要用 "" 引用起来 , 否则 gcc 将给出警告信息 .

(2) #pragma GCC poison token(s)
若源代码中出现了 #pragma 中给出的 token(s), 则编译时显示警告信息 . 它一般用于在调用你不想使用的函数时候给出出错信息 .
e.g.
#pragma GCC poison scanf
scanf("%d", &a);
warning: extra tokens at end of #pragma directive
error: attempt to use poisoned "scanf"
注意 , 如果调用了 poison 中给出的标记 , 那么编译器会给出的是出错信息 . 关于第一条警告 , 我还不知道怎么避免 , "" token(s) 引用起来也不行 .

(3) #pragma GCC system_header
#pragma GCC system_header 直到文件结束之间的代码会被编译器视为系统头文件之中的代码 . 系统头文件中的代码往往不能完全遵循 C 标准 , 所以头文件之中的警告信息往往不显示 . ( 除非用 #warning 显式指明 ).

由于 #pragma 不能用于宏扩展 , 所以 gcc 还提供了 _Pragma :
e.g.
#define PRAGMA_DEP #pragma GCC dependency "temp-file"
由于预处理之进行一次宏扩展 , 采用上面的方法会在编译时引发错误 , 要将 #pragma 语句定义成一个宏扩展 , 应该使用下面的 _Pragma 语句 :
#define PRAGMA_DEP _Pragma("GCC dependency /"temp-file/"")
注意 , () 中包含的 "" 引用之前引该加上 / 转义字符 .

9, #, ##
#
## 用于对字符串的预处理操作 , 所以他们也经常用于 printf, puts 之类的字符串显示函数中 .
#
用于在宏扩展之后将 tokens 转换为以 tokens 为内容的字符串常量 .
e.g.
#define TEST(a,b) printf( #a "<" #b "=%d/n", (a)<(b));
注意 : # 只针对紧随其后的 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;

(10) #warning, #error
#warning, #error
分别用于在编译时显示警告和错误信息 , 格式如下 :
#warning tokens
#error tokens

e.g.
#warning "some warning"
注意 , #error #warning 后的 token 要用 "" 引用起来 !
(
gcc , 如果给出了 warning, 编译继续进行 , 但若给出了 error, 则编译停止 . 若在命令行中指定了 -Werror, 即使只有警告信息 , 也不编译 .
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值