C语言宏的一些常规用法小结

时间:2023-01-29 18:31:38

前几天在《C标准库》一书中看到assert宏的实现方法,作者用到“隐藏宏”进行多次宏替换,将一个整型的内置宏转化为可输出的普通字符串。其实在一些库函数的头文件里经常看到类似的东西,例如:

#define SOMETHING(x) _SOMETHING(x)
#define _SOMETHING(x) ANOTHERTHING(x)

当时没仔细想想为啥要这样写,SOMETHING和_SOMETHING看起来就像同样的东西,最终都会被扩展成ANOTHERTHING,或许是为了新老版本兼容吧,Windows里面就有TEXT,_TEXT,_T等多种表示法来定义字符串的Unicode版本或ANSI版本。看了assert宏的实现,明白原来这么做是为了多重替换,以前还真没注意过。看来以后遇到一个疑惑的东西,不能只是想当然的飘过,还要刨根问底知其所以然才行。先看看书中的代码:

/* assert.h standard header */
#undef assert
#ifdef NDEBUG
#define assert(test) ((void)0)
#else
void _Assert(char *);
/* macros */
#define _STR(x) _VAL(x)
#define _VAL(x) #x
#define assert(test) ((test) ? (void)0 /
: _Assert(__FILE__ ":" _STR(__LINE__) " " #test))
#endif
/* _Assert function */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
void _Assert(char *mesg)
{
fputs(mesg, stderr);
fputs(" -- assertion failed/n", stderr);
abort();
}

这里test是一个表达式,如果不是在调试状态下(定义了NDEBUG),或者在表达式的值为真的情况下则扩展成(void)0,也就是什么都不做,否则输出错误信息,包括表达式所在的源文件名称和行号,以及表达式本身,接着中止程序。比如C盘文件sample.c中第13行有一句assert(1+1>2),那么标准错误输出将是"C:/sample.c:13 1+1>2 -- assertion failed"。C语言中,字符串可以自动连接,_Assert的参数便是由5个字符串连接而成。除了冒号和空格,__FILE__是编译器内置宏,实际是一个字符串,预处理器在进行assert到_Assert的替换时便会将其转为字符串。#test则是将符号的字面值转化为字符串,例如char my_string[] = {"Hello, World!"}; 那么#my_string不会被替换成"Hello, World!",而是成为字符串"my_string"。最重要的是__LINE__这个内置宏,它不是字符串类型,而是一个整型值,所以不能像__FILE__那样直接使用。如果企图直接用_VAL(__LINE__),会看到输出的不是断言所在的行号,而是"__LINE__"这个字符串,因为符号#取的仅仅只是字面值。如果多一次从_STR(x)到_VAL(x)转换,__LINE__会先被转换为整数值,比如13,接着#13再变成字符串"13"。很多诸如#define SOMETHING(x) _SOMETHING(x)的代码,往往一个宏的参数又是另一个宏,所以需要替换多次。

符号#给编程带来了不少方便,例如:

#define COMPUTE(exp) printf("%s = %d/n", #exp, exp);

可以在调试程序时方便的输出整型值,再如:

#define FILL_STRUCT(x) {x, #x}
enum STATUS {SUCCESS, FAIL};
typedef struct _MSG {
STATUS id;
const char *msg;
} MSG;
MSG msg[] = {FILL_STRUCT(SUCCESS), FILL_STRUCT(FAIL)};

上面最后一句相当于:MSG msg[] = {{SUCCESS, "SUCCESS"}, {FAIL, "FAIL"}}; 省得粗心输入了不正确的字符串。

符号##的基本作用是连接两个符号从而产生一个新的符号,这样就可以在预编译动态的生成符号。例如编译器需要生成一个临时变量:

#define __TMP_VAR(type, var, line)  type var##line
#define _TMP_VAR(type, line) __TMP_VAR(type, _tmp, line)
#define TMP_VAR(type) _TMP_VAR(type, __LINE__)

语句TMP_VAR(double)即是double _tmp30; 假设30表示该行所在的行号。同一个文件中,每次TMP_VAR所在的行不同,那么产生的变量自然也不会重名,看看它是怎么展开的:
首先:TMP_VAR(double);  ==>  _TMP_VAR(double, __LINE__);
接着:_TMP_VAR(double, __LINE__)  ==>  __TMP_VAR(double, _tmp, 30);
最后:__TMP_VAR(double, _tmp, 30);  ==>  double _tmp30;

运用符号##还可以动态的选择被调用的函数。假设有个程序接受用户输入的命令(字符串)并执行相应的操作(调用函数),当然也可以运用多个if-else语句或者switch-case语句来达到目的。下面是另一种实现方法,执行命令的函数一律采用如下命名方式:如果命令字符串是"XXX",那么相应的函数名则为"ExecXXX",看起来也清晰明了。

typedef void (*PF_EXEC)(void);
#define DEFINE_COMMAND(name) {#name, Exec##name}
struct CommandList
{
const char *szCmd;
PF_EXEC pfExec;
};
void ExecFunc1(void) { printf("Execute Function1!/n"); }
void ExecFunc2(void) { printf("Execute Function2!/n"); }
void ExecFunc3(void) { printf("Execute Function3!/n"); }
static CommandList cmdList[] =
{
DEFINE_COMMAND(Func1),
DEFINE_COMMAND(Func2),
DEFINE_COMMAND(Func3)
};
int SearchFunction(const char *szCommand, CommandList *commandList, int length)
{
for (int i=0; i<length; ++i)
if (strcmp(szCommand, commandList[i].szCmd) == 0)
return i;
return -1;
}
int main(int argc, char* argv[])
{
int index = SearchFunction("Func3", cmdList, sizeof(cmdList)/sizeof(cmdList[0]));
if (index >= 0)
(*(cmdList[index].pfExec))();
else
printf("Invalid Command!/n");
return 0;
}

上面的SearchFunctionIndex函数接收一个字符串,并查找相应的索引,这样就可以通过函数指针调用正确的函数。如果将来需要处理新的命令,那么除了定义新的执行函数,只需要在cmdList[]内加入新的DEFINE_COMMAND宏。当然,越是常用的命令越要放在前面定义,这样做可以加快程序的执行速度。个人觉得这种写法比一长串的条件判断要来的干净,只是限制了函数命名的*,还好在某种特定的场合下,这种限制是没啥坏处的。

宏是个蛮奇妙的东西,那些看上去眼花缭乱却能产生惊奇效果的C程序,大多是用了宏。C语言是简约型的程序设计语言,C++算是“魔幻型”的,不过由于宏的存在,C语言也能增添几分魔幻的味道。总之,宏不能乱用,否则只能让程序杂乱无章,何况编译器无法对宏进行类型检查,使用的时候一定要小心。C99标准的const和inline关键字在某种程度上可以代替宏来使用,当然,有些地方宏还是无可取代的。通常宏和常量采用全部大写的方式命名,长的单词之间用下划线分隔,这是很好的编程习惯。同时,慎用下划线作为开头,以免和函数库或特定平台的内部名称冲突,更不要乱用双下划线开头,这类宏通常是与编译器相关的,如__FILE__和__stdcall。