一、看#和##的真实案例

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_2 2
#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 }

查看预编译后的结果:

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 }
};

二、#和##的一些需要注意的地方

(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;
}

从预编译的结果#hellohello##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

为什么TAG1TAG2没有展开?

原因与#一样,你给##什么符号,它就将什么符号连接起来,它不会去识别这个符号是个宏并展开它

如果想将TAG1TAG2展开怎么办?

同样的,在中间再加一层宏定义,将参数宏TAG1TAG2展开后,在使用##连接起来

#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

加一层宏的原理就是:

先将TAG1TAG2展开为info, _teacher,然后再交给##,由##info, _teacher连接在一起


三、在看一些#和##的使用例子

#相比##来说,用的不如##多,所以我们这里就不在举#的例子了,我们这里重点再举一些##的例子

(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进行类型命名的操作,在结构体类型定义的前面可以吗?
答:完全可以!


四、如何分析#和

通过前面的例子可知,###是不能直接单独使用的,必须和宏结合在一起使用,
所以分析#和##的过程,其实就是分析一个复杂宏定义的过程

那么面对###的宏定义时,我们应该怎么办呢?

  • (1)如果通过宏名称就能明白该宏做了什么事情,此时完全不需要去分析这个宏的宏体
  • (2)如果不分析该宏就无法理解相关源码的话,那就必须分析该宏定义,如何分析呢?

    我们在前面就讲过,对于复杂宏定义的分析没有捷径可走,只能是一步一步的替换找到本源,
    只有找到本源后,才能知道该宏的真正用意是什么