预处理脚本?
在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);
的作用是限制内部代码作用域,防止与外部代码命名冲突。
总结
上述代码完成了自动处理命令的功能,从用户角度看,只需要在最开始的宏定义命令信息,然后手动编写该命令的回调函数,剩下的工作全部由预编译脚本生成,大大提高了代码的开发效率。
不难看出,宏的灵活使用是多么重要,该例子不仅可以放在命令处理中,也可以放到内存读写的应用里,加以配合其他技巧,很轻松的做到一般代码做不到的功能,做到事半功倍的效果。