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

作者:神秘网友 发布时间:2020-10-31 21:45:57

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

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

文章目录

  • C编译器运行原理
  • C编译器运行原理
    • c编译器的编译阶段
    • 标记
    • 编译器的各个阶段以及它们之间的接口
    • gcc编译过程
    • 预处理指令
  • 1.条件包含
      • defined 和define
      • defined
      • #if 和#elif以及#else
  • 2.源文件包含
  • 3.宏替换
      • constraints:
      • Semantics:
      • 关于object-like macro的使用
      • 关于function-like macro的使用
      • `do{ }while(0)`
      • 关于`#`和`##`的使用
      • 关于`__VA_ARGS__`的使用
  • 4.行控制
  • 5.错误指令
  • 6.空指令
  • 7.预定义宏名
  • 8.编译命令/操作
    • #pragma命令
    • _Pragma运算符
        • 实现定义行为控制
          • 语法
            • 解释
            • 标准 pragma
            • 非标准 pragma

文章主要:来源嵌入式软件实战派,可以在微信公众号搜索关注一波哦

附上大佬博客链接:https://blog.csdn.net/lianyunyouyou/article/details/106315120

下面内容我增加了一些自己的看法作为笔记,还望来到这的小伙伴先移步大佬文章阅读

C编译器运行原理

C编译器运行原理

一但使用文本编辑器编写完源代码, 就可以调用C编译器把源代码转换成机器码。 编译器对翻译单元进行处理,一个翻译单元由一个源代码文件以及所有通过#include命令引用的头文件组成。 如果编译器在翻译单元内没有发现错误, 则会生成包含了对应机器码的目标文件 (object file) 。 目标文件的扩展名通常为.o或.obj。 除此之外,编译器也可 以产生一个汇编器列表。

  • 目标文件也称为模块 (module) 。一个链接库(例如C标准库)包含了多个编译好的可以快速获取的模块, 模块里有许多标准函数。

编译器将一个C程序的每个翻译单元(指的是每个源代码文件,以及其所包含的所有头文件)翻译成一个独立的目标文件。然后编译器调用链接器(linker)将所有的目标文件和所用到的链接库函数结合起来,成为一个可执行文件(executable file)。下图展示了一个程序从几个源代码文件和链接库编译和链接的过程。 可执行文件内也包含了所有供目标操作系统加载与启动该程序的信息。

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

整个编译过程要经过8个逻辑步骤。只要不影响结果,某些编译器可以将多个步骤结合在一起。这些步骤包括:

  1. 从源代码文件中读取并转换字符, 如果必要,则将字符转换成源代码字符集的字符。 如果源代码中行尾 (end-of-line) 字符不是换行符 (newline character) 时,全部替换成换行符。同样,任何三字符组符号会被替换成其对应的单一字符(但是,双字符组不在这里做处理,它们不会被替换成对应的单一字符)。

  2. 无论何时, 只要反斜线符后面紧跟着换行符,预处理器就会将两者(反斜线符和换行符)都删除。 因为行尾字符视为预处理器命令的终止, 所以该处理步骤允许反斜线符放在一行的结尾处,以让预处理器命令(比如宏的定义)可以在下一行继续。每个源代码文件,如果不是完全为空,则必须以一个换行符作为结尾。(注意和下面预处理指令对应,联系“宏函数”)

  3. 将源代码文件分解成若干预处理器标记(参见下面标记)和空格符序列。每个注释都被看作一个空格。

  4. 执行预处理器命令, 展开宏调用。

    步骤1~4不仅只作用于源代码文件, 也作用于#include命令所插入的任何文件。 一且编译器执行预处理器命令, 编译器就会从源代码的工作副本中删除掉这些文件。

  5. 字符常量和字符串字面量中的字符和转义序列, 会被转换成运行字符集中对应的字符。

  6. 相邻的字符串字面量被连接为一个字符串。

  7. 实际的编译工作开始:编译器分析标记序列,并生成对应的机器码。

  8. 链接器解析对外部对象和函数的引用,并生成可执行文件。如果模块引用的外部对象或函数,在所有翻译单元中没有被定义,链接器就会从外部的标准库或其他指定 的链接库中复制它们。在一个程序中,不得多次定义外部对象和函数。

    对于大多数编译器而言, 预处理器可作为一个独立的程序, 也可以通过编译器提供选项只执行预处理(前述过程的1-4步)。 该步骤允许验证所编写的预处理命令是否达到预期的效果。

