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
這個比較常見的就是用來縮寫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 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語句的分支
完結撒花,感謝陪伴