C++
在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 格式控制器 可以對當時處理中的串流改變格式,如果想在程式進行過程中,始終維持指定的格式,可以使用格式旗標,透過 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
如果要在 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」。
使用二進位模式開啟檔案,在寫入或讀取檔案時不會發生字元轉換,數值在記憶體中的位元是如何,寫入檔案時就是如何,而讀入時也是相同。
下面這個程式可以讀入任意檔案,每次讀入一個位元組,並將讀入資料以 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;
}