vlambda博客
学习文章列表

基于C99规范,最全C语言预处理知识总结


00. 前言

面试官:请简单描述下编译器将C语言从源代码到可执行文件的过程。

应聘者:呃……就是……在IDE里写代码,然后点编译啊……

面试官:那你知道预处理命令这个东西吗,平时用过有哪些?

应聘者:这个我懂,不就是宏定义那些嘛,我平时用#define来定义某些数值,还有用#ifdef来做选择性编译,这些挺简单的啊。

面试官:嗯,好,今天先聊到这,回去等通知吧。

不知道这个问题的,有可能你一直都是用IDE做开发,而没思考过IDE里面经历了什么。如果你用过GCC,你应该大概知道这是一个怎样的过程:

这个过程里面有个Pre-processing,很不起眼,但是非常关键,如果用得好,你的代码也会变得很奇妙。

以下是基于C99标准,总结C语言预处理的知识点以及其相关用法和问题剖析。

:本文所有案例测试都是基于Windows上的GCC(gcc version 6.3.0 (MinGW.org GCC-6.3.0-1))  

抛开大脑里零零散散的几个概念,我们直接参考C99标准(ISO/IEC 9899:1999 (E) ),简单汇总有以下命令或用法:

分类 命令 解释 简单示例
01. 条件包含 Conditional inclusion #if#else#elif#endif#ifdef#ifndef#undef 即平时理解的条件编译 #ifdef identifier
02. 源文件包含 Source file inclusion #include 头文件/源文件包含 #include <h-char-sequence> new-line
03. 宏替换 Macro replacement #define, #, ## 宏替换 A##B
04. 行控制 Line control #line 行控制。一般很少用 #line digit-sequence new-line
05. 错误指示 Error directive #error 出错指示。做预处理检查,可以输出错误提示信息。 #error pp-tokensopt new-line
06. 空指令 Null directive # 空指。看起来没什么用令 # new-line
07. 预定义宏名 Predefined macro names __DATE__,  __FILE__, __LINE__, __STDC__,  __TIME__ 预定义宏名
08. Pragma命令/操作 Pragma directive/operator _Pragma, #pragma pragma指令 #pragma listing on "..\listing.dir"

以上内容,大部分都很常见,如果要说用得好,那是个艺术活。

1. 语法形式

预    处理有以下语法形式:

preprocessing-file:

groupoptgroup:

    group-partgroup group-part

group-part:

    if-section

    control-line

    text-line

    # non-directive

if-section:

    if-group elif-groupsopt else-groupopt endif-line

if-group:

    # if constant-expression new-line groupopt

    # ifdef identifier new-line groupopt

    # ifndef identifier new-line groupopt

elif-groups:

    elif-group

    elif-groups elif-group

elif-group:

    # elif constant-expression new-line groupopt

else-group:

    # else new-line groupopt
endif-line:



    # endif new-line

control-line:

    # include pp-tokens new-line

    # define identifier replacement-list new-line

    # define identifier lparen identifier-listopt )
replacement-list new-line
    # define identifier lparen ... ) replacement-list new-line

    # define identifier lparen identifier-list , ... )
replacement-list new-line
    # undef identifier new-line

    # line pp-tokens new-line

    # error pp-tokensopt new-line

    # pragma pp-tokensopt new-line

    # new-line

text-line:

    pp-tokensopt new-line

non-directive:

    pp-tokens new-line

