预处理命令就是我们程序开头以#字符开头的命令。为什么叫预处理命令?因为这些命令是在编译时的第一步就执行了的,不会转为汇编码。
编译器编译代码的步骤:
-
预处理。处理#include,#define等命令并删除注释,所以无论怎么写都不会再第一步CE。
-
编译。真编译会分析代码语法(开了O2还会进行优化)并生成汇编文件。
-
汇编。将汇编码转为机器码。
-
链接。根据电脑情况进行重定位,链接库等,生成最终可执行文件
使用-E
,-S
,-c
可以选择只执行第1步,12步,13步。如果对本文的知识有疑惑,您可以选择使用g++ -E 1.cpp -o 1.i
来获取预处理后的.i
文件来体会。另外-S
也可以用于获取汇编码。
#
符号应该是这一行的第一个非空字符。不过,也可以打\
把内容移到下一行,就跟注释一样。
#define pi 3.14159 \
26535
//This is an \
example
这样就把下一行内容上移了。
常见的预处理命令如下:
#include 包含头文件
#ifdef 或 #if defined 如果定义了一个宏, 就执行操作
#ifndef 或 #if !defined 如果没有定义一个宏,就指执行操作
#define 定义一个宏
#undef 删除一个宏
#pragma 自定义编译器选项,指示编译器完成一些事
这里介绍#include
和#define
。
#include
这是最常见的文件包含命令。
无论你再厉害,即使你什么东西都手写,也需要#include <cstdio>
命令本质是把指定的文件中的函数,变量,宏等全部导入,可以理解成把那个文件全部内容复制粘贴到你的代码里的这一行了。
也正因为只是单纯的“复制粘贴”,命令有可能出现重复包含,应避免此现象的发生
#include使用尖括号和引号
#include
命令不一定要使用尖括号,使用引号也是完全可以的。
区别在于引号会优先在要编译的文件目录下找,没找到才会调用标准库里的文件。
当然对于OIer来讲,#include <cstdio>
和#include "cstdio"
就没有任何区别了,但是此时尖括号更为规范。
往往用尖括号括起来代表编译器目录的标准文件,用引号括起来代表同目录文件等自定文件。
为什么引用标准库的头文件时不加.h?
在C语言中其实是要加的,只能写#include <stdio.h>
或者#include <math.h>
C++里把这些老文件的后缀名去掉并在前面加了一个c比如#include <cmath>
,代表原老版本的库。只是仍保留了#include <math.h>
等写法,两套文件的内容是一样的。但是对于C++的新内容(比如iostream
和stack
)就不能加.h
了。
有人试了,#include <string.h>
能用!但是string.h
对应的是C语言里的cstring
库而不是C++新增的那个string
。使用前者是定义不了string
类型的。cstring
库是提供一些内存操作的函数和char数组的函数比如memset,memcpy,strlen。
万能头文件真的万能吗?
现在的NOIP已经支持万能头文件#include <bits/stdc++.h>
。(关于斜杠:Unix系统的目录名分隔符为/
(斜杠);而Windows下默认为\
(反斜杠),但是同时也支持正斜杠,因此正斜杠是通用的。虽然Windows下可以用反斜杠,但反斜杠往往用作转义字符开头,在某些场合需要写成\\
,否则会出错)
里面包含的很多初学阶段能用到的头文件,缺点是会大幅增加编译时间。
使用万能头文件不要用的变量名:y1, next, time, rand
等
包括很多极常见单词最好都不用,有些Windows可以,但在其他系统下无法通过编译。
#define
命令#define 叫做宏定义,用于代码中的字符串替换。是最常见的预处理指令之一
1. 不带参数的宏
#define MAX 10000
if (9874 > MAX)
return 0;
上述代码定义宏MAX,这句以后的"MAX"就代表10000。if中的式子为false。
该方法可用于替代const定义常量,而且只做了编译时代码替换,运行时不占用空间。也可以用于简化标准库里名字超长的函数。
另外如果这个常量需要多次进行运算(比如模数),据说写成const是更快的,经过个人不完全测试的确是这样的,但是效率差别很小,所以也不必过多在意,还是看自己更喜欢哪种写法。
注意:
-
define不会替换字符串和注释中的宏(废话)_
-
替换宏时需要完全匹配,如定义宏“super”后,“supermarket”不会被部分替换。_
2. 带参数的宏
宏跟函数一样,可以带有参数。
例:用圆的半径求其周长和面积。
#define pi 3.14159
#define AREA(i) i*i*pi
double d;
int main()
{
cin >> d;
cout << AREA(d)<< endl ;
return 0;
}
我们把宏写成AREA这种像函数的形式,之后出现AREA(i)时,
先发现括号里为2,即i=2,然后再做替换。
由于只做字符串替换,所以#define不仅可以定义常量,还可以定义表达式,函数,甚至代码段。
#define sum(a,b,c) (a)+(b)+(c)
#define max(a,b) (a>b)?(a):(b)
#define fors(a,b) for(int i=(a);i<=(b);i++)
利用宏定义可以使代码更加简洁易懂,同时用#define定义max等函数。速度快于函数,但也没快多少。
-
命令#define命令后第一个单词为宏,其余为宏体。
#define int long long #define abc def ghi \ jkl #define register
在第一句中,第一个int为替换体,即以后int代表long long。
在第二句中,只有abc作为宏体,之后的abc被替换为def ghi jkl,反斜杠只有换行作用。
在第三句中,程序里所有的
register
会被删除,可以用于调试。 -
替换字符串时会在两端加上空格
我们都知道
vector <pair<int,int>>
会因为>>被识别为右移而CE所以必须补空格。但是如果这样写:#define pii pair<int,int> vector <pii> a;
却可以正常通过编译,这是因为替换时自动加上了空格。
两个运算符构成新运算符加空格:
<< >> -> ++ && += >=
这样可以解决一些宏直接的字符串替换带来的问题
3. 宏的高级应用
##
:连接左右两端的字符串
#
: 把后面的参数变为一个字符串(即强行加上"")
#define a(x) p##x
#define b(x) #x
int p1 = 3, p2 = 4;
int main()
{
printf("%d %d\n",a(1),a(2));
puts(b(qwqwq));
}
//Output:
//3 4
//qwqwq
这个比较常见的就是用来缩写for
,避免因b改变带来的问题,每次循环里终止变量都是另一个名字
#define F(i, a, b) for(int i=(a),end##i=(b); i<=end##i; i++)
#ifdef
如果定义了宏
#ifndef
如果没定义宏
#endif
以上两句的终止句(相当于右括号)
在标准库中,每包含一个头文件,这个头文件里就会define一个表示这个文件已被包含的宏,如果这个文件第二次被包含,#ifndef
为假不再执行,就会跳过文件,这样就可以避免重复包含导致CE。
#ifndef xxx //如果还没包含过
#define xxx //设定宏,下次遇到本文件将跳过
...
#endif
用宏来管理调试
-
有些宏是在不同编译环境里就定义好的,利用这些就可以做些趣事。
#ifndef ONLINE_JUDGE freopen("testdata.in","r",stdin); freopen("testdata.out","w",stdout); #endif //很多OJ(包括洛谷)都有这个宏
-
NDEBUG宏,定义NDEBUG宏表示“不调试”,此时程序的assert语句将不起作用。
#define NDEBUG assert()//不再起作用
其他预定义的宏(便于输出调试信息):
__cplusplus //C++版本号
__FILE__ //文件名
__DATE__ //编译日期
__TIME__ //编译时间
__LINE__ //这一行的行号
4. 宏的撤销
能定义的宏就能取消,使用#undef直接接宏名就可以撤销宏。
#define sum(a,b) a+b
#define e 2.718
int a=sum(9,6);
double b=e*3;
#undef sum(a,b)
#undef e
#undef __cplusplus
5. 宏替换的注意事项
宏虽然方便易用,但使用不当可能不会产生期望的结果
-
在语句两端加上括号
#define DEF 2+3 int a = DEF+5; int b = DEF*7;
DEF以2+3的形式直接带入,没有转化为5
在A的定义中,a将被解释为“2+3+5”,其值为10.
但B将被解释为“2+3*7”,乘法先算,值为23,不是我们希望的35.
解决方法就是在参数左右加上括号
-
将量指为我们希望的类型
#define MAX 1e6 int a[MAX];
此时会CE。因为1e6是一个double类型,数组大小只能用int,由于MAX是文本替换导致这里并不会转换类型。
这是可以在前面加上(int),或者使用const定义常量。