c——宏

0. 宏的语法

#define 标识符 参数列表  替换列表 换行符
#define 标识符(参数列表) 替换列表 换行符

宏和函数有点不同:宏可以不穿 实参,此时使用 占位标记 替换形参。

1. # 操作符

在替换列表中,若使用 # 操作符,则将 # 后跟的 形参部分,替换为 所对应的实参内容以字符串字面量形式表示。

#include <stdio.h>
#include <string.h>

// 这里定义了一个简单的带有#操作符的宏MY_MACRO1。
// 这里#与x之间有一个空格,这是合法的,并且与#x的效果一致
#define MY_MACRO1(x)    # x

// 这里定义了一个带有两个形参的宏函数,
// 其替换列表是将第一个实参所指定的符号作为字符串字面量与一个换行符拼接,
// 然后再与第二个实参所指定的符号作为字符串字面量进行拼接
#define MY_MACRO2(x, y)    #x "\n" #y


int main(int argc, const char * argv[])
{
    // 这里的MY_MACRO1(10ab)会被替换为"10ab"
    const char *s = MY_MACRO1(10ab);
    printf("The literal is: %s\n", s);
    
    // 这里字符串比较结果是相同的
    if(strcmp(s, "10ab") == 0)
        puts("Equal!");
    
    // 对于#操作符所作用的形参对应的实参,其前后空白符都会被删除,
    // 同时,这里的\符号在宏替换为字符串之后就变为'\\'
    s = MY_MACRO1(  10"ab\n"  );
    
    // 这里字符串比较结果是相同的
    if(strcmp(s, "10\"ab\\n\"") == 0)
        puts("Equal!");
    
    // 这里尽管第一个实参中有一个逗号,但它被包围在圆括号中,因此不作为实参分隔符。
    // 同样,在第二个参数中的逗号在''中,它作为一个字符token,因此也不作为实参分隔符。
    s = MY_MACRO2((123abc,45;'0'), [1a2b3c:?','=]);
    printf("string is: %s\n", s);

    // 这里第二个实参传的是不含任何预处理符号的实参,这里仅用一个逗号作为分隔符。
    // 此时,MY_MACRO2(abcd, )会被替换为"abcd\n",而忽略后续与#y的替换与拼接,
    // 由于y形参对应的实参不包含任何预处理符号,因此它用占位标记预处理符号代替,
    // 并且在宏替换的最后阶段#与占位标记预处理符号的拼接被完全移除
    s = MY_MACRO2(abcd, );
    printf("s = %s\n", s);

    // 这里与上述代码类似,只不过第一个实参作为不含任何预处理符号的实参,
    // 这里仅用一个逗号作为参数分隔符使用。MY_MACRO2(, abcd)这里会被替换为"\nabcd"
    s = MY_MACRO2(, abcd);
    printf("s = %s\n", s);
    
    // 以下宏实参是非法的,由于它不是一个有效的预处理符号,
    // 由于出现了一个',但没有找到另一个'与之匹配
    // s = MY_MACRO1(123'p);
}

2. ## 操作符

或 ## 前面或后面 跟 形参,则 使用对应的实参进行替换,
若没有对于的实参符号,则使用一个 占位标志 进行替换。

#include <stdio.h>

// 定义了一个宏函数MY_MACRO1,功能是将形参x对应的实参预处理器符号序列与10拼接在一起
#define MY_MACRO1(x)    x ## 10
// 定义了一个宏函数MY_MACRO2,功能是将0x与形参x对应的实参预处理器符号序列拼接在一起
#define MY_MACRO2(x)    0x ## x
// 定义了一个宏函数MY_MACRO3,功能是将形参x对应的实参预处理器符号序列与10拼接在一起
// 后面再加上将0x与形参y对应的实参预处理符号序列拼接在一起的数
#define MY_MACRO3(x, y) x##10 + 0x##y

// MY_MACRO4是一个宏对象,它直接表示++操作符
#define MY_MACRO4       + ## +

// 这里定义了宏函数CONCAT,将x与y对应的实参预处理符号序列拼接在一起
#define CONCAT(x, y)    x ## y

