0x06-C语言预处理器

预处理最大的标志便是大写,虽然这不是标准,但请你在使用的时候大写,为了自己,也为了后人。

预处理器在一般看来,用得最多的还是宏,这里总结一下预处理器的用法。

  1. #include <stdio.h>
  2. #define MACRO_OF_MINE
  3. #ifdef MACRO_OF_MINE
  4. #else
  5. #endif

上述五个预处理是最常看见的,第一个代表着包含一个头文件,可以理解为没有它很多功能都无法使用,例如C语言并没有把输入输入纳入标准当中,而是使用库函数来提供,所以只有包含了stdio.h这个头文件,我们才能使用那些输入输出函数。
#define则是使用频率第二高的预处理机制,广泛用在常量的定义,只不过它和const声明的常量有所区别:

  1. #define MAR_VA 100
  2. const int Con_va = 100;
  3. ...
  4. /*定义两个数组*/
  5. ...
  6. for(int i = 0;i < 10;++i)
  7. {
  8. mar_arr[i] = MAR_VA;
  9. con_arr[i] = Con_va;
  10. }
  • 区别1,定义上MAR_VA可以用于数组维数,而Con_va则不行
  • 区别2,在使用时,MAR_VA的原理是在文中找到所有使用本身的地方,用值替代,也就是说Con_va将只有一分真迹,而MAR_VA则会有n份真迹(n为使用的次数)
    剩下三个则是在保护头文件中使用颇多。

几个比较实用的用于调试的宏,由C语言自带

  • __LINE__和__FILE__
    用于显示当前行号和当前文件名
  • __DATE__和__TIME__
    用于显示当前的日期和时间
  • __func__(C99)
    用于显示当前所在外层函数的名字

上述所说的五种宏直接当成值来使用即可。

  • __STDC__

    • 如果你想检验你现在使用的编译器是否遵循ISO标准,用它,如果是他的值为1。

      1. printf("%d\n", __STDC__);

      输出: 1

    • 如果你想进一步确定编译器使用的标准版本是C99还是C89可以使用__STDC__VERSION__,C99(199901)

      1. printf("%d\n", __STDC_VERSION__);

      输出: 199901

可能很多人对这些宏没什么感触,实际上一般的确是用不到,但是:

当你在写一些隐晦的东西时
volatile int x = 10;

你试试把这个代码用 -std=c99 编译一下,如果不出意外应该是出错的

在 ISO 标准里,volatile是用__volatile__来实现的,这个对GCC,Clang,Visual C++而言都是如此
除此之外还有许多,有待你们自己发掘。

对于#define
  1. 预处理器一般只对同一行定义有效,但如果加上反斜杠,也能一直读取下去

    1. #define err(flag) \
    2. if(flag) \
    3. printf("Correctly")

    可以看出来,并没有在末尾添加;,并不是因为宏不需要,而是因为,我们总是将宏近似当成函数在使用,而函数调用之后总是需要以;结尾,为了不造成混乱,于是在宏定义中我们默认不添加;,而在代码源文件中使用,防止定义混乱。

  2. 预处理同样能够带来一些便利

    1. #define SWAP1(a, b) (a += b, b = a - b, a -= b)
    2. #define SWAP2(x, y) {x ^= y; y ^= x; x ^= y}

    引用之前的例子,交换两数的宏写法可以有效避免函数开销,由于其是直接在调用处展开代码块,故其比拟直接嵌入的代码。但,偶尔还是会出现一些不和谐的错误,对于初学者来说:

    1. int v1 = 10;
    2. int v2 = 20;
    3. SWAP1(v1, v2);
    4. SWAP2(v1, v2);//报错

    对于上述代码块的情况,为什么SWAP2报错?对于一般的初学者来说,经常忽略诸如 goto do...while等少见关键字用法,故很少见SWAP1的写法,大多集中于SWAP2的类似错误,错就错在{}代表的是一个代码块,不需要使用;来进行结尾,这便是宏最容易出错的地方
    宏只是简单的将代码展开,而不会做任何处理
    对于此,即便是老手也常有失足,有一种应用于单片机等地方的C语言写法可以在此借鉴用于保护代码:

    1. #define SWAP3(x ,y) do{ \
    2. x ^= y; y ^= x; x ^= y; \
    3. }while(0)

    如此便能在代码中安全使用花括号内的代码了,并且如之前所约定的那样,让宏的使用看起来像函数。

  3. 但正所谓,假的总是假的,即使宏多么像函数,它依旧不是函数,如果真的把它当成函数,你会在某些时候错的摸不着头脑,还是一个经典的例子,比较大小:

    1. #define CMP(x, y) (x > y ? x : y)
    2. ...
    3. int x = 100, y = 200;
    4. int result = CMP(x, y++);
    5. printf("x = %d, y = %d, result = %d\n", x, y, result);

    执行这部分代码,会输出什么呢?
    答案是,不知道!至少result的值我们无法确定,我们将代码展开得到

    1. int result = (x > y++ ? x : y++);

    看起来似乎就是y递增两次,最后result肯定是200。真是如此?C语言标准对于一个确定的程序语句中,一个对象只能被修改一次,超过一次那么结果是未定的,由编译器决定,除了三目操作符?:外,还有&&, ||或是,之中,或者函数参数调用,switch控制表达式,for里的控制语句
    由此可看出,宏的使用也是有风险的,所以虽然宏强大,但是依旧不能滥用。

  4. 对于宏而言,前面说过,它只是进行简单的展开,这有时候也会带来一些问题:

    1. #define MULTI(x, y) (x * y)
    2. ...
    3. int x = 100, y = 200;
    4. int result = MULTI(x+y, y);

    看出来问题了吧?展开之后会变成:
    int result = x+y * y;
    完全违背了当初我们设计时的想法,一个比较好的修改方法是对每个参数加上括号:
    #define MULTI(x, y) ((x) * (y))如此,展开以后:

    1. int result = ((x+y) * (y));

    这样能在很大程度上解决一部分问题。

  5. 如果对自己的宏十分自信,可以嵌套宏,即一个表达式中使用宏作为宏的参数,但是宏只展开这一级的宏,对于多级宏另有办法展开

    1. int result = MULTI(MULTI(x, y), y);

    展开成:int result = ((((x) * (y))) * (y));

