预处理指令
前题
这里仅讨论预处理在C++中的应用。
如何知道你的代码被预处理成了什么样子?用g++ -E file.cpp -o file.txt
。
正文
define
宏是一个很庞大的话题,其本质是字符串替换。
无参形式
#define
应该是最常见的宏定义了,它可以自定义一些标识符,来代替一些字符串,来在一定程度上简化代码。
例如#define LL long long
,后面就可以用LL
来代替long long
,这等价于typedef long long LL;
。这就是宏定义的无参形式。
带参形式
下面就是很多人认为的宏函数。
#define max(a,b) a>b?a:b
可是按照上面的方法是有问题的,例如!max(a,b)
会被翻译成(!a)>b?a:b
,而不是!(a>b?a:b)
。因此,正确的写法是这样的。
#define max(a,b) ((a)>(b)?(a):(b))
#
和##
#
:字符串化一个宏参数,即在参数名字前后加上"
。
有的人想,我能不能构造这样一个函数f(x)
,输出x=...
呢?当然可以!
#define f(x) printf(#x"=%d",x)
除了#
,还有##
符号,它拼接宏参数和另一个符号,即连接两个符号生成一个新的符号。例如:
#define f(y) var_##y
int main(){
int f(d);
}
会被解释成:
int main(){
int var_d;
}
变参宏
有的竞赛不能使用freopen
,而只能使用fopen
和fprintf
,而这样也导致了一定程度上的编程困难,能不能把printf(format,...)
变成fprintf(fp,format,...)
呢?好在系统提供了预定义宏__VA_ARGS__
,因此我们可以这样:#define printf(format,...) fprintf(fp,format,__VA_ARGS__)
唯一的缺点是printf必须至少有一个参数,同理,scanf也可以这么弄:#define scanf(format,...) fscanf(fp,format,__VA_ARGS__)
但是必须注意,当__VA_ARGS__
作为宏实参再次被传入另一个宏函数的时候,可能会被解释为一个参数(例如在VC中)
#define ATTR_1(arg) printf(arg);
#define ATTR_2(arg, ...) ATTR_1(arg) ATTR_1(__VA_ARGS__)
#define ATTR_3(arg, ...) ATTR_1(arg) ATTR_2(__VA_ARGS__)
int main(){
ATTR_3("123\n","456\n","789\n");
}
可能会被解释成:
int main(){
printf("123\n"); printf("456\n","789\n");
}
因为后面的"456\n","789\n"
被认为是同一个参数,那有什么解决方案吗?答案是借助辅助宏。
#define ATTR(args) args
#define ATTR_1(arg) printf(arg);
#define ATTR_2(arg, ...) ATTR_1(arg) ATTR(ATTR_1(__VA_ARGS__))
#define ATTR_3(arg, ...) ATTR_1(arg) ATTR(ATTR_2(__VA_ARGS__))
这样就可以了。但DEV_C++却并不会有这个问题,因此最好加上。
prescan
当一个宏参数被放进宏体时,这个宏参数会首先被全部展开。当展开后的宏参数被放进宏体时, 预处理器对新展开的宏体进行第二次扫描,并继续展开。例如:
#define put(x) printf("%d",x)
#define sign(x) INT_##x
int INT_1;
put(sign(1));
会被替换为
int INT_1;
printf("%d",INT_1);
但是有例外——当PARAM宏里对宏参数使用了#或##,那么宏参数不会被展开:
#define PARAM(x) #x
#define ADDPARAM(x) INT_##x
PARAM(ADDPARAM(1));
将被展开为"ADDPARAM(1)"
。使用这么一个规则,可以创建一个很有趣的技术:打印出一个宏被展开后的样子,这样可以方便你分析代码:
#define str(x) TO_STRING(x)
#define TO_STRING(x) #x
这样可以用printf("%s",str(宏))
,来输出展开后的样子。
内置宏
系统提供了一些内置宏。
-
__func__
和__FUNCTION__
:该宏所在的函数名。 -
__LINE__
:该宏所处的行数。 -
__DATE__
:该宏所在函数的当前编译日期。 -
__TIME__
:该宏所在函数的当前编译时间。 -
__FILE__
:该宏所在的程序文件的名字。
纯定义
除了无参形式和带参形式,还有很多人都不知道的一个用法——定义标识符。
#define Sign
它的具体用处会在#if
处有讲解。
include
几乎每条程序都不可避免地使用到#include
,可你真的知道它的意思吗?
#include<file>
这就是最常见的include
命令。它表示引用编译器的类库路径里面的头文件,例如#include<cstdio>
。
#include"file"
我敢肯定很多人(不是全部)都不知道include有这种用法。它和#include<file>
的区别是什么呢?它引用的是你程序目录的相对路径中的头文件。
比如,我想从网上下载了一个头文件,名为frac.h
,那么想要把在程序同一目录下的fun.h
文件导入代码,就可以用include"frac.h"
。但要注意,如果#include"cstdio"
没有找到cstdio
这个文件,那么它将去编译器的类库路径里面找。
本质
事实上,它不仅仅可以包含.h
文件和C++的无后缀文件,它还可以包含任何类型的文件,甚至是.txt
。
看起来“引用”这个概念很玄虚,很高级,其实编译器干的只是一件是个人都会做的事——头文件展开。其实就是把cstdio
的内容原封不动地放到你的代码中。
那事情就简单了,甚至我们都可以自己写头文件!
//mains.cpp
int main(){
printf("Hello Include!");
}
//run.cpp
#include<cstdio>
#include"mains.cpp"
把两份代码按照命名放在相同目录下,然后编译run.cpp,你会发现完美运行,其实编译器就仅仅是把文件原封不动地放到你的代码中。
上一篇: 哈希冲突——开散列的实现
下一篇: Python迭代器