关于C/C++中的预处理

关于C/C++中的预处理

来源 https://zhuanlan.zhihu.com/p/106545689

 

本文主要讲述C/C++中预处理命令相关的内容。主要参考资料在后面给出。

我们可以在C源程序中插入传给编译程序的各种指令(宏),这些指令被称为预处理器指令,它们扩充了程序设计的环境。

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

C 初学者可能对预处理器没什么概念, 这是情有可原的: 一般的C编译器都将预处理, 汇编, 编译, 连接过程集成到一起了,编译预处理往往在后台运行。 在有的C编译器中, 这些过程统统由一个单独的程序来完成, 在编译的不同阶段分别实现这些不同的功能,我们可以指定特定的命令选项来执行这些指定功能。有的C编译器则使用独立的程序来完成这些步骤. 可单独调用这些程序来完成特定步骤,比如在 gcc 中, 进行编译预处理的程序被称为 CPP , 它的可执行文件名为 cpp 。

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

预处理命令简介

预处理命令由#(hash字符)开头, 它独占一行, #之前只能是空白符. 以#开头的语句就是预处理命令, 不以#开头的语句为C中的代码行。

常用的预处理命令如下:

#define              定义一个预处理宏
#undef             取消宏的定义

#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配合使用, 判断某个宏是否被定义

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

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

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

预处理的文法

预处理并不分析整个源代码文件, 它只是将源代码分割成一些标记(token), 识别语句中哪些是C语句, 哪些是预处理语句。

预处理器能够识别C标记, 文件名, 空白符, 文件结尾标志。

预处理语句格式: #command name(...) token(s)

这里,

  • command 预处理命令的名称。
    它之前以 # 开头, #之后紧随预处理命令, 标准C允许 # 两边可以有空白符, 但比较老的编译器可能不允许这样. 若某行中只包含 # (以及空白符), 那么在标准C中该行被理解为空白. 整个预处理语句之后只能有空白符或者注释, 不能有其它内容。
  • name 代表宏名称,它可带参数。
    参数可以是可变参数列表(C99).
  • token(s) 宏体,将替换宏名称的语句。
    语句中可以利用 \ 来换行.

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__ 中的宏就不带任何参数, 也不扩展为任何标记. 这经常用于包含头文件.

要调用宏, 只需在代码中指定宏名称, 相应宏将被替代为它被定义的内容。

例如:我们使用 LEFT 代表1,用 RIGHT 代表0,我们使用两个 #define 指令:

#define LEFT 1
#define RIGHT 0

每当在源程序中遇到 LEFT 或 RIGHT 时,编译程序都用1或0替换。

宏代换就是用相关的串替代标识符。因此,如果希望定义一条标准错误信息时,可以如下定义:

#define ERROR_MS “Standard error on input \n”

如果一个串长于一行,可在行尾用反斜线”\”续行,如下:

#define LONG_STRING “This is a very very long \
String that is used as an example”

函数宏

