你可能有聽過「輸入流」和「輸出流」,但是流是什麼呢?
流(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 | freopen("filename", "w", stdout); |
這三個分別是把標準輸出、錯誤和輸入流導向到名為 filename 的檔案,在本機測試的時候,輸入或輸出量大時,把標準流導到檔案去是很實用的方法,也很方便,只要加幾行重新導向的程式碼就可以了,而上傳到 judge 前也只要移除這幾行就好。有些比賽例如 USACO,會要求你從特定的檔案輸入輸出。
也可以在 terminal 上做重新導向,在 Linux 的 bash 和 Windows 的 CMD 都是像這樣寫: 1
2
3
4
5command < 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 << flush
,cout
也可以換成其他有緩衝區的輸出流,自動 flush 的條件後面會提到。而 cerr 不緩衝,所以讓 cerr 輸出的東西就會馬上輸出。
重載 <<
和 >>
就能讓原本不能直接輸入輸出的東西可以直接 cin/cout 了,例如:
1 | ostream& operator<<(ostream& o, pair<int, int> p){ |
EOF 是 End of File 的縮寫。有些題目會在一個檔案放多筆測資,比較現代的題目會先輸入一個數字告訴你接下來有幾筆,但比較古老的題目或心理比較古老的人出的題目就會跟你說「以 EOF 結束」,所以你必須一直讀到檔案結束為止,做法很簡單:
1 | 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 | string s; |
getline 也可以放進 while 裡判 EOF。
getline 跟 >>
混用的話會發生一個問題,假設有兩行輸入,第一行有一個數字,下一行有未知數量的以空格隔開的數字,首先它們會全部進入緩衝區,如果你先用 >>
輸入了一個數字,那麼在緩衝區內的這個數字會被移除,但它後面的換行不會,所以你這個時候用 getline,就得到一個空字串,解決方法也很簡單,就是 getline 一次把空白行清掉後,再 getline 一次。
直接看題目:ZeroJudge c268,這題表面上是要你找三個數字當三角形的邊長,可以
可以發現到若輸入的數字都不能構成三角形,那麼在數字都盡量小的情況下,把它們排序後會變成費氏數列,而輸字的最大值又是
現在排序的問題解決了,那輸入的問題呢?它是一個檔案多筆測資,所以也不能讀到
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 不會失效。