宏编程

关注微信公众号塔容万物

宏定义

通过#define定义宏,宏定义的格式为:

#define N 1000

通过#undef取消宏定义

#undef N

所以宏的生命周期是从定义到取消定义,而不是从定义到程序结束。即

#define +
        |
        | // 存活周期
        |
#endif  +
        |
        | // 宏已经无法使用

宏在使用时,与普通变量一致

#define N 100
for(int i=0; i<N; i++){
    printf("%d\n", i);
}

宏变量可以通过\进行换行

#define N 1000 \
        + 100 \
        + 10

printf("%d\n", N); // 1110

宏与typedef的区别

宏只进行替换,而typedef是定义了一个新的类型,可以进行类型检查。两者大多数时候是可以互换的,但是在一些特殊情况下,比如指针类型,就会出现问题。

// 定义
#define ptr_int1 int *
typedef int * ptr_int2;

// 使用
ptr_int1 a, b; // a是int*,b是int
ptr_int2 a, b; // a和b都是int*

从上面的例子可以看出,宏定义只是进行了简单的替换

ptr_int1 a, b; // 预编译后:int * a, b;

而typedef定义了一个新的类型,所以ptr_int2是一个类型,而不是一个变量,所以a和b都是int*类型。

编译器定义的宏变量

编译器自身定义了一些宏变量,比如__FILE____LINE____DATE____TIME__等,这些宏变量在编译时会被替换成相应的值。比如__FILE__会被替换成当前文件的文件名,__LINE__会被替换成当前行号,__DATE__会被替换成当前编译日期,__TIME__会被替换成当前编译时间。

printf("当前文件名:%s\n", __FILE__);

定义宏函数

宏函数与普通函数相比,宏函数不需要提供参数的类型信息,也不需要返回值的类型信息,所以宏函数的定义比普通函数简单很多。比如

#define echo(x) printf("%d\n", x)

使用时,与普通函数一样

echo(123);

宏函数还支持可变参数定义时,通过...表示,使用时通过__VA_ARGS__表示。比如

#define echo(...) printf(__VA_ARGS__)
echo("%d\n", 123); // 预编译后:printf("%d\n", 123);

##与#

#是将宏参数转换成字符串,##是将两个宏参数连接成一个新的标识符。比如

#define STR(x) #x
#define CAT(x, y) x##y

printf("%s\n", STR(123)); // 123
printf("%d\n", CAT(1, 23)); // 123

一个更为复杂的例子

#define _STR(x) #x
#define STR(x) _STR(x)

这里定义了两个宏,STR宏将参数转换成字符串,_STR宏将参数转换成字符串,这里为什么要定义两个宏呢?因为如果只定义一个宏,比如

#define STR(x) #x
#define N 100

printf("%s\n", STR(N)); // N

上面这种情况下,STR(N)会被替换成#N,而不是#100,主要原因是因为N这个宏变量没有展开,要想展开N这个宏变量,需要在STR宏定义中再定义一个宏,比如

#define _STR(x) #x
#define STR(x) _STR(x)
#define N 100

printf("%s\n", STR(N)); // 100

上面这种写法之所以能够正常工作,是因为STR(N)会被替换成_STR(100),而_STR(100)会被替换成#100,这样N这个宏变量就展开了。

限制宏内部变量的作用域

宏定义的变量是全局的,如果想要限制宏定义的变量的作用域,可以使用do { ... } while(0)来限制。比如

#define DO(a, b) do { \
    int _a = (a); \
    int _b = (b); \
    printf("%d\n", _a + _b); \
} while(0)

int main(void){
    DO(1, 2); // 3
    DO(3, 4); // 7
    return 0;
}

上面的例子中,_a_b变量的作用域被限制在了do { ... } while(0)中,所以不会污染全局变量。

括号

由于宏定义只是进行简单的替换,所以在使用宏定义时,需要注意括号的使用。比如

#define MUL(x, y) x * y

int main(void){
    printf("%d\n", MUL(1 + 2, 3 + 4)); // 1 + 2 * 3 + 4 = 11
    return 0;
}

上面的例子中,MUL(1 + 2, 3 + 4)会被替换成1 + 2 * 3 + 4,所以结果是11,而不是21。为了解决这个问题,需要在宏定义中加上括号,比如

#define MUL(x, y) ((x) * (y))

int main(void){
    printf("%d\n", MUL(1 + 2, 3 + 4)); // (1 + 2) * (3 + 4) = 21
    return 0;
}

在程序优化时,编译器会对括号进行优化,所以不用担心括号会影响程序的性能。

副作用

同样由于宏定义只是进行简单的替换,所以在使用宏定义时,需要注意副作用。比如

#define double(x) (x) + (x)

int do_something(int x){
    printf("do something\n");
    return x;
}

int main(void){
    printf("%d\n", double(do_something(1)));
    return 0;
}

经过预编译以后

printf("%d\n", (do_something(1)) + (do_something(1)));

所以do_something函数会被调用两次,这就是副作用。因此在往宏定义中传递函数时,应该尽量避免传递函数调用。一个正确的写法是,先将函数调用的结果保存到一个变量中,然后再将变量传递给宏定义

int main(void){
    int i = do_something(1);
    printf("%d\n", double(i));
    return 0;
}