I/O 資料讀寫

tags: C++

點此回到 C++筆記 目錄

I/O 格式控制器

在C語言中可以使用printf來指定輸出的格式,或是在python中也可以使用format來指定輸出格式。 然而我們之前使用cout來輸出資訊時,卻都沒談到輸出的格式控制,其實只要透過幾種基本方式,我們也可以指定輸出的格式,而I/O格式控制器就是其中的一種方式。

I/O格式控制氣勢個特殊的函式,在C++中常見的 endl 就是I/O格式控制器的一種,它會輸出換行字元並清空串流,注意,一個I/O格式控制器只影響目前處理的串流。

I/O格式控制器也可以指定參數,如果要使用具參數的I/O格式控制器,必須指定 iomanip 標頭檔案。 先來看個基本的例子,了解I/O格式控制器的作用與使用方式:

#include <iostream> #include <iomanip> using namespace std; int main() { cout << oct << 50 << endl // 8 進位顯示 << hex << 50 << endl; // 16 進位顯示 // 九九乘法表 for(int j = 1; j < 10; j++) { for(int i = 2; i < 10; i++) { cout << i << "*" << j << "=" << dec << setw(2) << (i * j); cout << " "; } cout << endl; } return 0; }

oct 控制器會將後面的數字以8進為來顯示,而hex則會以16進位顯示,而setw可以設定欄位寬度,另外,因為前面用了 hex ,所以我們需要再使用 dec控制器 將數字以10進位的方式輸出,否則它會用16進位方式顯示數字。

在 C++ 中 1 可表示 true,而 0 可表示 false,輸出時也是直接輸出 0 與 1,下面這個程式使用 boolalpha 控制器,可以讓輸出以 true 與 false 來顯示:

#include <iostream> #include <iomanip> using namespace std; int main () { bool boolnum; bool num; boolnum = true; cout << boolalpha << boolnum << endl; boolnum = false; cout << boolalpha << boolnum << endl; num = 1; cout << boolalpha << num << endl; num = 0; cout << boolalpha << num << endl; system("pause"); return 0; } 輸出: true false true false

下面列出了一些常用的I/O控制器及其說明:

控制器 說明
boolalpha 讓 bool 輸出時顯示 true 與 false
dec 10 進位顯示
endl 輸出換行字元並清空串流
ends 輸出 Null 字元
fixed 以正常的數字格式顯示
flush 清空串流
hex 16 進位顯示
left 靠左對齊
oct 8 進位顯示
right 靠右顯示
scientific 科學記號表示
setbase(int b) 指定數字基底
setfill(int c) 指定填充字元
setprecision(int p) 指定顯示精確度
setw(int w) 指定欄位寬度,並以 16 進位顯示
showbase 顯示數字基底,例如 0x11
showpoint 顯示小數
showpos 正數顯示 + 號
skipws 忽略輸入的空白字元
upperbase 字母大寫
ws 忽略前導的空白字元
noboolalpha 關閉 boolalpha 的使用
noshowbase 關閉 showbase 的使用
noshowpoint 關閉 showpoint 的使用
noshowpos 關閉 showpos 的使用
noskipws 關閉 skipws 的使用
nouppercase 關閉 uppercase 的使用

I/O格式旗標

I/O 格式控制器 可以對當時處理中的串流改變格式,如果想在程式進行過程中,始終維持指定的格式,可以使用格式旗標,透過 setf 與 unsetf 方法來設定與取消。

以下列出一些常用的格式旗標:

格式旗標 說明
ios::boolalpha 將真與假以 true 與 false 顯示
ios::dec 10 進位顯示
ios::fixed 正常方式顯示(非科學記號)
ios::hex 16 進位顯示
ios::left 靠左
ios::oct 8 進位顯示
ios::scientific 科學記號
ios::showbase 顯示基底
ios::showpoint 顯示小數點
ios::showpos 正數顯示 +
ios::skipws 忽略空白字元
ios::uppercase 字母大寫

可以一次設定一個格式旗標,若要設定多個格式旗標,可以使用 | 來連結,例如:

cout.setf(ios::showbase | ios::hex);

下面這個程式顯示一些基本的格式旗標作用:

#include <iostream> using namespace std; int main() { cout.unsetf(ios::dec); // 取消 10 進位顯示 cout.setf(ios::hex | ios::scientific); // 16 進位顯示或科學記號顯示 cout << 12345 << " " << 100 << endl; cout.setf(ios::showpos | ios::showpoint); // 正數顯示 + 號且顯示小數點 cout << 10.0 << ": " << -10.0 << endl; return 0; } 輸出: 3039 64 +1.000000e+01: -1.000000e+01

上方的程式裡先解除了ios::dec格式旗標,這個動作並不一定需要,但在某些編譯器中,這個旗標會覆蓋其他的旗標,先清除會比較保險。

ios類別的flags方法會傳回目前串流的格式設定,如果傳遞參數給它,會設定指定的格式,並傳回上一個格式設定:

fmtflags flags(); fmtflags flags(fmtflags);

想一次設定指定的格式旗標,可以如下:

ios::fmtflags f = ios::showpos | ios::showbase | ios::oct | ios::right; cout.flags(f);

下面這個程式可以用來測試串流的格式設定:

#include <iostream> using namespace std; void info(ios::fmtflags current, const ios::fmtflags &flag, const string &flagName) { if(current & flag) { cout << flagName << " on" << endl; } else { cout << flagName << " off" << endl; } } int main() { cout.unsetf(ios::dec); cout.setf(ios::oct | ios::showbase); ios::fmtflags flags = cout.flags(); info(flags, ios::left, "left"); info(flags, ios::dec, "dec"); info(flags, ios::showbase, "showbase"); info(flags, ios::oct, "oct"); return 0; } 輸出: left off dec off showbase on oct on

文字檔案 I/O

如果要在 C++ 內讀寫檔案,我們需要將其連結至串流。

在一開始我們有談到,cout 是 ostream 的實例, cin 是 istream 實例,這兩個實例是定義在iostream 標頭內。

istream 型態是定義在 istream標頭內,它是 basic_istream 模板類別的 basic_istream<char> 特化版本,basic_stream 是字元輸入串流的基礎模板類別 ; 而ostream 型態是定義在 ostream標頭,它是 basic_ostream 模板類別的 basic_ostream<char> 特化版本, basic_ostream是字元輸出串流的基礎模板類別。

在文字檔案串流的處理方面,basic_ifstream 繼承了 basic_istream,而 ifstream 型態是 basic_ifstream<char> 特化版本,用來進行文字檔案輸入串流操作,basic_ofstream 繼承了 basic_ostream,而 ofstream 型態是 basic_ofstream<char> 特化版本,用來進行文字檔案輸出串流操作,ifstream、ofstream 定義在 fstream 標頭之中。

使用 ifstream 建立實例時,可以指定連結的檔案名稱,如果沒有指定檔案名稱,會建立一個沒有連結檔案的串流,後續必須以 open 來連結檔案:

void open( const char *filename, ios_base::openmode mode = ios_base::in ); void open( const std::string &filename, ios_base::openmode mode = ios_base::in );

例如,可以使用下面這個片段來開啟檔案輸入串流:

ifstream in; in.open("filename");

如果開啟失敗,串流物件在布林判別場合會是 false,可以使用下面的片段來判斷:

if(in) { ... 進行檔案處理 }

類似地,使用 ofstream 建立實例時,可以指定連結的檔案名稱,如果沒有指定檔案名稱,會建立一個沒有連結檔案的串流,後續必須以 open 來連結檔案:

void open( const char *filename, ios_base::openmode mode = ios_base::out ); void open( const std::string &filename, ios_base::openmode mode = ios_base::out );

mode 決定檔案的開啟模式,是由 ios 類別定義的常數來決定,下面列出 openmode 的值與用途:

用途
ios::in 輸入(basic_ifstream 預設)
ios::out 寫入(basic_ofstream 預設)
ios::ate 開啟後移至檔案尾端
ios::app 附加模式
ios::trunc 如果檔案存在,清除檔案內容
ios::binary 二進位模式

當然,程式的世界實際上並沒有文字檔案這東西,資料都是二進位,字元串流只是在讀取或寫入的過程,會進行文字編碼的轉換,例如 int 數字 9,在寫入的操作中,會轉換為編碼 57 的位元組資料,至於本身是 char 的資料,就直接以對應的位元組寫出。