标记 (token) 可以是关键字、 标识符、常量 字符串字面址或者符号。 C语言中的符号组成可以包括一个或多个标点符号, 以及作为运算符或双字符组的函数, 或者语义上的重要性, 例如, 分号表示一个简单语句的结束, 大括号{}表示一条语句块。下面的C语句包含了5个标记:

printf("Hello, world.\n"); 
#也可以写为:
printf 
(
"Hello, world.\n"
)
;

在编译过程第3阶段时,会分析由预处理器所翻译得到的标记。这与编译过程第7阶段的标记解释仅有微小的区别:

? 在#include命令中,预处理器识别额外的标记和"fliename"。
? 在预处理阶段,字符常量和字符串字面最还未从源代码字符集转换成运行字符集。
? 与编译器特点不同,预处理器不区分整数常量和浮点数常量。

在将源代码文件解析成标记过程中, 编译器(或预处理器)总是采用下面的原则:每个连续的非空格符必须附加到正在读取的标记后面, 直到出现附加后使得原有效标记变为无效为止。 该原则可以避免在后续的表达式中产生歧义, 例如:

a+++b

因为第一个+无法被当作标识符的一部分,也不能被当作以a开头的关键字的一部分,所以这个+是一个新标记的开始。第二个+附加到第一个后面,形成一个有效的记号(也就是++),但是如果第三个+如果再附加上来,就不是有效的记号。因此这个表达式必须被解析为:

a++ + b

网上的大多数文章对c的编译过程讲到的不到位,这里我们就不深究了看图,有兴趣的可以看《现代编译原理C语言描述》或 《编译原理》,需要资源的联系我。
基于C99规范,最全C语言预处理知识总结(转)

具体名词释义:
基于C99规范,最全C语言预处理知识总结(转)

预处理:gcc -E hello.c -o hello.i
编  译:gcc -S hello.i -o hello.s
汇  编:gcc -c hello.s -o hello.o
链  接:gcc    hello.o -o hello

gcc 选项释义

选项含义
-E只进行预处理
-S(大写)只进行预处理和编译
-c(小写)只进行预处理、编译和汇编
-o file指定生成的输出文件名为 file

文件后缀释义

文件后缀含义
.cC 语言文件
.i预处理后的 C 语言文件
.s编译后的汇编文件
.o编译后的目标文件

从C源代码到可执行文件之间的8个编译步骤。在前4个步骤中,C预处理器会为编译器准备好源代码。预处理器处理获得结果是修改过的源代码,注释被删除,并且预处理命令(preprocessing directive)也被它们自身的执行结果所取代。

对应预处理指令而言,这些指令可以将别的源代码内容插入到所指定的位置 ;可以标识出只有在特定条件下才会被编译的某一段程序代码;可以定义类似标识符功能的宏,在编译时,预处理器会用别的文本取代该宏。

每个预处理指令均独占一行,以#字符作为开头。只有空格符(space)和制表符(tab)可以出现在#字符的前面(包括替换了注释或可能的空格)。命令在遇到本行的第一个换行符时结束。最短的预处理命令是空命令(null directive)。该命令整行只有一个#,可能还伴随一些注释或空白符。空命令不起任何作用:预处理器会直接将它们从源代码中删除。

以下代码在Gcc中可以通过编译

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

命令可以跨行,可以在行尾放置一个反斜杠(\),并且在下一行继续上一行未完成的命令。如下所示:(源自linux/kernel.h)

Tips:要是用用内联函数是多么的快乐