lparen:

    a ( character not immediately preceded by white-space
replacement-list:
    pp-tokensopt

pp-tokens:

    preprocessing-token

    pp-tokens preprocessing-token

new-line:

    the new-line character

2. 描述
  1. 预处理指令由一系列预处理令牌组成,这些预处理令牌以预处理令牌开头,它是源文件中的第一个字符或紧跟着包含至少一个换行符的空白,并在下一个换行符结束。

  2. 文本行不要以#开头,非指令内容也不要用“语法形式”中的内容作为开头

3. 约束

在预处理指令中的预处理标记之间(从引入#预处理标记之后到结束换行符之前),唯一出现的空白字符是空格和水平制表符(包括替换了注释或可能的空格)

 /*comment*/#             /*comment*/   include <stdio.h>/*comment*/

以上代码在GCC上是可以编译通过的。

4. 语义

可以有条件地处理和跳过源文件的各个部分,包括其他源文件,并替换宏。这些功能称为预处理,因为从概念上讲,它们发生在生成的翻译单元的翻译之前。除非另有说明,否则预处理指令中的预处理token不会进行宏扩展。

 #define EMPTY
 EMPTY # include <file.h>

以上第2行是有问题的,它并非以#开头,不能以其为预处理指令

01. 条件包含

控制条件包含的表达式,一定是一个整型常量的。不能包含类型转换和标识符(如C语言中的关键字、枚举常量等),其只认宏与非宏。我们可以将以下表达式把defined当做一元操作符:defined identifierdefined (identifier)以上如果identifier是一个有效的宏名,也就是说上文有用了#define进行定义,并且没用#undef来取消这个定义,那么上述表达式的结果为1,否则为0

01.1 关于defined

我们用实际的例子来说明以上说法:

 enum
 {
     ENUM_NO,
     ENUM_YES
 };
 #define DEF_YES_NULL
 #define DEF_YES_0   0
 #define DEF_YES_1   1
 #define DEF_YES_2   2
 #define DEF_YES_STR "ABC"
 #define DEF_NO_ENUM ENUM_NO

根据以上定义  例1:

 #if defined(DEF_YES_NULL) == 1
     printf("DEF_YES_NULL should be printed.\n");
 #endif

DEF_YES_NULL是有效的宏定义,defined(DEF_YES_NULL)的值是1,能够打印出DEF_YES_NULL should be printed.  

例2:

 #if defined(DEF_YES_2) == 1
     printf("DEF_YES_2 should be printed.\n");
 #endif

DEF_YES_2是有效的宏定义,defined(DEF_YES_2)的值是1,能够打印出DEF_YES_2 should be printed.  

例3:

 #if defined(DEF_YES_STR) == 1
     printf("DEF_YES_STR should be printed.\n");
 #endif

DEF_YES_STR是有效的宏定义,defined(DEF_YES_STR)的值是1,能够打印出DEF_YES_STR should be printed.  

也许你想问,为什么啊?简单粗暴地记住一句:defined (identifier)认为,只要identifier是个女的宏就行……例4:

 #if defined(ENUM_YES) == 1
     printf("ENUM_YES should NOT be printed.\n");
 #endif

ENUM_YES不是有效的定义,defined(ENUM_YES)的值是0,以下代码不能打印出内容  

例5:

 #if defined(DEF_NO_ENUM) == 1
     printf("DEF_NO_ENUM should be printed.\n");
 #endif

DEF_NO_ENUM是有效的宏定义,defined(DEF_NO_ENUM)的值是1,能够打印出DEF_NO_ENUM should be printed.但是将enum常量套进#define里面,这个却是有效的。如果不能理解,那就再看一遍那句粗暴的话。

我们再来一个例子例5:

 #define DEF_XXX What the f**k define
 
 #if defined(DEF_XXX) == 1
     printf("DEF_XXX should be printed.\n");
 #endif

看到这里,你也许已经理解defined的用法了,但是我不建议你用#if defined(identifier) == 1的形式,而是用#if defined(identifier)或者#if !defined(identifier),如果要问为什么,我的回答是:习惯很重要。

01.2 关于#if/#elif/#else

我们用以下形式的预处理指令

 # if constant-expression new-line groupopt
 # elif constant-expression new-line groupopt

检测控制常量表达式的结果是否为0。  实际上这个#ifif是类似的,用法也很像,但有一点点需要注意的:这个是在预处理阶段执行的。

举例说明:

例1:

 enum
 {
     ENUM_NO,
     ENUM_YES
 };
 #if ENUM_YES
     printf("Cannot print this message!\n");
 #endif

例2:

 int n = 100;
 #if n
     printf("Cannot print this message!\n");
 #endif

例3:

 #define DEF_YES_NULL
 #if DEF_YES_NULL
     printf("Cannot print this message! Compile error!\n");
 #endif

例4:

 #define DEF_YES_0   0
 #if DEF_YES_0
     printf("Cannot print this message!\n");
 #endif

例5:

 #define DEF_YES_1   1
 #define DEF_YES_2   2
 #if DEF_YES_2 // DEF_YES_1
     printf("Can print this message!\n");
 #endif

例6:

 #define DEF_YES_STR "ABC"
 #if DEF_YES_STR
     printf("Cannot print this message! Compile error!\n");
 #endif

例7:

 #define DEF_YES_ENUM ENUM_YES
 #if DEF_YES_STR
     printf("Cannot print this message! Compile error!\n");
 #endif
 

以上例3/6/7会有编译错误,具体的错误原因可以看编译日志,实际上#if后面的宏会发生替换的。如果你还想问为什么,那就再复习下这句话:控制条件包含的表达式,一定是一个整型常量的。

01.3 关于#ifdef/#ifndef

#ifdef#ifndef实际上跟#if defined#if !defined是一样的。

02. 源文件包含

#include指令应标识实现可以处理的头文件或源文件。一般使用形式:

 # include <h-char-sequence>
 # include "q-char-sequence"

当你第一天学C语言写"Hello World"程序的时候,就应该知道这个#include了,例如#include <stdio.h>,好像也没什么好研究的。我先问几个问题:

  1. #include <stdio.h>能否写成#include "stdio.h"?<>和""有什么区别?

  2. #include是否一定要放在文件头,能放在文件中间吗?

  3. 可以#includeC文件吗,例如#include "plus.c"

  4. 头文件可以#include另一个头文件吗?

  5. #include "../plus.h"里面的../是什么东西?

  6. #include PLUS,而这个PLUS#define PLUS "plus.h",这样可以的吗?

  7. 同一个头文件被一个源文件#include了很多次会不会有问题?

答案:

  1. #include <filename>能否写成#include "filename"<>""有什么区别?  

    可以,但是有区别的(下文引用《C语言深度剖析》):

  2. 用尖括号<>括起来,也称为头文件,表示预处理到系统规定的路径中去获得这个文件(即 C 编译系统所提供的并存放在指定的子目录下的头文件)。找到文件后,用文件内容替换该语句。

    双引号""表示预处理应在当前目录中查找文件名为filename的文件,若没有找到,则按系统指定的路径信息,搜索其他目录。找到文件后,用文件内容替换该语句。

  3. #include是否一定要放在文件头,能放在文件中间吗?

    可以放在中间或者其他位置,它实际上会将这段include的内容嵌入到指定位置,当然你要留意include之前是否会用到这个内容了。

  4. 可以#includeC文件吗,例如#include "plus.c"

    可以,甚至可以#include "plus.txt",不信,你试试这个:

      // plus.txt
      int plus(int a, int b)
      {
          return a+b;
      }
      // main.c
      #include <stdio.h>
     
      int a = 1, b = 1;
     
      #include "plus.txt"
     
      int main(void)
      {
          printf("plus %d + %d = %d\n", a, b, plus(a,b));
          return 0;
      }
  5. 头文件可以#include另一个头文件吗?

    可以。STM32的一个Demo例子貌似就有这样的用法,例如:

      // includes.h
     
      #include <stdio.h>
      #include <file.h>
      #include "drivers.h"
      // main.c
     
      #include "includes.h"
      // ...
     

    必要的时候是挺好的,但建议尽可能不要,我个人觉得在庞大且复杂的项目中这种结构性不好,如果很多C文件都#include "includes.h"有很多头文件对本C文件可能是没必要的,也会增加编译/预处理时间。

  6. #include "../plus.h"里面的../是什么东西?

    这是相对路径的用法,如果你经常玩linux,这个肯定很熟悉,这种用法也叫trackant(蚁迹寻踪)。.代表当前目录, ..代表上层目录。

  7. #include PLUS,而这个PLUS#define PLUS "plus.h",这样可以的吗?

    可以,C99标准中提到这个,形式如# include pp-tokens

     #define PLUS "plus.h"
    #include PLUS

    但是不能:

     #include plus.h
  8. 同一个头文件被一个源文件#include了很多次会不会有问题?

    可能会。例如:

    // test.h
    enum{
    NO,
    YES,
    };
    // test.c
    include "test.h"
    include "test.h"
    // ...

    编译的时候会提示里面的enum常量重复定义了。怎么解决?可以将头文件加上个条件控制:

    // test.h
    #ifndef TEST_H
    #define TEST_H
    enum{
    NO,
    YES,
    };
    #endif

    多说一句,跟条件控制预处理命令配合使用,在同一个C文件多次include一个头文件在某些是会很巧妙的,这方面的内容打算在以后另外的文章中介绍。

    另外,对于选择性编译,我们也可以组合使用这些预处理命令,例如:

     #if VERSION == 1
    #define INCFILE "vers1.h"
    #elif VERSION == 2
    #define INCFILE "vers2.h" // and so on
    #else
    #define INCFILE "versN.h"
    #endif
    #include INCFILE

总而言之,对于这个#include,记住一句话:#include是将已存在文件的内容嵌入到当前文件中。

03. 宏替换

重头戏来了,这个内容也许是大家用的最多的了,里面的坑也特别多。我们先看看规则和要求:

  1. 当且仅当两者中的预处理令牌具有相同的编号,顺序,拼写和空格分隔(其中所有空格分隔均视为相同)时,两个替换列表相同。

  2. 不管是object-like macro还是function-like macro,我们都不要重复定义。如,不要出现以下使用:

    #define ABC(A)   A
    #define ABC 10

    如果你真的这样使用了,就看具体编译器了,有可能给你提示个诸如“warning: "ABC" redefined”的warning,或者错误(网上有人说会错误,但我没真实见到过)

  3. object-like macro定义中,identifier(宏名)和replacement list(内容)之间需要有空格。

    #define ABC#A

    会提示: warning: ISO C99 requires whitespace after the macro name

    但以下用法是没有警告的:

    #define ABC()12
  4. function-like macro中的参数要和定义的一致。

    #define SUM(a,b)  ((a)+(b))
    SUM(1,2,3);

    GCC提示:

    error: macro "SUM" passed 3 arguments, but takes just 2
    SUM(1,2,3);
    error: 'SUM' undeclared (first use in this function)
    SUM(1,2,3);
    #define SUM(a,b)  ((a)+(b))
    SUM(1+1);

    GCC提示:

    error: macro "SUM" requires 2 arguments, but only 1 given
    SUM(1+1);
    error: 'SUM' undeclared (first use in this function)
    SUM(1+1);

    但以下形式是没有问题的:

    #define SUM(a,b)  ((a)+(b))
    SUM(1,
    2);

    在触发function-like macro调用的预处理标记序列中,换行符被当作普通的空白字符对待。

  5. __VA_ARGS__ 只能用于function-like macro。这个__VA_ARGS__是可变参数的宏,是新的C99规范中新增的。

    不管你写成

    #define ABC __VA_ARGS__

    还是

    #define ABC #__VA_ARGS__

    又还是

    #define ABC() __VA_ARGS__

    GCC都会丢给你一个warning:warning: __VA_ARGS__ can only appear in the expansion of a C99 variadic macro 只有以下形式才不会警告

    #define ABC(...) __VA_ARGS__
  6. function-like macro里面的参数一定要是唯一的。

    #define SUM(a,a)  ((a)+(a))

    这样的用法是错的

  7. 如果在一个宏名(identifier)前面加一个#,实际上它不会进行替换的

    #define MACRO_IF if
    #MACRO_IF 1
    #endif

    这个#MACRO_IF 1是得不到#if 1效果的。

  8. 当替换序列中所有参数被置换之后,后续的预处理标记会一同被处理。

  9. 宏替换不可以递归替换,如以下是非法的:

       #define ABC  ABC+1

    还有一个交叉使用的例子:

    #define x (1 + y)
    #define y (10 * x)

    解析下:x就变成了(1 + (10 * x))y就变成了(10 * (1 + y))我想说的是:作为一个“遵纪守法”的优秀的程序员,千万别这么干。

  10. 已经替换过后的的预处理标记序列不会再进行进行预处理指令的识别和处理。如:

       #define ABC(ss)  ss
    ABC(#define AAA 123)

    GCC会甩给你一堆错误。

03.1 关于object-like macro的使用

这个好理解,例如教科书式的例子:

#define PI 3.14

还有

#define DEBUG_ERR -1

这不但表明了这个-1是error,还可以很方便地移植到别的平台,如果平台表达error有差异的话,可以统一地将DEBUG_ERR换成别的值。实际上,我们在上面的例子已经讲了好多关于这个object-like macro了。

但要记住一句话:宏的动作只是一个替换。宏是没有类型的。当别人问你:宏定义和const数据有什么区别?应该不需要我的答案了吧。

问个问题,宏可以用作注释的替换吗?

大写的不可以,参考《C语言深度剖析》:

#define BSC //

#define BMC /*

#define EMC */

D) BSC my single-line comment

E)  BMC my multi-line comment EMC

D)和E)都错误,为什么呢?因为注释先于预处理指令被处理,当这两行被展开成//…或//时,注释已处理完毕,此时再出现//…或//自然错误.因此,试图用宏开始或结束一段注释是不行的。

