C语言预处理脚本:宏的狂欢

C语言 Aug 25, 2022

预处理脚本?

在C语言编译过程中,第一个就是对代码进行预编译。预编译指的就是对#define,#include,#if,#else,#elif等预编译指令展开,并将头文件与源文件合并的过程。我们可以利用这一过程,编写一些宏,使这些宏在预编译阶段展开,生成一部分代码,将原本繁琐却又重复特征的代码简化。我将这种操作称为编写预处理脚本,使用编译器作为脚本解释器。

注意:一定要明白预处理脚本的运行机制,宏是一种静态量,本质上是文本替换!

宏的技巧

1. "#"和"##"

"#"和"##" 我们称这两符号为宏胶水。

"#"可以将后面的文字作为字符串,例如:

#include <stdio.h>
#define PRINT_LINE(x) printf("%s\r\n",#x)
int main()
{
   PRINT_LINE(HelloWorld!);
}

输出:

HelloWorld!

"##"可以将前面的文字与后面的文字连接起来,合成一段文字,例如:

#include <stdio.h>

#define Print(x) x##_print

void Log_print(char* str)
{
    printf("Log:%s\r\n",str);
}
void Err_print(char* str)
{
    printf("Err:%s\r\n",str);
}

int main()
{
    Print(Log)("Hello!");
    Print(Err)("Bye!");
}

输出:

Log:Hello!
Err:Bye!

字符串拼接

字符串拼接不能用"##",因为从原理上讲"##"只是把左右两端连起来,如果是字符串"A"和"B",用"##"相连接会变成"A""B",所以正确的做法应该是:

#include <stdio.h>

#define COM(A,B) (A B)

int main()
{
    printf("%s\r\n",COM("A","B"));
}

输出:

AB

2. 宏展开

在替换文本中,如果使用"#"或"##"修饰,则结果将被扩展为带引号的字符串,正如上文所述,但如果参数为一个宏,则该宏不被展开,例如:

#include <stdio.h>
#define PRINT_LINE(x) printf("%s\r\n",#x)
#define M_Hello HelloWorld
int main()
{
   PRINT_LINE(M_Hello);
}

输出:

M_Hello

由此可见,M_Hello这个宏并没有被展开,并不符合我们设计的初衷。所以正确的做法应该是在外面再套一层包装,使参数x在上层被展开,例如:

#include <stdio.h>
#define __PRINT_LINE(x) printf("%s\r\n",#x)
#define PRINT_LINE(x) __PRINT_LINE(x)
#define M_Hello HelloWorld

int main()
{
   PRINT_LINE(M_Hello);
}

输出:

HelloWorld

根据上述代码,我们可以推断宏被展开的顺序:

在这里插入图片描述

3. "\" 换行

如果本行的宏太长,影响代码的美观或可读性,我们可以使用符号"\"进行换行(实际上宏没有换行,只是看起来换行了)。

#define MICRO_DEMO \
	This is MICRO_DEMO

预处理脚本

Demo 1:生成枚举

1. 宏和枚举的区别

  • 宏的声明周期是预编译,它活不到预编译之后,并且他的本质是文本替换。
  • 枚举是一种数据类型,定义的是一个常量,并且它参与到编译的过程中,但他在预编译阶段根本不存在(只是一个文本而已),所以无法对其进行预编译操作(例如宏开关)。

2. 宏列表

所谓宏列表,就是将有规律性,但比较繁琐的东西放在一个宏里,对其进行统一管理,提高代码的可维护性。

举个例子,例如在通信协议中,我们有多个命令,往往需要通过if-else或者switch进行判断,逐条对比,然后执行其命令,如果命令众多,处理过程将会非常繁琐。接下来我们用预处理脚本的方式,代替这一繁琐的过程。

首先,建立一个宏,这个宏存放命令,以及该命令的名称。

#define COMMANDS(X)        \
   X(SetStudentName, 0x01) \
   X(GetStudentName, 0x11) \
   X(SetStudentID, 0x02)   \
   X(GetStudentID, 0x12)   \

显而易见,上述代码解释为:

  • 命令0x01 -> 设置学生名字
  • 命令0x11 -> 获取学生名字
  • 命令0x02 -> 设置学生学号
  • 命令0x12 -> 获取学生学号

在宏 COMMANDS(X) 中,X代表某种脚本方法,该脚本方法有两个参数,参数1是命令的名称,参数2是命令的ID。

3. 生成枚举

为了方便管理命令组,需要对每个命令分配一个序号,这个序号可以由枚举进行定义,这里,我们通过编写脚本X实现生成枚举:

#define GENERATE_COMMAND_INDEX(command,commandID)  \
   COMMAND_ID_##command,