#define MAX_S(x, y) ({ \
    const typeof(x) _x = (x);  \
    const typeof(y) _y = (y);  \
    (void)(&_x == &_y);       \
    _x > _y ? _x : _y; })

#define TMAX_S(type, x, y) ({ \
    type _x = (x);  \
    type _y = (y);  \
    _x > _y ? _x: _y; })

Gcc编译器将包含在圆括号和大括号双层括号内的复合语句看作是一个表达式,它可出现在任何允许表达式的地方;复合语句中可声明局部变量,判断循环条件等复杂处理。而表达式的最后一条语句必须是一个表达式,它的计算结果作为返回值。MAX_S和TMAX_S宏内就定义局部变量以消除参数副作用。

 MAX_S宏内(void)(&_x == &_y)语句用于检查参数类型一致性。当参数x和y类型不同时,会产生” comparison of distinct pointer types lacks a cast”的编译警告。

 注意,MAX_S和TMAX_S宏虽可避免参数副作用,但会增加内存开销并降低执行效率。若使用者能保证宏参数不存在副作用,则可选用普通定义(即MAX宏)。 

Tips:这个反斜杠必须是换行符前的最后一个字符。预处理器会直接删除反斜杠和换行符,并把两行命令连接成一行。因为预处理器也会把每个注释使用空格来代替, 所以如果再反斜杠和换行字符之间放置注释,那这样的反斜线就不具有跨行作用。

1.条件包含

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

defined 和define

#ifdef 和 #if defined 的区别在于,后者可以组成复杂的预编译条件,比如:

#if defined (AAA) && defined (BBB)
	printf("This is a complex  pragma condition.");
#endif
#if defined (AAA) || VERSION > 12
	printf("This is a complex  pragma condition.");
#endif

而#ifdef 就不能用上面的用法,也就是说,当你要判断单个宏是否定义时#ifdef 和 #if defined 效果是一样的,但是当你要判断复杂的条件时,只能用 #if defined

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
#define DEF_YES_ENUM ENUM_YES

根据以上定义有

例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
123

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),如果要问为什么,我的回答是:习惯很重要。

#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

没有ENUM_YES这个宏,printf语句将在预处理期间被删除。

例2:

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

没有n这个宏 预处理只做简单文本替换,printf语句将在预处理期间被删除。

例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

DEF_YES_0 宏替换为0 预处理后将printf语句删除了。

例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_ENUM
    printf("Cannot print this message! Compile error!\n");
#endif

这里预处理后 直接将 printf语句删除了, DEF_YES_NUM 宏替换后就是 ENUM_YES 而 这时ENUM_YES还没有值,别被枚举迷惑了,现在还是预处理期间呢预处理只做简单文本替换!!!

以上例3/6会有编译错误,具体

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

枚举补充:

C语言里枚举变量和整数变量可以互相赋值,尽管把一个不是枚举值的整数赋给枚举变量没有意义。例如表示星期的枚举值是1~7,如果把8赋给一个该类型枚举变量,没有任何意义,但编译不会报错。C++里避免了上面问题,枚举变量可以赋值给整数变量,整数变量不能赋给枚举变量。

C语言对枚举和整数变量类型的检查不严格,如果程序员不认真检查,会引起各种问题。

枚举的大小还好作用域等相关 具体还得看c++IOS

2.源文件包含

