您的位置 首页 > 数码极客

【宏是什么意思】c语言入门26,你会几种宏定义的使用姿势,有时比函数好用

上一节详细说明了C语言指针的使用。特别是,上一节讨论了如何使用C语言实现“伪类”。

有朋友评论说这样没有意义,的确,在上一节的例子中的确没有什么意义,伪类”甚至还让本来简单的代码变得更繁杂了。但是,在较大的复杂项目中,使用 C 语言“伪类”封装还是司空见惯的。

事实上,linux 内核源代码中有大量使用“伪类”的地方,本节不再讨论“伪类”。

谈到较大的项目,就不得不提一下“宏定义”了,较大的项目都会用大量的宏定义来组织代码,随便找一个开源项目,打开它的源代码头文件,看看能发现多少宏定义。你可能用过 #define N 20 这种宏定义,看起来宏定义只不过是做个替换而已,其实里面有比较复杂的规则。本节打算谈一谈宏定义,是因为宏定义在 C 语言中非常重要,也非常有用,有时有些实现甚至非宏定义莫属。

函数式宏定义

像 #define N 20 这种宏定义称为“变量式”宏定义,N 可以像变量一样使用,但是 N 属于常量表达式。实际上,还有一种可以像函数一样使用的宏定义,可称之为“函数式宏定义”,请看如下代码:

#define MIN(a, b) ( (a)<(b)?:(a):(b) ) x = MIN(3&0x0f, 5&0x0f)

将 x = MIN(3&0x0f, 5&0x0f) 表达式展开,得:

x = ( (3&0x0f)<(5&0x0f)?(3&0x0f):(5&0x0f) )
d = a?b:c 这个表达式的意思是,if(a) d=b; else d=c;

可以看出,函数式宏定义 MIN 可以像函数一样使用,两个实参被替换到宏定义形参 a 和 b 的位置了。应当注意,函数式宏定义和真正的函数是有区别的:

  • 函数式宏定义的参数没有类型,预处理时不做参数类型检查,所以使用时要确保类型正确。
  • 函数式宏定义本身不会被编译为函数,调用时就是直接把宏定义替换过来,而不是简单的几条传参和 call 指令,所以函数式宏定义编译生成的目标会比真正的函数大。
  • 定义函数式宏定义要非常小心,如果 MIN 定义成 #define MIN(a, b) ( a<b? a:b ),则 x = MIN(3&0x0f, 5&0x0f) 展开就成了 x = ( 3&0x0f<5&0x0f?3&0x0f:5&0x0f ),运算符的优先级就错了,不会得出正确结果。读者思考一下,外层括号能否省略?
  • 因为调用函数式宏定义就是简单替换,所以如果 MIN(i++, j++) 时,展开就是 ( (i++)<(j++)?(i++):(j++) ),i和j自加的次数是不确定的。如果是 MIN 真正的函数,则 i 和 j 确定是只自加一次。


在 linux 内核中,函数式宏定义通常使用 do{…}while(0) 包裹:

#define do_something(i) \ do{ \ i ++; \ printf("i = %d\n", i); \ }while(0)

为什么呢?请看下面这个例子,就明白了:

if(i>5) do_something(i); else printf("test\n");

如果没有使用 do{…}while(0) 包裹,把 do_something 展开后,变为:

if(i>5) i++; printf("i = %d\n", i); else printf("test\n");

printf(“i = %d\n”, i); 这句没有被包含在 if 判断语句里,而且 else 语句并没有与 if 配对,所以编译会报错。那能否在宏定义时,使用 {} 包裹呢?还是上面的例子,使用 {} 包裹展开后:

if(i>5) { i++; printf("i = %d\n", i); }; // ; else printf("test\n");

虽然 printf(“i = %d\n”, i); 这句被包含在 if 判断语句里了,但是 do_something(i); 最后的 “;”会被展开到 {} 后面,这样表示 if 的判断结束了,else 依然没有与 if 配对,还是会编译报错。那 do_something(i); 后面的这个“;”不写不就行了吗?的确,不写就没有错误了,但是不写 “;”,看起来就不像函数调用了,对不?整个语句显得怪怪的,哪天顺手一加,就又错了。


C 语言宏定义的 ## 运算符

请看实例:

#define test(a, b) a##b test(he, llo)

test(he, llo) 预处理后,就相当于 hello。再请看:

#include <; #define test(a) print##a void print1() { printf("hello 1\n"); } void print2() { printf("hello 2\n"); } int main() { test(1)(); test(2)(); return 0; }

显然,“##”是一个特殊的拼接符,说它特殊是因为它拼接的并不是字符串,以上代码执行结果如下:

利用 define 也可以实现一些不定参数的函数式宏定义,请看:

// 开发时 #define debug(format, ...) printf(format, ## __VA_ARGS__) // 发行时 #define debug(format, ...)

这么定义以后,debug 就可以像 printf 一样使用了。在开发阶段,需要打印出信息时,使用 debug 代替 printf。这么做的好处是,开发完毕不再需要打印信息时,只需要简单的把 debug 定义为空即可,而无需再去挨个删除 printf。以后还想打印信息,再把 printf 添加回来即可。

有时候,函数式宏定义可以做到函数难以实现的事

现在的 C 语言及其编译器支持了很多有趣的关键字,例如:

__FUNCTION__ 表示函数名 __LINE__ 表示所在行号 等等

请看如下代码:

#include <; int main() { printf("%s %d\n", __FUNCTION__, __LINE__); return 0; }

编译器在编译时,会自动的把 “__FUNCTION__” 和“__LINE__”替换为函数名和行号,这样就不用程序员逐个手动输入了,而且代码的可移植性也更强。

为了更方便的输出当前位置,我们可以定义函数式宏定义 location:

#include <; #define location() \ do{ \ printf("fun: %s, line: %d\n", __FUNCTION__, __LINE__);\ }while(0) void test() { location(); } int main() { test(); location(); return 0; }

打印出位置是有用的,它能帮助我们在大型项目的复杂代码中快速的找到出错的函数,出错的行号。(类似于 “__LINE__”的关键字还有一些,留给读者自行查阅了)


在 windows 环境中开发,各种 IDE 把程序员照顾的很好。但是有些错误,即使是 IDE 也无法定位,以后会看到这样的例子。另外,嵌入式开发中,可不一定有合适的 IDE 给程序员使用。

location 是一个函数式宏定义,所以调用它,就相当于把代码展开到调用位置,所以它可以打印出 test 中的位置,也可以打印出 main 中的位置。如果 location 是一个真正的函数,输出结果就不同了,请看:

这是为什么呢?原因留给读者自己分析了。


欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。

关于作者: admin

无忧经验小编鲁达,内容侵删请Email至wohenlihai#qq.com(#改为@)

热门推荐