另外,有必要提一下,宏是有作用域的,这个跟C语言的其他作用域的情况类似,但是还有个#undef要注意下

  1. #undef一个宏后,这个宏在后面就没有了

  2. 如果我们不知道一个宏是否已定义,可以用#ifdef来判断下,对于不管有没有定义过,我想重新定义呢?可以这样:

    #ifdef ABC
    #undef ABC
    #define ABC 0
    #else
    #define ABC 0
    #endif
03.2 关于function-like macro的使用

一个经典的笔试题:请写出一个MIN宏。也许你看这个例子看到烂了,也许你毫不犹豫写出个:

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

有可能面试官会给你0分,不信你试试,以下是不是你想要的结果:

#define MIN(x, y)   x < y? x : y
int n = 3 * MIN(4, 5);

那我加个括号行了吧:

#define MIN(x, y)   (x < y? x : y)
int n = 3 * MIN(4, 5);

面试官还是给你个0分呢?不信你试试:

#define MIN(x, y)   (x < y? x : y)
int n = 3 * MIN(3, 4 < 5 ? 4 : 5);

想想那句话:宏的动作是个替换,你一步步替换出来看看是啥结果?(此处答案:略)

写成以下这样应该可以了吧:

#define MIN(x, y)   ((x) < (y)? (x) : (y))
int n = 3 * MIN(3, 4 < 5 ? 4 : 5);