#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语言深度剖析》):

    • 用尖括号<>括起来,也称为头文件,表示预处理到系统规定的路径中去获得这个文件(即 C 编译系统所提供的并存放在指定的子目录下的头文件)。找到文件后,用文件内容替换该语句。
    • 双引号""表示预处理应在当前目录中查找文件名为filename的文件,若没有找到,则按系统指定的路径信息,搜索其他目录。找到文件后,用文件内容替换该语句。
  2. #include是否一定要放在文件头,能放在文件中间吗?
    可以放在中间或者其他位置,它实际上会将这段include的内容嵌入到指定位置,当然你要留意include之前是否会用到这个内容了。

  3. 可以#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;
     }
    
  4. 头文件可以#include另一个头文件吗?
    可以。STM32的一个Demo例子貌似就有这样的用法,例如:

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

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

  5. #include "../plus.h"里面的../是什么东西?
    这是相对路径的用法,如果你经常玩linux,这个肯定很熟悉,这种用法也叫trackant(蚁迹寻踪)。.代表当前目录, ..代表上层目录。

  6. #include PLUS,而这个PLUS#define PLUS "plus.h",这样可以的吗?
    可以,C99标准中提到这个,形式如# include pp-tokens

     #define PLUS "plus.h"
     #include PLUS
    

    但是不能:

     #include plus.h
    
  7. 同一个头文件被一个源文件#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
    

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

     #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是将已存在文件的内容嵌入到当前文件中。

3.宏替换

constraints:

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

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

    #define ABC(A)   A
    #define ABC 10
    

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

    最后实现编译器实现是以最后一个宏为准,这里的是ABC 10,我用的Gcc没报错误,有warning

    如果 使用:

    #define ABC 10
    #define ABC 10
    

    编译器错误都不报, c++11 ios里面描述大概就是这么意思如果要定义 一个同名的宏那么就得一模一样???

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

    #define ABC#A
    

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

    最后ABC被替换成 #A 暂时想不到又啥奇技巧淫了。。。

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

    #define ABC()12
    

    我的理解是参加语义分析部分 )和后面的1不能构成有效标记 所以自然可以用了,也许处理过程中就加了空格把,参见本文c编译器的编译阶段的3。最后试验是成功的,感谢嵌入式实战派大佬教的 gcc -dM -E 命令。

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

  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))
    

    这样的用法是错的 error: duplicate macro parameter “a”

Semantics:

  1. 紧随定义之后的标识符称为宏名称。宏名称只有一个名称空间。对于任何一种形式的宏,预处理令牌替换列表之前或之后的任何空格字符均不视为替换列表的一部分

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

    #define MACRO_IF if
    #MACRO_IF    1
    #endif
    

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

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

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

       #define ABC  ABC+1
    

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

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

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

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

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

    GCC会甩给你一堆错误。

关于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
    

关于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语言赋予了我们这么多特性和权力,就肯定会有人耍出“花样”来。

关于###的使用

首先说一下:

  • #是将内容字符串化
  • ##是连接字符串

直接在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!

例2:

#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  
};

是不是很爽?

关于__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

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

这应该是 空白符都 当划归为空格了处理,估计还是和语义分析有关,编译器又干的好事。。。。

4.行控制

#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

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

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

5.错误指令

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

其一般形式如下:

# error pp-tokensopt

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

6.空指令

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

# 

#后面什么都么有。

7.预定义宏名

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

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

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

预定义宏名含义
__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等修饰。

8.编译命令/操作

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

#pragma与__pragma的区别与联系`

2009-01-19 15:47

__pragma与#pragma的功能相同,所不同的是: 1.#pragma是预处理器指令;__pragma是关键字。 2.对于#pragma,warning,once等选项跟在其后面,中间以空格隔开;而对于__pragma,warning等选项放在__pragma后面的括号()中,如:__pragma(warning(disable:6246))。 3.__pragma可用在宏定义内部,而#pragma不可以。因为#pragma含有#符号,而该符号会被预处理器解释为字符串化符号。

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

[]: https://blog.csdn.net/diligentcatrich/article/details/6237559 “#pragmay与_pragma的区别与联系”

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

这个#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
  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"
    1234
    

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

  1. #pragma once

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

    #ifndef _XXX_
    #define _XXX_
    
    #endif
    1234
    
  2. #pragma message

    形式如下

    #paragma message("output this message")
    1
    

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

    具体用法可以像这样:

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

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

    #pragma warning(disable:4507)
    1
    

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

    #pragma warning(once:4385)
    1
    

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

    #pragma warning(error:164)
    1
    

    把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语言的预处理,基本讲完了,当然我们还有一些更奇妙的使用,将会用另一文章来讲解。