int main(int argc, const char * argv[])
{
    // 这里的MY_MACRO1(32)将被替换为3210
    int a = MY_MACRO1(32);
    // 这里将打印出3210
    printf("a = %d\n", a);

    // 这里对MY_MACRO1的实参传递不包含任何预处理符号的实参序列,
    // 这样使得这里的MY_MACRO1宏函数最终被替换为10,
    // 形参x用一个占位标记预处理符代替。
    // 占位标记预处理符与10(非占位标记预处理符)拼接,结果就是10
    a = MY_MACRO1();
    printf("a = %d\n", a);
    
    // 这里的MY_MACRO2(64)将被替换为0x64
    a = MY_MACRO2(64);
    // 这里将打印出100
    printf("a = %d\n", a);
    
    // 这里的MY_MACRO3(10, 16)将被替换为1010 + 0x16
    a = MY_MACRO3(10, 16);
    // 这里将打印出1032
    printf("a = %d\n", a);
    
    // 这里的MY_MACRO4将被替换为++,
    // 因此这条语句就相当于:a++;
    a MY_MACRO4;
    // 这里将打印出1033
    printf("a = %d\n", a);
    
    // 这里CONCAT(an, obj)将被替换为anobj,
    // 从而这里声明了一个名为anobj的int类型对象
    int CONCAT(an, obj);
    
    // 这里可直接引用anobj标识符
    anobj = a;
}

3. 嵌套使用