实际上,并不要太追求用宏去替换函数,例如这个交换函数,老老实实写函数,有时候比宏更好

对宏的应用
  1. 由于我们并不明白,在某些情况下宏是否被定义了,所以我们可以使用一些预处理保护机制来防止错误发生

    1. #ifndef MY_MACRO
    2. #define MY_MACRO 10000
    3. #endif

    如果定义了MY_MACRO那就不执行下面的语句,如果没定义那就执行。

  2. 在宏的使用中有两个有用的操作符,姑且叫它操作符#, ##

    • 对于#
      我们可以认为#操作符的作用是将宏参数转化为字符串。

      1. #define HCMP(x, y) printf(#x" is equal to" #y" ? %d\n", (x) == (y))
      2. ...
      3. int x = 100, y = 200;
      4. HCMP(x, y);

      展开以后

      1. printf("x is equal to y ? %d\n", (100) == (200));
      • 注:可以自行添加编译器选项,来查看宏展开之后的代码,具体可以查询GCC的展开选项,这里不再详述。特别是在多层宏的嵌套使用情况下,但是我不太推荐,故不做多介绍。

        • 能说的就是如何正确的处理一些嵌套使用,之所以不愿意多说也不愿意多用,是因为C预处理器就是一个奇葩
        • 举一个典型的例子,__LINE____FILE__的使用。

          1. /* 下方会说到的 # 预处理指示器,这里先用,实在看不懂,可以自己动手尝试 */
          2. #define WHERE_AM_I #__LINE__ " lines in " __FILE__
          3. ...
          4. fputs(WHERE_AM_I, stderr);

          这样能工作吗?如果能我还讲干嘛

          1. /* 常理上这应该能工作,但是编译器非说这错那错的 */
          2. /* 好在有前人踏过了坑,为我们留下了解决方案 */
          3. #define DEPAKEGE(X) #X
          4. #define PAKEGE(X) DEPAKEGE(X)
          5. #define WHERE_AM_I PAKEGE(__LINE__) " lines in " __FILE__
          6. ...
          7. fputs(WHERE_AM_I, stderr);

          不要问我为什么,因为我也不知道C预处理器的真正工作机制是什么。

          第一次看见这种解决方案是在 Windows 核心编程 中,这本书现在还能给我许多帮助,虽然已经渐渐淡出了书架

          总结起来,即将宏参数放于#操作符之后便由预处理器自动转换为字符串常量,转义也由预处理器自动完成,而不需要我们自行添加转义符号。

  • 对于##
    它实现的是将本操作符两边的参数合并成为一个完整的标记,但需要注意的是,由于预处理器只负责展开,所以程序员必须自己保证这种标记的合法性,这里涉及到一些写法问题,都列出来

    1. #define MERGE(x, y) have_define_ ## x + y
    2. #define MERGE(x, y) have_define_##x + y
    3. ...
    4. result = MERGE(1, 3);

    这里首先说明,上述写法由于习惯原因,我使用第二种,但是无论哪种都无伤大雅,效果一样。上述代码展开以后是什么呢?

    1. result = have_define_1 + 3;

    在我看来,这就有点C++中模版的思想了,虽然十分原始,但是总是有了一个方向,凭借这种方法我们能够使用宏来进行相似却不同函数的调用,虽然我们可以使用函数指针数组来存储,但需要提前知晓有几个函数,并且如果要实现动态增长还需要消耗内存分配,但宏则不同。

    1. inline int func_0(int arg_1, int arg_2) { return arg_1 + arg_2; }
    2. inline int func_1(int arg_1, int arg_2) { return arg_1 - arg_2; }
    3. inline int func_2(int arg_1, int arg_2) { return arg_1 * arg_2; }
    4. inline int func_3(int arg_1, int arg_2) { return arg_1 / arg_2; }
    5. #define CALL(x, arg1, arg2) func_##x(arg1, arg2)
    6. ...
    7. printf("func_%d return %d\n",0 ,CALL(0, 2, 10));
    8. printf("func_%d return %d\n",1 ,CALL(1, 2, 10));
    9. printf("func_%d return %d\n",2 ,CALL(2, 2, 10));
    10. printf("func_%d return %d\n",3 ,CALL(3, 2, 10));

    十分简便的一种用法,在我们增加减少函数时我们不必考虑如何找到这些函数只需要记下每个函数对应的编号即可,但还是那句话,不可滥用。

    1. #define CAT(temp, i) (cat##i)
    2. //...
    3. for(int i = 0;i < 5;++i)
    4. {
    5. int CAT(x,i) = i*i;
    6. printf("x%d = %d \n",i,CAT(x,i));
    7. }
  1. 对于宏,在使用时一定要注意,宏只能展开当前层的宏,如果你嵌套使用宏,即将宏当作宏的参数,那么将导致宏无法完全展开,即作为参数的宏只能传递名字给外部宏

    1. #define WHERE(value_name, line) #value_name #line
    2. ...
    3. puts(WHERE(x, __LINE__)); //x = 11

    输出: 11__LINE__

  2. 对于其他的预编译器指令,如:#pragma, #line, #error和各类条件编译并不在此涉及,因为使用上并未有陷阱及难点。

  3. C和C++混合编程的情况

    • 经常能在源代码中看见 extern "C" 这样的身影,这是做什么的?
    • 这是为了混合编程而设计的,常出现在 C++的源代码中,目的是为了让 C++能够成功的调用 C 的标准或非标准函数。

      1. #if defined(__cplusplus) || defined(_cplusplus)
      2. extern "C" {
      3. #endif
      4. /**主体代码**/
      5. #if defined(__cplusplus) || defined(_cplusplus)
      6. }
      7. #endif

      这样就能在C++中调用C的代码了。

    • C 中调用 C++ 的函数需要注意,不能使用重载功能,否则会失败,原因详见C++对于重载函数的实现。也可以称为 mangle

  4. 还有一种可以被称之为宏的小应用的技巧

    • 对于一个宏而言,是否有考虑过它的返回值是什么
    • 或者如何令其有一个函数那样的功能
    • 其实很简单

      1. #define TEST_RET(val, continues) ({continues = 19;val = 11;})
      2. ...
      3. {
      4. __attribute__((unused)) int oldval = 10;
      5. __attribute__((unused)) int newval = 18;
      6. fprintf (stderr, "%d\n", TEST_RET(oldval, newval));
      7. }
    • 可以尝试一下这个方法,其中原理自然就知道了。具体操作就是用({})包裹你想要的东西。

