第 14 章 輸出與輸入
本章提要 14-1 甚麼是串流? 14-2 認識 C++ 串流類別庫 14-3 輸出與輸入的控制 14-4 檔案串流 14-5 綜合演練
14-1 甚麼是串流? 串流 (Stream) 就是資料流通的管道, 它就像是一個封閉的通道一樣, 有個起點和終點, 而資料則是由起點流經通道而傳至終點, 其關係如下圖所示: 當起點送出資料時, 就將資料加進 (Insert) 串流中, 而終點則會由串流中取出 (Extract) 資料來加以處理。
甚麼是串流? 有些串流本身具有緩衝區 (Buffer), 可以將送入的資料暫存起來, 直到緩衝區滿了、或收到某些控制字元 (如換行或檔案結束符號)、或當我們下達強制送出的命令時, 才一次將所有資料送給終點。另外, 串流也可以做一些格式化 (例如將數值轉為文字) 或過濾 (例如跳過空白字元) 的工作, 以減輕程式設計者的負擔。
甚麼是串流? 每一種串流類別可代表一種輸出、入的設備。在做輸出時, 起點就是變數 (或資料), 而終點則是串流類別的物件 (例如標準輸出)。我們也已學過, 多個起點可以用 << 串接起來, 例如:
甚麼是串流? 在做輸入時, 則起點是串流類別的物件 (如鍵盤、檔案等輸入), 而終點則是變數 (或物件)。各接收端也可以用 >> 串接起來, 以鍵盤輸入為例來說明:
甚麼是串流? 我們可以把 << 和 >> 想成是資料流動的方向, << 就是向左流入串流物件中, 而 >> 則是向右流入變數內。 所有的基本資料型別, 包括 char* (視為字串) 在內, 及以大多數 C++ 類別庫中的類別均可直接用 C++ 的串流做輸入或輸出。但自定的類別則必須多載 << 及 >> 運算子、或是定義將類別轉換成基本型別的函式, 才可以使用串流功 能
14-2 認識 C++ 串流類別庫 串流類別庫 內建的串流物件 轉向對標準輸出入的影響
串流類別庫 在 C++ 串流類別庫中有許多的類別及樣版, 在此我們簡單認識一下其中的重要類別。在這些串流類別中, 最上層的基礎類別就是 ios_base, 此類別定義了輸出入串流所共有的基本屬性及行為;接著由 ios_base 衍生出一 個 basic_ios 樣版, 其繼承架構如下:
串流類別庫
串流類別庫 basic_istream 和 basic_ostream 都是以虛擬繼承的方式由 basic_ios 所衍生出的樣板, 在標準類別庫中就用它們分別建立了輸入串流類別 istream 及輸出串流類別 ostream, 我們常用的標準輸入及輸出物件 cin 及 cout 就是由這兩個類別產生的。basic_iostream<> 樣版則是多重繼承 basic_istream<> 及 basic_ostream<>, 可用以產生同時可做輸入或輸出的類別 (例如檔案就可做為輸入的來源或輸出的目的)。
串流類別庫 不過除非是有特殊的需要, 否則一般不需自行由這些樣板產生新的類別來使用, 而可直接使用標準類別庫中已定義好的類別 (例如 istream 和 ostream), 更簡單一點, 則是直接使用已事先產生的內建物件來進行輸入與輸出。
內建的串流物件 我們慣用的 cin 、cout 只是內建串流物件的四分之一, 在 C++ 類別庫中預先建立的串流物件共有 8 個, 可分為 4 組, 這些物件的功能及所屬的類別如下表所示:(如下頁) 其中標準輸入裝置預設為鍵盤, 而標準輸出、標準錯誤、標準記錄則預設都是螢幕。 cin、cout 等都是代表這些標準裝置的物件, 因此我們可直接用它們來進行輸入與輸出。
內建的串流物件
內建的串流物件 以 'w' 開頭的類別即為使用前述的樣版產生類別時, 指定了以 wchar_t 為處理的資料型別, 換言之就是適用於 wchar_t 型別資料的輸入與輸出。然而實際上要使用這些 wxxx 的串流物件並不如 cin、cout 方便, 因為各系統實作國際化字集的方式並不相同, 甚至各家編譯器實作的方式都不同。例如在 Windows XP 上若要以 wcout 輸出雙位元組的中文字, 還需先呼叫 ios_base::imbue() 成員函式修改所用的語系, 請參考以下的例子:
內建的串流物件
內建的串流物件 如範例所示, 在第 6、11、13 行都要個別用 wcout、wcerr、wclog 物件呼叫 imbue() 來修改所用的語系, 才能用該串流物件輸出中文。若刪除這幾行敘述, 則在輸出時, 各串流物件會將程式中所要輸出的字串視為有錯誤, 而不輸出, 所以在螢幕上就看不到輸出結果。至於 imbue()、locale() 等函式的功用及意涵, 限於篇幅, 在此就不說明。
內建的串流物件 第 7、12、14 行的字面常數字串前面所加的大寫 L 表示其後的文字為 wchar_t 型別, 且不可使用小寫的 l 代替。 上例只是簡單示範使用 wXXX 串流物件的不方便性, 為方便起見, 我們仍以使用 cout、cin 等物件為主。此外這個範例也示範了標準錯誤及標準記錄串流預設也都是輸出到螢幕。
轉向對標準輸出入的影響 請者或許會覺得奇怪, 既然 cout、cerr、clog 都是輸出到螢幕上, 為什麼不只用一個來代表即可。其實這 3 個串流物件都輸出到螢幕是在個人電腦上的設定情形, 在不同的裝置上可能就有不同的結果;再者, 在包括 Windows、Unix、Linux 等作業系統下, 我們也能用轉向的方式, 將標準輸出及輸入轉向到其它的裝置, 例如檔案、印表機、或遠端的終端機等等。在這種情況下, cout 的輸出目的就會和 cerr 不同了, 以下簡單說明如何將標準輸出入轉向。
標準輸出的轉向 以 Windows XP 為例, 在命令提示字元視窗中, 我們可以用 "dir > test" 的方式, 使 dir 原本會顯示在螢幕上的資料夾資訊轉向存到 "test" 這個檔案中。(在 Unix/Linux 系統下也可用相同的轉向技巧, 例如 "ls > test")。所以假設我們寫好一個程 式 ABC.exe, 要讓它的標準輸出變成檔案 test, 只要執行 "abc > test", 就會使 abc 中輸出到標準輸出的內容, 寫到檔案 test 中。我們用以下的程式來測試:
標準輸出的轉向
標準輸出的轉向
標準輸出的轉向 由第 2 個執行結果可發現, 當我們用轉向的方式執行編譯好的程式時, 程式中第 9 行用 cout 輸出的內容就不會出現在螢幕上了, 而會被轉存到轉向的裝置 (上例中為 test.txt)。至於 cerr、clog 的輸出仍是出現在螢幕上, 所以當我們需要設計可以轉向輸出的程式, 但希望程式的示誤訊息仍輸出到螢幕上, 就可用 cerr 來輸出這些訊息。
標準輸入的轉向 標準輸入也可以轉向, 轉向的符號是 '<', 同樣以 Windows XP 命令提示字元為例, 若程式 abc.exe 需要使用者輸入一筆數字, 而檔案 test 中已存有該數字, 所以只要執行 "abc < test", abc 就能從 test 取得所需的數字並進行處理。 假如有個文字檔 five.txt, 其中只有一行文字 '5', 則我們要用它當前面範例程式 Ch14-02 的輸入, 可用如下的方式執行:
標準輸入的轉向 不過使用轉向的的方式來讀寫檔案只算是權宜之計, 如果要專門進行讀寫檔案, 則應使用 14-4 節的檔案串流。
14-3 輸出與輸入的控制 之前我們使用輸出入串流時, 都只很簡單地直接進行輸出與輸入, 其實 C++ 的串流類別提供了許多的輸出與輸入控制方法, 這些控制方法有些是透過函式呼叫, 有些則是透過像 endl 這樣的控制器 (Manipulator), 以下就先來認識什麼是控制器。
認識控制器 從第 2 章我們使用 cout 時, 就使用了 endl 讓 cout 做換行的動作, 而 endl 就是輸出串流的控制器 (Manipulator)。在串流類別中提供了許多可控制輸出內容及方式的控制器, 雖然其用法 (像是 "cout << endl") 讓我們覺得控制器好像是個變數或物件, 但其實所有的控制器都是串流類別的成員函式, 稍後我們也會提到需加上參數來呼叫的控制器。
認識控制器 有些控制器是產生一定的特殊效果, 例如 endl 會輸出一個換行字元 '\n' 並呼叫 flush() 函式, 這個函式會將串流中尚未輸出的內容立即送出;而有些控制器則是修改串流物件內部的狀態, 使後續的輸出入都會改以不同的方式輸出。而且一經設定後, 其效果會一直持續, 直到我們再用另一個控制器或串流類別的成員函式修改成另一狀態為止。
認識控制器 例如我們曾用過的 boolalpha 控制器, 可將 bool 型別的資料改以 “true”、“false” 的方式輸出。若程式稍後又想將 bool 型別的資料以預設 10 的方式輸出, 就必須再用 noboolalpha 控制器設回來:(如下頁)
認識控制器
認識控制器 如上所示, cout 預設會將布林資料以數字顯示, 但只要呼叫過一次 boolalpha 控制器, 它就會變成用字元的方式顯示布林資料。 此外由第 8 行程式的輸出結果也可發現, 每個串流物件都有各自的狀態, 所以第 7 行在 cout 上使用 boolalpha 控制器, 並不會影響 cerr, 所以 cerr 仍是以數字輸出布林資料, 要讓 cerr 也改成以文字輸出布林資料, 則需執行 "cerr << boolalpha;"。
輸出串流的格式控制 在輸出串流類別的格式控制當中, 大部份都是與數字的輸出格式有關, 例如是否顯示正負號、小數點後要顯示幾位數等等, 當然有部份也會影響非數字資料的輸出格式。
正負號與小數點 預設的狀態下, 只有負數會顯示負號、含小數的浮點數才會有小數點, 但我們可用以下的控制器來改變設定:
正負號與小數點
正負號與小數點 請注意, 對整數變數而言, 即使用 showpoint 也不會顯示小數點。除了控制有無正負號及小數點外, 我們還可用以下的方式控制輸出時的表示法及有效位數。
有效位數及浮點數表示法 使用串流物件輸出浮點數時, 串流物件會自行依實際的數值選用一般的表示法或科學符號表示法 (例如 3.14e+00), 而且預設會顯示的有效位數 (precision) 也只有 6 位數。我們可用下列的成員函式或控制器來調整這些設定:(以下粗體字者為成員函式、其餘為控制器)
有效位數及浮點數表示法 請注意, 使用 setprecision 控制器之前必須先含括 <iomanip> 含括檔後介紹的 setw()、setbase()、setfill() 等有參數的控制器, 也都必須含括 <iomanip> 才能使用), 其用法請參考以下範例:
有效位數及浮點數表示法
有效位數及浮點數表示法
有效位數及浮點數表示法 設定較大的有效位數後, showpoint 的效果就會出現了, 對 "1234.0" 這樣的數字, 就不再只顯示到整數部份為止, 而會多顯示幾個符合有效位數的 '0'。
數字系統 預設串流物件都是以十進位來輸入、輸出數字, 其實它們也可直接用來輸入或輸出 8 進位及 16 進位制的數字:
數字系統 請注意, 這些設定只對整數有效, 輸出或輸入浮點數時, 不管如何設定, 仍是採用十進位的數字系統, 以下是使用上列控制器的簡例:
數字系統
數字系統
數字系統 在 16 進位中, 分別是用英文字母的 A~F (大小寫均可) 代表 10~15 的數字, 所以第 9 行程式將 cin 使用 hex 控制器後, 即可輸入 abcd 這樣的數字, 由後面的輸出可發現 abcd 換算成十進位數字為 43981。
數字系統 第 15 行的 cin 雖未加上 hex 控制器, 但第 9 行的設定仍然有效, 因此此處仍將輸入視為 16 進位數字。而我們在執行結果中也故意用 8 進位的表示法輸入 "012", 結果發現 cin 並不會理會我們輸入時的表示法, 還是把 "012" 當成 16 進位數字, 換算成 10 進位則為 18。
欄位寬度與對齊方式 在前面的章節中, 我們輸出資料時, cout 總是依資料本身的寬度來輸出, 例如 5 位數的數值就一定只佔 5 個字元;若是 3 個字元的字串也只剛好用 3 個字元的空間來顯示。如果我們想讓每一筆不同寬度的資料, 輸出時都佔用相同寬度的欄位, 可用以下的成員函式或控制器來修改之:
欄位寬度與對齊方式 請注意, 上列的寬度設定的效果只能維持一次, 輸出下一筆資料時 (不管是串接或另一個敘述), 若未再使用 setw(), 則 cout 又會回復預設的方式輸出。此外若設定的寬度小於資料長度, 仍是以資料實際長度輸出:
欄位寬度與對齊方式 設定欄寬之後, 若欄寬遠大於資料長度, 預設輸出資料會向右對齊, 而多餘的部分則填上空白字元, 我們可用下列控制器及成員函式修改對齊方式及填補的字元:
欄位寬度與對齊方式 我們用以下的程式示範上述成員函式及控制器的用法:
欄位寬度 與對齊方式
欄位寬度與對齊方式 1. 第 7 行設定填充字元為 '*', 所以接下來 3 行由於資料長度小於欄位寬度, 所以多出的空位就會自動填上 '*'。 2. 第 12、14 行改用 width() 設定欄寬。 3. 第 16 行是用串接的方式輸出, 所以已不受先前的 width() 的影響, 所以其寬度和字串長度相同, 因此不會出現填充字元。
欄寬對輸入的影響 欄寬的設定也能用於輸入串流, 換句話說, 我們可用寬度設定來限制使用者可輸入的字數。但此時有一點要特別注意, 若使用者真的輸入了超過欄寬的資料內容, 此時 cin 將會在下次取得輸入時, 取得前次剩餘的資料, 請參考下面這個例子:
欄寬對輸入的影響
欄寬對輸入的影響 第 10 行設定欄寬為 5, 表示 cin 最多只取 5 個 字給 ss但在執行結果中, 我們故意輸入超過 5 個字的內容, 結果後面的 "national" 就自動成 為第 14 行 cin 的輸入內容, 因此我們還未輸入 任何文字, 程式就已取得輸入, 所以會接著執行 第 15 行的程式, 也就是輸 出 "national" 這幾 個字。
其它控制器 除了控制格式外, 還有幾個其它用途的控制器:
其它控制器 關於 flush 的效果要特別解釋一下:基本上我們要輸出到輸出串流的資料, 都會先存於一個系統緩衝區, 遇下列狀況, 緩衝區的內容才會真的輸出到輸出裝置上: 緩衝區已滿了。 程式要正常結束執行了。 程式呼叫 flush 控制器。 可做輸入及輸出的串流, 要從輸出狀態切換到輸入時。
其它控制器 請注意第 2 點, 是指程式正常結束執行, 若是程式是被意外中止執行, 例如當機、或被使用者強迫中止, 則可能輸出緩衝區仍有尚未寫到輸出裝置的資料, 因此如果是輸出到檔案, 將會導致資料喪失。
無格式化的輸出與輸入 前幾章曾用過 getline() 這個 istream 的成員函式來取得含空白的輸入字串, 在輸出與輸入串流中都有幾個特殊的成員函式, 使用這些函式時, 稱為無格式化 (unformatted) 的輸出與輸入, 因為用它們進行輸出與輸入時, 不能用前面介紹的控制器或成員函式控制格式。相對而言, 可控制格式的 <<、>> 輸出與輸入就稱為格式化的 (formatted) 輸出與輸入。
輸入串流的無格式化輸入 istream 類別提供的輸入函式可分為字元與字串兩部份, 以下先看字元的輸入函式: 和 "cin >>" 不同的是, 這二個函式可讀取到空白符號, 而且有參數的 get() 版本會傳回原呼叫的串流物件, 所以也可以串接使用, 甚至和 >> 混用, 例如:
輸入串流的無格式化輸入 字串的輸入函式則稍微複雜, 如下:
輸入串流的無格式化輸入 前兩個函式均可用來讀取含空白字元的字串, 並存入參數 str 中, 最多會讀取 (len-1) 個字元, 函式會再將一個字串結束字元 '\0' 放到 str 中;而 delim 則是表示讀到該字元時, 即使未達 len 的字數, 也停止讀取, 預設的 delim 為 '\n', 也就是換行時停止輸入, 所以若指定其它的字元為 delim 參數, 我們就能將多行文字存入同一字串中。read() 函式和前 2 個函式最大的不同是它最多會讀取 len 個字元, 且不會將字串結束字元放到 str 中。
輸入串流的無格式化輸入 此外 getline() 讀到 delim 字元時會將它自緩衝區移除;get() 則否, 所以如果未做其它處理, 重複在 cin 使用 get() 讀取時, 將會讀不到內容, 請參考下面這個例子:
輸入串流的無格式化輸入
輸入串流的無格式化輸入 第 9 行用 cin.getline(ss, 10, '$'); 所以會讀到 '$' 為止, 但只會取 9 個字元存到 ss 中 (外加字 串的結束字元 '\0')。所以輸出時只看到 "Good" 加換行字元'\n' 再加 "Morn"。
輸入串流的無格式化輸入 由於輸入 “Morning$” 時, 最後有個按鍵 Enter 的動作, 所以輸入緩衝區會留有一個換行字元 '\n', 接下來第 13、16 行的 get() 都會直接讀到這個換行字元, 而認定使用者沒有輸入, 所以程式只顯示兩行 " 請輸入一個字串:" 而未取得任何輸入資料。解決方式之一就是另外用讀取字元 的 get() 函式先讀入該換行字元, 或是呼叫 ignore() 成員函式忽略一個字元:
輸入串流的無格式化輸入 另外要注意一點:若程式中將格式化輸入 >> 與 getline() 混合使用, 例如先用 >> 運算子取得一項輸入, 接著又想用 getline() 讀取一個字串, 也會出現如上 '\n' 還留在緩衝區, 使 getline() 根本讀不到資料的問題。此時當然也可用上列將換行字元自緩衝區清除的方法。
輸出串流的無格式化輸出 ostream 的無格式化輸出成員函式有: write() 函式會持續輸出 str 中的字元, 直到指定的 count 字數為止, 就算 str 字串中間有字串結束字 元 '\0', 也不會中止其輸出。所以輸出字串的字數, 不可少於 count 值。
14-4 檔案串流 對於已熟悉 cin/cout 用法的讀者來說, 讀寫檔案並無什麼不同, 唯一要做的, 就是將原來用 的 cin/cout 換成檔案串流的物件, 其它用 >>、<< 輸入與輸出的方式都相同。 建立檔案串流物件 循序讀寫檔案 非循序讀寫檔案
建立檔案串流物件 在 C++ 中要讀寫檔案, 只要建立代表檔案的輸出入串流物件即可, 此時輸出到串流就是寫入檔案, 而讀取串流就是從檔案讀取資料了。要進行檔案讀寫, 首先要做的就是用內建的檔案串流類別建構檔案物件: ifstream 類別:使用 basic_ifstream 樣版建立的讀取檔案類別, basic_ifstream 樣版是由 basic_istream 樣版所衍生的, 因此 ifstream 物件的用法和 cin 類似。
建立檔案串流物件 ofstream 類別:使用 basic_ofstream 樣版建立的寫入檔案類別, basic_ofstream 樣版是由 basic_ostream 樣版所衍生的, 因此 ofstream 物件的用法和 cout 類似。 fstream 類別:使用 basic_fstream 樣版建立的可同時供讀取及寫入的檔案類別, basic_fstream 樣版是由 basic_iostream 樣版所衍生的, 因此 fstream 物件兼具 cin/cout 的輸入與輸出性質。
建立檔案串流物件 上列的檔案串流類別及樣版都宣告於 <fstream> 之中, 因此以下的程式都會先含括這個檔案。 依據您要做的動作選好適用的類別後, 即可建構物件並開啟檔案來進行讀寫。我們可在建構物件時即指定檔案名稱及路徑;也可先只宣告物件, 稍後再以檔案名稱及路徑呼叫物件的 open() 函式開啟檔案:
建立檔案串流物件
建立檔案串流物件
建立檔案串流物件 1. 第 7 行是用 ifstream 類別建立檔案串流物件, 且在建立時即以常數字串指定檔案名稱。 2. 第 8 行是用 fstream 類別建立可讀可寫的檔案串流物件 file2 (雖然程式中仍只用它做讀取動作), 且未指定檔名, 而是在第 9 行才用 open 成員函式開啟指定的檔案。 3. 第 11 行用 ! 運算子判斷檔案是否開啟, C++ 串流類別多載了 ! 運算子, 可用以判斷串流物件的狀態, 當串流物件有問題時, 用 ! 運算子就會傳回 true。
建立檔案串流物件 若先前開啟檔案的動作出現找不到檔案或檔案被別的程式佔用的狀況, 我們的程式就無法成功開啟檔案, 用 ! 運算子就會傳回 true, 因此會執行第 12 行的敘述輸出相關的訊息。 4. 第 15 行用 getline() 讀取一整行內容, 由於檔案一開啟時, 預設都是從頭開始讀取, 所以此時就會讀到第 1 行的內容;讀入後隨即在第 16 行輸出到 cout。
建立檔案串流物件 5. 第 18、19 行則是連續讀 入 file2 的前 2 行內容, 並在第 20 行輸出第 2 次讀到的內容。 檔案的讀取就是這麼簡單, 除了改用檔案串流外, 其它好像都和使用標準輸出入差不多, 但其實讀寫檔案時, 還是有些地方與使用標準輸出入不同, 舉例來說讀取檔案會碰到檔案結尾 (標準輸入可沒有鍵盤結尾), 這時就要做特別的處理。
循序讀寫檔案 剛剛提到, 開啟檔案時, 預設都是從頭開始讀或寫, 並一直循序讀寫到後面, 因此這種讀寫方式稱為循序讀寫。 檔案開啟方式 檔案結束 二元檔的讀寫
檔案開啟方式 fstream 類別雖然是用於可讀且可寫的狀況, 但其實我們可在建構函式及 open() 成員函式中指定以下旗標 (flag) 為參數, 以唯讀或其它方式開啟檔案, 而 ofstream、ifstream 也可透過這些旗標以多種方式開啟檔案。這些旗標都宣告於 ios_base 類別中, 所以使用時要加上 ios_base:: 的標示:
檔案開啟方式
檔案開啟方式 上述旗標可利用 '|' 運算子組合使用, 但要注意互斥或不合理的用法: 以下範例示範幾個旗標的應用及效果:
檔案開啟方式
檔案開啟方式
檔案開啟方式 1. 第 9 行用 ios_base::out 以寫入模式開啟檔案。在檔名字串中可加入路徑, 但要記得第 3 章提過, 在 C++ 程式中 '\' 代表 Escape Sequence 字元開頭, 所以要表示 '\' 符號時, 需寫成 "\\"。 2. 第 13 行寫入一字串後, 在第 14 行用 close() 成員函式關閉檔案。已關閉的檔案就不能再供讀寫, 必需再用 open() 開啟後才能讀寫之。
檔案開啟方式 程式執行完畢後, 您可用文字編輯器開啟 "c:\test.txt", 即可看到程式所寫入的內容: 3. 第 18 行用 ios_base::trunc 以附加的方式開啟檔案, 所以再寫入的內容, 不會蓋掉檔案原有的內容。 程式執行完畢後, 您可用文字編輯器開啟 "c:\test.txt", 即可看到程式所寫入的內容:
檔案結束 讀取檔案串流時, 一旦讀到檔案結尾時, 就無法再取得輸入, 此時程式就必須自行停止讀取檔案, 並做後續處理。我們可用檔案串流物件呼叫 eof() 成員函式檢查 (End Of File, 意指檔案結尾), 若函式傳回 true 即表示已讀到檔案結尾。以下範例就使用 eof() 檢查是否已讀到檔案結尾:
檔案結束
檔案結束
檔案結束 1. 第 8~10 行讓使用者輸入一個檔案名稱, 並存於 string 物件中。 2. 第 12 行建構檔案輸入串流物件 file 時, 用字串物件呼叫 c_str() 成員函式將字串轉成 char* 型別的字串。
檔案結束 3. 第 17 行 while 迴圈的條件式中, 因 file 物件呼叫 get() 取得一個字元時, 函式會傳回 istream& 參考型別, 所以可再用傳回值呼叫 eof() 檢查是否已讀到檔案結尾。是就結束迴圈, 否就執行第 18 行的敘述, 將讀到的字元送往標準輸出。 因為是用迴圈每讀一個字元就由 cout 輸出一個字元, 而迴圈會一直到 eof() 傳回 true 時才停止, 所以這個迴圈將會由 cout 輸出整個檔案的內容。
二元檔的讀寫 以上介紹的檔案讀寫方式, 都是以文字檔的形式進行讀寫, 但對電腦程式來說, 使用二元檔 (binary file, 或稱二進位檔) 就可以了, 例如執行檔、圖形檔、影片檔等, 都是以二元檔的形式存於電腦中。
二元檔的讀寫 以 123456 為例, 採文字檔的形式儲存, 每個數字都要個別存成一個字元, 就相當於存了一個字元陣列的內容是 “123456” 共六個字元;但如果是以二元檔的格式, 就相當於將之視為一個整數來存放, 以 4 位元組的 int 為例, 其 4 個位元組的值是 “00 01 E2 40”, 如此一來還比文字檔節省了 2 個位元組的空間, 若數值更大, 節省的空間更多。但如果我們看到這樣的檔案內容, 將無法理解它們是什麼意思, 所以說二元檔是給程式 (電腦) 看的檔案。
二元檔的讀寫 要讀寫二元檔, 就是在開檔時指定以 ios_base::binary 的方式開啟即可。其次, 對二元檔案, 我們不能使用 <<、>> 運算子, 因為它們是做格式化的輸出 /, 對二元檔, 必須以 read()、write() 這類非格式化的輸出入方式來進行:
二元檔的讀寫 雖然上列函式中用來表示要讀取或寫入的參數是 char* 型別, 但只要利用如下方式即可進行其它型別的資料讀取或寫入 (以下為寫入的例子): 請參考以下範例:
二元檔的讀寫
二元檔的讀寫
二元檔的讀寫
二元檔的讀寫 這個程式的內容可分為 2 部份, 第 12~22 行是開啟二元檔, 並寫入 1~10 立方的浮點數值;第二部份在第 24~35 行, 此處以唯讀方式開啟二元檔, 讀取十個 double 數值並輸出到 cout。如果我們用文書編輯器開啟程式所寫入的檔案 (上例中為 cubic.bin), 只會看到一些不知所云的內容, 並不會看到像 1、8、27… 這樣的數字, 因為這些數值都以二元的形式儲存, 而不是以文字的形式儲存。
非循序讀寫檔案 前面說過, 開啟檔案時, 預設都是從檔案開頭讀取或寫入, 這是因為在檔案串流中, 會記錄目前要讀取或寫入的位置 (position), 以一般方式開啟檔案時, 這個位置就是檔案開頭;以 ios_base::ate、ios_base::app 模式開檔, 則會將讀寫位置設定到最後面。
非循序讀寫檔案
非循序讀寫檔案 採用此種循序式來讀寫檔案時, 有一個缺點:假設檔案非常大, 要讀取或修改檔案中後方位置的一筆資料, 必須耗費一些時間跳過前面的資料後, 才能將讀寫位置指到所需的資料。 如果不想用原本循序的方式將檔案從頭讀到尾, 就必須用檔案串流物件呼叫相關成員函式改變檔案讀寫的位置, 如此就能任意讀寫檔案中的任何位置, 此種非循序的讀寫方式也稱為隨機存取 (random access)。
非循序讀寫檔案 與讀寫位置相關的成員函式包括:
非循序讀寫檔案 其中讀取位置的函式其名稱都是以 g (get) 結尾;而寫入位置的函式則以 p (put) 結尾。單一個參數的 seekX() 函式都是以檔案開頭開始計算的絕對位置;而 2 個參數的版本則是設定相對位置, 代表相對位置參考點的參數 rpos 可設為以下幾個常數:
非循序讀寫檔案 指定相對位置時, 若是正值表示是向後, 若是負值則是向前, 例如:
非循序讀寫檔案
非循序讀寫檔案
非循序讀寫檔案
非循序讀寫檔案 這個範例是針對範例程式 Ch14-13 所產生的二元檔而設的, Ch14-13 寫入的檔案內容為 10 個 double 變數, 而我們在第 19 行及第 26 行移動串流的讀取位置, 分別移到第 6 及第 9 個 double 變數的開頭, 讓程式能直接讀到這 2 筆數值。
非循序讀寫檔案 程式在每次移動讀取位置及讀取一 個 double 數值時都顯示 tellg() 傳回的目前位置, 由輸出結果可知每讀一次 double, 讀取位置就會向後移 8 (個位元組), 所以連續讀取就會讀到下一筆 double 的數值。
14-5 綜合演練 計算檔案中英文字母個數 用檔案存放電話通訊錄
計算檔案中英文字母個數 我們可以利用 get() 函式逐字元讀取檔案, 然後檢視讀入的字元為何, 即可計算檔案中各英文字母的數量。
計算檔案中英文字母個數
計算檔案中英文字母個數
計算檔案中英文字母個數 1. 第 20 行 while 迴圈也是用與範例 Ch14-12.cpp 相同的方式讀取整個檔案中的所有字元。 2. 第 21 行, 判斷讀取的字元是否為大寫英文字母, 是就將其值減 65 (大寫英文字母的字碼為 65 至 90), 然後用該值為索引, 將對應的 int 陣列元素值遞增。
計算檔案中英文字母個數 3. 第 23 行, 判斷讀取的字元是否為小寫英文字母, 是就將其值 減 97 (小寫英文字母的字碼為 97 至 112), 然後用該值為索引, 將對應的 int 陣列元素值遞增。 4. 第 28 行的 for 迴圈則將統計結果輸出到標準輸出。
用檔案存放電話通訊錄 利用檔案串流可將含有人名與電話號碼的資料紀錄在一個檔案中, 之後即可利用程式來搜尋特定對象的電話號碼。我們將輸入資料及查詢資料的功能放在同一程式中, 使用者可選擇是要輸入新資料或查詢資料, 以下範例是以一個通訊錄項目類別來表示姓名 / 電話號碼的資訊, 並多 載 <<、>> 運算子設計將物件寫入檔案及由檔案讀出的行為。
用檔案存放電話通訊錄
用檔案存放電話通訊錄
用檔案存放電話通訊錄
用檔案存放電話通訊錄
用檔案存放電話通訊錄
用檔案存放電話通訊錄
用檔案存放電話通訊錄
用檔案存放電話通訊錄 1. 第 6~16 行為代表通訊錄項目的類別, 其中用 string 物件來存放姓名及電話。 2. 第 18~25 行是請使用者輸入新的通訊錄項目的函式, 因為類別中是用 string 物件存放資料, 所以此處用的是 <string> 的 getline() 函式由 cin 取得輸入資料。 3. 第 27~30 行是為多載的 << 運算子, 函式中依序將通訊錄項目中的姓名、逗號、電話及換行字元寫入串流物件。
用檔案存放電話通訊錄 4. 第 32~37 行是為多載的 >> 運算子, 此處仍是用 <string> 的 getline() 函式由檔案串流取得輸入, 並依據寫入時的格式, 分別指定逗號、換行字元為讀取結束的符號。 5. 第 39~46 行的 addone() 函式會呼叫 Entry 類別的 keyin() 成員函式請使用者輸入一筆通訊錄資料, 並將寫入位置移到檔案最後, 再將資料寫入檔案。
用檔案存放電話通訊錄 6. 第 48~66 行為查詢的函式, 函式一開始先請使用者輸入要尋找的姓名, 接著用迴圈逐一讀入檔案中的記錄進行比對, 若找到相符的名稱 (請注意 string 類別的 == 比較會分辨大小寫), 即輸出其電話號碼。 7. 第 64 行呼叫的 clear() 成員函式會清除串流物件的狀態, 因為若之前 while 迴圈一直讀到檔尾都沒找到資料, 將會使串流物件 的 eof() 一直保持為 true (基於執行效率問題, seekg() 並不會清除 eof() 的狀態), 使得下次再進入函式尋找資料時, 根本不會進入迴圈。
用檔案存放電話通訊錄 8. 第 68~85 行為主程式 main() 的部份, 其中先建立 fstream 物件並開啟檔案, 開啟模式設為讀取 / 寫入, 以供稍後查詢或寫入新資料。 9. 在開檔成功後, 第 75~82 行以 do-while 迴圈請使用者選擇要輸入新資料或進行查詢, 並用 swith-case 判斷要呼叫前面的 addone() 函式新增一筆資料、或呼叫用來查詢的 lookup() 函式。