前言
在c语言中,以"#"开头的语句叫做预处理命令,如#include,#define等。这些命令通常放在函数之外,放在源文件前,被称为预处理部分。预处理是在编译之前完成的,所以有些语法或逻辑错误可能一开始发现不了。懂得如何聪明地使用预处理命令,对于程序的阅读、修改、和调试非常有利。在这里我整理了一些关于#define预处理命令的运用,虽然不是很深入,但以实用为目的。一、无参宏定义
一般的形式为:
#define 标识符 字符串
例如:
#define pi 3.14159
#define M (x * x + y * y)
#define hour (clock() / 6000 % 24)
顾名思义,无参宏定义就是不带参数,所作的就是单纯的进行字符替换。
第一行把pi替换成3.14159,避免了使用时的反复输入和可能的输入错误。便于阅读,因为pi比起数字有着更明确的含义。也便于修改参数时的维护等操作。
第二、三行表明,可以用简单的符号来替代程序中反复出现的表达式中。符号最好有明确的意义,比如用hour代表小时,这样便于阅读和修改。
这里值得注意的是,在表达式的两边要加上一对小括号。因为只是简单的字符替换,替换后表达式的运算符号的优先级会影响结果(除非你要的就是这种效果),加上小括号是很好的习惯。举个例子:
#define M x * x + y * y //如果不加小括号
当使用表达式M / 2
,替换后就是x * x + y * y / 2
,运算的顺序不能如愿。如果宏定义时有小括号替换之后就是(x * x + y * y) / 2
这样就没有改变运算的顺序了。
当使用第二行宏定义时,程序中必须已经定义了x和y变量,也不能对其他的变量进行操作。如果想要更自由的使用,这时就需要带参宏定义了。
二、带参宏定义
一般的形式为:
#define 宏名(参数表) 字符串
例如:
#define Pow(x) ((x) *( x))
#define M(x,y) ((x) * (x) + (y) * (y))
#define Min(a,b) ((a) < (b) ? (a) : (b))
参数的个数可以是一个也可以是两个或者多个。另外,宏名和参数表之间不能有空格。这里再次提醒一下记得加小括号,而且不仅表达式两边要加,变量的两边也要加,不然仍然可能因为运算优先级的问题出现问题。举个例子:
#define Pow(x) (x * x) //如果变量两边没有括号
当我们这么使用时Pow(2+3)
,我们期望的结果是5*5=25,然而替换后的结果是(2+3*2+3)
,计算结果是11。加上小括号之后就对了((2+3)*(2+3))
。
但是不得不说的是,因为宏定义进行的只是死板字符的替换,有些使用还是会有问题,比如我这么使用第四行的宏定义Min(a++,b)
,进行替换之后就是((a++)<(b)?(a++):(b))
,我们注意到a++
有可能被执行两次,这就可能会出问题。
三、代码段定义
当我们想要用宏定义做更复杂的事情的时候,不是一句表达式能完成的时候,我们可以对一段代码进行宏定义,常见有如下两种方法:
#define Swap_1(a,b) do {int t = a; a = b; b = t; } while(0)
#define Swap_2(a,b) { \
int t = a; \
a = b; \
b = t; \
}
第一种方法是将代码段放在一个do-while
语句中,这种写法的有个巧妙之处是do-while
语句的语法是最后while(0)
后面需要一个;
的,所以我们可以像调用函数一样调用Swap_1(a,b)
,然后在其后加一个;
而不会有语法错误。
但是宏定义每行能写下的代码是有限的(80个字符),当我们需要宏定义更多的代码段时就不能用这种方法。再者,这种写法不利于阅读。所以有了第二种方法。我们可以在每一句的最后加上一个\
来将下一行的内容包含进宏定义。
这样的写法就很像函数了,但相比函数有很多优点:程序在预编译阶段就将代码段进行了替换,省去了创建函数、传参和销毁函数的时间空间消耗。
四、# 操作符
这里指的不是用在define前的#
,而是指用在#define
后面的#
操作符。#
操作符的作用就是把修饰的参数转换为一个字符串。使用方法和带参宏定义类似,例如:
#define MKSTR(str) #str
#define DOBUG_OUT(var) printf(#var"=%d\n",var)
第一行。#str
和str
的区别就是,前者将str对应的标识符转换成了字符串,后者只是表示str对应的标识符。所以当我们这么使用printf(MKSTR(baidu.com\n))
时,#
操作符把baidu.com\n
替换成了字符串"baidu.com\n"
,所以结果就是输出了字符串baidu.com\n
。当你需要输出变量名时,这可以是个很好的运用。
第二行。#
操作符可以用于输出调试信息。比如我想知道reg的值,可以这么使用DEBUG_OUT(reg)
,替换后就是printf("reg""=%d\n",reg)
。编译时会把两个双引号隔开的字符串当作一个字符串来处理,等效于printf("reg=%d\n",reg)
。这样输出信息里包含变量名,从而方便调试。
五、# # 操作符
# #
操作符的用途是合并变量名,常用于嵌入式开发的寄存器命名中,例如:
#define PT(X) PT# #X
当我们这么使用PT(A)
时,替换后就是PTA
又例如:
#define PT(X,n,REG) BITBAND_REG(PT# #X# #_BASE_PTR-># #REG,n) //位操作
#define PTA0_out PT(A,0,PDOR)
连用两个宏定义,当我们使用PTA0_out
,就会被替换成BITBAND_REG(PTA _BASE_PTR->PDOR,0)
,从而避免输入太长的代码和错误的风险,极大的方便了编程。
有一点需要注意的就是被#
和# #
修饰的宏参数是不支持宏的,我们无法通过宏定义再将被#
和# #
修饰的宏参数进行替换
六、预定义的宏
在c语言中,系统封装了一些很好用的宏,我们可以直接调用。有部分宏是是非标准的,有时候有些编译器是没有的。我在这里罗列一些,具体用法就不啰嗦了:
宏 | 说明 |
---|---|
__DATE __ | 日期:Mmm dd yyyy |
__TIME __ | 时间:hh:mm:ss |
__LINE __ | 行号 |
__FILE __ | 文件名 |
__FUNC __ | 函数名/非标准 |
__PRETTY_FUNCTION __ | 更详细的函数说明/非标准 |
七、条件式编译
条件式编译常用在代码的调试和代码的裁剪上,以适配不同的环境。此处不做细说,稍作罗列。
函数 | 说明 |
---|---|
#ifdef DEBUG | 是否定义了DEBUG宏,DEBUG不能是变量 |
#ifndef DUBUG | 是否定义了DEBUG宏 |
#if MAX_N == 5 | 宏MAX_N是否等于5 |
#elif MAX_N == 4 | 否则宏MAX_N是否等于4,elif是else if的简写 |
#else | |
#endif | 只要用到了条件式编译,最后一定要使用这个 |
总结
宏定义因其特性,用与替代一些简单的函数时可以相比函数效率更高。善于使用宏定义可以使我们的代码更容易阅读,修改,调试,移植和维护。对于一些宏定义使用中的小细节比如加括号等要注意,并养成习惯。还要注意使用时的习惯,避免参数表达式的重复执行等。了解系统预定义的宏,可以方便的实现一些此前难以实现的操作。还有条件式编译虽然不在大学课程中强调,但在工程实践中运用非常广泛。
总之懂得聪明的使用宏定义是很重要的事。