grape

全部视频:https://segmentfault.com/a/11...

原视频地址:http://replay.xesv5.com/ll/24...

引入

我们知道宏定义的优点有方便程序的修改,提高程序运行效率等等。并且在我们日常的代码学习中,我们会碰到过很多很多的宏定义。针对这些宏定义,我们通常都是秉承着“宏即是替换”的“法则”来进行分析。然而,对于一些简单的宏定义来说,我们直接进行替换即可完美的解决问题,但是针对于一些复杂的宏定义来说,我们会发现,替换也是有些门道的。那么,我们今天就来探索一下宏定义的神奇吧。

宏的基础知识

一、宏替换基础知识:

#define 宏名 字符串
#define 宏名(形参列表) 字符串
允许宏带有参数,在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数

二、C宏展开的几个注意事项:

  1. 每次宏展开的结果会被重复扫描,知道没有任何可展开的宏为止。
  2. 每展开一个宏,都会记住这次展开,在这个宏展开的结果及其后续展开中,不再对相同的宏做展开。
  3. 带参数的宏,先对参数做展开,除非定义体中包含#或##

    a. '#'表示将后续标识转化为字符串。
    b. '##'标识将两个标识连接成一个标识符。
    c. 注意参数展开的结果中即使有逗号,也不要视为参数的分隔符。
  4. 如果宏定义中带有参数,而代码中出现同样标识时没有参数,不视为宏。

示例

  1. 首先我们看一个最简单的替换:

    #include <stdio.h>
    
    #define foo(bar) bar
    
    int main()
    {
       printf("%s\n",foo("grape")); 
        return 0;
    }

    结果相信大家一眼就可以看出来,是的输出“grape”,如图所示:
    clipboard.png

  2. 对应于注意事项中的的一项,展开所有的宏,我们来看这样一个代码:

    #include <stdio.h>
    
    #define foo(bar) bar1
    #define bar1 "hello"
    int main()
    {
       printf("%s\n",foo("grape")); 
        return 0;
    }

    结果是什么呢?
    好的,结果和大家想的一样,就是hello,如图所示:

    clipboard.png

  3. 继续,对于第二个注意事项,首先我们分析一下这个事项是为什么。相信大家都知道递归,倘若一个递归没有结束条件会怎么样,结果肯定是无限的执行下去,如果,我们的宏定义也会出现这个情况,那。。。读者自行脑补吧。基于这个场景我们来看看这第二条规则,我们看一下这种情况,当然为了简单,这段代码是不可执行的:

    #define foo foo bar

    我们来看这个foo的定义,如果我们不知道这项规则,这段代码被我们来解析,按照替换来讲,我们是不是会认为是"... bar bar foo ..."这样子?然而真实的情况是这样子的:

    foo
    //|->foo bar
    //|  |~    |->bar bar foo
    //|  |-> foo bar bar foo (至此展开完毕)

    所以,同一个宏定义是不可循环展开的。

  4. 对于#和##的注意,在我们的日常代码学习中,我们很少遇见#和##,所以相信大家对此都十分陌生,现在让我们来看看它究竟有什么作用。见代码:

    #include <stdio.h>
      
    #define f(a,b) a##b
    #define g(a) #a
    #define h(a) g(a)
    
    int main()
    {
        printf("%s\n",h(f(1,2))); //result1
        printf("%s\n",g(f(1,2))); //result2
        return 0;
    }

    大家可以先看一下代码,考虑一下result1和result2会输出什么?
    结果如图所示:

    clipboard.png

    然后我们可以想一下,如果没有#和##会输出?

    #include <stdio.h>
      
    #define f(a,b) b
    #define g(a) a
    #define h(a) g(a)
    
    int main()
    {
        printf("%d\n",f(1,2));
        printf("%d\n",h(f(1,2)));
        printf("%d\n",g(f(1,2)));
        return 0;
    }

    结果如图所示:

    clipboard.png

    对比两者我们会发现#和##的作用。即带参数的宏执行时,我们通常先对参数的宏进行展开,但是,在参数的宏中拥有#或者##的时候,会最后才进行展开。

  5. 第四点注意事项,就会很容易理解,举个例子,声明一个有入参的函数,如果你只去调用函数名会出现什么问题?当然,还有另外一种情况,例如:

    #define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size,
    static const uint32_t bin_data_size[] = {
      ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y)
    };
    #define ZEND_MM_BINS_INFO(_, x, y) \
        _( 0,    8,  512, 1, x, y) \
        _( 1,   16,  256, 1, x, y) \
        _( 2,   24,  170, 1, x, y) \
        _( 3,   32,  128, 1, x, y) \
        _( 4,   40,  102, 1, x, y) \
        _( 5,   48,   85, 1, x, y) \
        _( 6,   56,   73, 1, x, y) \
        _( 7,   64,   64, 1, x, y) \
        _( 8,   80,   51, 1, x, y) \
        _( 9,   96,   42, 1, x, y) \
        _(10,  112,   36, 1, x, y) \
        _(11,  128,   32, 1, x, y) \
        _(12,  160,   25, 1, x, y) \
        _(13,  192,   21, 1, x, y) \
        _(14,  224,   18, 1, x, y) \
        _(15,  256,   16, 1, x, y) \
        _(16,  320,   64, 5, x, y) \
        _(17,  384,   32, 3, x, y) \
        _(18,  448,    9, 1, x, y) \
        _(19,  512,    8, 1, x, y) \
        _(20,  640,   32, 5, x, y) \
        _(21,  768,   16, 3, x, y) \
        _(22,  896,    9, 2, x, y) \
        _(23, 1024,    8, 2, x, y) \
        _(24, 1280,   16, 5, x, y) \
        _(25, 1536,    8, 3, x, y) \
        _(26, 1792,   16, 7, x, y) \
        _(27, 2048,    8, 4, x, y) \
        _(28, 2560,    8, 5, x, y) \
        _(29, 3072,    4, 3, x, y)
    

    我们在第一次看到_BIN_DATA_SIZE只认为是一个形量传入到函数中,没有做宏替换,在_替换之后会被扫描到重新做替换。具体的解析见【PHP源码学习】2019-03-11 PHP内存管理3笔记

结尾

在我们的工作或者学习中,会出现很多复杂的宏替换,只要我们认定“宏即是替换”以及记住以上注意事项,那么一切复杂宏替换都是纸老虎。


NoSay
449 声望544 粉丝