面试官可能会给你满分,但是我们这里分享干货,继续探讨下,试试这个:

#define MIN(x, y)   ((x) < (y)? (x) : (y))
double xx = 1.0;
double yy = MIN(xx++, 1.5);
printf("xx=%f, yy=%f\n",xx,yy);

结果是不是很意外?

xx=3.000000, yy=2.000000

GNU有个改进的方法:

#define MIN(A,B)	({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })
double xx = 1.0;
double yy = MIN(xx++, 1.5);
printf("xx=%f, yy=%f\n",xx,yy);

你测试下看看,这回应该是你想要的结果了。

xx=2.000000, yy=1.000000

还有个do{ }while(0)要讲讲

为什么要用这个东西,有什么好处?

举个例子:

#define set_on()	set_on_func1(1); set_on_func2(1);
set_on();

这样做似乎没啥问题。万一有以下形式呢?

#define set_on()	set_on_func1(1); set_on_func2(1);
if(on)
set_on();

实际上就变成了

#define set_on()	set_on_func1(1); set_on_func2(1);
if(on)
set_on_func1(1);
set_on_func2(1);

这应该不是你想要的效果吧。那就改进下吧,加个{}可以么?

#define set_on()	{set_on_func1(1); set_on_func2(1);}
if(on)
set_on();

