二進制文件的讀寫對于普通文本要稍微麻煩一些,對二進制文件的讀寫同樣需要打開文件和關閉文件,打開和關閉方式與文本文件相同,只不過需要在打開方式上加上ios::binary以指明以二進制方式進行讀寫。

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