Macro
# 写 Multi-line Macro 时使用 do/while(0)
为什么需要使用 do/while(0) ?
假设我们有如下的代码,其中 foo 和 bar 为两个函数:
#define CALL_FUNCS1(x) \
{ \
func1(x); \
func2(x); \
func3(x); \
}
#define CALL_FUNCS2(x) \
do { \
func1(x); \
func2(x); \
func3(x); \
} while (0)
if (<condition>)
foo(a);
else
bar(a);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
如果我们想把 foo 函数替换为 CALL_FUNCS1 :
...
...
if (<condition>)
CALL_FUNCS1(a);
else
bar(a);
2
3
4
5
6
7
在预编译阶段会被解释为:
...
...
if (<condition>)
{ \
func1(x); \
func2(x); \
func3(x); \
};
else
bar(a);
2
3
4
5
6
7
8
9
10
因为 if 语句后面的 ; , 会使得 else 语句没有 if 语句与之对应。为了编译正确,我们可以将上面的代码改为:
...
...
if (<condition>)
CALL_FUNCS1(a)
else
bar(a);
2
3
4
5
6
7
但是这会使得 CALL_FUNCS1 和 bar 看起来不统一,一个有 ; 一个没有 ; ,所以为了保持一致性,我们可以用使用了 do/while(0) 的 macro:
...
...
if (<condition>)
CALL_FUNCS2(a);
else
bar(a);
2
3
4
5
6
7
参考资料:
C multi-line macro: do/while(0) vs scope block (opens new window)
# 写 Function-like Macro 时每个参数都要用 ()
假如有一个宏定义为:
#define CUBE(I) (I * I * I)
如果调用 CUBE:
int a = 81 / CUBE(2 + 1);
会被扩展为:
int a = 81 / (2 + 1 * 2 + 1 * 2 + 1); /* Evaluates to 11 */
从而导致错误。
所以应该给 function-like macro 的每一个参数加上 () :
#define CUBE(I) ( (I) * (I) * (I) )
int a = 81 / CUBE(2 + 1); // Expands to: int a = 81 / ((2 + 1) * (2 + 1) * (2 + 1))
2
有两种例外情况不需要加 () 见参考资料。
参考资料:
CMU PRE01-C. Use parentheses within macros around parameter names (opens new window)
# 可变参数宏作为非首函数参数时需要使用 ## 处理末尾的逗号
假设我有如下的宏定义:
#define debug(M, ...) fprintf(stderr,M "\n", __VA_ARGS __)
如果有如下的调用:
debug("message");
因为此时参数只有一个,所以将会被展开为:
fprintf(stderr,"message",);
因为末尾的 , 会使得编译器产生错误。
为了解决这个问题,在 GNU C 中引入了 tocken paste operator ##
#define debug(M, ...) fprintf(stderr,M "\n",## __VA_ARGS __)
debug("message");
2
此时没有可变参数,预编译后得到不带 , 的结果:
fprintf(stderr,"message");
## 在这里的作用是:如果没有可变参数,则 ## 将会移除 ,
注意
注意这里的 ## 没有连接的作用,它只是用来移除 , 而产生的一种标记 (约定)。
# 使用 Assert 进行防御性编程
- Assert () 是 assert () 的升级版,当测试条件为假时,在 assertion fail 之前可以输出一些信息
- Perror 除了可以打印出客制化的错误信息,还可以打印出部分库函数和系统调用错误的原因 (要求库函数使用了 errno, 具体 RTFM: man errno)
- panic () 用于输出信息并结束程序,相当于无条件的 assertion fail
因为 assert 只能终止程序的运行,并显示错误出现的行号,无法给出更多的信息,所有可以通过封装给 assert 实现更多的功能:
# Assert
Assert 可以打印出客制化的错误信息:
// Assert with customized message
#define Assert(cond, format, ...) \
do { \
if (!(cond)) { \
fprintf(stderr, format "\n", ## __VA_ARGS__); \
assert(cond); \
} \
} while (0)
2
3
4
5
6
7
8
使用举例:
// Sanity check
Assert(argc >= 2, "Program is not given"); // 要求至少包含一个参数
2
# Perror
Perror 除了可以打印出客制化的错误信息,还可以打印出部分库函数和系统调用错误的原因:
// Assert and output error message of corresponding error number
#define Perror(cond, format, ...) \
Assert(cond, format ": %s", ## __VA_ARGS__, strerror(errno))
2
3
使用举例:
#include <stdio.h>
#include <errno.h> // errno is in errno.h
#include <string.h> // strerror is in string.h
#include <assert.h>
#define Assert(cond, format, ...) \
do { \
if (!(cond)) { \
fprintf(stderr, format "\n", ## __VA_ARGS__); \
assert(cond); \
} \
} while (0)
// Assert and output error message of corresponding error number
#define Perror(cond, format, ...) \
Assert(cond, format ": %s", ## __VA_ARGS__, strerror(errno))
int main(int argc, char* argv[])
{
FILE *fp = fopen(argv[1], "r");
Perror(fp != NULL, "Fail to open %s", argv[1]); // 要求bin是一个可以成功打开的文件
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
运行:
$ gcc main.c
$ ls ynk.txt
$ ./a.out ynk.txt
Fail to open ynk.txt: No such file or directory
a.out: main.c:21: main: Assertion `fp != ((void *)0)' failed.
[1] 672084 abort (core dumped) ./a.out ynk.txt
2
3
4
5
6
7
笔记
在命令行的输出中, No such file or directory ,即为 strerror 的输出。
# panic
panic 可以用在程序理想状况下无法执行到的地方,如果执行到,说明程序的逻辑出现了问题,用 panic 立刻停止程序。
// Stop execute and print some customized mesaasge
#define panic(...) Assert(0, __VA_ARGS__)
2
使用举例:
static void handle_ebreak() {
switch (R[R_a0]) {
case 0: putchar(R[R_a1] & 0xff); break;
case 1: halt = true; break;
default: panic("Unsupported ebreak command");
}
}
2
3
4
5
6
7
笔记
我在 NEMU (opens new window) 中发现了大量的 panic 和 Assert 的使用,这是我可以学习的地方!
参考资料:
# 使用 Log
- Log () 是 printf () 的升级版,专门用来输出调试信息,同时还会输出使用 Log () 所在的源文件,行号和函数。当输出的调试信息过多的时候,可以很方便地定位到代码中的相关位置
#ifndef __COMMON_H__
#define __COMMON_H__
#define Log(format, ...) \
_Log(ANSI_FMT("[%s:%d %s] " format, ANSI_FG_BLUE) "\n", \
__FILE__, __LINE__, __func__, ## __VA_ARGS__)
#define _Log(...) \
do { \
printf(__VA_ARGS__); \
} while (0)
#define ANSI_FMT(str, fmt) fmt str ANSI_NONE
#define ANSI_FG_BLACK "\33[1;30m"
#define ANSI_FG_RED "\33[1;31m"
#define ANSI_FG_GREEN "\33[1;32m"
#define ANSI_FG_YELLOW "\33[1;33m"
#define ANSI_FG_BLUE "\33[1;34m"
#define ANSI_FG_MAGENTA "\33[1;35m"
#define ANSI_FG_CYAN "\33[1;36m"
#define ANSI_FG_WHITE "\33[1;37m"
#define ANSI_BG_BLACK "\33[1;40m"
#define ANSI_BG_RED "\33[1;41m"
#define ANSI_BG_GREEN "\33[1;42m"
#define ANSI_BG_YELLOW "\33[1;43m"
#define ANSI_BG_BLUE "\33[1;44m"
#define ANSI_BG_MAGENTA "\33[1;35m"
#define ANSI_BG_CYAN "\33[1;46m"
#define ANSI_BG_WHITE "\33[1;47m"
#define ANSI_NONE "\33[0m"
#define ANSI_FMT(str, fmt) fmt str ANSI_NONE
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
使用举例:
#include <stdio.h>
#include "common.h"
int main()
{
Log("sizeof 'a' is %ld", sizeof('a'));
return 0;
}
2
3
4
5
6
7
8
9
# Macro Stringizing
在 C 语言中,Stringizing operator (字符串化运算符) 是一种预处理器运算符,用于将宏参数转换为字符串常量 (只能在 macro 中使用 stringizing operator)。
#define str_temp(x) #x
#define str(x) str_temp(x)
2
为什么要将 macro stringizing 定义成上面的这种形式,而不是定义为 #define str(x) #x ?
Arguments to macros are themselves macro-expanded, except where the macro argument name appears in the macro body with the stringifier # or the token-paster ##.
宏的参数在宏展开时会被先进行宏展开,除非它们出现在带有字符串化运算符 #或符号粘贴运算符 ## 的宏体中,所以两者的区别如下:
$ cat main.c
#include <stdio.h>
#define str_temp(x) #x
#define str1(x) str_temp(x)
#define str2(x) #x
int main()
{
printf(__FILE__ str1(__LINE__) "hello!\n");
printf(__FILE__ str2(__LINE__) "hello!\n");
return 0;
}
$ gcc main.c && ./a.out
main.c9hello!
main.c__LINE__hello!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
总结来说,这种方法是为了宏展开发生错误 (比如例子中的__LINE__)。
# strlen() for string constant
#define STRLEN(CONST_STR) (sizeof(CONST_STR) - 1)
sizeof vs STRLEN 计算常量字符串:
使用宏定义计算字符串的好处是,可以在编译时计算字符串的长度,而不是在运行时计算。这样可以避免在运行时计算字符串长度的开销,从而提高程序的效率。相比之下,函数 strlen 需要在运行时才能计算字符串的长度,因此会带来一定的开销。
::: notice
STRLEN 只适合于计算字符串的长度 (不包含结尾的 \0 ),不能使用它来计算其他东西,比如 STRLEN(char) == 0 。
:::
参考资料:
difference between sizeof and strlen in c (opens new window)
# Calculate the length of an array
#define ARRLEN(arr) (int)(sizeof(arr) / sizeof(arr[0]))
::: notice
这种方法只适用于计算定长数组的长度。可以在编译时计算出数组的长度,而不需要在运行时计算,可以提高程序的效率。
:::
# Macro concatenation
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
#define concat3(x, y, z) concat(concat(x, y), z)
#define concat4(x, y, z, w) concat3(concat(x, y), z, w)
#define concat5(x, y, z, v, w) concat4(concat(x, y), z, v, w)
2
3
4
5
其中 concat_temp 和 concat 的作用和 Macro Stringizing 中介绍的一样,
#define concat_temp(x, y) x ## y 和 #define concat(x, y) concat_temp(x, y) 这两个宏定义的组合,与 #define concat(x, y) x ## y 的区别在于前者可以实现对宏参数的展开。
当我们使用 concat(a, b) 时,如果 a 和 b 是宏的话,那么在 concat_temp(x, y) 中, x 和 y 会被展开成它们所代表的值,然后再进行连接。而直接使用 #define concat(x, y) x ## y 的话, x 和 y 不会被展开,而是直接连接。
举个例子,假设我们有如下的宏定义:
#define a 1
#define b 2
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
2
3
4
那么,当我们使用 concat(a, b) 时,会先将 a 和 b 展开成它们所代表的值,即 1 和 2 ,然后再进行连接,得到结果为 12 。
而如果我们直接使用 #define concat(x, y) x ## y 的话,那么当我们使用 concat(a, b) 时,会直接将 a 和 b 连接起来,得到结果为 ab 。
# Macro testing
# MUX
MUX (multiplexer) 和数字电路中的多路选择器的使用方法类似:
MUXDEF(macro, X, Y) : 如果 macro 是一个 BOOLEAN macro,那么选择 X 作为输出,否则选择 Y 作为输出。
MUXNDEF(macro, X, Y) : 如果 macro 不是一个 BOOLEAN macro,那么选择 X 作为输出,否则选择 Y 作为输出。
MUXONE(macro, X, Y) : 如果 macro 为 1,那么选择 X 作为输出,否则选择 Y 作为输出。
MUXZERO(macro, X, Y) : 如果 macro 为 0,那么选择 X 作为输出,否则选择 Y 作为输出。
// See https://stackoverflow.com/questions/26099745/test-if-preprocessor-symbol-is-defined-inside-macro
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
// define placeholders for some property
#define __P_DEF_0 X,
#define __P_DEF_1 X,
#define __P_ONE_1 X,
#define __P_ZERO_0 X,
// define some selection functions based on the properties of BOOLEAN macro
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#define MUXNDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, Y, X)
#define MUXONE(macro, X, Y) MUX_MACRO_PROPERTY(__P_ONE_, macro, X, Y)
#define MUXZERO(macro, X, Y) MUX_MACRO_PROPERTY(__P_ZERO_,macro, X, Y)
2
3
4
5
6
7
8
9
10
11
12
13
14
提示
人脑的短期记忆容量是有限的 (7±2 个单元), 用笔写下宏展开过程是很奏效的 (把短期记忆永久存储到纸上).
使用举例:
#define ONE 1
int main()
{
MUXONE(ONE, printf("Should execute here\n"), printf("Never execute\n"));
}
2
3
4
5
# ISXXX
ISDEF/ISNDEF - 可以判断宏是否符合某个条件,符合条件返回 1,否则返回 0.
isdef - 可以判断宏是否定义 (但是有特殊情况无法判断,具体看 [链接](https://stackoverflow.com/questions/26099745/test if preprocessor symbol is defined inside macro))
ISDEF(macro) : 如果 macro 是一个 BOOLEAN macro,那么选择 1 作为输出,否则选择 0 作为输出。
ISNDEF(macro) : 如果 macro 不是一个 BOOLEAN macro,那么选择 1 作为输出,否则选择 0 作为输出。
ISONE(macro) : 如果 macro 为 1,那么选择 1 作为输出,否则选择 0 作为输出。
ISZERO : 如果 macro 为 0,那么选择 1 作为输出,否则选择 0 作为输出。
// test if a boolean macro is defined
#define ISDEF(macro) MUXDEF(macro, 1, 0)
// test if a boolean macro is undefined
#define ISNDEF(macro) MUXNDEF(macro, 1, 0)
// test if a boolean macro is defined to 1
#define ISONE(macro) MUXONE(macro, 1, 0)
// test if a boolean macro is defined to 0
#define ISZERO(macro) MUXZERO(macro, 1, 0)
// test if a macro of ANY type is defined
// NOTE1: it ONLY works inside a function, since it calls `strcmp()`
// NOTE2: macros defined to themselves (#define A A) will get wrong results
#define isdef(macro) (strcmp("" #macro, "" str(macro)) != 0)
2
3
4
5
6
7
8
9
10
11
12
isdef 的原理见 [test if preprocessor symbol is defined inside macro](https://stackoverflow.com/questions/26099745/test if preprocessor symbol is defined inside macro)
# IFXXX
ISDEF(...)/ISNDEF(...) - 可以判断宏是否符合某个条件,符合条件执行 ... ,否则不执行.
IFDEF(...)/IFNDEF(...) - 可以替代 #ifdef/#ifndef , 写出更紧凑的代码,因为后者还需要在末尾加上 #endif 。
// simpljjjification for conditional compilation
#define __IGNORE(...)
#define __KEEP(...) __VA_ARGS__
// keep the code if a boolean macro is defined
#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
// keep the code if a boolean macro is undefined
#define IFNDEF(macro, ...) MUXNDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
// keep the code if a boolean macro is defined to 1
#define IFONE(macro, ...) MUXONE(macro, __KEEP, __IGNORE)(__VA_ARGS__)
// keep the code if a boolean macro is defined to 0
#define IFZERO(macro, ...) MUXZERO(macro, __KEEP, __IGNORE)(__VA_ARGS__)
2
3
4
5
6
7
8
9
10
11
# MAP
#define MAP(c, f) c(f)
MAP 的使用方法和 X-MACRO 类似,X-MACRO 的使用举例:
X-MACRO 的本质:它通过定义一组宏代码,以减少手写代码的工作量,并提高代码的可读性和可维护性。
#define MACRO_GROUP \
DEFINE_COLOR(Red, Cyan) \
DEFINE_COLOR(Cyan, Red) \
DEFINE_COLOR(Green, Magenta) \
DEFINE_COLOR(Magenta, Green) \
DEFINE_COLOR(Blue, Yellow) \
DEFINE_COLOR(Yellow, Blue)
Color GetOppositeColor(Color c) {
switch (c) {
#define DEFINE_COLOR(color, opposite) case: color return opposite;
MACRO_GROUP
#undef DEFINE_COLOR
default : return c; // Unknown color, undefined result.
}
}
Color GetColor(Color c) {
switch (c) {
#define DEFINE_COLOR(color, opposite) case: color return color;
MACRO_GROUP
#undef DEFINE_COLOR
default : return c; // Unknown color, undefined result.
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26