1、#和##到底有什么实际意义 —————— 看看#和##的真实案例
我从Linux内核中抽取了一个真实的案例,在这个案例中#和##都用到了,通过这个例子看看#和##到底有什么使用价值。
(1)没有#和##时的正常做法
#define QDSP_MODULE_AUDPPTASK 1 #define QDSP_MODULE_AUDRECTASK2 #define QDSP_MODULE_UDPREPROCTASK 3 struct adsp_module_info { const char *name; const char *pdev_name; uint32_t id; }; struct adsp_module_info module_info[] = { {.name="AUDPPTASK", .pdev_name=adsp_AUDPPTASK, .id=QDSP_MODULE_AUDPPTASK}, {.name="AUDRECTASK", .pdev_name=adsp_AUDRECTASK, .id=QDSP_MODULE_AUDRECTASK}, {.name="UDPREPROCTASK", .pdev_name=adsp_UDPREPROCTASK, .id=QDSP_MODULE_UDPREPROCTASK} };
从这个例子中可以看出,给结构体数组初始化的值的名字很有规律,比如。
"AUDPPTASK"
adsp_AUDPPTASK
QDSP_MODULE_AUDPPTASK_1
基于这个规律完全可以使用#和##处理,在Linux内核中确实也是这么做的。
(2)使用#和##处理后
#define QDSP_MODULE_AUDPPTASK_1 1 #define QDSP_MODULE_AUDRECTASK_22 #define QDSP_MODULE_AUDRECTASK_3 3 #define QDSP_MODULE(n) { .name = #n, .pdev_name = "adsp_" #n, .id = QDSP_MODULE_##n } struct adsp_module_info { const char *name; const char *pdev_name; uint32_t id; }; struct adsp_module_info module_info[] = { QDSP_MODULE(AUDPPTASK), QDSP_MODULE(AUDRECTASK), QDSP_MODULE(UDPREPROCTASK) };
使用#和##修改后,其实代码的执行效率并没有发生变化,但是使用了#和##后,确使得源码更加的简洁,在Linux内核、框架的C/C++源码中,大量充斥着这种用法,希望通过这里的介绍后,大家不再陌生这样的用法。
预编译时处理的过程:
QDSP_MODULE(AUDPPTASK)
V
{ .name = #AUDPPTASK, .pdev_name = "adsp_" #AUDPPTASK, .id = QDSP_MODULE_##AUDPPTASK }
V
{ .name = "AUDPPTASK", .pdev_name = "adsp_" "AUDPPTASK", .id = QDSP_MODULE_AUDPPTASK }
QDSP_MODULE(AUDRECTASK)和QDSP_MODULE(UDPREPROCTASK)处理也是一样的过程。
查看预编译后的结果:
struct adsp_module_info { const char *name; const char *pdev_name; uint32_t id; }; struct adsp_module_info module_info[] = { { .name = "AUDPPTASK", .pdev_name = "adsp_""AUDPPTASK", .id = QDSP_MODULE_AUDPPTASK }, { .name = "AUDRECTASK", .pdev_name = "adsp_""AUDRECTASK", .id = QDSP_MODULE_AUDRECTASK }, { .name = "AUDRECTASK", .pdev_name = "adsp_""AUDRECTASK", .id = QDSP_MODULE_AUDRECTASK } };
2、 #和##的一些需要注意的地方
(1)直接使用#和##行不行
#include <stdio.h> int main(void) { int helloworld = 100; printf("%s\n", #hello); printf("%d\n", hello##world); return 0; }
查看预编译结果:
# 4 "helloworld.c" int main(void) { int helloworld = 100; printf("%s\n", #hello); printf("%d\n", hello##world); return 0; }
从预编译的结果#hello、hello##world可以看出,#和##没有起到任何的作用。
从这里的例子可以看出,#和##只能和宏一起结合使用时,才能起到作用。
#include <stdio.h> #define MACR1(s) #s #define MACR2(a, b) a##b int main(void) { int helloworld = 100; printf("%s\n", MACR1(hello)); printf("%d\n", MACR2(hello, wolrd)); return 0; }
(2)无参宏行不行
#include <stdio.h> #define MACRO1 #helloworld #define MACRO2 hello##world int main(void) { MACRO1; MACRO2; return 0; }
查看预处理后的结果:
... ... # 8 "helloworld.c" int main(void) { #helloworld; //失败了 helloworld; //成功了 return 0; }
从这个例子可以看不出,#只对宏参数有效,但是##就不是,对于##来说,以下写法都是OK的。
#define MACRO(a, b) a##b #define MACRO(a, b) a##b##_tag #define MACRO info##_tag
(3)使用#和##时,如果宏的参数也是一个宏的话,会怎样
1)在没有#和##的情况下,如果宏的参数是另一个宏的话,会怎样
#include <stdio.h> #define PI 3.14 #define AREA(r, p) (r)*(r)*(p) int main(void) { printf("%s\n", AREA(2,PI)); return 0; }
查看预编译结果:
... ... # 8 "helloworld.c" int main(void) { printf("%d\n", (2)*(2)*(3.14)); return 0; }
在没有#和##的情况下,参数是宏时,“参数宏”也可以正常展开。
2)当有#和##时,如果宏参数是另一个宏的话,会怎样
(a)#的例子
#include <stdio.h> #define NUM 100 #define STR(num) #num int main(void) { printf("%s\n", STR(NUM)); return 0; }
查看预编译结果:
int main(void) { printf("%s\n", "NUM"); //参数宏NUM没有被展开,或者说替换为100 return 0; }
展开的过程:
STR(NUM) ———> #NUM ————> "NUM"
为什么没有展开?
因为#是直接将NUM作为一个符号给处理了,你给它什么它就处理什么,它直接将得到玩意变为字符串,它并不会理会它是不是一个宏,是的话然后去展开它。
如果NUM想要展开的话,怎么办?
再加一层宏定义,先展开参数宏NUM,再展开有#的宏。
改进如下:
#include <stdio.h> #define NUM 100 #define STR(num) #num #define _STR(num) STR(num) int main(void) { printf("%s\n", _STR(NUM)); return 0; }
展开的过程:
_STR(NUM) ——> STR(NUM) ——>STR(100) ——> #100 ———>"100"
查看预编译结果:
int main(void) { printf("%s\n", "100"); //展开成功 return 0; }
NUM展开后,#处理的符号就是100,所以就得到了字符串100。
(b)##的例子
#include <stdio.h> #define TAG1 info #define TAG2 _teacher #define STRUCT(a, b) struct a##b STRUCT(TAG1, TAG2) { int num; } int main(void) { return 0; }
查看预编译后的结果:
struct TAG1TAG2 { int num; } int main(void) { return 0; }
展开的过程:
STRUCT(TAG1, TAG2) struct TAG1##TAG2 struct TAG1TAG2
——> struct TAG1##TAG2 ——> struct TAG1TAG2
为什么TAG1和TAG2没有展开?
原因与#一样,你给##什么符号,它就将什么符号连接起来,它不会去识别这个符号是个宏并展开它。
如果想将TAG1和TAG2展开怎么办?
同样的,在中间再加一层宏定义,将参数宏TAG1和TAG2展开后,在使用##连接起来。
#include <stdio.h> #define TAG1 info #define TAG2 _teacher #define STRUCT(a, b) struct a##b #define _STRUCT(a, b) STRUCT(a, b) //加的一层 _STRUCT(TAG1, TAG2) { int num; } int main(void) { return 0; }
查看预编译后的结果:
struct info_teacher { int num; } int main(void) { return 0; }
展开的过程:
_STRUCT(TAG1, TAG2) ——>STRUCT(TAG1, TAG2) ——> STRUCT(info, _teacher) ——>
——>struct info##_teacher ——> struct info_teacher
加一层宏的原理就是:
先将TAG1和TAG2展开为info, _teacher,然后再交给##,由##将info, _teacher连接在一起。
3、 在看一些#和##的使用例子
#相比##来说,用的不如##多,所以我们这里就不在举#的例子了,我们这里重点再举一些##的例子。
(1)##的例子1
1)定义结构体的例子
struct info_student { char name[30]; int num; }; typedef struct info_student student; struct info_teacher { char name[30]; int id; }; typedef info_teacher teacher; struct info_administor { char name[30]; int id; }; typedef struct info_administor administor;
比如像以上的这些例子,当需要定义好多结构体类型,而且这些结构体类型的格式还非常相似时,我们可以使用##来简化操作,让代码变的更简洁。
2)使用##简化后
#define STRUCT(type) typedef struct info_##type type; struct info_##type STRUCT(student) { char name[30]; int num; }; STRUCT(teacher) { char name[30]; int id; }; STRUCT(administor) { char name[30]; int id; }; int main(void) { return 0; }
查看预处理的结果:
... # 6 "helloworld.c" typedef struct info_student student; struct info_student { char name[30]; int num; }; typedef struct info_teacher teacher; struct info_teacher { char name[30]; int id; }; typedef struct info_administor administor; struct info_administor { char name[30]; int id; }; int main(void) { return 0; }
疑问:typedef进行类型命名的操作,在结构体类型定义的前面可以吗?
答:完全可以。
(2)module_init(mouse_device)
#define __define_initcall(level,fn,id) \ static initcall_t __initcall_##fn##id __used \ __attribute__((__section__(".initcall" level ".init"))) = fn
module_init宏最终对应的是__define_initcall宏,其中就有使用##,由于之前介绍过,这里就不在赘述。
4、 如何分析#和##
通过前面的例子可知,#和##是不能直接单独使用的,必须和宏结合在一起使用,所以分析#和##的过程,其实就是分析一个复杂宏定义的过程。
那么面对#和##的宏定义时,我们应该怎么办呢?
(1)如果通过宏名称就能明白该宏做了什么事情,此时完全不需要去分析这个宏的宏体
(2)如果不分析该宏就无法理解相关源码的话,那就必须分析该宏定义,如何分析呢?
我们在前面就讲过,对于复杂宏定义的分析没有捷径可走,只能是一步一步的替换找到本源,只有找到本源后,才能知道该宏的真正用意是什么。