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

知识,预处理,字符串,操作符 · 浏览次数 : 393

小编点评

**C语言中的标记** 标记是编程语言处理的基本单元,也叫最小划分元素。它指的是在代码中最小的单位,可以将多个操作或语句组合在一起。 **标记的用途** * **定义结构体变量:**标记可以用于定义多个结构体变量,并初始化其内部成员。 * **连接操作符:**标记可以用于连接多个操作符,例如#define TOKEN_CONCATENATE(param1, param2) param1##paramBasic Usage:#include <stdio.h>#define TOKEN_CONCATENATE(param1, param2) param1##param2。 * **可变参数:**标记可以用于引用可变参数,例如#define MY_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)。

正文

以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「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 <stdio.h>

#define MACRO_NAME(param)  #param

int main()
{
    printf(MACRO_NAME(hello world));

    return 0;
}

Output:

hello world

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

Best Practice:

#include <stdio.h>

#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 <stdio.h>

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

int main()
{
    printf("%d\n", TOKEN_CONCATENATE(12, 34));

    return 0;
}

Output:

1234

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

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

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

#include <stdio.h>
#include <string.h>

#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 <stdio.h>

#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 function ‘main’:
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 macro ‘LOG_INFO’
    6 |   LOG_INFO("info...");
      |   ^~~~~~~~

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

与冷知识:预处理字符串操作符相似的内容:

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

当年学习C语言的第一门课就提到过标记(Token)的概念,不过,相信在多年之后你再次听到这个术语时会一脸懵逼,比如我。那么就来聊聊比较冷门的预处理字符串操作符吧。

[转帖]冷知识:Mysql最大列限制和行限制

冷知识:Mysql最大列限制和行限制 一、Mysql列数限制1.Mysql限制每个表的最大列数为4096列2.InnoDB限制每个表的最大列数为1017列 二、Mysql行大小限制 一、Mysql列数限制 这里说的限制分为两种,一种是Mysql的限制,一种是存储引擎的限制,比如Innodb、MyIS

[转帖]350W+ CPU、700W+ GPU功耗:冷板和浸没式液冷哪个更有前途?

https://aijishu.com/a/1060000000363530 液冷技术方面我算不上专业,因为最近在一篇博客中看到不错的资料,这里分享给大家顺便做点讨论。 大家应该知道,Intel代号Sapphire Rapids的下一代Xeon处理器最高TDP将达到350W,AMD EPYC4功耗也

Serverless冷扩机器在压测中被击穿问题

有次全链路压测中,有位同事负责的服务做Serverless扩容(负载达到50%之后自动扩容并上线接入流量)中,发现新扩容的机器被击穿,理论分析之后我们重新进行现象回放,模拟问题重现

[转帖]Innodb存储引擎-备份和恢复(分类、冷备、热备、逻辑备份、二进制日志备份和恢复、快照备份、复制)

文章目录 备份和恢复分类冷备热备逻辑备份mysqldumpSELECT...INTO OUTFILE恢复 二进制日志备份与恢复快照备份(完全备份)复制快照+复制的备份架构 备份和恢复 分类 (1)根据备份的方法可以分为: Hot Backup(热备):指在数据库运行中直接备份,对正在运行的数据库没有

对于Vue3和Ts的心得和思考

Vue3已经正式发布了一段时间了,各种生态已经成熟。最近使用taro+vue3重构冷链的小程序,经过了一段时间的开发和使用,有了一些自己的思考。

交易日均千万订单的存储架构设计与实践

服务业务线:快递、快运、中小件、大件、冷链、国际、B2B合同物流、CLPS、京喜、三入三出(采购入、退货入、调拨入、销售出、退供出、调拨出)等

Harbor的逻辑备份与学习

Harbor的逻辑备份与学习 背景 一直想处理一下一个有网络冲突的Harbor镜像服务器 但是因为网络层自己水平一直是不是非常自信 加上Harbor容器使用的compose的玩法, 自己不敢直接处理. 所以想着至少能够备份一下Harbor仓库内的镜像. 冷备方式不用说了, 想着处理一下逻辑备份. 正

云小课|GaussDB(DWS)数据存储尽在掌控,冷热数据切换自如

阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说)、深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云。更多精彩内容请单击此处。 摘要: GaussDB(DWS)支持根据业务系统的不同使用需求,对膨胀的数据进行冷热分级管理,将数据按照时间分为热数据、冷数

实例解读丨关于GaussDB ETCD服务异常

摘要:本文通过对ETCD服务异常问题分析,代码展示解决方案。 本文分享自华为云社区《【实例状态】GaussDB ETCD服务异常》,作者:酷哥。 首先确认是否是虚拟机、网络故障 虚拟机故障导致ETCD服务异常告警 问题现象 管控面上报etcd服务异常告警,虚拟机发生重启,热迁移、冷迁移,HA等动作。