今天整理了一下C语言一些奇奇怪怪的宏,给大家瞧一瞧。
TO_STR 添加双引号
通过在一个标识符前添加 # 号,可以给这个标识符加上双引号。
1
2
3
4
| #define TO_STR(x) #x
printf("%s\n", TO_STR(hello world));
printf("%s\n", TO_STR(234));
|
TO_CHAR 添加单引号
1
2
3
| #define TO_CHAR(x) #@x
printf("%c\n", TO_CHAR(a));
|
a并不是变量,啥也不是,只是通过预处理器给它套了一个单引号,它就变成字符了。
PRIMITIVE_CAT 字符拼接
符号 ## 可以拼接两个标识符。
至于为什么这里给它取名叫PRIMITIVE_CAT宏,是因为宏扩展的一个机制,往下看就知道了。
1
2
3
4
| #define PRIMITIVE_CAT(x,y) x ## y
int age = 10;
printf("age: %d\n", PRIMITIVE_CAT(a, ge));
|
CAT 字符拼接
可能你会感到奇怪,这不就是套了一层皮吗,有啥区别呢?
1
2
3
4
| #define CAT(x,y) PRIMITIVE_CAT(x,y)
#define X a
int age = 10;
printf("%d\n", CAT(X, ge));
|
但是如果把上面的 CAT 替换成 PRIMITIVE_CAT,看看会发生啥?
1
2
3
4
| #define CAT(a,b) PRIMITIVE_CAT(a,b)
#define X a
int age = 10;
printf("%d\n", PRIMITIVE_CAT(X, ge));
|
报错了
预处理器直接把 X 拿来拼接了,并没有把 X 替换成 a.
那原因是什么呢?
首先宏函数展开有两种情况:
- 参数参与了 ## 或者 # 符号的操作
此时,由于
PRIMITIVE_CAT 宏函数中的参数X Y参与了## 拼接,那么就不会对宏 X Y 进行展开后传入,而是直接传入。1
2
3
4
5
| #define X 0
#define Y 1
#define XY 2
#define PRIMITIVE_CAT(a,b) a##b
printf("%d\n", PRIMITIVE_CAT(X,Y));
|
所以拼接结果是 PRIMITIVE_CAT(X,Y) -> XY -> 2.
拼接后得到了宏XY,直接展开成 2. - 参数没有参与 ## 或者 # 符号的操作
此时,
CAT 宏函数的参数并没有参与拼接,则先对参数 X Y进行展开,分别展开为 0 1,然后传入宏函数。1
2
3
4
| #define X 0
#define Y 1
#define CAT(a,b) PRIMITIVE_CAT(a,b)
printf("%d\n", CAT(X,Y));
|
所以拼接结果是CAT(X,Y) -> PRIMITIVE_CAT(0,1) -> 01
可以看出这两个拼接宏函数,都是很常用的。
OFFSET_OF 得到一个字段在结构体struct中的偏移量(字节数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #define OFFSET_OF(type,field) ( (size_t) &(( type *) 0)-> field )
#define NAME_SIZE 20
struct Person
{
char name[NAME_SIZE];
int score;
int age;
};
int main()
{
printf("age offset of struct Person: %u\n", OFFSET_OF(struct Person, age));
return 0;
}
|
在我的电脑上,int类型是4字节的,char类型是1字节的。所以字段 age 排在 name 和 score的后面,他到结构体首地址偏移就是 24字节。
什么?你要问这个宏函数原理是啥?
这个很简单的,首先上面那个宏函数展开的结果是这个:
1
| (size_t) &((struct Person *)0)->age
|
他把地址为0,强转为一个struct Person *类型,所以它变成了一个指针,然后就可以访问结构体中的 age字段了,再然后通过取地址符 & 得到age字段的地址,很显然,因为结构体首地址是0,所以age字段的地址就是偏移量啦。
FILED_SIZE 得到一个结构体中字段的大小(字节数)
1
2
3
| #define FILED_SIZE( type, field ) sizeof( ((type *) 0)->field )
printf("filed size of age: %u\n", FILED_SIZE(struct Person, age));
|
在我的电脑上int类型4个字节.
CONTAINER_OF 根据结构体变量字段地址获取整个结构体的存储空间首地址
ptr:结构体字段的指针type:结构体类型filed: 字段名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #define CONTAINER_OF(ptr, type, filed) \
(type *)((char *)(ptr) - (char *) &((type *)0)->filed )
int main()
{
struct Person p = {
.name = "dog",
.score = 30,
.age = 2332432
};
struct Person *pp = CONTAINER_OF(&p.age, struct Person, age);
printf("name: %s\n", pp->name);
}
|
虽然这个样例看起来多此一举,但是这个宏函数在操作系统底层用是很常用的。
可以用于C的面向对象设计,类的继承,从父类访问子类。
原理是啥?
其实只要理解了C语言的内存布局,这个就不是问题。
UPPERCASE 字母大小写转换
1
2
3
4
5
| #define UPPERCASE(c) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) )
#define LOWERCASE(c) ( ((c) >= 'a' && (c) <= 'z') ? (c) : ((c) + 0x20))
printf("Uppercase: %c -> %c\n", 'x', UPPERCASE('x'));
printf("Lowercase: %c -> %c\n", 'X', LOWERCASE('X'));
|
1
2
| Uppercase: x -> X
Lowercase: X -> x
|
ARRAY_SIZE 获取一个数组元素的个数
1
2
3
4
| #define ARRAY_SIZE(arr) (sizeof((arr)) / sizeof((arr[0])))
struct Person p_arr[10] = {0};
printf("p_arr size: %u\n", ARRAY_SIZE(p_arr));
|
LOG 打印日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #define LOG_ON 1
#if LOG_ON
#define LOG(fmt, ...) \
printf("[FILE: %s] [FUNCTION: %s] [LINE: %d] " fmt "\n", \
__FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#else
#define LOG(fmt, ...)
#endif
LOG("一次LOG测试");
LOG("测试可变参: %d", 4);
|
1
2
| [FILE: D:\xx.c] [FUNCTION: main] [LINE: 72] 一次debug测试
[FILE: D:\xx.c] [FUNCTION: main] [LINE: 73] 测试可变参: 4
|
FOR_EACH 函数宏递归实现For循环
count: 循环次数,我的这个实现最多只能循环三次,不过你可以继续增加marco: 相当于回调宏idx: 当前枚举下标的,顺序是 count, count - 1, count - 2, ..., 2, 1, 倒序下标x:这个是枚举的值
...:可变参数,枚举的值
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
| #include <stdio.h>
#define PRIMITIVE_CAT(a,b) a ## b
#define CAT(a,b) PRIMITIVE_CAT(a,b)
#define INT_DEFINES(idx, x) int CAT(a_, x) = idx
#define PRINT(idx, x) printf(#x": %d idx: %d\n", CAT(a_, x), idx)
#define FOR_EACH(count, marco, ...) CAT(FOR_EACH_, count)(marco, ##__VA_ARGS__)
#define FOR_EACH_3(marco, arg, ...) marco(3, arg); FOR_EACH_2(marco, ##__VA_ARGS__)
#define FOR_EACH_2(marco, arg, ...) marco(2, arg); FOR_EACH_1(marco, ##__VA_ARGS__)
#define FOR_EACH_1(marco, arg, ...) marco(1, arg)
int main()
{
// IF_ELSE (0) ( \
// printf("YES\n"); \
// )( \
// printf("NO\n"); \
// )
FOR_EACH(3, INT_DEFINES, dog, cat, pig);
FOR_EACH(3, PRINT, dog, cat, pig);
return 0;
}
|
宏展开后真实的样子是:
1
2
| int a_dog = 3; int a_cat = 2; int a_pig = 1;
printf("dog"": %d idx: %d\n", a_dog, 3); printf("cat"": %d idx: %d\n", a_cat, 2); printf("pig"": %d idx: %d\n", a_pig, 1);
|
1
2
3
| dog: 3 idx: 3
cat: 2 idx: 2
pig: 1 idx: 1
|
不得不说,宏的代码可读性确实太差了,哈哈哈!
注意!!! 我用MSVC编译上述FOR_EACH宏代码出现了奇怪的问题: x86 的 Microsoft (R) C/C++ 优化编译器 19.39.33521 版。
但是在 GNU C, Clang 上没有问题,大家也可以自己实验一下。