C++ 預處理命令


1. 概述

預處理命令就是我們程序開頭以#字符開頭的命令。為什么叫預處理命令?因為這些命令是在編譯時的第一步就執行了的,不會轉為匯編碼。

編譯器編譯代碼的步驟:

  1. 預處理。處理#include,#define等命令並刪除注釋,所以無論怎么寫都不會再第一步CE。

  2. 編譯。真編譯會分析代碼語法(開了O2還會改一些)並生成匯編文件。

  3. 匯編。將匯編碼轉為機器碼。

  4. 鏈接。根據電腦情況進行重定位,鏈接庫等,生成可執行文件

使用-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++的新內容(比如iostreamstack)就不能加.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里還新包括了randomunordered_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")

注意:

  1. 這類優化的效果玄學,因人而異,有時很猛有時一點用都沒有,也與編譯環境相關。但是最壞情況也就沒有用,這些代碼不會因為編譯環境CE。
  2. 由於O2/O3/Ofast優化已經到達了改寫循環,刪除多余代碼等毀天滅地的程度,很容易改變代碼的原意導致玄學錯誤。使用這些優化的時候一定要保證自己的代碼規范,否則就會有玄學問題出現。
  3. 並不知道NOI)(P能不能用,最好不用(你也不可能背下來)

5. Others

還有一些命令,這里花上幾行介紹一下。

#error //在這一行顯示一個CE信息,並中斷編譯
#warning //在這一行顯示警告信息
#line //指定下一行的行號
#if //如果滿足則執行,后面應接布爾表達式,以#endif結尾
#elif //#if語句的分支

完結撒花,感謝陪伴


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM