tartarus's bolg tartarus's bolg
  • Linux and Unix Guide
  • CMake
  • gcc
  • gdb
  • bash
  • GNU Make
  • DDCA-ETH
  • CS106L
  • CS144
  • NJU PA
  • NJU OS(jyy)
  • C
  • C++
  • Python
  • reveal-md
  • LaTex
  • Paper Reading
  • TBD
  • Linux and Unix Guide
  • CMake
  • gcc
  • gdb
  • bash
  • GNU Make
  • DDCA-ETH
  • CS106L
  • CS144
  • NJU PA
  • NJU OS(jyy)
  • C
  • C++
  • Python
  • reveal-md
  • LaTex
  • Paper Reading
  • TBD
  • c

    • Introduction
    • Representing and Manipulating Information
    • C Programming Tips and Tricks
    • Macro
      • 写Multi-line Macro时使用do/while(0)
      • 写Function-like Macro时每个参数都要用()
      • 可变参数宏作为非首函数参数时需要使用##处理末尾的逗号
      • 使用Assert进行防御性编程
        • Assert
        • Perror
        • panic
      • 使用Log
      • Macro Stringizing
      • strlen() for string constant
      • Calculate the length of an array
      • Macro concatenation
      • Macro testing
        • MUX
        • ISXXX
        • IFXXX
        • MAP
  • cpp

  • Programming_Knowledge
  • c
tartarus
2023-07-05
目录

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);
1
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);
1
2
3
4
5
6
7

在预编译阶段会被解释为:

...
...
if (<condition>)
  { \
    func1(x); \
    func2(x); \
    func3(x); \
  };
else
  bar(a);
1
2
3
4
5
6
7
8
9
10

因为 if 语句后面的 ; , 会使得 else 语句没有 if 语句与之对应。为了编译正确,我们可以将上面的代码改为:

...
...

if (<condition>)
  CALL_FUNCS1(a)
else
  bar(a);
1
2
3
4
5
6
7

但是这会使得 CALL_FUNCS1 和 bar 看起来不统一,一个有 ; 一个没有 ; ,所以为了保持一致性,我们可以用使用了 do/while(0) 的 macro:

...
...

if (<condition>)
  CALL_FUNCS2(a);
else
  bar(a);
1
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)
1

如果调用 CUBE:

int a = 81 / CUBE(2 + 1);
1

会被扩展为:

int a = 81 / (2 + 1 * 2 + 1 * 2 + 1);  /* Evaluates to 11 */
1

从而导致错误。

所以应该给 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))
1
2

有两种例外情况不需要加 () 见参考资料。
参考资料:
CMU PRE01-C. Use parentheses within macros around parameter names (opens new window)

# 可变参数宏作为非首函数参数时需要使用 ## 处理末尾的逗号

假设我有如下的宏定义:

#define debug(M, ...) fprintf(stderr,M "\n", __VA_ARGS __)
1

如果有如下的调用:

debug("message");
1

因为此时参数只有一个,所以将会被展开为:

fprintf(stderr,"message",);
1

因为末尾的 , 会使得编译器产生错误。
为了解决这个问题,在 GNU C 中引入了 tocken paste operator ##

#define debug(M, ...) fprintf(stderr,M "\n",## __VA_ARGS __)
debug("message");
1
2

此时没有可变参数,预编译后得到不带 , 的结果:

fprintf(stderr,"message");
1

## 在这里的作用是:如果没有可变参数,则 ## 将会移除 ,

注意

注意这里的 ## 没有连接的作用,它只是用来移除 , 而产生的一种标记 (约定)。

# 使用 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)
1
2
3
4
5
6
7
8

使用举例:

// Sanity check
Assert(argc >= 2, "Program is not given");  // 要求至少包含一个参数
1
2

# Perror

Perror 除了可以打印出客制化的错误信息,还可以打印出部分库函数和系统调用错误的原因:

// Assert and output error message of corresponding error number
#define Perror(cond, format, ...) \
  Assert(cond, format ": %s", ## __VA_ARGS__, strerror(errno))
1
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是一个可以成功打开的文件
}
1
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
1
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__)
1
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");
  }
}
1
2
3
4
5
6
7

笔记

我在 NEMU (opens new window) 中发现了大量的 panic 和 Assert 的使用,这是我可以学习的地方!

参考资料:

  • 一生一芯 YEMU v2.0 (opens new window)

# 使用 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
1
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;
}
1
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)
1
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!
1
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)
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]))
1

::: 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)
1
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)
1
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)
1
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"));
}
1
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)
1
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__)
1
2
3
4
5
6
7
8
9
10
11

# MAP

#define MAP(c, f) c(f)
1

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. 
  } 
}
1
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
上次更新: 12/27/2023, 8:55:47 AM
C Programming Tips and Tricks
Introduction

← C Programming Tips and Tricks Introduction→

Theme by Vdoing | Copyright © 2023-2023 tartarus | CC BY-NC-SA 4.0
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式