輸出流提供了不少方法格式化內置類型的輸出。對於用戶自定義類型,則須要由程序員定義適合的<<操做。ios
整型值能夠輸出爲八進制、十進制和十六進制(十六進制多用於輸出與硬件相關的信息,緣由在於一個十六進制數字精確地表示了4位二進制值)。
例如:git
cout<<1234<<'\t'<<hex<<1234<<'\t'<<oct<<1234<<endl; cout<<1234; //八進制的基數仍然起做用
這段代碼會輸出:程序員
1234 4d2 2322 2322 //整數將以八進制的形式輸出,直到輸出格式被改變
也就是說,oct、hex和dec是持久的——後面的整數一直按照這種數制輸出,直至咱們指定新的數制。hex和oct這種用來改變流的行爲的關鍵字被稱爲操縱符。
咱們能夠要求ostream顯示每一個整數的基數。例如:app
cout<<1234<<'\t'<<hex<<1234<<'\t'<<oct<<1234<<endl; cout<<showbase<<dec; cout<<1234<<'\t'<<hex<<1234<<'\t'<<oct<<1234<<endl;
會輸出:函數
1234 4d2 2322 1234 0x4d2 02322
這樣,十進制數將沒有前綴,八進制數將帶前綴0,而十六進制將帶前綴0x(或0X)。這與C++源程序中的整數文字常量的表示方法是徹底一致的。例如:測試
cout<<1234<<'\t'<<0x4d2<<'\t'<<02322<<endl;
若是是十進制輸出格式,這段代碼會輸出:this
1234 1234 1234
與oct和hex同樣,showbase也是持久的。想去掉其效果的話,能夠用noshowbase操縱符,它會恢復默認效果——輸出整數時不顯示基數。
對整數輸出操縱符總結以下:spa
oct //使用8爲基數的(八進制)表示 dec //使用10爲基數的(十進制)表示 hex //使用16爲基數的(十六進制)表示 showbase //爲八進制加前綴0,爲十六進制加前綴0x noshowbase //取消前綴
默認狀況下,>>假定數值使用十進制表示,但你能夠指定讀入十六進制或八進制數:操作系統
int a; int b; int c; int d; cin>>a>>hex>>b>>oct>>c>>d; cout<<a<<'\t'<<b<<'\t'<<c<<'\t'<<d;
若是你鍵入:日誌
1234 4d2 2322 2322
上面程序會輸出:
1234 1234 1234 1234
注意,這意味着oct、dec和hex對輸入也是持久的,如同在輸出操做中同樣。
你可讓>>接受前綴0和0x並正確解釋。爲了實現這一效果,你須要「復位」全部默認設置,例如:
cin.unsetf(ios::dec); cin.unsetf(ios::oct); cin.unsetf(ios::hex);
流的成員函數unsetf()將參數中給出的一個或多個標識位復位。這時若是咱們鍵入:
若是你鍵入:
1234 0x4d2 02322 2322
上面程序會輸出:
1234 1234 1234 1234
操縱符fixed、scientific和defaultfloat用來選擇浮點數格式。如今,咱們能夠這麼寫:
cout<<1234.56789<<'\t' <<fixed<<1234.56789<<'\t' <<scientific<<1234.56789<<endl; cout<<1234.56789<<endl; //浮點格式是持久的 cout<<defaultfloat<<1234.56789<<'\t' //浮點值輸出的默認格式 <<fixed<<1234.56789<<'\t' <<scientific<<1234.56789<<endl;
會輸出:
1234.57 1234.567890 1.234568e+003 1.234568e+003 //scientific操做符的效果是持久的 1234.57 1234.567890 1.234568e+003
對基本的浮點數格式化輸出操縱符總結以下:
fixed //使用定點表示 scientific //使用尾數和指數表示方式。尾數總在[1:10)之間,也就是說,在小數點以前有單個非0數字 defaultfloat //在defaultfloat的精度範圍內自動選擇fixed或者scientific中更爲精確的一種表示
默認狀況下,defaultfloat格式用總共6位數字來輸出一個浮點值。流會選擇最適合的格式,浮點值按6位數字(defaultfloat格式的默認精度)所能表示的最佳近似方式進行舍入。例如:
1234.567輸出爲1234.57
1.2345678輸出爲1.23457
1234567輸出爲1234567(由於這是一個整數)
1234567.0輸出爲1.23457e+006
基本上,defaultfloat格式在scientific和fixed兩種格式間進行選擇,指望將浮點數以最精確的表示形式呈現給用戶,所採用的精度限定爲general格式的精度——默認爲6位數字長度。
程序員可使用操縱符setprecision()來設置精度,例如:
cout<<1234.56789<<'\t' <<fixed<<1234.56789<<'\t' <<scientific<<1234.56789<<endl; cout<<defaultfloat<<setprecision(5) <<1234.56789<<'\t' <<fixed<<1234.56789<<'\t' <<scientific<<1234.56789<<endl; cout<<defaultfloat<<setprecision(8) <<1234.56789<<'\t' <<fixed<<1234.56789<<'\t' <<scientific<<1234.56789<<endl; cout<<defaultfloat<<setprecision(12) <<1234.56789<<'\t' <<fixed<<1234.56789<<'\t' <<scientific<<1234.56789<<endl;
會輸出(注意舍入):
1234.57 1234.567890 1.234568e+003 1234.6 1234.56789 1.23457e+003 1234.5679 1234.56789000 1.23456789e+003 1234.56789 1234.567890000000 1.234567890000e+003 //defaultfloat下大於精度也不補0
幾種格式的精度分別定義爲:
defaultfloat //精度就是數字的個數 scientific //精度爲小數點以後數字的個數 fixed //精度爲小數點以後數字的個數
使用scientific和fixed格式,程序員能夠精確控制一個值輸出所佔用的寬度。整數輸出也有相似的機制,稱爲域。你可使用「設置域寬度」操縱符setw()精確指定一個整數或一個字符串輸出佔用多少個位置。例如:
cout<<12345 //不使用域 <<'|'<<setw(4)<<123456<<'|' //123456和4字符的域寬度不匹配 <<setw(8)<<123456<<'|' //設置8字符的域寬度 <<123456<<"|\n"; //域的大小不會持久化
會輸出:
12345|123456| 123456|123456|
首先注意第三個123456以前的兩個空格,這就是咱們所指望的效果——一個6位數字的數佔用一個8個字符的域。可是,當你指定一個4個字符的域時,123456不會被截取來適應域寬,由於壞的格式總比「壞的輸出數據」更好些。
域也可做用於浮點數和字符串,例如:
cout<<12345<<'|'<<setw(4)<<12345<<'|' <<setw(8)<<12345<<'|'<<12345<<'|'<<endl; cout<<1234.5<<'|'<<setw(4)<<1234.5<<'|' <<setw(8)<<1234.5<<'|'<<1234.5<<'|'<<endl; cout<<"asdfg"<<'|'<<setw(4)<<"asdfg"<<'|' <<setw(8)<<"asdfg"<<'|'<<"asdfg"<<'|'<<endl;
會輸出:
12345|12345| 12345|12345| 1234.5|1234.5| 1234.5|1234.5| asdfg|asdfg| asdfg|asdfg|
注意,域的寬度不是持久的。除非你在語句中直接在輸出操做以前設置域寬,不然不會有域的限制。
從C++程序的角度看,文件是操做系統提供的一個抽象。一個文件就是一個從0開始編號的簡單的字節序列。問題是咱們如何訪問這些字節。若是使用iostream,訪問方式很大程度上在咱們打開文件將其與一個流相關聯時就肯定了。流的屬性決定了文件打開後咱們能夠對它執行哪些操做,以及這些操做的意義。
ios_base::app //追加模式(即添加在文件末尾) ios_base::ate //「末端」模式(打開文件並定位到文件尾) ios_base::binary //二進制模式———注意系統特有的行爲 ios_base::in //讀模式 ios_base::out //寫模式 ios_base::trunk //將文件截爲長度0
能夠在文件名以後指定文件模式,例如:
ofstream of1{name1}; //默認設置爲ios_base::out ifstream if1{name2}; //默認設置爲ios_base::in ofstream ofs{name3,ios_base::app}; //帶ios_base::out模式的默認設置的輸出流 fstream fs{name4,ios_base::in|ios_base::out}; //同時帶in和out模式的流
後一個例子中的「|」是「位或」運算符,可用於組合多個模式。app模式經常使用於寫日誌文件,由於你老是將新的日誌追加到文件末尾。在每一個例子中,打開文件的確切效果依賴於操做系統,並且若是操做系統不能使用某種特定的模式打開文件的話,流可能會進入非good()狀態。
注意,若是以寫模式打開一個文件,而文件不存在的話,一般操做系統會建立一個新文件,但若是以讀模式打開一個不存在的文件,就不會建立新文件。
ofstream ofs{"no-such-file"}; //建立名爲「no-such-file"的新文件 ifstream ifs{"no-file-of-this-name"}; //錯誤:ifs將處於非good()狀態
只要條件容許,就應堅持只讀取由istream打開的文件,只寫入到由ostream打開的文件中。
默認狀況下,iostream使用字符表示方式,也就是說,istream從文件讀取字符序列,並將其轉換爲所需類型的對象。而ostream將指定類型的對象轉換爲字符序列,而後寫入文件。可是,咱們能夠令istream和ostream將對象在內存中對應的字節序列簡單地複製到文件。這稱爲二進制I/O,經過在打開文件時指定ios_base::binary模式來實現。例如:
int main() { //以二進制文件讀取模式打開一個istream cout<<"please enter input file name\n"; string iname; cin>>iname; ifstream ifs{iname,ios_base::binary}; //注意:流模式 //brinary告知流不要自做聰明地處理這些字節 if(!ifs) error("can't open input file",iname); //以二進制文件寫入模式打開一個ostream cout<<"please enter output file name\n"; string oname; cin>>oname; ofstream ofs{oname,ios::binary}; //注意:流模式 //brinary告知流不要自做聰明地處理這些字節 if(!ofs) error("can't open output file",oname); vector<int>v; //從二進制文件中讀取 for(int x;ifs.read(as_bytes(x),sizeof(int));) //注意:讀入字節 v.push_back(x); //對v進行處理 //寫入到二進制文件 for(int x:v) ofs.write(as_bytes(x),sizeof(int)); //注意:寫入字節 return 0; }
在上述例子中,咱們使用相對複雜但也更爲緊湊的二進制表示方式。當咱們從面向字符的I/O轉向二進制I/O時,要放棄經常使用的>>和<<操做符。這兩個操做符按默認約定將值轉換爲字符序列(如,字符串」asdf「轉換爲字符a、s、d、f,整數123轉換爲字符一、二、3)。只有在默認模式不能知足需求時,咱們才須要使用二進制文件。咱們使用二進制模式,就是告知流不要自做聰明地處理字節序列。
咱們如何處理int型值纔是」聰明的「?顯然是用4歌字節存儲4字節寬的int型值,也就是說,咱們能夠查看int型值在內存中的表示方式(4個字節的序列),並直接將這些字節傳輸到文件。隨後,咱們就能夠用一樣的方式讀回這些字節重組出int值:
ifs.read(as_bytes(x),sizeof(int)) //注意:讀字節 ofs.write(as_bytes(x),sizeof(int)) //注意:寫字節
ostream的write()函數和istream()的read()函數都接受兩個參數:地址(這裏用函數as_bytes()獲取)和字節(字符)數量(這裏咱們用運算符sizeof得到)。對於咱們要讀/寫的值,地址參數指向保存它的內存區域的第一個字節。
函數as_bytes()能夠用來獲取對象存儲區域的第一個字節。它能夠定義以下:
template<class T> char *as_bytes(T& i) //將T視爲一個字節序列 { void* addr=&i; //獲得保存對象的內存區域的第一個字節的地址 return static_cast<char*>(addr); //將內存視爲多個字節
只要有可能,儘可能使用從頭到尾的文件讀寫方式。不少時候,當你以爲必須對文件進行修改時,最好的方法是建立一個新的文件。
C++也支持在文件中定位到指定位置以進行讀寫。基本上,每一個以讀方式打開的文件,都有一個「讀/獲取位置」,而每一個以寫方式打開的文件,都有一個「寫/放置位置」。
使用方法以下:
fstream fs{name}; //打開文件進行輸入輸出 if(!fs) error("can't open",name); fs.seekg(5); //移動讀位置(g表示「獲取」)到5(從0開始,所以是第6個字符處) char ch; fs>>ch; //進行讀操做,並增長讀位置 cout<<"character[5] is "<<ch<<'('<<int(ch)<<")/n"; fs.seekp(1); //移動寫位置(p表示「放置」)到1 fs<<'y'; //進行寫操做,並增長寫位置
注意,seekg()和seekp()增長了它們的相對位置。請當心:這段代碼中在文件定位以前進行了運行時錯誤檢測,這是必要的。另外特別要注意的是,若是你試圖定位(用seekg()或者seekp())到文件尾以後,結果如何是未定義的,不一樣操做系統會表現出不一樣行爲。
你能夠將一個string對象做爲istream的源,或者ostream的目標。從一個字符串讀取內容的istream對象稱爲istringstream,保存字符並將其寫入字符串的ostream對象稱爲ostringstream。例如,從字符串提取數值時,istringstream就頗有用:
double str_to_double(string s) //若是可能,將字符轉換爲浮點數 { istringstream is{s}; //定義一個流來從s中讀出 double d; is>>d; if(!is) error("double format error:",s); return d; }
相反,對於要求一個簡單字符串參數的系統,如GUI系統,ostringstream可用於格式化輸出來生成單一字符串。例如:
void my_code(string label,Temperature temp) { //... ostringstream os; //生成一條消息的流 os<<setw(8)<<label<<":" <<fixed<<setprecision(5)<<temp.temp<<temp.unit; someobject.display(Point(100,100),os.str().c_str()); //... }
ostringstream的成員函數str()返回由輸出操做到ostringstream對象的內容構成的字符串。c.str()是string的成員函數,它返回不少系統接口所須要的C風格字符串。
stringstream一般用於將真實I/O和數據處理分離,能夠看做一種裁剪I/O以適應特殊需求和偏好的機制。
ostringstream的一個簡單應用是鏈接字符串,例如:
int seq_no=get_next_number(); //獲取日誌文件的編號 ostringstream name; name<<"myfile"<<seq_no<<".log"; //例如,myfile17.log ofstream logfile{name.str()}; //例如,打開myfile.log
一般狀況下,咱們用一個string來初始化istringstream,而後用輸入操做從該字符串中讀取字符。相反,咱們一般用一個空字符串初始化ostringstream,而後用輸出操做向其中填入字符。有一種更爲直接的方法來訪問stringstream中的字符:ss.str()返回ss的字符串的一個拷貝,而ss.str(s)則將ss的字符串設置爲s的一個拷貝。
若是但願一次讀取整行的內容,隨後再決定如何從中格式化輸入數據,可使用函數getline(),例如:
string name; getline(cin,name); cout<<name<<'\n';
使用整行輸入的一個常見緣由是,默認的空白符不符合咱們的要求。例如,咱們可能將一行做爲一句話,咱們能夠先讀入一行,而後從中提取單個單詞。
string command; getline(cin,command); stringstream ss{command}; vector<string>words; for(string s;ss>>s;) words.push_back(s); //提取單個單詞
咱們能夠寫出以下代碼實現正確的單詞分解:
for(char ch;cin.get(ch);){ if(isspace(ch)){ //若是ch是一個空白符 //什麼也不作(也就是說,跳過空白符) } if(isdigit(ch)){ //讀入一個數值 } else if(isalpha(ch)){ //讀入一個標識符 } else{ //處理操做符號 } } }
函數istream::get()讀入單個字符,賦予它的參數。它不跳過空白符。與>>相似,get()返回其istream對象的引用,便於咱們檢測其狀態。
當咱們採起逐個字符讀取方式時,一般須要對字符進行分類,下面是實現字符分類的標準庫函數:
isspace(c) //c是空白符嗎(' '、'\t'、'\n',等等)? isalpha(c) //c是字母嗎('a'..'z'、'A'..'Z')(注意,不包括'_')? isdigit(c) //c是十進制數字嗎('0'..'9')? isxdigit(c) //c是十六進制數字嗎(十進制數字或者'a'..'f'、'A'..'F')? isupper(c) //c是大寫字母嗎? islower(c) //c是小寫字母嗎? isalnum(c) //c是字母或十進制數字嗎? iscntrl(c) //c是控制字符嗎(ASCII碼0..31和127)? ispunct(c) //c是標點(除字母、數字、空白符或不可見控制字符以外的字符)嗎? isprint(c) //c是可打印字符嗎(ASCII字符' '..'~')? isgraph(c) //c是字母、十進制數字或者標點嗎(注意:不包括空白符)?
注意,多個字符分類能夠用「或」運算符(||)進行組合。例如,isalnum(c)意味着isalpha(c)||isdigit(c)。
另外,標準庫還提供了另外兩個有用的函數,用來轉換大小寫:
toupper(c) //C或者c對應的大寫字母 tolower(c) //C或者c對應的小寫字母
若是你想忽略大小寫的話,這兩個函數頗有用。咱們能夠爲任何字符串定義tolower函數:
void tolower(string& s) //將s置爲小寫格式 { for(char& x:s) x=tolower(x); }
在處理這類問題時,使用tolower()比使用toupper()更好些。由於對於某些天然語言如德語,並非全部小寫字母都有對應的大寫字母,所以前者能得到更好的效果。
例如,咱們要分割開每一個單詞,但是,若是咱們輸入
As planned,the guests arrived;then,
咱們會獲得這些「單詞」:
As
planned,
the
guests
arrived;
then,
如何處理達到咱們的目的呢?咱們能夠逐個處理字符,將標點字符刪除或者轉換爲空白符,而後再從「清理乾淨的」輸入中讀取數據:
string line; getline(cin,line); for(char& ch:line) //將每一個標點字符替換爲一個空格 { switch(ch){ case ';':case '.':case '?':case '!': ch=' '; } } stringstream ss{line}; //使用istream ss讀取整行 vector<string>vs; for(string word;ss>>word;) //讀取不帶標點字符的單詞 vs.push_back(word);
一樣是前面給出的輸入,如下代碼會獲得咱們想要的單詞:
As
planned
the
guests
arrived
then
接下來咱們提出一種更爲通用、有效的輸入流中刪除不須要字符的方法。基本思想是先從一個普通輸入流讀入單詞,而後使用用戶指定的「空白符」來處理輸入內容,也就是說,咱們並不將「空白符」交給用戶,咱們只是用它們來分割單詞。咱們用一個類來實現:
class Punct_stream{ public: Punct_stream(istream& is) :source{is},sensitive{true} {} void whitespace(const string& s) //定義s爲空白符集 { white=s; } void add_white(char c) { white+=c; } //加入到空白符集 bool is_whitespace(char c); //c在空白符集中? void case_sensitive(bool b) { sensitive=b; } bool is_case_sensitive() { return sensitive; } Punct_stream& operator>>(string& s); operator bool(); private: istream& source; //符號源 istringstream buffer; //使用buffer處理格式 string white; //被視爲「空白符」的符號 bool sensitive; //該stream是否大小寫敏感? };
輸入操做符>>的基本策略是從istream讀取一整行,存入一個名爲line的字符串,而後將全部自定義空白符轉換爲空格符,咱們將line放入名爲buffer的istringstream中。注意,從buffer中讀取數據直接就能夠進行,但只有它爲空的狀況下,才能向其寫入內容。
Punct_stream& Punct_stream::operator>>(string& s) { while(!(buffer>>s)){ //嘗試從buffer中讀取 if(buffer.bad()||!source.good()) return *this; buffer.clear(); string line; getline(source,line); //從source中讀入一行 //按需進行字符替換 for(char& ch:line) if(is_whitespace(ch)) ch=' '; //替換爲空格 else if(!sensitive) ch=tolower(ch); //替換爲小寫符號 buffer.str(line); //將符號串放入到流中 } return *this; }
若是名爲buffer的istringstream中存有字符,讀操做buffer>>s就能夠進行,s會收到「空白符」分隔的單詞,隨後就沒有什麼可作的了。只要buffer中有咱們能夠讀取的字符,這個過程就會發生。可是,當buffer>>s失敗時,也就是!(buffer>>s)爲真時,咱們必須利用source中的內容將buffer從新填滿。注意讀操做buffer>>s是在一個循環中,當咱們嘗試從新填充buffer後,會再次嘗試這個讀操做,所以有以下代碼:
while(!(buffer>>s)){ //嘗試從buffer中讀取 if(buffer.bad()||!source.good()) return *this; buffer.clear(); //從新填充buffer
若是buffer處於bad()狀態,或者source有問題的話,咱們將放棄讀取操做。不然,咱們清空buffer,再次嘗試。咱們清空buffer的緣由是,只有在讀失敗的狀況下,一般是buffer遇到eof()時,咱們才進入「從新填充循環」,而這意味着buffer中沒有供咱們讀取的字符。
一旦咱們正確處理完line中內容,就須要將其存入istringstream。buffer.str(line)完成這一工做,這條語句能夠讀成「將istringstream對象buffer的字符串設置爲line的內容」。
istream的一種習慣用法是測試>>的結果,例如:
while(ps>>s) {/*...*/}
這意味着咱們須要一種方法將ps>>s的結果看成布爾值來進行檢查。但ps>>s結果是一個Punct_stream,所以咱們須要一種方法將一個Punct_stream隱式轉換爲一個bool值。這就是Punct_stream的運算符bool()所作的事情。
Punct_stream::operator bool() { return !(source.fail()||source.bad())&&source.good(); }
如今咱們就能夠寫程序了。
#include "std_lib_facilities.h" using namespace std; class Punct_stream{ public: Punct_stream(istream& is) :source{is},sensitive{true} {} void whitespace(const string& s) //定義s爲空白符集 { white=s; } void add_white(char c) { white+=c; } //加入到空白符集 bool is_whitespace(char c); //c在空白符集中? void case_sensitive(bool b) { sensitive=b; } bool is_case_sensitive() { return sensitive; } Punct_stream& operator>>(string& s); operator bool(); private: istream& source; //符號源 istringstream buffer; //使用buffer處理格式 string white; //被視爲「空白符」的符號 bool sensitive; //該stream是否大小寫敏感? }; Punct_stream& Punct_stream::operator>>(string& s) { while(!(buffer>>s)){ //嘗試從buffer中讀取 if(buffer.bad()||!source.good()) return *this; buffer.clear(); string line; getline(source,line); //從source中讀入一行 //按需進行字符替換 for(char& ch:line) if(is_whitespace(ch)) ch=' '; //替換爲空格 else if(!sensitive) ch=tolower(ch); //替換爲小寫符號 buffer.str(line); //將符號串放入到流中 } return *this; } bool Punct_stream::is_whitespace(char c) { for(char w:white) if(c==w) return true; return false; } Punct_stream::operator bool() { return !(source.fail()||source.bad())&&source.good(); } int main() //給定文本輸入,產生一個該文本中全部單詞的升序列表 //忽略標點符號和大小寫的區別 //去掉輸入中的重複結果 { Punct_stream ps{cin}; ps.whitespace(":;,.?!()\"{}<>/&$@#%^*|~"); //注意,\"表示在字符串中的" ps.case_sensitive(false); cout<<"please enter words\n"; vector<string>vs; for(string word;ps>>word;) vs.push_back(word); //讀入單詞 sort(vs.begin(),vs.end()); //按字典序進行排序 for(int i=0;i<vs.size();++i) //寫入字典 if(i==0||vs[i]!=vs[i-1]) cout<<vs[i]<<endl; //去掉重複單詞 return 0; }