2. `#pragma message`

形式如下

#paragma message(“output this message”)


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

具体用法可以像这样:

```c
#ifdef _X86
	#pragma message(“_X86 macro activated!”)
#endif
  1. #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 )
    
  2. #pragma pack

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

    #pragma pack(1)
    struct abc
    {
    	int a;
    	char b;
    	short c;
    	int d;
    };
    #pragma pack() // cancel pack(1)
    
  3. #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语言的预处理,基本讲完了,当然我们还有一些更奇妙的使用,将会用另一文章来讲解。
(文章主要来源:关注微信公众号:嵌入式软件实战派)

附上大佬博客链接:https://blog.csdn.net/lianyunyouyou/article/details/106315120

基于C99规范,最全C语言预处理知识总结(转)相关教程

  1. 基于决策树方法的专利被引影响因素研究(python代码 图文 超详细

    基于决策树方法的专利被引影响因素研究(python代码 图文 超详细) 目录 综述 1.数据来源与指标选取 1.1数据来源 1.2指标选取 2.数据清洗与转换 2.1数据清洗 2.2数据转换 3.决策树模型构建及准确性评估与优化 3.1模型构建 准确性评估与优化 4.分析结果 本次研

  2. 百度AI人脸

    百度AI人脸 人脸识别技术 人脸识别是基于人的脸部特征信息进行身份识别的一种生物识别技术,用摄像机或者摄像头采用含有人脸的图像或视频流,并自动在图像中检测和跟踪人脸,进行对检测到的人脸进行脸部的一系列相关技术操作,叫人脸识别。 人脸识别是一项热

  3. 基于电商中台架构-商品系统设计(一)

    基于电商中台架构-商品系统设计(一) 文章目录 一、 总体设计 基础层 平台层 二、 概念定义 Item-sku 前后端商品 关联关系 商品快照 商品打标 类目 属性 三、技术设计 关系图 商品关键字段介绍 商品历史表Item_history设计 商品快照设计 商品打标设计 商品扩展

  4. 基于RateLimiter实现单机版限流方案

    基于RateLimiter实现单机版限流方案 RateLimiter 限流方案只适合轻量级别的单机限流,并不适合分布式限流 pom.xm文件 dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.

  5. 基于电商中台架构-商品系统设计(二):类目设计

    基于电商中台架构-商品系统设计(二):类目设计 类目设计 概念定义 什么是类目 前后台类目 属性和属性值 导航属性 销售属性 普通属性 子属性和子属性值 技术设计 关系图 类目属性树形结构图 类目表 缓存 分布式缓存 分布式本地缓存 总结 概念定义 类目简单来说

  6. 基于Thinkphp使用同一个域名,PC和M端访问不同模板

    基于Thinkphp使用同一个域名,PC和M端访问不同模板 一、首先目录结构展示:(主要修改这几个文件) 二、更改入口文件 index.php require DIR . ‘./isMobile.php’; 三、在入口文件index.php同级目录下,增加common.php 文件,代码为: ?phpfunction isMobile

  7. 基于arcgis的遥感影像标签制作(目标检测)

    基于arcgis的遥感影像标签制作(目标检测) 文章目录 1. 在arcgis中新建线矢量 2. 检测框绘制 3. 检测框坐标转换(线矢量) 4. 检测框坐标转换(面矢量) 1. 在arcgis中新建线矢量 新建线矢量,添加空间参考,例如wgs_1984。 2. 检测框绘制 3. 检测框坐标转换

  8. 基于mat做简单的堆内存溢出分析

    基于mat做简单的堆内存溢出分析 文章目录 下载启动mat 生成 dump文件 加载dump文件 首先我们需要下载启动mat 下载mat 官网下载地址 这里更加自己的系统选择合适的压缩包 我选择的是Windows(x86),下载之后是个zip包,解压就直接可以使用 如果分析的dump文件过