enum
{
   COMMANDS(GENERATE_COMMAND_INDEX)
   COMMAND_COUNT
};
#undef GENERATE_COMMAND_INDEX

查看预编译结果:

enum
{
   COMMAND_ID_SetStudentName, 
   COMMAND_ID_GetStudentName, 
   COMMAND_ID_SetStudentID, 
   COMMAND_ID_GetStudentID,
   COMMAND_COUNT
};

由于枚举若不指定开始值,顺序是从0逐项递增,正好满足我们对命令标定序号的需求。

枚举项如下:

序号 枚举名称
0 COMMAND_ID_SetStudentName
1 COMMAND_ID_GetStudentName
2 COMMAND_ID_SetStudentID
3 COMMAND_ID_GetStudentID

Demo 2:批量生成函数

继续按照Demo1的例子,接下来我们就要创建该命令的处理函数。

同样编写一个脚本方法X,如下:

#define __weak __attribute__((weak))
#define GENERATE_COMMAND_FUNC(command,commandID)  \
  __weak void command(void){}

COMMANDS(GENERATE_COMMAND_FUNC)

#undef GENERATE_COMMAND_FUNC

说明:属性定义只适用于GNUC,weak修饰的函数是一个虚函数,意味着可以在外部被重写,相当于一种回调函数。

查看其预编译结果:

__attribute__((weak)) void SetStudentName(void){} 
__attribute__((weak)) void GetStudentName(void){} 
__attribute__((weak)) void SetStudentID(void){}
__attribute__((weak)) void GetStudentID(void){}

Demo 3:Map映射

首先创建一个数组常量,用于存放命令信息。

typedef struct
{
   void (*func)(void);
   int commandId;
}Command;

#define GENERATE_COMMAND_DATAS(command,commandID) {command,commandID},
static const Command commands[] = {COMMANDS(GENERATE_COMMAND_DATAS)};
#undef GENERATE_COMMAND_DATAS

预编译输出:

static const Command commands[] = 
{
   {SetStudentName,0x01}, 
   {GetStudentName,0x11}, 
   {SetStudentID,0x02}, 
   {GetStudentID,0x12},
};

可以发现,数组中每个命令的数组下标与我们创建的枚举命令序号一样。

根据Demo1,我们可以生成一个枚举,帮助我们进行程序设计。Map是一种表,但这个表本身并不存在,我们可以用枚举构建一个桥梁,使命令ID与命令序号联系起来:

#define GENERATE_COMMAND_INDEX(command,commandID)  \
   Index_##commandID,
enum
{
   COMMANDS(GENERATE_COMMAND_INDEX)
};
#undef GENERATE_COMMAND_INDEX

生成枚举如下:

enum
{
   Index_0x01, 
   Index_0x11, 
   Index_0x02, 
   Index_0x12,
};

枚举的顺序就是命令序号,而Index_xxxx指的就是其命令ID。

这样,我们就可以用类似于这样的方式访问到命令信息本身,形成一种映射:

commands[Index_0x11];
commands[COMMAND_ID_SetStudentName];

Demo 4:批量生成判断语句

在实际应用中,我们需要接收指令,然后分析解析,最后得出命令ID,然后判断比对执行其相关函数。这个过程我们也可以通过宏脚本完成:

#define GENERATE_COMMAND_IF(command, commandID) \
   if (commandID == curId)                      \
   {                                            \
      commands[Index_##commandID].func();       \
      break;                                    \
   }
#define GENERATE_COMMAND_EXE(commandId) \
   do                                   \
   {                                    \
      int curId = commandId;            \
      COMMANDS(GENERATE_COMMAND_IF)     \
   } while (0)

void command_exe(int id)
{
   GENERATE_COMMAND_EXE(id);
}

#undef GENERATE_COMMAND_EXE
#undef GENERATE_COMMAND_IF

预编译输出:

void command_exe(int id)
{
   do
   {
      int curId = id;
      if (0x01 == curId)
      {
         commands[Index_0x01].func();
         break;
      }
      if (0x11 == curId)
      {
         commands[Index_0x11].func();
         break;
      }
      if (0x02 == curId)
      {
         commands[Index_0x02].func();
         break;
      }
      if (0x12 == curId)
      {
         commands[Index_0x12].func();
         break;
      }
   } while (0);
}

do{...}whle(0);的作用是限制内部代码作用域,防止与外部代码命名冲突。

总结

上述代码完成了自动处理命令的功能,从用户角度看,只需要在最开始的宏定义命令信息,然后手动编写该命令的回调函数,剩下的工作全部由预编译脚本生成,大大提高了代码的开发效率。

不难看出,宏的灵活使用是多么重要,该例子不仅可以放在命令处理中,也可以放到内存读写的应用里,加以配合其他技巧,很轻松的做到一般代码做不到的功能,做到事半功倍的效果。

标签