1、第一部分第十課:文件讀寫,海闊憑魚躍
2、第一部分第十一課預告:小練習,猜單詞
上一課《【C++探索之旅】第一部分第九課:數組威武,動靜合一》中,我們學習了動態數組和靜態數組,也看到其實字符串很類似字符數組(到了之後的第二部分,學習面向對象,我們會知道其實string是一個類)。
到目前為止,我們寫的程序還比較簡單,當然了,因為我們剛開始學習C++嘛。但只要加以訓練,我們就慢慢地能夠寫一些真正的應用了。我們也開始逐漸了解C++的基礎知識了,不過缺了很重要的一環:與文件交互。
我們已經學會如何將信息輸出到控制台(console)以及如何提取用戶在控制台中輸入的數據(使用cin和cout)。但是,我們豈能就此罷休。想想我們之前介紹過的一些程序,例如:記事本,一些IDE(VS, CodeBlocks, xCode, Eclipse, etc),繪圖軟件,等等,都能夠讀寫文件。
在游戲領域就更是如此啦(我知道一幫宅男已經激動了):游戲裡的數據要保存,游戲的圖片,音樂,道具,等等。都需要存檔。
總之,如果一個軟件不會與文件交互,那麼它的功能是比較有限的。
因此,一起來學習如何讀寫文件吧。你會發現,如果你掌握了cin和cout的用法,那其實你已經知道大半啦。
寫入文件
我們要讀寫文件,首先需要打開文件。就好像平時我們要記筆記一樣,你總得先打開筆記本吧,才能閱讀內容,或者往裡面寫東西。
一旦文件被打開之後,接下來的操作就很類似之前用cin和cout來進行標准輸入和輸出了。我們又會與老朋友<<和>>見面。
用術語來說,我們會將一個程序和外界的通信方式用"流"來描述。流,英語是stream。記得嗎?我們要使用cin和cout,需要用
#include
因為cin和cout定義在iostream這個C++的標准庫中。而這裡的iostream就是input output stream的縮寫,表示"輸入輸出流"。所以,其實我們早就在不知不覺地接觸流的概念了。
在這一章中,我們要和文件交互,那麼就需要文件流來幫忙了。聰明如你一定想到了,是的,文件的英語是file,那麼文件流就是file stream。是不是很簡單呢?
因此,我們需要用到fstream這個標准庫,fstream就是file stream的縮寫。
當然了,如果你不是參加一個程序員的派對,也不需要顯得很專業,那麼說"讀寫文件"就可以了。
fstream頭文件
在C++中,我們要使用一個功能,需要引入合適的頭文件。因此,我們在程序一開始須要這樣做:
#include
接下來,我們就學習如何創建一個文件流,以便我們能讀寫文件。
以寫模式打開文件
流其實是對象,還記得我們說C++是一門面向對象的語言嗎?當然我們現在還不深究,要到第二部分講面向對象編程時才會暢聊類和對象。暫時只需要知道這些流其實都是C++的對象(不是找對象的對象,少年你想多了)。
也完全無需害怕,因為我們之後還會不斷提到流。暫時,只需把其看作比較高級的變量就可以了。這些文件流包含了文件的很多信息,提供給我們很多功能,例如可以關閉文件,在文件中移動,等等。
你會看到,聲明一個流的對象,其實就和我們聲明變量一樣簡單。首先,我們來看看如何創建用於寫文件的流,須要用到ofstream,也就是output file stream,因為是從程序向文件輸入數據,因此對於程序來說是"出去"的流,因此是output(輸出),而不是input(輸入)。
話休絮煩。翠花,上"栗子":
#include#include using namespace std; int main() { ofstream myStream("C:/Cpp/Files/scores.txt"); //聲明用於寫入文件的流, //此文件是在C盤下的Cpp文件夾的子文件夾Files中的scores.txt文件 ?return 0; }
在上面程序中,我在myStream後面的括號中指定了文件的路徑.這個路徑可以有兩種形式:
絕對路徑:就是文件的所在,不過是從根文件夾開始的路徑。例如:C:/Cpp/Files/scores.txt
相對路徑:也是文件的所在,不過是相對於你的程序的路徑。例如,你的程序位於C:/Cpp/,那麼如果你的文件是在C:/Cpp/Files/scores.txt,你在程序裡指定文件的相對路徑時就要寫 Files/scores.txt
自此,我們就可以使用這個文件流來寫文件啦。
如果文件不存在,那麼會被自動創建。不過,至少指定的目錄要存在,不然會出現"目錄不存在"的錯誤。在我們上面的例子中,至少目錄C:/Cpp/Files必須事先存在。
在打開文件的時候,也會有其他問題。例如文件不屬於你,或者磁盤已滿,等等,總之,打開失敗。因此,我們為了保險起見,總要測試文件是否順利被打開。我們使用 if (myStream) 的方法來測試。
ofstream myStream("C:/Cpp/Files/scores.txt"); //試著打開這個文件 if(myStream) //測試打開文件是否成功 { //一切順利,我們可以使用此文件了 } else { cout << "出錯: 無法打開此文件." << endl; }
至此,我們已經做好了寫文件的准備工作。你會看到,接下來的操作還是有點眼熟的。
向流中寫入數據
前面我們說過寫入文件的操作就和以前我們使用cout類似。因此當我對你說要使用<<運算符來進行操作的時候,你應該不會太驚訝。
#include#include #include using namespace std; int main() { string const fileName("C:/Cpp/Files/scores.txt"); ofstream myStream(fileName.c_str()); if(myStream) { myStream << "大家好,我是被寫入文件的一句話." << endl; myStream << 54.26 << endl; int age(23); myStream << "我" << age << "歲了." << endl; } else { cout << "出錯: 無法打開此文件." << endl; } return 0; }
上面的程序中,可以看到我們首先聲明了一個string的變量,裡面存放了C:/Cpp/Files/scores.txt這個字符串,不過之後在將其賦給ofstream的對象myStream時,我們卻用了c_str()這個函數,這是為什麼呢?
其實,ofstream接受的參數是char *(暫時不需要知道是什麼,馬上我們會學習指針的知識,到時就清楚了),c_str()函數就是用於將string轉換成char *
運行此程序,不出意外的話,你的電腦的C:/Cpp/Files/目錄下就會多出一個文件 scores.txt, 裡面的內容如下所示:
你也來試試
你也可以寫一個程序,請求用戶輸入自己的名字和年齡,然後你的程序將這些信息寫入文件。
文件的不同打開模式
我們只需要再處理一個小問題:
假如文件已經存在,那怎麼辦呢?
如果運行上面的已有程序,那麼文件的內容會被刪除,然後替換為你寫入的內容。但是假如我們想要保留文件本來的內容,只是想在文件末尾追加我們的新內容呢?
不用怕,肯定有辦法的。只需要在打開文件的時候添加第二個參數,用於指明文件的打開模式,如下所示:
ofstream myStream("C:/Cpp/Files/scores.txt", ios::app);
app是英語append的縮寫,表示"追加",也就是說寫入的內容不會覆蓋原本文件裡的內容,而是追加到文件末尾。
讀取文件
我們學習了如何寫文件,現在來學習如何讀取文件內容吧。你會看到,兩種操作是很類似的。
以讀的形式打開文件
之前我們用了ofstream的對象,那麼這次就要用到ifstream的對象了,ifstream是input file stream的縮寫。當然也需要測試文件是否順利被打開。
ifstream myStream("C:/Cpp/Files/scores.txt"); //試著打開文件 if(myStream) { //可以讀取文件 } else { cout << "出錯: 無法以讀的形式打開此文件." << endl; }
沒有什麼新的難點不是嗎?
接下來我們就可以讀取文件內容了。
要讀取文件內容,有三種不同的方式:
一行一行地讀取,用getline()函數
一個詞一個詞地讀取,用>>
一個字符一個字符地讀取,用get()函數
我們分別來學習這三種方式:
一行一行地讀取
第一種方式可以一次讀取整一行的內容,將其存儲在一個字符串裡。舉例如下:
string line; // 儲存整行內容的字符串變量 getline(myStream, line); //讀取整一行,存儲到line中
此函數的原理和cin是類似的。
一個詞一個詞地讀取
第二種方式,其實你也早就知道了,畢竟聰慧如你嘛。舉例如下:
double number; myStream >> number; //從文件中讀取一個浮點數 string word; myStream >> word; //從文件中讀取一個單詞
這個方法會讀取當前所在的文件位置處的內容和之後的一個空格("詞"並不是我們平時說的一個單詞,而是以空格來分隔的,假如中間沒有空格,那麼就是一個詞,例如heusyg3這是一個詞,但是heu syg3卻被認為是兩個詞,因為中間存在空格)。讀取的內容根據變量的類型會被轉換成double,int,string,等等。
一個字符一個字符地讀取
第三種方式,我們之前沒學過,不過也很簡單就是了。舉例如下:
char a; myStream.get(a);
上面的代碼讀取一個字符,將其存儲在char型變量a中。
這個方法可以讀取所有字符,不管是字母,空格,回車符,制表符,等等。
還記得在【C++探索之旅】第一部分第五課:簡易計算器中,我們學習過cin的用法嗎?還記得我們說過在cin>>和getline之間需要使用cin.ignore()嗎?因此,這裡我們從一個詞一個詞地讀取(用cin>>)轉換到一行一行地讀取(用getline()),也需要在之間加入ignore()。不過,因為我們這裡是在讀取文件,所以不能用cin.ignore(),而要使用ifstream的ignore方法,如下所示:
ifstream myStream("C:/Cpp/Files/scores.txt"); string word; myStream >> word; //讀取一個詞 myStream.ignore(); //改變讀取方式 string line; getline(myStream, line); //讀取一整行
一次讀取整個文件
很多時候,我們會希望讀取整個文件。我們已經學習了如何讀取文件,但是還沒學習當到達文件結尾時,如何停止。
為了獲知我們是否還可以繼續讀取,可以用getline函數的返回值。getline函數的返回值是一個bool(布爾值),如果等於true,還可以繼續讀,說明還沒到文件末尾;如果等於false,那麼說明已經讀取了文件的最後一行或者出錯了。在false的情況下,就不能再繼續讀取了。
還記得我們學過的循環嗎?只要還沒到達文件末尾(getline函數返回是true),我們就繼續讀取文件。while循環就是最好的選擇啦。看如下例子:
#include#include #include using namespace std; int main() { ifstream file("C:/Cpp/Files/scores.txt"); // 嘗試打開文件 if(file) { //文件順利打開,可以讀取了 string line; //存儲讀取的一整行的變量 while(getline(file, line)) //只要沒到達文件末尾,我們就一直一行一行地讀取 { cout << line << endl; //在控制台顯示讀取的行 //或者隨便你拿這一行干什麼,由你決定 } } else { cout << "出錯: 無法以讀的形式打開此文件." << endl; } return 0; }
一旦我們讀取了這些行,我們就可以非常方便地操作它們了。在上面的例子中,我們只是把讀取的每一行顯示在控制台中,但是你可以隨便怎麼用。
一些小技巧
這一課的最後,我們來學習幾個小技巧,這樣文件讀寫我們就學習得差不多了。
提前關閉文件
我們已經知道怎麼打開一個文件,但還沒演示如何關閉文件。倒不是因為我忘記了,而是之前關閉文件顯得沒有那麼必要。一旦我們跳出了文件流聲明的區塊,打開的文件就會被自動關閉。例如:
void f() { ofstream myStream("C:/Cpp/Files/scores.txt"); //打開文件 // 操作文件 } //當我們跳出這個函數,文件就自動被關閉了
因此,並不需要做任何操作來顯式地關閉文件。
但是,有時候我們想要提前關閉文件,在它被自動關閉前。為了達到這個目的,我們必須"不擇手段"... 哦,不是,是使用close函數。例如:
void f() { ofstream myStream("C:/Cpp/Files/scores.txt"); //打開文件C:/Cpp/Files/scores.txt //使用文件 myStream.close(); //關閉文件 //自此,我們將不能再往文件裡寫東西了 }
同樣地,我們也可以推遲打開文件。用open函數。例如:
void f(){ ofstream myStream; //聲明文件流,但沒有綁定文件 myStream.open("C:/Cpp/Files/scores.txt"); //打開文件C:/Cpp/Files/scores.txt //使用文件 myStream.close(); //關閉文件 //自此,我們將不能再往文件裡寫東西了 }
正如你所見,以上的操作都很簡單。然而,在大部分時候,沒必要使用open和close函數來顯示地打開和關閉文件。
文件裡的游標
我們再來深入一些技術細節,"研究"一下文件的讀取是怎麼運作的。
你還記得平時用文本編輯器的時候,我們在編輯文本時總會有一個一閃一閃的光標(cursor),指示了我們當前編輯的位置嗎?如下圖所示:
可以看到,目前光標位於Oscar的後面。
在C++中操作文件時,也是同樣的原理。有一個游標(cursor)一直指示當前在文件中的位置。
例如,當我們運行這一行的時候:
ifstream file("C:/Cpp/Files/scores.txt");
文件C:/Cpp/Files/scores.txt會被打開,游標會定位於文件最開始處。
如果之後我們讀取第一個詞,就會讀取到Oscar這個詞。讀取完之後,我們的游標就會位於下一個單詞的開始處了,如下圖所示:
可以看到,現在游標位於is這第二個詞的開始處了。然後我們可以接著讀取第二個詞,第三個,... 一直到文件結束。
但如果這樣的話,我們只能按順序讀取文件,這可太束縛了。我們需要自由,需要飛翔,"在你的心上,自由地飛翔~" (小編,你的藥已經准備好了...)
幸好,我們能夠在文件中移動,說到移動,那就是移動那個cursor(游標)了。例如,我們可以說"我要移動到距離文件開始處20個字符的地方",或者"我要從當前位置前進32個字符"。這樣,我們就可以很方便地讀取我們真正想要的內容了。
首先,我們要了解游標目前位於哪裡。然後才能正確地移動。
獲得在文件中的位置
有一個方法可以獲知當前我們的游標位於文件的第幾個字符處(從文件開始處算起)。不過,對於輸入文件流(ifstream)和輸出文件流(ofstream),所用的函數不一樣,而且名字也有點古怪,我們列在下面:
針對
ifstream
針對ofstream
tellg()
tellp()
然而,這兩個函數的使用方法完全一樣。因此只介紹其中一個就可以了。舉例如下:
ofstream file("C:/Cpp/Files/scores.txt"); int position = file.tellp(); //獲取當前位置 cout << "目前位於文件中的第" << position << "個字符處." << endl;
在文件中移動
用於在文件中移動的函數也有兩個,成對的,每一個對應一種流的形式:
針對ifstream
針對ofstream
seekg()
seekp()
用法和之前的兩個函數類似。
這兩個函數接受兩個參數:一個是在文件中的位置,另一個是相對文件中的位置的距離數(字符數/字節數)。
myStream.seekp(numberOfCharacters, position);
對於此函數的position參數,有三種可能的位置:
文件開始處 : ios::beg
;
文件末尾處 : ios::end
;
當前位置 : ios::cur
.
例如,我想要移動到距離文件開始處10個字符的地方,我會這麼做:
myStream.seekp(10, ios::beg);
假如我想要移動到距離當前游標所在位置的20個字符處,我會這麼做:
myStream.seekp(20, ios::cur);
相信你已經理解啦。
獲知文件大小(所包含字節數)
這第三個小技巧需要用到前兩個。為了獲知文件的大小,我們首先移動到文件末尾,然後詢問我們所在的位置。你知道怎麼做了嗎?一起來看看吧:
#include#include using namespace std; int main() { ifstream file("C:/Cpp/Files/scores.txt"); //打開文件 file.seekg(0, ios::end); //移動到文件末尾 int size; size = file.tellg(); //在文件結尾處調用tellg這個函數,以獲得目前位於第幾個字符處,因此也就知道了文件的大小 cout << "文件的大小是: " << size << "個字節." << endl; return 0; }
好了,我們學完了文件讀寫的大致概念。不過肯定不只於此,還有很多知識點需要慢慢在實踐中去探索。
總結
在C++中,為了能讀寫文件,需要引入fstream頭文件。
為了寫入文件,我們需要創建一個ofstream對象;為了讀取文件,我們需要創建一個ifstream對象。
寫入文件的操作其實很類似 cout : myStream << "文本"; 讀取文件的操作其實很類似 cout : myStream >> variable;
可以用getline()函數一行一行地讀取文件。
游標(cursor)指示了寫入操作或讀取操作時,在文件中的位置。如果需要,可以移動這個游標。
今天的課就到這裡,一起加油吧!
下一課我們學習:小練習,猜單詞