I/O Stream

簡介

你可能有聽過「輸入流」和「輸出流」,但是流是什麼呢?

流(stream)就像河流一樣,先從上游流進去的東西會先從下游流出來,而輸入流和輸出流顧名思議,就是用來控制輸入和輸出的東西。簡單來講,你輸入的東西會進入輸入流,然後再依序流往它流向的地方,像是被程式讀取,輸出流也是,程式輸出的東西會先到輸出流,再到它流向的地方,可能是你的 terminal、或是某些檔案,分成多個輸入、輸出流的目的就是為了控管不同的來源和去向。流會有「緩衝區」,就是流裡的東西會暫時停在那裡,不會馬上跑到流向的地方。

非常簡略地講解一下 C++ 裡輸入輸出的物件關係(詳細可以看這裡)。有兩個類型分別叫 ostream 和 istream,分別是輸出流和輸入流,繼承 ostream 的都是輸出流、繼承 istream 的都是輸入流,然後有另一個類型叫 iostream,它同時繼承 ostream 和 istream,所以它是雙向的,一個例子是 stringstream。

所有 istream 的用法都和 cin 一樣、所有 ostream 的用法都和 cout 一樣。

標準流

標準流是指三種流:標準輸入流、標準輸出流和標準錯誤流,標準輸入和輸出流就如同其名,分別是輸入流和輸出流,至於標準錯誤流,它是一個輸出流,它們預設都是在 terminal 上做事的,從 terminal 輸入和輸出到 terminal,在 C++ 中,它們三個分別由 cin(繼承 istream)、cout(繼承 ostream)和 cerr(繼承 ostream)控制,也就是大家最常用的輸入輸出流啦。

標準輸出流和標準錯誤流都是輸出流,那它們有什麼分別呢?首先,judge 是只會讀標準輸出流的資訊的,所以你可以輸出一堆 debug 用的訊息到標準錯誤流,忘記刪掉也不會怎麼樣,只要不要造成 TLE 就好了(像是輸出量過大)。而且它們可以被重新導向到不同的地方。

這裡提到了一個東西,叫做「重新導向(redirect)」,標準流不一定要在 terminal 做事,也可以把它導到別的地方去,如果直接在 C++ 寫,用法是這樣的:

1
2
3
freopen("filename", "w", stdout);
freopen("filename", "w", stderr);
freopen("filename", "r", stdin);

這三個分別是把標準輸出、錯誤和輸入流導向到名為 filename 的檔案,在本機測試的時候,輸入或輸出量大時,把標準流導到檔案去是很實用的方法,也很方便,只要加幾行重新導向的程式碼就可以了,而上傳到 judge 前也只要移除這幾行就好。有些比賽例如 USACO,會要求你從特定的檔案輸入輸出。

也可以在 terminal 上做重新導向,在 Linux 的 bash 和 Windows 的 CMD 都是像這樣寫:

1
2
3
4
5
command < in
command > out
command 2> err
command < in > out 2> err
command < in > out

command 是執行檔名稱,in 是輸入流導向的檔案,out 是輸出流導向的檔案,err 是錯誤流導向的檔案,可以像前三行一樣只重新導向其中一個、也可以像第四行一樣重新導向全部三個,或是像第五行只重新導向其中兩個。

對檔案輸入輸出也可以用 fstream,但比賽理論上用不到,所以有興趣可以自己查。

cin 和 cout 都有緩衝區,所以 cin 從來源讀取的東西會留在緩衝區裡等你用程式讀取,而你讓 cout 輸出的東西也會先留在緩衝區,不會馬上出現在它導向到的地方,讓輸出流真的輸出緩衝區裡的東西的動作叫 flush,手動 flush 的方法是 cout << flushcout 也可以換成其他有緩衝區的輸出流,自動 flush 的條件後面會提到。而 cerr 不緩衝,所以讓 cerr 輸出的東西就會馬上輸出。

使用

運算子重載

重載 <<>> 就能讓原本不能直接輸入輸出的東西可以直接 cin/cout 了,例如:

1
2
3
4
5
6
7
ostream& operator<<(ostream& o, pair<int, int> p){
return o << p.first << " " << p.second;
}

istream& operator>>(istream& i, pair<int, int> p){
return i >> p.first >> p.second;
}

輸入格式

EOF

EOF 是 End of File 的縮寫。有些題目會在一個檔案放多筆測資,比較現代的題目會先輸入一個數字告訴你接下來有幾筆,但比較古老的題目或心理比較古老的人出的題目就會跟你說「以 EOF 結束」,所以你必須一直讀到檔案結束為止,做法很簡單:

1
2
3
while(cin >> /*...*/){
//...
}

cin >> ... 除了把東西輸入到變數裡,也會回傳 cin 本身(這就是為什麼可以像 cin >> a >> b >> c >> ... 串一大串),而 cin 可以轉型成 bool,如果遇到 EOF(或其他原因導致輸入失敗)了它就是 false,否則它就是 true。

EOF 這東西不是一個字元,所以你可能會想,terminal 不是 file,不就沒有 EOF 了嗎?在 Windows 可以用 ctrl+z 製造出 EOF,其他系統可以用 ctrl+d。

輸入一整行

用像是 cin >> ... 的方式輸入的話,會先忽略緩衝區前面的所有空白字元(空格、換行等等),然後讀取非空白字元,直到遇到空白字元再停下來。

但是,當遇到這種情況,就會需要一次輸入一整行:你需要知道哪些東西在同一行,而且又不確定一行有幾個,方法是這樣:

1
2
string s;
getline(in, s); //in 是輸入流

getline 也可以放進 while 裡判 EOF。

getline 跟 >> 混用的話會發生一個問題,假設有兩行輸入,第一行有一個數字,下一行有未知數量的以空格隔開的數字,首先它們會全部進入緩衝區,如果你先用 >> 輸入了一個數字,那麼在緩衝區內的這個數字會被移除,但它後面的換行不會,所以你這個時候用 getline,就得到一個空字串,解決方法也很簡單,就是 getline 一次把空白行清掉後,再 getline 一次。

忽略輸入

直接看題目:ZeroJudge c268,這題表面上是要你找三個數字當三角形的邊長,可以 做完,可是第一 超大,把數字排序好後就 TLE 了,就算不排序,雖然輸入的複雜度是 ,但輸入常數很大,輸入這麼多東西也會 TLE 的。

可以發現到若輸入的數字都不能構成三角形,那麼在數字都盡量小的情況下,把它們排序後會變成費氏數列,而輸字的最大值又是 ,費氏數列還沒到第 50 項就會超過了,所以可以得出 的時候保證有解。

現在排序的問題解決了,那輸入的問題呢?它是一個檔案多筆測資,所以也不能讀到 就直接 return,解決方法是:

1
cin.ignore(100000000, '\n');

這麼做可以忽略掉到一部分的輸入,第一個參數是忽略的字元上限,打一個很大的數字就行了,第二個是停下來的字元,到下一個這個字元之前和它本身的字元都會被忽略掉。

這也是 getline 解決空行的解法之一。

輸出格式

小數位數

需要輸出浮點數的題目,要嘛就是要求跟答案的差距要在某個範圍內,不然就是要求你四捨五入到第幾位,所以我們要能夠控制輸出的小數點後位數。方法是這樣:

1
cout << fixed << setprecision(10) << ...;

沒加 fixed 的話,會變成是指定總共的位數,如果小數點前的位數超過就會變科學記號,而加上 fixed 就是指定小數點後的位數了,然後它也會自己四捨五入,非常方便。還有它只作用在浮點數,像是 int 不會跑出 .0000,字元也還是以字元的形式輸出。

數字寬度

數字寬度和數字位數不一樣,輸字寬度是指輸出的總字元數不到寬度的時候,就會在它之前補上數個某固定字元。指定寬度是 setw(n),補上的字元是 setfill(c),預設空格,用法就一樣是 cout << setw(n) << setfill(c) << ...。要特別注意的是,setw 只作用在下一個輸出的數字(可以是浮點數或整數)上,而 setfill 不會失效。