问题是这样编译会出错的,在于后面那个;

我们直接用do{ }while(0)试试?

#define set_on()	do{set_on_func1(1); set_on_func2(1);}while(0)
if(on)
set_on();

实际上,这个do{ }while(0)用在宏后面,可以保证其内容在替换后不会被拆散,保持其一致性。

另外,在此说句题外话:你居然要做一个接口给别人使用,就应当把你的接口做得万无一失。C语言赋予了我们这么多特性和权力,就肯定会有人耍出“花样”来。

03.3 关于###的使用

首先说一下:

  • #是将内容字符串化

  • ##是连接字符串

直接在tutorialspoint找两个例子说明下:

例1:

#include <stdio.h>

#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")

int main(void) {
message_for(Carole, Debra);
return 0;
}

输出结果是:

Carole and Debra: We love you!

例1:

#include <stdio.h>

#define tokenpaster(n) printf ("token" #n " = %d", token##n)

int main(void) {
int token34 = 40;
tokenpaster(34);
return 0;
}

输出结果是:

token34 = 40

tokenpaster(34);这个通过预处理后就变成了printf ("token34 = %d", token34)

到这来,我想大家基本上理解这###是什么意思了吧。

我们再来看看网上的另一个例子:

#define f(a,b)  a##b
#define g(a) #a
#define h(a) g(a)
printf("h(f(1,2))-> %s, g(f(1,2))-> %s\n", h(f(1,2)), g(f(1,2)));

输出的结果是:

h(f(1,2))-> 12, g(f(1,2))-> f(1,2)

我们一步一步来解析下:先看h(f(1,2))

  1. h(f(1,2))预处理先找到h这个宏

  2. 然后替换成g(f(1,2))

  3. 继续往后走,得到f(1,2)

  4. 再继续往后,得到12

再看g(f(1,2))

  1. g(f(1,2))预处理先找到g这个宏

  2. 然后替换成#f(1,2)

  3. 再然后也没有然后了,将这个#f(1,2)变成了字符串f(1,2)了,因为预处理遇到#不会继续了。(详见章节前的规则)

再来一个例子:

#define _STR(x) #x
#define STR(x) _STR(x)
char * pc1 = _STR(__FILE__);
char * pc2 = STR(__FILE__);
printf("%s %s %s\n", pc1, pc2, __FILE__);

输出:

__FILE__ "c_test_c_file.c" c_test_c_file.c

想想为什么,提示:宏中遇到###时就不会再展开宏中嵌套的宏了。

我们不钻牛角尖了,来个实际应用的例子:

typedef struct os_thread_def  {
os_pthread pthread; ///< start address of thread function
osPriority tpriority; ///< initial thread priority
uint32_t instances; ///< maximum number of instances of that thread function
uint32_t stacksize; ///< stack size requirements in bytes; 0 is default stack size
} osThreadDef_t;

#define osThreadDef(name, priority, instances, stacksz) \
const osThreadDef_t os_thread_def_##name = \
{ (name), (priority), (instances), (stacksz) }

这个osThreadDef会根据输入的参数创建一个结构体变量(名字还根据输入的参数name不一样而不一样),然后包含了部分参数当做结构体内容。这样做不但简洁,而且还防止名字重复。

osThreadDef (Thread_Mutex, osPriorityNormal, 1, 0);

这个会预处理成一个这样的变量:

const osThreadDef_t os_thread_def_Thread_Mutex =
{
Thread_Mutex,
osPriorityNormal,
1,
0
};

是不是很爽?

03.4 关于__VA_ARGS__的使用

在C语言的标准库中,printfscanf等函数的参数是可变的。而这个__VA_ARGS__就是C99定义的。为可变参数函数在宏定义中提供可能。那么,我们一般用来干嘛呢?举个例子,我们在调试程序时,不想直接用printf来打印log,而想通过一个宏函数来做,当不需要输出log的时候,可以将其定义成空的东西。

#define DEBUG_PRINTF(format, ...)   printf(format, ...)
DEBUG_PRINTF("Hello World!\n");

然后当你高高兴兴地编译的时候,GCC无情地丢给你一个error:

error: expected expression before '...' token
#define DEBUG_PRINTF(format, ...) printf(format, ...)
^
note: in expansion of macro 'DEBUG_PRINTF'
DEBUG_PRINTF("Hello World!\n");
^

WHY??你需要__VA_ARGS__了,怎么搞?来试试这个:

#define DEBUG_PRINTF(format, ...)   printf(format, __VA_ARGS__)
DEBUG_PRINTF("Hello World!\n");

然而,GCC还是给你个错误:

error: expected expression before ')' token
#define DEBUG_PRINTF(format, ...) printf(format, __VA_ARGS__)
^
note: in expansion of macro 'DEBUG_PRINTF'
DEBUG_PRINTF("Hello World!\n");
^

什么鬼?是不是使用方法有问题?

#define DEBUG_PRINTF(format, ...)   printf(format, __VA_ARGS__)
DEBUG_PRINTF("%s","Hello World!\n");

诶,好像可以了哦,但是我想用上面那样调用,怎么办?这就要请出##了:

#define DEBUG_PRINTF(format, ...)   printf(format, ##__VA_ARGS__)
DEBUG_PRINTF("Hello World!\n");
DEBUG_PRINTF("Hello %s", "World!\n");

这个##,的作用是将token(如format等)连接起来,如果token为空,那就不连接。

那么用宏定义一个开关,愉快地实现一个log输出宏了:

#ifdef DEBUG
#define DEBUG_PRINTF(format, ...) printf(format, ##__VA_ARGS__)
#else
#define LOG(format, ...)
#endif

在来个例子:

#define ABC(...) #__VA_ARGS__
printf(ABC(123, 456));

你觉得会输出什么结果?

123, 456

04. 行控制

#line这个东西,是不是没见过,到底干嘛用的呢?

可以简单地理解为,可以改变行号的,甚至文件名都可以改变。它的基本形式如下:

# line digit-sequence "s-char-sequenceopt"

其中

  • digit-sequence是数值,范围为1~2147483647  

  • "s-char-sequenceopt"是字符串,可以省略

我们直接用代码示例来看看其作用:

    #line 12345 "abcdefg.xxxxx"
printf("%s line: %d\n", __FILE__, __LINE__);
printf("%s line: %d\n", __FILE__, __LINE__);

输出:

abcdefg.xxxxx line: 12345
abcdefg.xxxxx line: 12346

可以看出,其可以改变下一行内容所在的行号,以及当前文件的文件名。看起来,这货貌似没啥用。

实际上,我们通过这个指令可以固定文件名和行号,以分析某些特定问题。

05. 错误指示

#error,这个东西很好理解,就是在编译器遇到这个#error的时候就会停下来,然后输出其后面的信息。

其一般形式如下:

# error pp-tokensopt

这个pp-tokensopt比较随意,可以省略,也不用是字符串,其他内容也行。

06. 空指令

这个在我看来,真没看到有实际作用。它就是什么都不干。其形式为:

#

#后面什么都么有。

07. 预定义宏名

我们可以通过预定义宏名来或者某些信息,特别在调试的时候,是挺有用的。

其实我们在前面的04.行控制章节的例子就有例子了。

在此,我们汇总下各个名称以及其所代表的的含义:

预定义宏名 含义
__LINE__ 当前源码的行号,是一个整数
__FILE__ 当前源码的文件名,是一个字符串
__DATE__ 源文件的翻译日期,是一个“Mmm dd yyyy”的字符串文字
__TIME__ 源文件的翻译时间,是一个“hh:mm:ss”的字符串文字
__STDC__ 由编译器具体决定
__STDC_HOSTED__ 如果编译器的目标系统环境中包含完整的标准C库,那么这个宏就定义为1,否则宏的值为0
__STDC_VERSION__ 是一个整数199901L
__STDC_IEC_559__ 整数1,以指示是否遵守这个规格的附件F(IEC 60559 floating-point arithmetic)
__STDC_IEC_559_COMPLEX__ 整数1,以指示是否遵守这个规格的附件F(IEC 60559 compatible complex arithmetic)
__STDC_ISO_10646__ yyyymmL形式的整数(如199712L),旨在表明类型wchar_t的值是ISO / IEC 10646定义的字符以及指定年份和月份的所有修订和技术勘误的编码表示形式。
__cplusplus 如果是在编译一个C++文件,这是一个整数值199711L

大家可以将这些内容打印出来看看具体是什么内容,在此不累述了。

另外,特别注意,这些宏名,不可以被#define#undef等修饰。

08. Pragma命令/操作

这个#Pragma在众多预处理命令中最为复杂了。

我先参考下cppreference.com的说法:

实现定义行为控制

#pragma 指令控制实现定义行为。

语法

#pragma 语用形参    (1)_Pragma (字符串字面量 )  (2) 1) 以实现定义方式行动(除非 语用形参 是后述的标准 pragma 之一)。

