# [15] 通過 `<iostream>` 和 `<cstdio>`輸入/輸出
## FAQs in section [15]:
* [15.1] 為什么應該用 `<iostream>` 而不是傳統的 `<cstdio>`?
* [15.2] 當鍵入非法字符時,為何我的程序進入死循環?
* [15.3] 那個古怪的`while?(std::cin?>>?foo)`語法如何工作?
* [15.4] 為什么我的輸入處理會超過文件末尾?
* [15.5] 為什么我的程序在第一個循環后,會忽略輸入請求呢?
* [15.6] 如何為 `class` `Fred` 提供打印?
* [15.7] 但我可以總是使用 `printOn()` 方法而不是一個友元函數嗎?
* [15.8] 如何為 `class` `Fred` 供輸入?
* [15.9] 如何為完整繼承層次的類提供打印?
* [15.10] 在DOS 和/或 OS/2環境下,如何以二進制模式“重打開” `std::cin` 和 `std::cout` ?
* [15.11] 為何我不能在如“`..\test.dat`”這樣的不同的目錄中打開文件?
* [15.12] 如何將一個值(如,一個數字)轉換為 `std::string`?
* [15.13] 如何將 `std::string` 轉換為數值?
## 15.1 為什么應該 `<iostream>` 而不是傳統的 `<cstdio>`?
因為`<iostream>`加強了類型安全,減少了錯誤,提升了性能,可擴展,并且提供繼承。
`printf()` 不錯,`scanf()` 不管其可能導致錯誤,也還是有價值的,然而對于 C++ I/O(譯注:I/O即輸入/輸出) 所能做的來說,它們的功能都是非常有限的。相對于C (使用 `printf()` 和 `scanf()`)來說,C++ I/O (使用 `<<` 和 `>>`)是:
* _更好的類型安全:_使用 `<iostream>`,編譯器靜態地知道被 I/O 的對象的類型。相反,`<cstdio>`使用“`%`”域來動態地指出類型。
* _更少的錯誤傾向:_使用 `<iostream>,`沒有多余的必須與實際被 I/O的對象相一致的“`%`”。去除多余的,意味著去除了一類錯誤。
* _可擴展:_C++ `<iostream>` 機制允許在不破壞現有代碼的情況下,新的用戶定義類型能夠被I/O。(可以想象一下,每個人同事添加新的不相容的“`%`” 域到`printf()` 和 `scanf()`,是怎樣的混亂場面?!)。
* _可繼承:_C++ `<iostream>` 機制是建立在真正的類上的,如 `std::ostream` 和 `std::istream`。不象 `<cstdio>`的 `FILE*`,有真正的類,因此可繼承。這意味著你可以你可以擁有其他的用戶定義的看上去以及其效果都類似流的東西,而它可以做任何你需要的奇怪的和有趣的事情。你將自動的得到無數行的你所不認識的用戶寫的 I/O代碼,并且,他們不需要認識你寫的“extended stream”類。
## 15.2 當鍵入非法字符時,為何我的程序進入死循環?
舉個例子,假設你有如下的代碼,從`std::cin`讀取一個整數:
```
?#include?<iostream>
?int?main()
?{
???std::cout?<<?"Enter?numbers?separated?by?whitespace?(use?-1?to?quit):?";
???int?i?=?0;
???while?(i?!=?-1)?{
?????std::cin?>>?i;????????//?不良的形式?—?見如下注釋
?????std::cout?<<?"You?entered?"?<<?i?<<?'\n';
???}
?}
```
該程序沒有檢查鍵入的是否是合法字符。尤其是,如果某人鍵入的不是整數(如“x”),`std::cin`流進入“失敗狀態”,并且其后所有的輸入嘗試都不作任何事情而立即返回。換句話說,程序進入了死循環;如果`42`是最后成功讀到的數字,程序會反復打印“`You?entered?42`”消息。
檢查合法輸入的一個簡單方法是將輸入請求從 `while` 循環體中移到 `while` 循環的控制表達式,如:
```
?#include?<iostream>
?int?main()
?{
???std::cout?<<?"Enter?a?number,?or?-1?to?quit:?";
???int?i?=?0;
???while?(std::cin?>>?i)?{????//?良好的形式
?????if?(i?==?-1)?break;
?????std::cout?<<?"You?entered?"?<<?i?<<?'\n';
???}
?}
```
這樣的結果就是當你敲擊end-of-file,或鍵入一個非整數,或鍵入 `-1`時, while 循環會退出。
(自然,你也可以不用`break`,而將`while`循環表達式`while?(std::cin?>>?i)`改為`((std::cin?>>?i)?&&?(i?!=?-1))`,但這不是本FAQ的重點,本 FAQ 處理iostream,而不是一般的結構化編程指南。)
## 15.3 那個古怪的`while?(std::cin?>>?foo)`語法如何工作?
“古怪的 `while?(std::cin?>>?foo)`語法”的例子見前一個FAQ。
`(std::cin?>>?foo)`表達式調用了適當的`operator>>`(例如,它調用了左邊帶有`std::istream`參數以及,如果的類型是`int`,并且右邊有一個`int&`的`operator>>`)。`std::istream` `operator>>`函數按慣例地返回左邊的參數,在這里,它返回`std::cin`。下一步編譯器注意到返回的 `std::istream`處于一個布爾型的上下文中,因此編譯器將`std::istream`轉換為一個布爾值。
編譯器調用一個稱為`std::istream::operator?void*()`的成員函數來將`std::istream`轉換成布爾。它返回一個被轉換成布爾的`void*`指針(`NULL`成為`false`,任何其他的指針成為`true`)。因此在這里,編譯器產生了`std::cin.operator?void*()`的調用,就如同你象`(void*)?std::cin`這樣顯式地強制類型轉換。
如果stream處于良好狀態,那么轉換算符`operator?void*()`返回非指針,如果處于失敗狀態,則返回 `NULL`。例如,如果讀了太多次(也就是說,已經處于end-of-file),或實際輸入到流的信息不是`foo`的合法類型(如,如果 `foo`是一個`int`,而數據是一個“x”字符),流會進入失敗狀態并且轉換算符會返回`NULL`。
`operator>>`不是簡單地返回一個`bool`(或 `void*`)以支出是否成功或失敗的原因是為了支持“級聯”語法:
```
std::cin?>>?foo?>>?bar;
```
`operator>>`是向左結合的,意味著如上的代碼會解釋為:
```
(std::cin?>>?foo)?>>?bar;
```
換句話說,如果我們將`operator>>`變為一個普通的函數名稱,如`readFrom()`,將變為這樣的表達式:
```
readFrom(?readFrom(std::cin,?foo),?bar);
```
我們總是從最內部開始計算表達式。因為 `operator>>`的左結合性,就成了最左邊表達式`std::cin?>>?foo`。該表達式返回`std::cin` (更合適的,他返回一個它左邊參數的引用)給下一個表達式。下一個表達式也返回(一個引用)給`std::cin`,但第二個引用被忽略了,因為它是這個“表達式語句”的最外邊的表達式了。
## 15.4 為何我的輸入處理會超過文件末尾?
因為只有在試圖超過文件末尾后,eof標記才會被設置。也就是,在從文件讀最后一個字節時,還沒有設置 eof 標記。例如,假設輸入流映射到鍵盤——在這種情況下,理論上來說,C++庫不可能預知到用戶所鍵入的字符是否是最后一個字符。
如,如下的代碼對于計數器 `i` 會有“超出 1”的錯誤:
```
?int?i?=?0;
?while?(!?std::cin.eof())?{???//?錯誤!(不可靠)
???std::cin?>>?x;
???++i;
???//?Work?with?x?...
?}
```
你實際需要的是:
```
?int?i?=?0;
?while?(std::cin?>>?x)?{??????//?正確!(可靠)
???++i;
???//?Work?with?x?...
?}
```
## 15.5 為什么我的程序在第一個循環后,會忽略輸入請求呢?
因為數字的提取器將非數字留在了輸入緩沖器之后。
如果你的代碼看上去象這樣:
```
?char?name[1000];
?int?age;
?for?(;;)?{
???std::cout?<<?"Name:?";
???std::cin?>>?name;
???std::cout?<<?"Age:?";
???std::cin?>>?age;
?}
```
而你實際需要的是:
```
?for?(;;)?{
???std::cout?<<?"Name:?";
???std::cin?>>?name;
???std::cout?<<?"Age:?";
???std::cin?>>?age;
???std::cin.ignore(INT_MAX,?'\n');
?}
```
當然,你也許想將`for?(;;)`語句變為`while?(std::cin)`,但不要搞錯,在循環末尾通過`std::cin.ignore(...);`這一行跳過非數字字符。
## 15.6 如何為`class` `Fred`提供打印?
用算符重載提供一個友元的左切換的算符 `operator<<`。
```
?#include?<iostream>
?class?Fred?{
?public:
???friend?std::ostream&?operator<<?(std::ostream&?o,?const?Fred&?fred);
???//?...
?private:
???int?i_;????//?只是為了說明
?};
?std::ostream&?operator<<?(std::ostream&?o,?const?Fred&?fred)
?{
???return?o?<<?fred.i_;
?}
?int?main()
?{
???Fred?f;
???std::cout?<<?"My?Fred?object:?"?<<?f?<<?"\n";
?}
```
由于 `Fred` 對象是 `<<` 算符的右邊的操作數,我們使用非成員函數(在這里是一個友元)。如果 `Fred` 對象被期望為在`<<`的左邊(那就是 `myFred?<<?std::cout`而不是 `std::cout?<<?myFred`),則就會有一個命名為`operator<<`的成員函數。
注意,`operator<<`返回流。這就使得輸出算符能夠被級聯。
## 15.7 但我可以總是使用 `printOn()` 方法而不是一個友元函數嗎?
不。
通常人們_總是_愿意使用`printOn()`方法而不是一個友元函數的原因是因為他們錯誤地相信友元破壞了封裝并且/或者友元是不良的。這些信仰是天真的和錯誤的:適當的使用,友元實際上可以增強封裝。
這也不是說`printOn()` 方法沒用。例如:為一個完整的繼承層次的類提供打印時就是有用的。但如果你看到一個`printOn()` 方法,它通常應該是`protected`的,而不是`public`的。
為完整,這里給出“`printOn()` 方法”。想法是有一個成員函數(通常被稱為`printOn()`,來完成實際的打印,然后有一個`operator<<`來調用`rintOn()`方法)。當錯誤地完成它時,`printOn()`方法是`public` 的,因此`operator<<`不需要成為友元——它成為一個簡單的頂級函數,即不是類的友元,也不是類的成員函數。這是一些示例代碼:
```
?#include?<iostream>
?class?Fred?{
?public:
???void?printOn(std::ostream&?o)?const;
???//?...
?};
?//?operator<<?可以被聲明為非友元?[不推薦!]
?std::ostream&?operator<<?(std::ostream&?o,?const?Fred&?fred);
?//?實際打印由內部的?printOn()?方法完成?[不推薦!]
?void?Fred::printOn(std::ostream&?o)?const
?{
???//?...
?}
?//?operator<<?調用?printOn()?[不推薦!]
?std::ostream&?operator<<?(std::ostream&?o,?const?Fred&?fred)
?{
???fred.printOn(o);
???return?o;
?}
```
人們錯誤地假定“由于避免了出現一個友元函數”而減少了維護成本。這個假定是錯誤的,因為:
1. **在維護成本上,“頂級函數調用成員”方法不會帶來任何好處。**我們假設 _N_ 行代碼來完成實際的打印。在使用友元函數的情況下,那 _N_ 行代碼將直接訪問類的 `private`/`protected` 部分,這意味著某人無論何時改變了類的 `private`/`protected` 部分,那 _N_ 行代碼將需要被掃描并且可能被修改,這增加了維護成本。然而,使用 `printOn()` 方法并沒有改變:我們仍然有 _N_ 行代碼直接訪問類的 `private`/`protected` 部分。因此將代碼從友元函數移到成員函數根本就并不減少維護成本。沒有減少。在維護成本上沒有好處。(如果有的話,`printOn()`方法更差一點,因為你有了一個額外的原先沒有的函數,現在有更多行的代碼需要被維護)
2. **“頂級函數調用成員”方法使得類更難被使用,尤其是程序員不是類的設計者時。**這種方法將一個并不期望被調用的`public`方法暴露給程序員。當程序員閱讀類的`public`方法時,他們會看見兩種方法做同一件事情。文檔需要象這樣說明:“這個和那個并不完全一樣,但不要用這個;而應該用那個”。并且通常的程序員會說:“唔?如果我不應該使用它,為什么它是 `public`的?”事實上`printOn()`方法是`public`的唯一理由是避免將友元授權給 `operator<<`,這個主張對于某些僅僅想使用這個類的程序員來說,是微妙的并且難以理解的。
總之,“頂級函數調用成員”方法有成本,沒有收益。因此,通常,不是好主意。
注意:如果 `printOn()`方法是`protected`或`private`的,第二個異議將不成立。有些情況這方法是合理的,如為一個完整的繼承層次的類提供打印時。同樣要注意,當`printOn()`方法是非`public`的時, `operator<<` 需要成為友元。
## 15.8 如何為 `class` `Fred`提供輸入?
使用算符重載](operator-overloading.html)提供一個友元的右切換的算符`operator>>`。除了參數沒有一個`const`:“`Fred&`”而不是“`const?Fred&`”,其他和[輸出算符類似。
```
?#include?<iostream>
?class?Fred?{
?public:
???friend?std::istream&?operator>>?(std::istream&?i,?Fred&?fred);
???//?...
?private:
???int?i_;????//?只是為了說明
?};
?std::istream&?operator>>?(std::istream&?i,?Fred&?fred)
?{
???return?i?>>?fred.i_;
?}
?int?main()
?{
???Fred?f;
???std::cout?<<?"Enter?a?Fred?object:?";
???std::cin?>>?f;
???//?...
?}
```
注意`operator>>`返回流。這就使得輸入算符能被級聯和/或在循環或語句中使用。
## 15.9 如何為完整繼承層次的類提供打印?
提供一個友元調用一個`protected` `virtual`函數:
```
?class?Base?{
?public:
???friend?std::ostream&?operator<<?(std::ostream&?o,?const?Base&?b);
???//?...
?protected:
???virtual?void?printOn(std::ostream&?o)?const;
?};
?inline?std::ostream&?operator<<?(std::ostream&?o,?const?Base&?b)
?{
???b.printOn(o);
???return?o;
?}
?class?Derived?:?public?Base?{
?protected:
???virtual?void?printOn(std::ostream&?o)?const;
?};
```
最終結果是`operator<<` 就象是動態綁定,即使它是一個友元函數。這被稱為“虛友元函數用法”。
注意派生類重寫了`printOn(std::ostream&)` `const`。尤其是,它們不提供他們自己的 `operator<<`。
自然的,如果 `Base`是一個ABC(抽象基類),`Base::printOn(std::ostream&)` `const`可以用“`=?0`”語法被聲明為純虛函數。
## 15.10 在DOS 和/或 OS/2環境下,如何以二進制模式“重打開” `std::cin` 和 `std::cout` ?
這依賴于實現,請查看你的編譯器的文檔。
例如,假設你想使用`std::cin`和 `std::cout`進行二進制 I/O。更假設你的操作系統(如DOS 或 OS/2)堅持將從`std::cin`輸入的“`\r\n`”翻譯為“`\n`”,將從 `std::cout`或`std::cerr`輸出的“`\n`”翻譯為“`\r\n`”。
不行的是沒有標準方法使得`std::cin`,`std::cout`和/或`std::cerr`以二進制模式被打開。關閉流并且試圖以二進制方式重打開它們,可能會得到非期望的或不合需要的結果。
在系統的區別處,實現可能提供了一種方法使它們成為二進制流,但你必須查看手冊來找到。
## 15.11 為何我不能在如“`..\test.dat`”這樣的不同的目錄打開文件?
因為“`\t`”是一個 tab 字符。
你應該在文件中使用正斜杠,即使在使用反斜杠的操作系統,如 DOS, Windows, OS/2等中。例如:
```
?#include?<iostream>
?#include?<fstream>
?int?main()
?{
???#if?1
?????std::ifstream?file("../test.dat");??_//?正確!_
???#else
?????std::ifstream?file("..\test.dat");??_//?錯誤!_
???#endif
???_//?..._
?}
```
記住,反斜杠(“`\`”)被用來在字符串中建立特殊字符:“`\n`”是換行,“`\b`”是退格,以及“`\t`”是一個tab,“`\a`”是一個警告(alert),“`\v`”是一個vertical-tab等。因此文件名“`\version\next\alpha\beta\test.dat`”被解釋為一堆有區的字符;應該用“`/version/next/alpha/beta/test.dat`”來替代,即使系統中使用“`\`”作為目錄分隔符,如DOS, Windows, OS/2等。這是因為操作系統中的庫例程是可交換地處理“/”和“\”的。
## 15.12 如何將一個值(如,一個數字)轉換為 `std::string`?
有兩種方法:可以使用`<stdio>`工具或`<iostream>`庫。通常,你應該使用`<iostream>`庫。
`<iostream>` 庫允許你使用如下的語法(轉換一個`double`的示例,但你可以替換美妙的多的任何使用`<<`算符的東西)將任何美妙得多的東西轉換為 `std::string`:
```
?#include?<iostream>
?#include?<sstream>
?#include?<string>
?std::string?convertToString(double?x)
?{
???std::ostringstream?o;
???if?(o?<<?x)
?????return?o.str();
???//?這兒進行一些錯誤處理...
???return?"conversion?error";
?}
```
`std::ostringstream` 對象 `o` 提供了類似`std::cout`提供的格式化工具。你可以使用操縱器和格式化標志來控制格式化的結果,就如同你用`std::cout`可以做到的。
在這個例子中,我們通過被重載了的插入運算符`<<`,將 `x` _插入_到 `o`。它調用了iostream的格式化工具將 `x` 轉換為一個`std::string`。 `if` 測試保證轉換正確工作——對于內建/固有類型,總是成功的,但 `if` 測試是良好的風格。
表達式`os.str()`返回包含了被插入到流 `o` 中的任何東西的`std::string` ,在這里,是 x 的值的字符串。
## 15.13 如何將 `std::string` 轉換為數值?
有兩種方法:可以使用`<stdio>`工具或`<iostream>`庫。通常,你應該使用`<iostream>`庫。
`<iostream>` 庫允許你使用如下的語法(轉換一個 `double`的示例,但你可以替換美妙的多的任何能使用 `>>` 算符被讀取的東西)將一個`std::string`轉換為美妙得多的任何東西:
```
?#include?<iostream>
?#include?<sstream>
?#include?<string>
?double?convertFromString(const?std::string&?s)
?{
???std::istringstream?i(s);
???double?x;
???if?(i?>>?x)
?????return?x;
???//?這兒進行一些錯誤處理...
???return?0.0;
?}
```
`std::istringstream` 對象 `i` 提供了類似`std::cin`提供的格式化工具。你可以使用操縱器和格式化標志來控制格式化的結果,就如同你用`std::cin`能做到的。
在這個示例中,我們傳遞了`td::string` `s`來初始化`std::istringstream` `i` (例如,`s` 可能是字符串“`123.456`”),然后我們通過被重載了的抽取運算符 `>>`,將 `i` _抽取_到 `x`。它調用了iostream的格式化工具對字符串進行盡可能的/適當的基于 x 的類型的轉換。
`if` 測試保證了轉換正確地工作。例如,如果字符串包含不適合 x 類型的字符,`if` 測試將失敗。
- C++ FAQ Lite
- [1] 復制許可
- [2] 在線站點分發本文檔
- [3] C++-FAQ-Book 與 C++-FAQ-Lite
- [6] 綜述
- [7] 類和對象
- [8] 引用
- [9] 內聯函數
- [10] 構造函數
- [11] 析構函數
- [12] 賦值算符
- [13] 運算符重載
- [14] 友元
- [15] 通過 &lt;iostream&gt; 和 &lt;cstdio&gt;輸入/輸出
- [16] 自由存儲(Freestore)管理
- [17] 異常和錯誤處理
- [18] const正確性
- [19] 繼承 — 基礎
- [20] 繼承 — 虛函數
- [21] 繼承 — 適當的繼承和可置換性
- [22] 繼承 — 抽象基類(ABCs)
- [23] 繼承 — 你所不知道的
- [24] 繼承 — 私有繼承和保護繼承
- [27] 編碼規范
- [28] 學習OO/C++
- [31] 引用與值的語義
- [32] 如何混合C和C++編程
- [33] 成員函數指針
- [35] 模板 ?
- [36] 序列化與反序列化
- [37] 類庫