• 冷知识:预处理字符串操作符


    以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「englyf」https://mp.weixin.qq.com/s/Xr2pFCJ4j0DZYo2PO6-KQg


    当年学习C语言的第一门课就提到过标记(Token)的概念,不过,相信在多年之后你再次听到这个术语时会一脸懵逼,比如我。

    因此特地翻了翻资料,整理下来这些笔记。

    在C语言中什么是标记?

    标记是编程语言处理的基本单元,也叫最小划分元素,比如关键字、操作符、变量名、函数名、字符串、数值等等。
    下面举例说明一下:

    printf("hello world!");
    

    对上面的语句进行标记划分,可分为5个标记,如下:

    printf              // 函数名
    (                   // 左小括号操作符
    "hello world!"      // 字符串
    )                   // 右小括号操作符
    ;                   // 分号
    

    预处理字符串操作符

    在C语言中,预处理字符串操作符有两个,###

    # 字符串化操作符

    用途是,将标记(Token)转成字符串。

    Syntax:

    #define TOKEN_NAME(param) #param
    

    Basic Usage:

    #include 
    
    #define MACRO_NAME(param)  #param
    
    int main()
    {
        printf(MACRO_NAME(hello world));
    
        return 0;
    }
    

    Output:

    hello world
    

    在项目实践中,用宏定义的值的同时也需要将宏名转成字符串使用,对日志的输出尤其管用。

    Best Practice:

    #include 
    
    #define NAME(param)  #param
    
    #define LEN_MAX     10
    
    int main()
    {
        int array[LEN_MAX] = {0};
        int index = 10;
        if (index >= LEN_MAX) {
            printf("error: %s:%d is over %s:%d\n", NAME(index), index, NAME(LEN_MAX), LEN_MAX);
        } else {
            printf("read %s[%d]=%d\n", NAME(array), index, array[index]);
        }
    
        return 0;
    }
    

    Output:

    error: index:10 is over LEN_MAX:10
    

    如果修改如下:

    int index = 9;
    

    Output:

    read array[9]=0
    

    ## 标记(Token)连接操作符

    用途是,将##前后的标记(Token)串接成新的单一标记。

    syntax:

    #define TOKEN_CONCATENATE(param1, param2) param1##param
    

    Basic Usage:

    #include 
    
    #define TOKEN_CONCATENATE(param1, param2) param1##param2
    
    int main()
    {
        printf("%d\n", TOKEN_CONCATENATE(12, 34));
    
        return 0;
    }
    

    Output:

    1234
    

    通常,编码实践中,代码中会出现一些书写看上去雷同的片段,极其啰嗦冗余。为了压缩源码篇幅,可以参考代码生成器的思想,在预编译阶段用宏定义代码片段展开替换,同时根据输入的参数用##组合各种标记。

    假设有个需求是声明定义一组同一类型的结构体的变量,并初始化其内部成员。既然声明定义的这些变量属于同一类型的结构体,那么按照直接编码的方式,就会有多次重复的代码片段出现,里边包括了声明定义语句,以及初始化各个成员的语句,不同的只是变量名或者参数而已。

    举个栗子,下面基于同一类型的结构体,声明定义两个变量,并初始化,看代码

    #include 
    #include 
    
    #define NAME(param)     #param
    
    typedef struct {
        char *data;
        int   data_size;  /* number of byte real */
        int   max_size;   /* maximnm data size.*/
    } my_type;
    
    #define my_type_create(name, size) \
        char name ## _ ## data[size] = {0}; \
        my_type name; \
        memset(&name, 0x00, sizeof(name)); \
        name.data = name ## _ ## data; \
        name.max_size = size; \
        printf("variable name=%s\nmember data=%s, data_size=%d, max_size=%d\n", \
                NAME(name), NAME(name ## _ ## data), name.data_size, name.max_size); \
    
    int main() {
        my_type_create(var1, 10)
        my_type_create(var2, 20)
    }
    

    上面的代码中,定义了宏my_type_create,内部实现了结构体变量的声明定义,以及内部成员的初始化。如果按照直接编码的方式,代码量相对于上面的代码量会虚增n-1倍,n=变量的个数。

    在main函数中,调用宏的时候输入参数var和10,那么在编译预处理阶段,根据输入的参数,宏my_type_create会展开为以下的代码段。

    char var_data[10] = {0}; \
    my_type var; \
    memset(&var, 0x00, sizeof(var)); \
    var.data = var_data; \
    var.max_size = 10; \
    printf("variable name=%s\nmember data=%s, data_size=%d, max_size=%d", \
            “var”, var_data, var.data_size, var.max_size); \
    

    Output:

    variable name=var1
    member data=var1_data, data_size=0, max_size=10
    variable name=var2
    member data=var2_data, data_size=0, max_size=20
    

    ## 还有个特殊的用途

    在宏定义中,也支持用...代表可变参数。

    #define MY_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)
    

    由于可变参数数目不确定,所以没有具体的标记。于是为了引用可变参数,语言层面提供了可变宏(Variadic macros)__VA_ARGS__来引用它。

    但是,在宏定义时,如果直接使用__VA_ARGS__来引用可变参数,一旦可变参数为空就会引起编译器报错,看看下面的例子

    #include 
    
    #define LOG_INFO(fmt, ...) printf("[I]" fmt "\n", __VA_ARGS__)
    
    int main() {
      LOG_INFO("info...");
      LOG_INFO("%s, %s", "Hello", "world");
    }
    

    Output:

    main.c: In functionmain’:
    main.c:3:62: error: expected expression before ‘)’ token
        3 | #define LOG_INFO(fmt, ...) printf("[I]" fmt "\n", __VA_ARGS__)
          |                                                              ^
    main.c:6:3: note: in expansion of macroLOG_INFO6 |   LOG_INFO("info...");
          |   ^~~~~~~~
    

    为了解决上面的问题,在__VA_ARGS__前面添加上##,这样的目的是告诉预处理器,如果可变参数为空,那么前面紧跟者的逗号,在宏定义展开时会被清理掉。

  • 相关阅读:
    OPenGL 基本知识(根据自己理解整理)
    【Logback+Spring-Aop】实现全面生态化的全链路日志追踪系统服务插件「Logback-MDC篇」
    【lwip】11-UDP协议&源码分析
    tkinter控件样式
    详解 Flink 的容错机制
    el-table中的el-input标签修改值,但界面未更新,解决方法
    c++日期类的实现
    【USRP】软件无线电基础篇:美国完整的军用通信系统
    如何设计一个点赞系统
    小话 Spring AOP 源码
  • 原文地址:https://www.cnblogs.com/englyf/p/16887861.html