宏简介


前置处理器语言,顾名思义,并不是 C 语言的一部份,而是编译过程中前置处理部份处理的简单语言,以最简单的 Hello, World 程序为例:

#include <stdio.h>

int main(void) {
    printf("Hello! World!\n");
    printf("哈啰!C 语言!\n");

    return 0;
}

#include是前置处理器的源码含括指令,表示将含括的文件插入目前源码之中,使用gcc的话,可以指定-E表示只进行前置处理,例如:

gcc -E main.c -o main.i

开启 main.i 的话,你会发现在main函数定义之前,安插了 stdio.h 的内容。

至目前为止,常使用到的另一个前置处理器指令是#define,它本质上是个字符串取代(或说为扩展、展开),例如:

#define LEN 10
int arr[LEN];

被定义的内容称为宏(Macro),gcc编译时指定-E,会产生以下内容,LEN被展开为 10:

# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "test.c"

int arr[10];

#define常用来定义一个模版,以取代经常编写的程序片段,例如最常见的教学范例是交换两变量:

#include <stdio.h>

int main(void) {
    int x = 10;
    int y = 20;

    printf("%d %d\n", x, y);
    {
        int temp = x;
        x = y;
        y = temp;
    }
    swap(x, y)

    printf("%d %d\n", x, y);

    return 0;
}

temp定义在区块之中,因此不为区块外所见,可以将其定义为模版:

#include <stdio.h>

#define swap(a, b) { \
    int temp = a;    \
    a = b;           \
    b = temp;        \
}

int main(void) {
    int x = 10;
    int y = 20;

    printf("%d %d\n", x, y);
    swap(x, y)
    printf("%d %d\n", x, y);

    return 0;
}

#define的内容跨越多行时,每行结尾必须使用\,可以在swap(x, y)之后加上分号,这会令其看来像是函数调用,实际上是展开了swap的内容后加上分号,也会是合法的代码罢了,类似地,swap的定义看来像是定义函数,实际上那对大括号只是定义了语句区块,而不是函数区块。

如果上例定义宏时不加上大括号会如何呢?

#include <stdio.h>

#define swap(a, b)  \
    int temp = a;   \
    a = b;          \
    b = temp;       \

int main(void) {
    int x = 10;
    int y = 20;

    printf("%d %d\n", x, y);
    swap(x, y)
    printf("%d %d\n", x, y);

    return 0;
}

就以上来说,结果是正确的,只不过main范畴中多了个temp变量,也就是说,如果同一范畴内也有temp变量,编译就会失败,另一个问题是以下也会编译失败:

#include <stdio.h>

#define swap(a, b)  \
    int temp = a;   \
    a = b;          \
    b = temp;       \

int main(void) {
    int x = 10;
    int y = 20;

    printf("%d %d\n", x, y);

    if(x > y) 
        swap(x, y)

    printf("%d %d\n", x, y);

    return 0;
}

因为if的部份展开后会是:

if(x > y) 
    int temp = x;   
    x = y;          
    y = temp;

也就是temp只有if中可见,y = temp该行也就编译失败了:

if(x > y) 
    int temp = x;   
x = y;          
y = temp;

如果是一开始有加上大括号的swap宏就不会有问题:

if(x > y) {
    int temp = x;   
    x = y;          
    y = temp;    
}

#define只是文本替代,因此要小心项目展开后计算先后顺序的问题:

#include <stdio.h>

#define pow(a) a * a

int main(void) {
    int x = 10;
    printf("%d\n", pow(x));     
    printf("%d\n", pow(x + x)); 

    return 0;
}

pow目的是计算二次方,pow(x + x)预期结果应该是 400,实际上显示会是 120,因为展开后会是x + x * x + x,为了避免这个问题,可以在定义宏时,将输入项目加上括号:

#include <stdio.h>

#define pow(a) (a) * (a)

int main(void) {
    int x = 10;
    printf("%d\n", pow(x));     // (x) * (x)
    printf("%d\n", pow(x + x)); // (x + x) * (x + x)

    return 0;
}

#define的输入项目要避免副作用,例如:

#include <stdio.h>

#define pow(a) (a) * (a)

int main(void) {
    int x = 10;
    printf("%d\n", pow(x++));

    return 0;
}

你觉得结果应该会是多少呢?若觉得是 100 就错了,因为pow(x++)会被展开为(x++) * (x++),结果会是 110;别在宏中重复使用输入项目,虽然可以解决问题,然而这有时无法做到,因此最重要的是记得,使用宏时,输入项目要避免副作用,上例应该写为以下:

#include <stdio.h>

#define pow(a) (a) * (a)

int main(void) {
    int x = 10;
    printf("%d\n", pow(x));
    x++;

    return 0;
}

这就是为何有些开发者认为,应该避免使用宏的原因,因为编写不易、调试不易,然而使用上又容易出错;然而有些功能又只有宏办得到,C 语言本身的标准实际上也包含了一些以宏提供的功能,只能说宏是把双面刃、必要之恶了。

#define用来定义宏,相对地,#undef用来取消宏。

C 语言本身预先定义了__STDC____LINE__等名称,可以在〈Replacing text macros〉找到,例如,可以透过__FILE____LINE__来写个简单的调试信息:

#include <stdio.h>

int main(void) {
    int x = 10;
    int y = 20;

    fprintf(stderr, "(%s:%d) %s %d\n", __FILE__, __LINE__, "Shit happen!", 1);

    return 0;
}

fprintf定义为宏是个不错的主意,可以简化程序的编写:

#include <stdio.h>

#define debug(fmt, ...) { \
    fprintf(stderr, "(%s:%d) "fmt"\n", __FILE__, __LINE__, ##__VA_ARGS__); \
}

int main(void) {
    debug("%s %d", "Shit happen!", 1);

    return 0;
}

...在宏中表示其余的项目,后续可以使用__VA_ARGS__来代表;#会将项目加上双引号含括,因此#__VA_ARGS__的话,表示将其余项目展开为字符串。

##的话是合并项目,例如若项目是ab,宏中编写ab是不会分别展开的,因为项目必须使用空白区隔,这时可以编写a##b,这么一来,ab会分别展开后合并,例如若a为 12、b为 34,那么a##b就会是 1234。

如果##出现在逗号之后,有些编译器(例如gcc)会在__VAR_ARGS__为空时,自动移除逗号,上面的范例若将##拿掉,debug时若没有指定fmt外的实参,展开后编译就会出错。

那为什么不把debug定义为函数就好,而是要定义为宏?同样的疑问应该也会发生在先前的swappow宏,毕竟它们也可以定义为函数!

在过去也许有个好理由将swappow等定义为宏:「不会产生函数调用,比较有效率」。不过在不用这么斤斤计较的场合,将swappow等定义为宏的价值不大。

宏的本质是文本替换,如果经常写出某个 C 语言片段,而该片段不适合封装为函数,或者封装为函数时使用上突冗,才是适用宏的场合,例如方才的debug定义为函数会比较麻烦,因为得使用到不定长度实参、字符串连接等,相对来说,定义宏反而容易得多,另一个情况是顺序迭代数组,这可以参考〈foreach 与数组〉。

前置处理指令中,还有#if#endif#ifdef#ifndef#elif#else#endif,可用来判定宏是否存在,根据条件进行不同的代码含括。例如:

#include <stdio.h>

#define __DEBUG__

#define debug(fmt, ...) { \
    fprintf(stderr, "(%s:%d) "fmt"\n", __FILE__, __LINE__, ##__VA_ARGS__); \
}

int main(void) {

#ifdef __DEBUG__    
    debug("%s %d", "Shit happen!", 1);
#endif

    return 0;
}

只要在__DEBUG__有定义的情况下,debug("%s %d", "Shit happen!", 1)该行才会被纳入源码,而后进行编译的动作,如此一来,就可以透过__DEBUG__是否有定义,来决定要不要包含调试信息。

在〈Conditional inclusion〉有个范例,可以看到defined以及条件式中还可以进行简单的运算:

#define ABCD 2
#include <stdio.h>

int main(void)
{

#ifdef ABCD
    printf("1: yes\n");
#else
    printf("1: no\n");
#endif

#ifndef ABCD
    printf("2: no1\n");
#elif ABCD == 2
    printf("2: yes\n");
#else
    printf("2: no2\n");
#endif

#if !defined(DCBA) && (ABCD < 2*4-3)
    printf("3: yes\n");
#endif
}




展开阅读全文