带参数的宏也被称为"函数宏". 利用宏可以提高代码的运行效率: 子程序的调用需要压栈出栈, 这一过程如果过于频繁会耗费掉大量的CPU运算资源. 所以一些代码量小但运行频繁的代码如果采用带参数宏来实现会提高代码的运行效率.

  1. 函数宏的参数是固定的情况
    函数宏的定义采用这样的方式: #define name( args ) tokens
    其中的 args 和 tokens 都是可选的. 它和对象宏定义上的区别在于对象宏名称之后不带括号.
    注意: 定义时 , name 之后的左括号 ( 必须紧跟 name , 之间不能有空格, 否则这就定义了一个对象宏, 它将被替换为以 ( 开始的字符串;但在 用函数宏时 , name 与 ( 之间却可以有空格.
    例如:
    #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));
    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;)) 可解决上述问题.
  • 在定义和调用函数宏时候, 要注意一些问题
  1. 使用 do-while(0) 解决结尾 ; 问题
    我们经常用 {} 来引用函数宏被定义的内容, 这就要注意调用这个函数宏时的 ; 问题. example_3.7:
    #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) 实现的宏
    有的函数宏是无法用 do-while(0) 来实现的, 所以在调用时不能带上 ;, 最好在调用后添加注释说明。 eg_3.8:
    #define incr(v, low, high) \ for ((v) = (low),; (v) <= (high); (v)++)
    只能以这样的形式被调用: incr(a, 1, 10) /* increase a form 1 to 10 */

 

  1. 函数宏中的参数包括可变参数列表的情况
    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. (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. (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

  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"! */
  2. (4) 递归嵌套定义
    宏的替换可以是递归的, 所以可以嵌套定义宏。也就是说,定义一个宏名字之后,可以在其他宏定义中使用。
    e.g.
    # define ONE NUMBER_1 # define NUMBER_1 1 int a = ONE /* a = 1 */ #define ONE 1 #define TWO ONE+ONE #define THREE ONE+TWO

#undef

#undef 用来取消宏定义, 它与 #define 对立,经常用来删除前面定义的宏名字。也就是说,它“不定义”宏。一般形式为:

#undef macro-name

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

#, ##

预处理操作符 # 和 ## 主要作用是允许预处理程序对付某些特殊情况,多数程序中并不需要,它们可以在 #define 中使用。

由于经常用于对字符串的预处理操作, 所以他们也经常用于 printf, puts 之类的字符串显示函数中。例如:

e.g.

#define TEST(a,b) printf( #a "<" #b "=%d\n", (a)<(b));

# 命令

操作符 # 通常称为字符串化的操作符,它把其后的串变成用双引号包围的串。用于在宏扩展之后将 tokens 转换为以 tokens 为内容的字符串常量。

例如:

#include <stdio.h>
#define mkstr(s) #s
int main(void)
{
 printf(mkstr(I like C));
 Return 0;
}

预处理程序把以下的语句: printf(mkstr(I like C)); 变成 printf(“I like C”);

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

## 命令

操作符 ## 把两个标记拼在一起,形成一个新标记。用于将它前后的两个 token 组合在一起转换成以这两个 token 为内容的字符串常量。

#include <stdio.h>
#define concat(a,a) a##b
int main(void)
{
 int xy = 10;
 printf(“%d”,concat(x,y));
 Return 0;
}

预处理程序把以下语句: printf(“%d”,concat(x,y)); 变成 printf(“%d”,xy);

注意: ## 前后必须要有 token !

2、条件编译

若干编译指令允许程序员有选择的编译程序源代码的不同部分,这种过程称为条件编译。

#if, #elif, #else, #endif

#if, #elif, #else, #endif 允许程序员根据常数表达式的结果有条件的包围部分代码。一般的表达形式是:

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

它们后面所判断的宏只能是对象宏. 如果 name 为名的宏未定义, 或者该宏是函数宏. 那么在 gcc 中使用 -Wundef 选项会显示宏未定义的警告信息。

#if

表示“如果”, 其一般形式是:

#if constant-expression
 Statement sequence
#endif

如 #if 后的常数表达式为真,则 #if 和 #endif 中间的代码被编译,否则忽略该代码段。 #endif 标记 #if 块的结束。

#elif 指令

表示“否则,如果”,为多重编译选择建立一条 if-else-if (如果-否则-如果链)。一般形式如下:

#if expression
 Statement sequence
#elif expression1
 Statement sequence
#elif expression2
 Statement sequence
#elif expression
 Statement sequence
#endif

如果 #if 表达式为真,该代码块被编译,不测试其他 #elif 表达式。否则,序列中的下一块被测试,如果成功则编译之。

#else 指令

作用与C语言的 else 相似, #if 指令失败时它可以作为备选指令。例如:

#include <stdio.h>
#define MAX 100

int main(void)
{
 #if MAX>99
 printf(“Compiled for array greater than 99.\n”);
 #else
 printf(“Complied for small array.\n”);
 #endif
 return 0;
}

注意, #else 既是标记 #if 块的结束,也标记 #else 块的开始。因为每个 #if 只能写一个 #endif 匹配。

与代码中的 if/else 类似

#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 。

#ifdef, #ifndef, defined

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

与 #if, #elif, #else 不同, #indef, #ifndef, defined 测试的宏可以是对象宏, 也可以是函数宏。

在 gcc 中使用 -Wundef 选项不会显示宏未定义的警告信息。

#ifdef 指令

一般形式如下:

#ifdef macro-name
 Statement sequence
#endif

如果 macro-name 原先已经被一个 #define 语句定义,则编译其中的代码块。

#ifndef 指令

一般形式如下:

#ifndef macro-name
 Statement sequence
#endif

如果 macro-name 当前未被 #define 语句定义,则编译其中的代码块。

#ifndef 经常用于避免头文件的重复引用,例如:

#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif

defined 指令

除 #ifdef 之外,还有另外一种确定是否定义宏名字的方法,即可以将 #if 指令与 defined 指令一起使用。 defined 操作符的一般形式如下:

defined macro-name

若 macro-name 当前被定义,则表达式为真返回1,否则为假返回0。

defined 与 #if, #elif, #else 结合使用来判断宏是否被定义, 乍一看好像它显得多余, 因为已经有了 #ifdef 和 #ifndef ,其实使用 defined 的一个原因是,它允许被做为 #ifdef 与 #ifndef , #elif 的判断条件(如:由 #elif 语句确定宏名字存在),因此显得更为灵活。

  1. 与 #ifdef 等价的判断
    例如,确定宏 MY 是否定义,可以使用下列两种预处理命令之一:
    #if defined MY

    #ifdef MY
  2. 与 #ifndef 等价的判断
    也可以在 defined 之前加上感叹号”!”来反转相应的条件。例如,只有在DEBUG未定义的情况下才编译。
    #if !defined DEBUG printf(“Final Version!\n”); #endif

    #ifndef DEBUG printf(“Final Version!\n”); #endif
  3. defined 做为条件语句的判断
    比如在一条判断语句中声明多个判别条件,如:
    #if defined(VAX) && defined(UNIX) && !defined(DEBUG)
    注:可以没有括号。

结合 #else / #elif 指令使用

#ifdef / #ifndef 可与 #else / #elif 结合使用,如下:

#inlucde <stdio.h>
#define T 10
int main(void)
{
 #ifdef t
 printf(“Hi T\n”);
 #else
 printf(“Hi anyone\n”);
 #endif
 #ifndef M
 printf(“M Not Defined\n”);
 #endif
 Return 0;
}

3、文件包含

#include 指令

用于文件包含. 含义是要求编译程序读入另一个源文件。在 #include 命令所在的行不能含有除注释和空白符之外的其他任何内容。一般形式:

#include "headfile"
#include <headfile>
#include 预处理标记

前面两种形式大家都很熟悉,被读入文件的名字必须用双引号(“”)或一对尖括号(<>)包围,例如:

#include “stdio.h”
#include <stdio.h>

表示都使C编译程序读入并编译头文件以用于I/O系统库函数。

在 #include 预处理标记 形式中, 预处理标记会被预处理器进行替换, 替换的结果必须符合前两种形式中的某一种。

包含文件中可以包含其他 #include 指令,称为嵌套包含。允许的最大嵌套深度随编译器而变。

被包含文件名用双引号或尖括号包围决定了对指定文件的搜索方式。当文件名被尖括号包围时,搜索按编译程序(如 gcc)作者的定义进行,一般用于搜索某些专门放置包含文件的特殊目录。当文件名被双引号包围时,搜索按编译程序实时的规定进行,一般搜索当前目录。如未发现,再按尖括号包围时的办法重新搜索一次。通常,绝大多数程序员使用尖括号包围标准的头文件,双引号用于包围与当前程序相关的文件名。

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

关于 #include "headfile"和#include <headfile> 的区别以及如何在 gcc 中包含头文件的详细信息, 具体需要参考GCC相关的文档。

#include_next 指令

相对于 #include, 我们对 #include_next 不太熟悉。 #include_next 仅用于特殊的场合. 它被用于头文件中(#include 既可用于头文件中, 又可用于 .c 文件中)来包含其他的头文件. 而且包含头文件的路径比较特殊: 从当前头文件所在目录之后的目录来搜索头文件。

比如:

若头文件的搜索路径依次为 =A,B,C,D,E=, 而 =#include_next= 所在的当前头文件位于 =B= 目录, 
那么 =#include_next= 使得预处理器从 =C,D,E= 目录来搜索 =#include_next= 所指定的头文件。

可参考预处理命令 cpp 的手册进一步了解 #include_next 。

4、预定义宏

标准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.
                   我用gcc, 如果不指定-std=c99, 其他情况都给出__STDC_VERSION__未定义的错误信息, 咋回事呢?
__STDC_HOSTED__     如果当前系统是"本地系统(hosted)", 那么它被定义为1. 本地系统表示当前系统拥有完整的标准C库.

另外, gcc 定义的预定义宏:

__OPTMIZE__         如果编译过程中使用了优化, 那么该宏被定义为1.
__OPTMIZE_SIZE__   同上, 但仅在优化是针对代码大小而非速度时才被定义为1.
__VERSION__         显示所用gcc的版本号.

具体可参考 GCC the complete reference 。

要想看到 gcc 所定义的所有预定义宏, 可以运行:

$cpp -dM /dev/null

5、扩展控制

#line

#line 指令用来修改 __LINE__ 和 __FILE__ 的内容。前面介绍了, __LINE__和__FILE__ 都是编译程序中预定义的标识符。标识符 __LINE__ 的内容是当前被编译代码行的行号,=__FILE__= 的内容是当前被编译源文件的文件名。 #line 的一般形式是:

#line number “filename”

其中, number 是正整数并变成 __LINE__ 的新值;可选的 filename 是合法文件标识符并变成 __FILE__ 的新值。 #line 主要用于调试和特殊应用。

常见的使用:

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

一个完整的例子, main.cpp 文件内容如下:

1 #include<iostream>
 2 using std::cout;
 3 using std::cin;
 4 using std::endl;
 5 #line 2 "myfile.h"
 6 int main(int argc, char *argv[])
 7 {
 8     cout<<__LINE__<<":"<<__FILE__<<endl;
 9     return 0;
10 }

上述代码编译运行输出如下:

[quietheart@lv-k pre_test]$ ls
main.cpp
[quietheart@lv-k pre_test]$ make main
g++     main.cpp   -o main
[quietheart@lv-k pre_test]$ ls
main  main.cpp
[quietheart@lv-k pre_test]$ ./main 
4:myfile.h

由上面输出可以看出,

  • __LINE__ 的内容变成了相对 #line 宏的内容,即 #line 下面的第一行就是 #line 指定的2;
  • 同时 __FILE__ 变成了 #line 指定的 myfile.h 。

#pragma, _Pragma

#pragma 编译器用来添加新的预处理功能或者显示一些编译信息. 是编译程序实现时定义的指令,它允许由此向编译程序传入各种指令。

例如:

一个编译程序可能具有支持跟踪程序执行的选项,此时可以用 =#pragma= 语句选择该功能。

编译程序忽略其不支持的 #pragma 选项, #pragma 提高C源程序对编译程序的可移植性。

#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 语句还没发现用什么大的用处)

_Pragma 实现将 #pragma 用于宏扩展

由于 #pragma 不能用于宏扩展, 所以gcc还提供了 _Pragma :

e.g.

#define PRAGMA_DEP #pragma GCC dependency "temp-file"

由于预处理之时进行一次宏扩展, 采用上面的方法会在编译时引发错误, 要将 #pragma 语句定义成一个宏扩展, 应该使用下面的 _Pragma 语句:

#define PRAGMA_DEP _Pragma("GCC dependency \"temp-file\"")

注意, () 中包含的""引用之前引该加上\转义字符.

#warning, #error

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

#warning tokens
#error tokens

注意, #error 和 #warning 后的 token 要用""引用起来!

在 gcc 中,

  • 如果给出了 warning , 编译继续进行,
  • 但若给出了 error , 则编译停止,
  • 若在命令行中指定了 -Werror ( gcc 手册上意思是将所有 warning 变为 error ), 即使只有警告信息, 也不编译.

#warning 指令

e.g.

#warning "some warning"

error 指令

#error 指令强制编译程序停止编译,它主要用于程序调试。 #error 指令的一般形式是:

#error error-message

注意,宏串 error-message 不用双引号包围。遇到 #error 指令时,错误信息被显示,可能同时还显示编译程序作者预先定义的其他内容。

例如 main.cpp 文件内容如下:

1 #include<iostream>
 2 using std::cout;
 3 using std::cin;
 4 using std::endl;
 5 #ifdef MMM
 6 #error myerror //如果执行了这个就会阻止编译通过
 7 #endif
 8 int main(int argc, char *argv[])
 9 {
10     cout<<"hello world!"<<endl;
11     return 0;
12 }

编译过程如下:

[quietheart@lv-k pre_test]$ ls
main.cpp
[quietheart@lv-k pre_test]$ make CXXFLAGS+=-DMMM pre_error
g++ -DMMM    pre_error.cpp   -o pre_error
pre_error.cpp:6:2: error: #error myerror
 #error myerror //如果执行了这个就会阻止编译通过
  ^~~~~
<内置>: recipe for target 'pre_error' failed
make: *** [pre_error] Error 1
[quietheart@lv-k pre_test]$ ls
main.cpp
[quietheart@lv-k pre_test]$ make main
g++     main.cpp   -o main
[quietheart@lv-k pre_test]$ ls
main  main.cpp
[quietheart@lv-k pre_test]$ ./main 
hello world!

C语言常用宏定义

这里给出C语言中一些常用的宏定义。

01: 防止一个头文件被重复包含

#ifndef COMDEF_H
#define COMDEF_H
//头文件内容
#endif

02: 重新定义一些类型,防止由于各种平台和编译器的不同,而产生的类型字节数差异,方便移植。

typedef  unsigned char      boolean;     /* Boolean value type. */
typedef  unsigned long int  uint32;      /* Unsigned 32 bit value */
typedef  unsigned short     uint16;      /* Unsigned 16 bit value */
typedef  unsigned char      uint8;       /* Unsigned 8  bit value */
typedef  signed long int    int32;       /* Signed 32 bit value */
typedef  signed short       int16;       /* Signed 16 bit value */
typedef  signed char        int8;        /* Signed 8  bit value */

//下面的不建议使用
typedef  unsigned char     byte;         /* Unsigned 8  bit value type. */
typedef  unsigned short    word;         /* Unsinged 16 bit value type. */
typedef  unsigned long     dword;        /* Unsigned 32 bit value type. */
typedef  unsigned char     uint1;        /* Unsigned 8  bit value type. */
typedef  unsigned short    uint2;        /* Unsigned 16 bit value type. */
typedef  unsigned long     uint4;        /* Unsigned 32 bit value type. */
typedef  signed char       int1;         /* Signed 8  bit value type. */
typedef  signed short      int2;         /* Signed 16 bit value type. */
typedef  long int          int4;         /* Signed 32 bit value type. */
typedef  signed long       sint31;       /* Signed 32 bit value */
typedef  signed short      sint15;       /* Signed 16 bit value */
typedef  signed char       sint7;        /* Signed 8  bit value */

03: 得到指定地址上的一个字节或字

#define  MEM_B(x) (*((byte *)(x)))
#define  MEM_W(x) (*((word *)(x)))

04: 求最大值和最小值

#define  MAX(x,y) (((x)>(y)) ? (x) : (y))
#define  MIN(x,y) (((x) < (y)) ? (x) : (y))

05: 得到一个field在结构体(struct)中的偏移量

#define FPOS(type,field) ((dword)&((type *)0)->field)

06: 得到一个结构体中field所占用的字节数

#define FSIZ(type,field) sizeof(((type *)0)->field)

07: 按照LSB格式把两个字节转化为一个Word

#define FLIPW(ray) ((((word)(ray)[0]) * 256) + (ray)[1])

08: 按照LSB格式把一个Word转化为两个字节

#define FLOPW(ray,val) (ray)[0] = ((val)/256); (ray)[1] = ((val) & 0xFF)

09: 得到一个变量的地址(word宽度)

#define B_PTR(var)  ((byte *) (void *) &(var))
#define W_PTR(var)  ((word *) (void *) &(var))

10: 得到一个字的高位和低位字节

#define WORD_LO(xxx)  ((byte) ((word)(xxx) & 255))
#define WORD_HI(xxx)  ((byte) ((word)(xxx) >> 8))

11: 返回一个比X大的最接近的8的倍数

#define RND8(x) ((((x) + 7)/8) * 8)

12: 将一个字母转换为大写

#define UPCASE(c) (((c)>='a' && (c) <= 'z') ? ((c) - 0x20) : (c))

13: 判断字符是不是10进值的数字

#define  DECCHK(c) ((c)>='0' && (c)<='9')

14: 判断字符是不是16进值的数字

#define HEXCHK(c) (((c) >= '0' && (c)<='9') ((c)>='A' && (c)<= 'F') \
((c)>='a' && (c)<='f'))

15: 防止溢出的一个方法

#define INC_SAT(val) (val=((val)+1>(val)) ? (val)+1 : (val))

16: 返回数组元素的个数

#define ARR_SIZE(a)  (sizeof((a))/sizeof((a[0])))

17: 返回一个无符号数n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n)

#define MOD_BY_POWER_OF_TWO( val, mod_by ) ((dword)(val) & (dword)((mod_by)-1))

18: 对于IO空间映射在存储空间的结构,输入输出处理

#define inp(port) (*((volatile byte *)(port)))
#define inpw(port) (*((volatile word *)(port)))
#define inpdw(port) (*((volatile dword *)(port)))
#define outp(port,val) (*((volatile byte *)(port))=((byte)(val)))
#define outpw(port, val) (*((volatile word *)(port))=((word)(val)))
#define outpdw(port, val) (*((volatile dword *)(port))=((dword)(val)))

19: 使用一些宏跟踪调试

ANSI标准说明了五个预定义的宏名。它们是:

__LINE__
__FILE__
__DATE__
__TIME__
__STDC__

C++中还定义了 __cplusplus

如果编译器不是标准的,则可能仅支持以上宏名中的几个,或根本不支持。记住编译程序也许还提供其它预定义的宏名。

__LINE__ 及 __FILE__ 宏指示,#line指令可以改变它的值,简单的讲,编译时,它们包含程序的当前行数和文件名。
__DATE__ 宏指令含有形式为月/日/年的串,表示源文件被翻译到代码时的日期。
__TIME__ 宏指令包含程序编译的时间。时间用字符串表示,其形式为: 分:秒
__STDC__ 宏指令的意义是编译时定义的。一般来讲,如果__STDC__已经定义,编译器将仅接受不包含任何非标准扩展的标准C/C++代码。如果实现是标准的,则宏__STDC__含有十进制常量1。如果它含有任何其它数,则实现是非标准的。
__cplusplus 与标准c++一致的编译器把它定义为一个包含至少6为的数值。与标准c++不一致的编译器将使用具有5位或更少的数值。

可以定义宏,例如:当定义了 _DEBUG ,输出数据信息和所在文件所在行

#ifdef _DEBUG
#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_)
#else
#define DEBUGMSG(msg,date)
#endif

20:宏定义防止错误使用小括号包含。

例如: 有问题的定义:

#define DUMP_WRITE(addr,nr) {memcpy(bufp,addr,nr); bufp += nr;}

应该使用的定义:

#difne DO(a,b) do{a+b;a++;}while(0)

例如:

if(addr)
    DUMP_WRITE(addr,nr);
else
    do_somethong_else();

宏展开以后变成这样:

if(addr)
    {memcpy(bufp,addr,nr); bufp += nr;};
else
    do_something_else();

其它

主要参考:

 

=========== End

 

posted @ 2023-06-18 22:41  lsgxeva  阅读(161)  评论(0编辑  收藏  举报