二進(jìn)制文件的讀寫對(duì)于普通文本要稍微麻煩一些,對(duì)二進(jìn)制文件的讀寫同樣需要打開文件和關(guān)閉文件,打開和關(guān)閉方式與文本文件相同,只不過需要在打開方式上加上ios::binary以指明以二進(jìn)制方式進(jìn)行讀寫。
C++二進(jìn)制文件讀寫對(duì)比文本文件的好處:
使用文本方式儲(chǔ)存信息比較我浪費(fèi)空間,也不便于檢索,如:一個(gè)學(xué)籍管理程序需要記錄所有學(xué)生的學(xué)號(hào)、姓名、年齡信息,并且能夠按照姓名查找學(xué)生的信息。程序中可以用一個(gè)類來表示學(xué)生:
- class CStudent
- {
- char szName[20]; //假設(shè)學(xué)生姓名不超過19個(gè)字符,以 '\0' 結(jié)尾
- char szId[l0]; //假設(shè)學(xué)號(hào)為9位,以 '\0' 結(jié)尾
- int age; //年齡
- };
Micheal Jackson 110923412 17
Tom Hanks 110923413 18
這種存儲(chǔ)方式不但浪費(fèi)空間,而且查找效率低下。因?yàn)槊總€(gè)學(xué)生的信息所占用的字節(jié)數(shù)不同,所以即使文件中的學(xué)生信息是按姓名排好序的,要用程序根據(jù)名字進(jìn)行查找仍然沒有什么好辦法,只能在文件中從頭到尾搜索。
如果把全部的學(xué)生信息都讀入內(nèi)存并排序后再查找,當(dāng)然速度會(huì)很快,但如果學(xué)生數(shù)巨大,則把所有學(xué)生信息都讀人內(nèi)存可能是不現(xiàn)實(shí)的。
可以用二進(jìn)制的方式來存儲(chǔ)學(xué)生信息,即把 CStudent 對(duì)象直接寫入文件。在該文件中,每個(gè)學(xué)生的信息都占用 sizeof(CStudent) 個(gè)字節(jié)。對(duì)象寫入文件后一般稱作“記錄”。本例中,每個(gè)學(xué)生都對(duì)應(yīng)于一條記錄。該學(xué)生記錄文件可以按姓名排序,則使用折半查找的效率會(huì)很高。
讀寫二進(jìn)制文件不能使用前面提到的類似于 cin、cout 從流中讀寫數(shù)據(jù)的方法。這時(shí)可以調(diào)用 ifstream 類和 fstream 類的 read 成員函數(shù)從文件中讀取數(shù)據(jù),調(diào)用 ofstream 和 fstream 的 write 成員函數(shù)向文件中寫入數(shù)據(jù)。
用 ostream::write 成員函數(shù)寫文件
ofstream 和 fstream 的 write 成員函數(shù)實(shí)際上繼承自 ostream 類,原型如下:
ostream & write(char* buffer, int count);
該成員函數(shù)將內(nèi)存中 buffer 所指向的 count 個(gè)字節(jié)的內(nèi)容寫入文件,返回值是對(duì)函數(shù)所作用的對(duì)象的引用,如 obj.write(...) 的返回值就是對(duì) obj 的引用。write 成員函數(shù)向文件中寫入若干字節(jié),可是調(diào)用 write 函數(shù)時(shí)并沒有指定這若干字節(jié)要寫入文件中的什么位置。那么,write 函數(shù)在執(zhí)行過程中到底把這若干字節(jié)寫到哪里呢?答案是從文件寫指針指向的位置開始寫入。
文件寫指針是 ofstream 或 fstream 對(duì)象內(nèi)部維護(hù)的一個(gè)變量。文件剛打開時(shí),文件寫指針指向文件的開頭(如果以 ios::app 方式打開,則指向文件末尾),用 write 函數(shù)寫入 n 個(gè)字節(jié),寫指針指向的位置就向后移動(dòng) n 個(gè)字節(jié)。
下面的程序從鍵盤輸入幾名學(xué)生的姓名和年齡(輸入時(shí),在單獨(dú)的一行中按 Ctrl+Z 鍵再按回車鍵以結(jié)束輸入。假設(shè)學(xué)生姓名中都沒有空格),并以二進(jìn)制文件形式存儲(chǔ),成為一個(gè)學(xué)生記錄文件 students.dat。
例子,用二進(jìn)制文件保存學(xué)生記錄:
- #include <iostream>
- #include <fstream>
- using namespace std;
- class CStudent
- {
- public:
- char szName[20];
- int age;
- };
- int main()
- {
- CStudent s;
- ofstream outFile("students.dat", ios::out | ios::binary);
- while (cin >> s.szName >> s.age)
- outFile.write((char*)&s, sizeof(s));
- outFile.close();
- return 0;
- }
Tom 60↙
Jack 80↙
Jane 40↙
^Z↙
則形成的 students.dat 為 72 字節(jié),用“記事本”程序打開呈現(xiàn)亂碼:
Tom燙燙燙燙燙燙燙燙 Jack燙燙燙燙燙燙燙? Jane燙燙燙燙燙燙燙?
第 13 行指定文件的打開模式是 ios::out|ios::binary,即以二進(jìn)制寫模式打開。在 Windows平臺(tái)中,用二進(jìn)制模式打開是必要的,否則可能出錯(cuò)。
第 15 行將 s 對(duì)象寫入文件。s 的地址就是要寫入文件的內(nèi)存緩沖區(qū)的地址。但是 &s 不是 char * 類型,因此要進(jìn)行強(qiáng)制類型轉(zhuǎn)換。
第 16 行,文件使用完畢一定要關(guān)閉,否則程序結(jié)束后文件的內(nèi)容可能不完整。
用 istream::read 成員函數(shù)讀文件
ifstream 和 fstream 的 read 成員函數(shù)實(shí)際上繼承自 istream 類,原型如下:
istream & read(char* buffer, int count);
該成員函數(shù)從文件中讀取 count 個(gè)字節(jié)的內(nèi)容,存放到 buffer 所指向的內(nèi)存緩沖區(qū)中,返回值是對(duì)函數(shù)所作用的對(duì)象的引用。如果想知道一共成功讀取了多少個(gè)字節(jié)(讀到文件尾時(shí),未必能讀取 count 個(gè)字節(jié)),可以在 read 函數(shù)執(zhí)行后立即調(diào)用文件流對(duì)象的 gcount 成員函數(shù),其返回值就是最近一次 read 函數(shù)執(zhí)行時(shí)成功讀取的字節(jié)數(shù)。gcount 是 istream 類的成員函數(shù),原型如下:
int gcount();
read 成員函數(shù)從文件讀指針指向的位置開始讀取若干字節(jié)。文件讀指針是 ifstream 或 fstream 對(duì)象內(nèi)部維護(hù)的一個(gè)變量。文件剛打開時(shí),文件讀指針指向文件的開頭(如果以ios::app 方式打開,則指向文件末尾),用 read 函數(shù)讀取 n 個(gè)字節(jié),讀指針指向的位置就向后移動(dòng) n 個(gè)字節(jié)。因此,打開一個(gè)文件后連續(xù)調(diào)用 read 函數(shù),就能將整個(gè)文件的內(nèi)容讀取出來。下面的程序?qū)⑶懊鎰?chuàng)建的學(xué)生記錄文件 students.dat 的內(nèi)容讀出并顯示。
- #include <iostream>
- #include <fstream>
- using namespace std;
- class CStudent
- {
- public:
- char szName[20];
- int age;
- };
- int main()
- {
- CStudent s;
- ifstream inFile("students.dat",ios::in|ios::binary); //二進(jìn)制讀方式打開
- if(!inFile) {
- cout << "error" <<endl;
- return 0;
- }
- while(inFile.read((char *)&s, sizeof(s))) { //一直讀到文件結(jié)束
- int readedBytes = inFile.gcount(); //看剛才讀了多少字節(jié)
- cout << s.szName << " " << s.age << endl;
- }
- inFile.close();
- return 0;
- }
Tom 60
Jack 80
Jane 40
第 18 行,判斷文件是否已經(jīng)讀完的方法和 while(cin>>n) 類似,歸根到底都是因?yàn)?istream 類重載了 bool 強(qiáng)制類型轉(zhuǎn)換運(yùn)算符。
第 19 行只是演示 gcount 函數(shù)的用法,刪除該行對(duì)程序運(yùn)行結(jié)果沒有影響。
思考題:關(guān)于 students.dat 的兩個(gè)程序中,如果 CStudent 類的 szName 的定義不是“char szName[20] ”而是“string szName”,是否可以?為什么?
用文件流類的 put 和 get 成員函數(shù)讀寫文件
可以用 ifstream 和 fstream 類的 get 成員函數(shù)(繼承自 istream 類)從文件中一次讀取一個(gè)字節(jié),也可以用 ofstream 和 fstream 類的 put 成員函數(shù)(繼承自 ostream 類) 向文件中一次寫入一個(gè)字節(jié)。
例題:編寫一個(gè) mycopy 程序,實(shí)現(xiàn)文件復(fù)制的功能。用法是在“命令提示符”窗口輸入:
mycopy 源文件名 目標(biāo)文件名
就能將源文件復(fù)制到目標(biāo)文件。例如:mycopy src.dat dest.dat
即將 src.dat 復(fù)制到 dest.dat。如果 dest.dat 原本就存在,則原來的文件會(huì)被覆蓋。解題的基本思路是每次從源文件讀取一個(gè)字節(jié),然后寫入目標(biāo)文件。程序如下:
- #include <iostream>
- #include <fstream>
- using namespace std;
- int main(int argc, char* argv[])
- {
- if (argc != 3) {
- cout << "File name missing!" << endl;
- return 0;
- }
- ifstream inFile(argv[l], ios::binary | ios::in); //以二進(jìn)制讀模式打開文件
- if (!inFile) {
- cout << "Source file open error." << endl;
- return 0;
- }
- ofstream outFile(argv[2], ios::binary | ios::out); //以二進(jìn)制寫模式打開文件
- if (!outFile) {
- cout << "New file open error." << endl;
- inFile.close(); //打開的文件一定要關(guān)閉
- return 0;
- }
- char c;
- while (inFile.get(c)) //每次讀取一個(gè)字符
- outFile.put(c); //每次寫入一個(gè)字符
- outFile.close();
- inFile.close();
- return 0;
- }
操作系統(tǒng)在接收到寫文件的請(qǐng)求時(shí),也是先把要寫入的數(shù)據(jù)在一個(gè)內(nèi)存緩沖區(qū)中保存起來,等緩沖區(qū)滿后,再將緩沖區(qū)的內(nèi)容全部寫入磁盤。關(guān)閉文件的操作就能確保內(nèi)存緩沖區(qū)中的數(shù)據(jù)被寫入磁盤。
盡管如此,要連續(xù)讀寫文件時(shí),像 mycopy 程序那樣一個(gè)字節(jié)一個(gè)字節(jié)地讀寫,還是不如一次讀寫一片內(nèi)存區(qū)域快。每次讀寫的字節(jié)數(shù)最好是 512 的整數(shù)倍。
1. 二進(jìn)制文件寫入示例:
//采用C模式寫二進(jìn)制文件 void DataWrite_CMode() { //準(zhǔn)備數(shù)據(jù) double pos[200]; for(int i = 0; i < 200; i ++ ) pos[i] = i ; //寫出數(shù)據(jù) FILE *fid; fid = fopen("binary.dat","wb"); if(fid == NULL) { printf("寫出文件出錯(cuò)"); return; } int mode = 1; printf("mode為1,逐個(gè)寫入;mode為2,逐行寫入\n"); scanf("%d",&mode); if(1==mode) { for(int i = 0; i < 200; i++) fwrite(&pos[i],sizeof(double),1,fid); } else if(2 == mode) { fwrite(pos, sizeof(double), 200, fid); } fclose(fid); } 2. 二進(jìn)制文件讀取示例:
//采用C模式讀二進(jìn)制文件 void DataRead_CMode() { FILE *fid; fid = fopen("binary.dat","rb"); if(fid == NULL) { printf("讀取文件出錯(cuò)"); return; } int mode = 1; printf("mode為1,知道pos有多少個(gè);mode為2,不知道pos有多少個(gè)\n"); scanf("%d",&mode); if(1 == mode) { double pos[200]; fread(pos,sizeof(double),200,fid); for(int i = 0; i < 200; i++) printf("%lf\n", pos[i]); free(pos); } else if(2 == mode) { //獲取文件大小 fseek (fid , 0 , SEEK_END); long lSize = ftell (fid); rewind (fid); //開辟存儲(chǔ)空間 int num = lSize/sizeof(double); double *pos = (double*) malloc (sizeof(double)*num); if (pos == NULL) { printf("開辟空間出錯(cuò)"); return; } fread(pos,sizeof(double),num,fid); for(int i = 0; i < num; i++) printf("%lf\n", pos[i]); free(pos); //釋放內(nèi)存 } fclose(fid); }