对宏的敬畏

  1. 为什么有这么一说,因为使用宏真的是处处危险,而且代码难以调试
  2. 经常会遇到这种情况,你将代码写成函数的时候没有任何问题,但是改成宏却出现了问题
    • 当然更可能的是你一开始就写宏,却发现总是得不到到预期的结果!
  3. 不知道诸位对反转链表这种知识点掌握的如何?
    1. 如果很有信心不妨挑战一下下面的东西,看看是否能在我说出原由之前意识到问题
    2. 如果不太懂,那就跟着看下去,一定有收获!

举个例子最好说明问题

  • 假设要写一个双向链表的插入操作
    • 我想要提供的是两个功能,后方插入前方插入
    • 我的设计原型是Linux内核的链表原型。

所谓的Linux内核的链表原型 就是在内核编程中使用的链表数据结构,我以它为例子,自己写了一个插入操作

  1. #define _list_add_inner(_add_pos, _add_node) \
  2. do {\
  3. (_add_node)->next = (_add_pos)->next;\
  4. (_add_node)->prev = (_add_pos);\
  5. (_add_pos)->next->prev = (_add_node);\
  6. (_add_pos)->next = (_add_node);\
  7. } while(0)
  8. static inline void list_add_after(struct list * add_pos, struct list * add_node) {
  9. _list_add_inner(add_pos, add_node);
  10. }
  11. static inline void list_add_before(struct list * add_pos, struct list * add_node) {
  12. _list_add_inner(add_pos->prev, add_node);
  13. }
  • 很好,可以试着测试一下最后这两个函数list_add_afterlist_add_before看看是否达到预期目的?

有时候代码真的就是要测试才行

  • 不啰嗦,这样是不行的!
    • 为何?问题就出在list_add_before这个函数的add_pos->prev参数上,原因就是宏只是做一个简单的替换,而不是值代入
    • 这里需要自己体会一下。修正一下代码

替换和值代入可是大不相同的

  1. #define _list_add_inner(_add_pos, _add_node) \
  2. do {\
  3. struct list * tmp = _add_pos;\
  4. (_add_node)->next = tmp->next;\
  5. (_add_node)->prev = tmp;\
  6. tmp->next->prev = (_add_node);\
  7. tmp->next = (_add_node);
  8. } while(0)
  • 不知是否看出了什么门道,这就是关键所在,构造一个值,而不是简单的替换。可以自己动手画一画流程图。