1. 概述 预处理命令就是我们程序开头以#字符开头的命令。为什么叫预处理命令?因为这些命令是在编译时的第一步就执行了的,不会转为汇编码。 编译器编译代码的步骤: 预处理。处理#include,#define等命令并删除注释,所以无论怎么写都不会再第一步CE。 编译。真编译会分析代码语法(开了O2还会改一些)并生成汇编文件。 汇编。将汇编码转为机器码。 链接。根据电脑情况进行重定位,链接库等,生成可执行文件
使用-E ,-S ,-c 可以选择只执行第1步,1~2步,1~3步。如果对本文的知识有疑惑,您可以选择使用g++ -E 1.cpp -o 1.i 来获取预处理后的.i 文件深刻体会。另外-S 也可以用于获取汇编码。 绝大部分预处理命令在OI里用处不大,但也有功能强大的预处理命令。 # 符号应该是这一行的第一个非空字符。不过,也可以打\ 把内容移到下一行,就跟注释一样。
#define pi 3.14159 \ 26535 //This is an \ example
这样就把下一行内容上移了。 洛谷的编辑器不会这么显示,但本地编辑器上你能发现下一行也变成了注释或预处理样式。 常见的预处理命令如下: #include 包含头文件 #ifdef 或 #if defined 如果定义了一个宏, 就执行操作 #ifndef 或 #if !defined 如果没有定义一个宏,就指执行操作 #define 定义一个宏 #undef 删除一个宏 #pragma 自定义编译器选项,指示编译器完成一些事
这里介绍3个最常用的预处理命令:#include ,#define ,#pragma 2. #include这是最常见的文件包含命令。 无论你再厉害,什么东西可以手写,也需要#include <cstdio> 命令本质是把指定的文件中的函数,变量,宏等全部导入,可以理解成把那个文件全部内容复制粘贴到你的代码里了。 不过,如果是单纯的粘贴,#include两遍应该会有重复定义CE才对。但是标准库使用宏定义避免了这一点(参见后文)。自己写头文件时也要注意。 Question 0: #include必须接尖括号吗?事实上,#include 命令不一定要使用尖括号,使用引号也是完全可以的。 区别在于引号会优先在要编译的文件中找,没找到才会调用标准库里的文件。 当然对于OIer来讲,#include <cstdio> 和#include 'cstdio' 就没有任何区别了,但是此时尖括号更为规范。 在自己用C++开发小游戏时,为了便于管理,可以像标准库一样把用途相似的函数单独用一个文件保存。在需要时就将其包含,此时就需要用到引号了。 Question 1: 为什么引用标准库的头文件时不加.h?在C语言中其实是要加的,只能写#include <stdio.h> 或者#include <math.h> C++里把这些文件的后缀名去掉并在前面加了一个c比如#include <cmath> 但是这些传统的库你如果使用老写法,仍然是可以过编译的,只是不规范。 但是对于C++的新内容(比如iostream 和stack )就不能加.h 了。 有人试了,会说#include <string.h> 能用!但是string.h 对应的是C语言里的cstring 库而不是C++新增的那个string 。使用前者是定义不了string 类型的。cstring 库是提供一些内存操作的函数和char数组的函数比如memset,memcpy,strlen。 Question 2: 万能头文件真的万能吗?现在的NOI)(P已经支持万能头文件#include <bits/stdc++.h> 。 (注意是正斜杠不是反斜杠,写错了有可能CE) 事实上他包含的东西你是不可能记完的,但是您能用到的东西里面绝对都有。 C++11 里还新包括了random ,unordered_map 等库。 详见stdc++.h原文件
虽然说不上万能,OI里的确完全够用了。 辟谣!!!万能头文件并不会减慢程序运行速度,内存上的增加几乎可以忽略。在编译时main里没有用到的东西就会被优化掉。 而且你随时带上十几个头文件,又在说万头不好,根本没说服力 当然有可能增加编译时间和源程序大小,然并卵 Question 3: 为什么我看到别人有在程序中途包含文件的神奇操作?之前说过#include 的本质是把指定文件复制进这一行,所以如果是在函数内写的这个命令,就只对这一个函数有效。 void func() { #include 'test.h' mmm();//可以使用test.h里的函数 }
int main() { func(); mmm()//CE。不能使用test.h里的函数。 }
但是OI里不能这么用,因为标准库还涉及到命名空间的问题。 Quetion 4: 自己写的头文件到底是怎么用的?按照标准的话,.h 用于存放大篇的宏定义和函数,变量的声明(也就是函数第一行的函数名和参数列表),而同名的.cpp 则存放函数的具体实现。.h 里写一个#include <test.cpp> 。主程序只要包含test.h就可以使用库里的函数了。 不过为了节省工作量,我们可以在.h 里就直接定义好函数,也可以选择在主程序里直接#include <test.cpp> 。包含命令的本质是复制粘贴,这样写也是完全没有问题的。 使用万能头文件不要用的变量名:y1, next, time, rand 包括很多常见单词最好都不用,有些Windows可以,但是评测时会CE。 3. #define命令#define 叫做宏定义,用于代码中的字符串替换。是最有用的预处理指令 1. 不带参数的宏#define MAX 10000 if (9874 > MAX) return 0;
上述代码定义宏MAX,这句以后的'MAX'就代表10000。if中的式子为false。 该方法可用于替代const定义常量,而且只做了代码替换,运行时不占用空间。也可以用于简化标准库里名字超长的函数。 另外如果这个常量需要多次进行运算(比如模数),据说写成const是更快的,经过个人不完全测试的确是这样的,但是效率差别很小,所以也不必过多在意,还是看自己更喜欢哪种写法。 注意: 1. #define不会替换字符串和注释中的宏(废话) 2. 替换宏时需要完全匹配,如定义宏“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 会被删除,可以用于调试。 特例(不是完全字符串替换,感谢@Black_white_Tony dalao): 我们都知道vector <pair<int,int>> 会因为>>被识别为右移而CE所以必须补空格。但是如果这样写: #define pii pair<int,int> vector <pii> a;
却可以正常通过编译,这是因为如果define中的最后一个字符和后面第一个字符能构成新运算符时,就会自动加上空格。大家可以用g++ -E 指令看得更透彻一些。 两个运算符构成新运算符加空格:<< >> -> ++ && += >= 这个特例也许就是为了STL套STL的问题设计的吧。 注:C++11 里是可以直接写vector <pair<int,int>> 的,但是你如果使用了宏定义,第一步预处理后的文件在这里仍会加上空格。 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
#ifdef 如果定义了宏
#ifndef 如果没定义宏
#endif 以上两句的终止句(相当于右括号)
在标准库中,每包含一个头文件,这个头文件里就会define一个表示这个文件已被包含的宏,如果这个文件第二次被包含,#ifndef 为假不再执行,就会跳过文件,这样就可以避免重复包含导致CE。 有些宏是在不同编译环境里就定义好的,利用这些就可以做些趣事。 #ifndef ONLINE_JUDGE freopen('testdata.in','r',stdin); freopen('testdata.out','w',stdout); #endif //很多OJ(包括洛谷)都有这个宏
或者也可以在开头定义一个debug宏,把调试输出的语句用#ifndef 括上,这样删除调试输出就只需要注释一行。 其他预定义的宏:(摘自cppreference) __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. 宏的缺点宏虽然方便易用,但也有许多缺点。 I. 改变运算优先级#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. 解决方法就是在参数左右加上括号 II. 没有固定的数据类型#define MAX 1e6 int a[MAX];
此时会CE。因为1e6是一个double类型,数组大小只能用int,由于MAX是文本替换导致这里并不会转换类型。 这是可以在前面加上(int),或者使用const定义常量。 4. #pragma在我们寻找一道题最优解的时候,最快的人(如果没打表)往往会有几十行的#pragma来卡常。那么这个命令有什么用?卡常的原理是什么呢? #pragma 命令可以指定编译选项,或者让编译器完成一些命令。功能非常强大,这里只做非常浅显的介绍。
部分内容摘自百度百科。 1. #pragma once添加在头文件的开头,可以告诉编译器这个文件最多编译一次,也可以用于防止重复包含头文件。比前文#ifndef 好用,只是标准库里没用这个。 2. #pragma message()让编译器输出括号里的字符串,配合#ifdef ,可以在编译时就输出一些特定的信息。 3. #pragma comment()本身用于链接文件,OI里可以用来手动扩栈 #pragma comment(linker,'/STACK:1024000000,1024000000')
4. #pragma GCC target()这个找遍全网也没有准确定义,大概就是将括号里的东西识别为指令。指令的速度比函数更快,借此加速。 #pragma GCC target('popcnt') 可以让内置函数__builtin_popcount()的速度提高一倍以上。
另外,如果你想使用指令集,也可以使用这条指令把指令集括上。 #pragma GCC target('avx,avx2,sse,sse2,sse3,sse4.1,sse4.2')
5. #pragma pack() & pop()用于对齐结构体 //#pragma pack(4) struct Node { int a; long long b; }x;
本来一个结构体的每个变量都会与最大的那个对齐,比如例子中int就与long long对齐了,字节数也为8。所以sizeof x = 16 。 但是如果有了那句#pragma,每个变量就会与4对齐,所以int字节数为4,long long由于本来就大于4就被忽略,sizeof x = 12 。这样做一定程度上可以省空间。 但是对齐其实效率更高,所以x大一点好。 pop()可以用来取消pack()指令 6. #pragma GCC optimize()将括号里的字符串带入编译参数,相当于可以自定义编译参数。 如果输入数字的话就会进行O1/O2/O3优化。用这个命令可以开启编译器自带的优化。 但是只能是编译优化方面的参数,比如-o 指定文件名肯定不能加在里面。 最后附赠网络上广泛流传的40行优化: #pragma GCC target('sse,sse2,sse3,sse4.1,sse4.2,popcnt,abm,mmx,avx') #pragma comment(linker,'/STACK:102400000,102400000') #pragma GCC optimize('Ofast') #pragma GCC optimize('inline') #pragma GCC optimize('-fgcse') #pragma GCC optimize('-fgcse-lm') #pragma GCC optimize('-fipa-sra') #pragma GCC optimize('-ftree-pre') #pragma GCC optimize('-ftree-vrp') #pragma GCC optimize('-fpeephole2') #pragma GCC optimize('-ffast-math') #pragma GCC optimize('-fsched-spec') #pragma GCC optimize('unroll-loops') #pragma GCC optimize('-falign-jumps') #pragma GCC optimize('-falign-loops') #pragma GCC optimize('-falign-labels') #pragma GCC optimize('-fdevirtualize') #pragma GCC optimize('-fcaller-saves') #pragma GCC optimize('-fcrossjumping') #pragma GCC optimize('-fthread-jumps') #pragma GCC optimize('-funroll-loops') #pragma GCC optimize('-fwhole-program') #pragma GCC optimize('-freorder-blocks') #pragma GCC optimize('-fschedule-insns') #pragma GCC optimize('inline-functions') #pragma GCC optimize('-ftree-tail-merge') #pragma GCC optimize('-fschedule-insns2') #pragma GCC optimize('-fstrict-aliasing') #pragma GCC optimize('-fstrict-overflow') #pragma GCC optimize('-falign-functions') #pragma GCC optimize('-fcse-skip-blocks') #pragma GCC optimize('-fcse-follow-jumps') #pragma GCC optimize('-fsched-interblock') #pragma GCC optimize('-fpartial-inlining') #pragma GCC optimize('no-stack-protector') #pragma GCC optimize('-freorder-functions') #pragma GCC optimize('-findirect-inlining') #pragma GCC optimize('-fhoist-adjacent-loads') #pragma GCC optimize('-frerun-cse-after-loop') #pragma GCC optimize('inline-small-functions') #pragma GCC optimize('-finline-small-functions') #pragma GCC optimize('-ftree-switch-conversion') #pragma GCC optimize('-foptimize-sibling-calls') #pragma GCC optimize('-fexpensive-optimizations') #pragma GCC optimize('-funsafe-loop-optimizations') #pragma GCC optimize('inline-functions-called-once') #pragma GCC optimize('-fdelete-null-pointer-checks')
注意: 这类优化的效果玄学,因人而异,也与编译环境相关。但是最坏情况也就没有用,这些代码不会因为编译环境CE。 由于O2/O3/Ofast优化已经到达了改写循环,删除多余代码等毁天灭地的程度,很容易改变代码的原意导致玄学错误。使用这些优化的时候一定要保证自己的代码规范,否则就会有玄学问题出现。 并不知道NOI)(P能不能用,最好不用(你也不可能背下来)
5. Others还有一些命令,这里花上几行介绍一下。 #error //在这一行显示一个CE信息,并中断编译 #warning //在这一行显示警告信息 #line //指定下一行的行号 #if //如果满足则执行,后面应接布尔表达式,以#endif结尾 #elif //#if语句的分支
完结撒花,感谢陪伴
|