当使用一个宏时,宏实例会用替换列表中的预处理符号进行替换,完了之后预处理器还会再次扫描更多的宏名。也就是说,一个宏定义中可以引用另外一个已定义的宏,因此预处理器会不断迭代解析替换列表中所有出现的宏,直到把它们全都解析完成。
当预处理器识别到使用一个宏函数时,其形参会用实参进行替换。替换列表中的形参(若不作为#或##的操作数)在该宏的替换列表中所包含的所有宏名全都被扩展之后,才用相应的实参进行替换。
在做实参替换之前,每个实参的预处理符号需要进行完全的宏替换。也就是说,宏函数的替换顺序是先处理替换列表中出现的#与##操作符,然后对替换列表中所出现的宏进行展开替换;接着检查实参是否引用了宏,如果引用了则先对所有引用了宏的实参进行完全的宏替换;最后才将替换列表中出现的形参替换为宏扩展后的实参对应的预处理符号。

在对宏函数调用过程中,当替换列表中的所有形参已被替换,并且对#与##的处理也完成之后,所有占位标记预处理符号被移除,然后所获得的预处理符号序列再被重新扫描。

宏定义与函数定义不同,不能做“递归式”定义,也就是说在一个宏的替换列表中不能出现所定义宏自身的标识符;此外,“间接递归式”定义也不行,比如宏A的
替换列表中引用了宏B,而宏B的替换列表中又引用了宏A。C语言标准指出,发生以上这两种情况时宏名不会被替换。这些没被替换的宏名预处理符号对于后续的替换也不再可用。

#include <stdio.h>

// 这里先定义一个宏函数MY_MACRO1
#define MY_MACRO1(x)    x + x ## 0

// 这里定义的MY_MACRO1与上述定义的完全等同,因此是允许的
#define MY_MACRO1(x)    x + x ## 0

// 这里定义LITERAL,将形参所对应的实参预处理符号序列进行字符串字面量化
#define LITERAL(x)      #x

// 这里定义宏函数MY_MACRO2,其替换列表引用了宏函数LITERAL
#define MY_MACRO2(x)    LITERAL(x)

// 这里定义了一个用于拼接的宏函数CONCAT,
// 它将x与y所对应的实参预处理符号进行拼接,然后再拼接一个ELLO
#define CONCAT(x, y)    x ## y ## ELLO

int main(void)
{
    // 这里,MY_MACRO1(10)会被替换为:10 + 100
    int a = MY_MACRO1(10);
    printf("a = %d\n", a);
    
    // 这里是对宏LITERAL的使用:LITERAL(MY_MACRO1(20)),
    // 第一步就是先对LITERAL进行宏替换,直接就是#x,
    // 由于对 # 操作符的处理次序比实参中出现宏名进行替换的次序要优先,
    // 所以这里的第1步完成后直接做第2步,将实参MY_MACRO1(20)替换掉形参x。
    // 替换后相当于:# MY_MACRO1(20),因此最后的替换结果就是字符串"MY_MACRO1(20)"
    const char *s = LITERAL(MY_MACRO1(20));
    printf("s = %s\n", s);
    
    // 这里对宏MY_MACRO2的使用:MY_MACRO2(MY_MACRO1(20)),
    // 第一步先对MY_MACRO2进行宏替换,结果为LITERAL(x),由于这里不涉及
    // #与## 操作符,然而却存在宏名LITERAL,因此需要进一步对它扩展。
    // 所以第二步就是宏展开LITERAL(x),得到 #x 。
    // 这里需要大家注意,由于这里的#x不是MY_MACRO2宏里的替换列表中的,
    // 而是被替换扩展出来的,因此此时不需要直接对 # 符号做处理。
    // 所以第三步就是对实参中出现的宏MY_MACRO1进行替换;
    // 对实参MY_MACRO1(20)进行宏替换后结果为:20 + 200,
    // 然后第四步将扩展后的实参20 + 200传给形参x,得:#<20 + 200>。
    // 最终获得字符串字面量——"20 + 200"
    s = MY_MACRO2(MY_MACRO1(20));
    printf("s = %s\n", s);
    
    // 下面这个对CONCAT的使用结果会替换为:#define ELLO,
    // 使得宏替换结果为一个预编译指示符,这会导致编译报错。
    CONCAT(#, define)
    
#define def 100
#define H abcd

    // 这里对宏MY_MACRO2的使用与上述类似,
    // 第一步先对MY_MACRO2进行宏替换,结果为LITERAL(x);
    // 第二步是再对LITERAL进行宏展开,得到#x;
    // 第三步对实参CONCAT(if, def H)进行宏替换,
    // 得到:x ## y ## ELLO ;
    // 由于这里CONCAT宏的替换列表含有##操作符,
    // 因此不会对实参宏def和H进行扩展,
    // 所以第四步将实参if和def H替换形参之后,结果为ifdef HELLO ;
    // 第五步将扩展后的实参传给形参x,得:#<ifdef HELLO> ;
    // 最终获得字符串字面量——"ifdef HELLO"
    s = MY_MACRO2(CONCAT(if, def H));    
    printf("s = %s\n", s);
}

4. 变参宏

... 对应的实参,完全替换 VA_ARGS

#include <stdio.h>

/** 
 * 定义了带有可变参数的宏VARIADIC_MACRO1,
 * 其替换列表是将整个实参列表内容作为字符串字面量,
 * 然后与"The string is: "进行拼接
*/
#define VARIADIC_MACRO1(...)    "The string is: "  #__VA_ARGS__

/**
 * 定义了带有可变参数的宏VARIADIC_MACRO2,
 * 其替换列表是将实参列表与100结合,然后用( )包围起来
 */
#define VARIADIC_MACRO2(...)    (__VA_ARGS__ ## 100)

/**
 * 定义了带有两个形参的宏VARIADIC_MACRO3,其第二个形参为可变参数;
 * 其替换列表是先执行形参a对应的表达式,然后执行可变实参列表,最后放表达式10。
 * 它们用( )包围起来
 */
#define VARIADIC_MACRO3(a, ...)    (a  __VA_ARGS__  10)

int main(void)
{
    // 这里VARIADIC_MACRO1(Good luck!)将会被替换为:
    // "The string is: " "Good luck!"
    const char *s = VARIADIC_MACRO1(Good luck!);
    // 输出:s = The string is: Good luck!
    printf("s = %s\n", s);
    
    // 这里实参列表Say "Hi"!, Byebye, Thank you作为一个整体,
    // 然后通过#操作符被替换为"Say \"Hi\"!, Byebye, Thank you"
    s = VARIADIC_MACRO1(Say "Hi"!, Byebye, Thank you);
    // 输出:s = The string is: Say "Hi"!, Byebye, Thank you
    printf("s = %s\n", s);
    
    // 这里使用VARIADIC_MACRO1宏的时候没有传任何实参,
    // 因此宏替换后就是"The string is: "这单一的字符串字面量
    s = VARIADIC_MACRO1();
    // 输出:s = The string is:
    printf("s = %s\n", s);
    
    // 这里VARIADIC_MACRO2(20)将被替换为:(20100)
    int a = VARIADIC_MACRO2(20);
    // 输出:a = 20100
    printf("a = %d\n", a);
    
    // 这里VARIADIC_MACRO2(10, 20, 30)将被替换为:(10, 20, 30100),
    // 很明显它是一个逗号表达式。而整条语句为:a = (10, 20, 30100);
    a = VARIADIC_MACRO2(10, 20, 30);
    // 输出:a = 30100
    printf("a = %d\n", a);
    
    int b = 0;
    
    // 这里VARIADIC_MACRO3(++b, -20 + )将被替换为:(++b - 20 + 10),
    // 其中第一个实参为++b,后面的-20 + 作为可变参数的实参
    a = VARIADIC_MACRO3(++b, -20 + );
    // 输出:a = -9, b = 1
    printf("a = %d, b = %d\n", a, b);
    
    // 这里缺省VARIADIC_MACRO3的第一个实参,可变实参部分为30 * ,
    // 因此VARIADIC_MACRO3(, 30 * )被替换为:(30 * 10)
    a = VARIADIC_MACRO3(, 30 * );
    // 输出:a = 300
    printf("a = %d\n", a);
    
    // 这里同时缺省了第一个实参以及可变参数列表的实参,
    // 因此VARIADIC_MACRO3( , )被替换为:(10)
    a = VARIADIC_MACRO3( , );
    // 输出:a = 10
    printf("a = %d\n", a);
}

5 条件编译

通过条件语句控制是否编译。

#if 常量表达式
条件编译代码块
#elif 常量表达式
条件编译代码块
#else
条件编译代码块
#endif

常量表达式 返回的值 为 1 或 0
示例

#include <stdio.h>

int main(void)
{
    // 这里3 + 5是一个非零整数常量表达式,
    // 因此下面预编译组的puts函数调用表达式将被编译
#if 3 + 5
    puts("Non-zero expression");
#endif
    
    // 这里做一次puts函数调用,而实参内容则根据预编译条件来选
    puts(
    
// 这里#if后面的表达式为0,因此不会将"0"编译进去
#if 0
    "0"
// 这里#elif后面的整数常量表达式结果为0,因此后面的"1"不会编译进去
#elif 3 - 3
    "1"
// 由于上述条件判定均失败,所以编译#else下面的预编译组,"2"
#else
    "2"
#endif
         );     // 因此这里将输出2
    
// 这里定义了宏HELLO
#define HELLO
    
// 这里判定的是倘若定义了HELLO这个宏,则编译下面的puts函数调用。
// 显然,这里将会输出HELLO defined
#if defined(HELLO)
    puts("HELLO defined!");
#endif
    
// 这里判定的是倘若定义了HELLO这个宏,同时也定义了HI宏,
// 则编译下面的puts函数调用。
// 显然,这里的puts函数调用不会被编译
#if defined(HELLO) && defined(HI)
    puts("Both defined");
#endif
    
// 这里定义了宏HI,并将该宏的值定义为2
#define HI  2
    
// 这里判定的是倘若定义了HELLO这个宏,同时也定义了HI宏,
// 并且两个defined表达式的值加起来等于HI的值时,则编译下面的puts函数调用。
// 显然,defined表达式的值均为1,因为这两个宏此时都被定义。
// 同时,HI的值被定义为了2,因此条件完全满足,下面的printf函数调用将被编译
#if defined(HELLO) && defined(HI) && (defined(HELLO) + defined(HI)) == HI

    // 这里输出:HI value: 2
    printf("HI value: %d\n", HI);
    
#endif

    int aa = 100;
    
// 这里大家要注意,由于上面定义的aa不是预处理中定义的宏,因此在预处理中找不到该标识符,
// 因此这里由于找不到aa标识符,#if条件不成立,输出:aa is not defined!
#if aa == 100
    puts("aa is defined!");
#else
    puts("aa is not defined!");
#endif
    
#define aa  20
    
// 由于此时定义了aa宏,并且其替换列表的整数常量表达式确实为20,所以这里输出Yep!
#if aa == 20
    puts("Yep!");
#endif
    
    // 这里将输出aa = 20,因为aa宏定义将之前的aa对象标识符给覆盖掉了
    printf("aa = %d\n", aa);

    enum
    {
        MY_ENUM1,
        MY_ENUM2,
        MY_ENUM3
    };
    
// 对于枚举符而言,它也不属于预定义标识符,因此下面将输出:MY_ENUM2 is not defined!
#if MY_ENUM2 == 1
    puts("MY_ENUM2 is defined!");
#else
    puts("MY_ENUM2 is not defined!");
#endif
    // 以下预编译语句会发生预处理错误!由于sizeof操作符只能用于C代码的编译期间,
    // 而不能用于预处理期间,因此sizeof(int)是一个非法的预处理常量表达式
#if aa == sizeof(int)
    puts("Yep!");
#endif
}

posted on 2022-04-23 10:30  开心种树  阅读(57)  评论(0编辑  收藏  举报