因為 ifstream、ofstream 各是 istream、ostream 的子類別,>> 與 << 運算子也可以用在 ifstream、ofstream 實例上,結果就是使用 ifstream、ofstream 時,可以如同使用 cin、cout 一樣地操作。

來看個讀寫檔案的範例:

#include <iostream> #include <fstream> using namespace std; struct Account { string id; string name; double balance; Account(string id = "", string name = "", double balance = 0.0) : id(id), name(name), balance(balance) {}; }; void print(ostream &out, Account &acct) { out << acct.id << " " << acct.name << " " << acct.balance; } void read(istream &in, Account &acct) { in >> acct.id >> acct.name >> acct.balance; } int main() { Account acct = {"123-456-789", "Justin Lin", 1000}; ofstream out("account.data"); print(out, acct); out.close(); // 記得關閉檔案 Account acct2; ifstream in("account.data"); read(in, acct2); in.close(); // 記得關閉檔案 print(cout, acct2); return 0; }

因為 ifstream、ofstream 各是 istream、ostream 的子類別,cin 與 cout 也各是 istream、ostream 實例,因此 print、read 對它們來說是通用的,執行過後,account.data 檔案中會存有「123-456-789 Justin 0」,而最後標準輸出中,也會顯示「123-456-789 Justin 0」。

二進位檔案 I/O

使用二進位模式開啟檔案,在寫入或讀取檔案時不會發生字元轉換,數值在記憶體中的位元是如何,寫入檔案時就是如何,而讀入時也是相同。

下面這個程式可以讀入任意檔案,每次讀入一個位元組,並將讀入資料以 16 進位數顯示,若讀入的資料前導位元為 1,為了輸出的對齊,使用其補數加以顯示:

#include <iostream> #include <fstream> #include <iomanip> using namespace std; void print(ifstream &in) { char ch; int count = 0; while(!in.eof()) { in.get(ch); if(ch < 0) { ch = ~ch; // 負數取補數 } cout << setw(2) << hex << static_cast<int>(ch) << " "; count++; if(count > 16) { // 換行 cout << endl; count = 0; } } cout << endl; } int main(int argc, char* argv[]) { ifstream in(argv[1], ios::in | ios::binary); if(!in) { cout << "無法讀取檔案" << endl; return 1; } print(in); in.close(); return 0; }

執行結果:

23 69 6e 63 6c 75 64 65 20 3c 69 6f 73 74 72 65 61 6d 3e a 23 69 6e 63 6c 75 64 65 20 3c 66 73 74 72 65 61 6d 3e a 23 69 6e 63 6c 75 64 65 20 3c 69 6f 6d 61 6e 69 70 3e a 75 73 69 6e 67 20 6e 61 6d 65 73 70 61 63 65 20 73 74 64 3b a a 69 6e 74 20 6d 61 69 6e 28 69 6e 74 20 61 72 67 63 2c 20 63 68 61 略....

下面這個程式可以讓將檔案複製至另一指定名稱:

#include <iostream> #include <fstream> using namespace std; int main(int argc, char* argv[]) { char ch; ifstream in(argv[1], ios::in | ios::binary); ofstream out(argv[2], ios::out | ios::binary); while(!in.eof()) { in.get(ch); if(!in.eof()) out.put(ch); } in.close(); out.close(); return 0; }

在寫入或讀取檔案時,也可以用 read 與 write 函式以區塊的方式寫入,它們的函式雛型如下:

istream &read(char *buf, streamsize num); ostream &write(const char* buf, streamsize num);

其中 num 是要寫入的資料位元組數目,下面這個程式示範如何將陣列資料寫入檔案,然後再將之讀出:

#include <iostream> #include <fstream> using namespace std; int main(int argc, char* argv[]) { ofstream out("temp", ios::out | ios::binary); int arr[5] = {1, 2, 3, 4, 5}; out.write(reinterpret_cast<char*>(arr), sizeof(arr)); out.close(); ifstream fin("temp", ios::in | ios::binary); fin.read(reinterpret_cast<char*>(arr), sizeof(arr)); for(int i = 0; i < 5; i++) { cout << arr[i] << ' '; } cout << endl; fin.close(); return 0; }
Select a repo