2) 移除 字符串字面量 的编码前缀(若存在)、外层引号,及开头/尾随空白符,将每个 \"" ,每个 \\\ 替换,然后记号化结果(如翻译阶段 3 中),再如同在 (1) 中输出到 #pragma 一般使用结果。

解释

pragma 指令控制编译器的实现指定行为,如禁用编译器警告或更改对齐要求。忽略任何不被识别的 pragma 。

标准 pragma

语言标准定义下列三个 pragma :

#pragma STDC FENV_ACCESS 实参 (1)

#pragma STDC FP_CONTRACT 实参 (2)

#pragma STDC CX_LIMITED_RANGE 实参 (3)

其中 实参ONOFFDEFAULT 之一。

1) 若设为 ON ,则告知编译器程序将访问或修改浮点环境,这意味着禁用可能推翻标志测试和模式更改(例如,全局共用子表达式删除、代码移动,及常量折叠)的优化。默认值为实现定义,通常是 OFF

2) 允许缩略浮点表达式,即忽略舍入错误和浮点异常的优化,被观察成表达式以如同书写方式准确求值。例如,允许 (x*y) + z的实现使用单条融合乘加CPU指令。默认值为实现定义,通常是 ON

3) 告知编译器复数的乘法、除法,及绝对值可以用简化的数学公式 。换言之,程序员保证传递给这些函数的值范围是受限的。默认值为 OFF

注意:不支持这些 pragma 的编译器可能提供等价的编译时选项,例如 gcc 的 -fcx-limited-range-ffp-contract

非标准 pragma

#pragma once...

这个标准的pragma貌似平时很少用,也有点费解。C99标准也有一些描述:

# pragma pp-tokensopt
  1. 如果没有用这个_STDC_跟着#param后面,编译器会按其实际方式执行,该行为可能会导致翻译失败或者不符合标准。

  2. 如果用到_STDC_跟着#param后面,则不会对该指令进行宏替换。例如:

    #pragma STDC FP_CONTRACT on-off-switch
    #pragma STDC FENV_ACCESS on-off-switch
    #pragma STDC CX_LIMITED_RANGE on-off-switch
    // on-off-switch: one of "ON OFF DEFAULT"

对于这个非标准的 pragma,好像我们用的还挺多的,例如#pragma once#pragma message#pragma warning等。

  1. #pragma once

    这个很多编译器都支持,放在头文件里面,让其只参与一次编译,放在头文件重复包含,效果类似于:

    #ifndef _XXX_
    #define _XXX_

    #endif
  2. #pragma message

    形式如下

    #paragma message("output this message")

    简单地说,他可以在预处理时输出一串信息,这个在预处理的时候非常有用,我经常用它来输出log。

    具体用法可以像这样:

    #ifdef _X86
    #pragma message(“_X86 macro activated!”)
    #endif
  3. #pragma warning

    这个是对警告信息的处理,例如:

    #pragma warning(disable:4507)

    将4507号经过关闭,即你看不到这个警告。

    #pragma warning(once:4385)

    只让4385这个警告只显示一次。

    #pragma warning(error:164)

    把164号经过当error显示出来。

    以上还可以合并起来写成:

    #pragma warning( disable : 4507; once : 4385; error : 164 )
  4. #pragma pack

    这个可以改变结构体内存对齐的方式。例如,以下结构体内存可以按1字节对齐:

    #pragma pack(1)
    struct abc
    {
    int a;
    char b;
    short c;
    int d;
    };
    #pragma pack() // cancel pack(1)
  5. #pragma comment

    我们可以用其导入一个lib,例如:

    #pragma comment ( lib,"wpcap.lib" )

还有一个值得一提的是_Pragma,这个是C99新增加的,实际上跟#param一样,但是其有什么特别作用吗?

我们可以把_Pragma放在宏定义后面,因为它不需要这个#,不存在不能展开宏替换问题,例如:

#define LISTING(x) PRAGMA(listing on #x)
#define PRAGMA(x) _Pragma(#x)
LISTING ( ..\listing.dir )

到这来,关于C语言的预处理,基本讲完了,当然我们还有一些更奇妙的使用,将会用另一文章来讲解。