****=== 正確的二進位制檔案 I/O 方法 ===****
1) 定義你的構建塊
二進位制檔案,在其核心,不過是一系列位元組。這意味著任何大於位元組(注意:幾乎所有東西)的東西都需要以位元組為單位來定義。對於大多數基本型別來說,這很簡單。
C++ 提供了一些常用的整數型別。有
char
、
short
、
int
和
long
(以及其他)。
這些型別的問題在於它們的大小沒有被很好地定義。`int` 在一臺機器上可能是 8 位元組,但在另一臺機器上可能只有 4 位元組。唯一一致的是 `char`... 它保證始終是 1 位元組。
對於你的檔案,你需要定義你自己的整數型別。
這裡有一些基礎
u8 = 無符號 8 位 (1 位元組) (即:unsigned char)
u16 = 無符號 16 位 (2 位元組) (即:unsigned short -- 通常)
u32 = 無符號 32 位 (4 位元組) (即:unsigned int -- 通常)
s8, s16, s32 = 以上型別的有符號版本
u8 和 s8 都是 1 位元組,所以它們實際上不需要被定義。它們可以“按原樣”儲存。但對於較大的型別,你需要選擇一種位元組序。
在本例中,我們選擇小端序,這意味著一個 2 位元組的變數 (u16) 將先儲存低位元組,再儲存高位元組。因此,當在十六進位制編輯器中檢查檔案時,值
0x1122
將在檔案中顯示為
22 11
。
使用 iostream 安全讀寫 u16 的示例方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
u16 ReadU16(istream& file)
{
u16 val;
u8 bytes[2];
file.read( (char*)bytes, 2 ); // read 2 bytes from the file
val = bytes[0] | (bytes[1] << 8); // construct the 16-bit value from those bytes
return val;
}
void WriteU16(ostream& file, u16 val)
{
u8 bytes[2];
// extract the individual bytes from our value
bytes[0] = (val) & 0xFF; // low byte
bytes[1] = (val >> 8) & 0xFF; // high byte
// write those bytes to the file
file.write( (char*)bytes, 2 );
}
|
u32 的處理方式相同,但你需要將其分解並以 4 個位元組而不是 2 個位元組來重構它。
2) 定義你的複雜型別
這裡主要是字串,所以我將重點介紹字串。
有幾種儲存字串的方法。
1) 你可以規定它們是固定寬度。例如:你的字串將以 128 位元組的寬度儲存。如果實際字串較短,檔案將被填充。如果實際字串較長,寫入檔案的資料將被截斷(丟失)。
- 優點:實現最簡單
- 缺點:如果有大量短字串,檔案空間使用效率低下,字串的最大長度受到限制。
2) 你可以使用 C 字串的“空終止符”來標記字串的結尾
- 優點:任意長度的字串。
- 缺點:字串中不能嵌入空字元。如果你的字串在寫入時包含空字元,它將導致檔案載入不正確。可能是最難實現的
3) 你可以寫入一個 u32 來指定字串的長度,然後在其後寫入字串資料。
- 優點:任意長度的字串,可以包含任何字元(包括空字元)。
- 缺點:每個字串有 4 個額外的位元組,使得空間效率比方法 #2 稍低(但也不是很多)。
我更傾向於選項 #3。以下是可靠地讀寫二進位制檔案字串的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
string ReadString(istream& file)
{
u32 len = ReadU32(file);
char* buffer = new char[len];
file.read(buffer, len);
string str( buffer, len );
delete[] buffer;
return str;
}
void WriteString(istream& file, string str)
{
u32 len = str.length();
WriteU32(file, len);
file.write( str.c_str(), len );
}
|
向量/列表/等也可以用類似的方式處理。你開始寫入大小作為 u32,然後讀取/寫入檔案中相應數量的單個元素。
3) 定義你的檔案格式
這是關鍵。既然你已經定義了術語,你就可以構建你想要的檔案外觀。我通常會開啟一個文字編輯器,在一個看起來像這樣的頁面上勾勒出它
1 2 3 4 5 6 7
|
char[4] header "MyFi" - identifies this file as my kind of file
u32 version 1 for this version of the spec
u32 foo some data
string bar some more data
vector<u16> baz some more data
...
|
這勾勒出了檔案將如何外觀/行為。舉個例子,如果你在十六進位制編輯器中檢視此檔案,你會看到這樣
1 2
|
4D 79 46 69 01 00 00 00 06 94 00 00 03 00 00 00
4D 6F 6F 02 00 00 00 EF BE 0D F0
|
由於檔案格式定義得如此清晰,僅僅檢查這個檔案就能告訴你檔案確切的包含內容。
前 4 個位元組:
4D 79 46 69
- 這些是字串 "MyFi" 的 ASCII 碼,它將此檔案標識為我們型別的檔案(而不是 wav 或 mp3 檔案之類的,它們將有不同的頭)。
接下來的 4 個位元組:
01 00 00 00
- 字面值 1,表示此檔案是“版本 1”。如果你以後決定修改此檔案格式,你可以使用此版本號來支援讀取舊檔案。
接下來的 4 個位元組用於我們的“foo”資料:
06 94 00 00
表示 foo == 0x9406
之後是一個字串(“bar”)。字串以 4 個位元組開始表示長度:
03 00 00 00
表示長度為 3。所以接下來的 3 個位元組
4D 6F 6F
形成了字串的 ASCII 資料(在本例中是:“Moo”)
之後是我們的向量(“baz”)。想法一樣... 以 4 個位元組開始表示長度:
02 00 00 00
,表示長度為 2
然後檔案中有兩個 u16。第一個是
EF BE
(0xBEEF),第二個是
0D F0
(0xF00D)
你會發現所有常見的二進位制檔案格式,如 .zip, .rar, .mp3, .wav, .bmp 等等,都是這樣定義的。它絕對不留任何偶然。
鳴謝 Disch,他寫了所有這些,我只是把它複製在這裡,因為
(Disch 在上面教程之後的那篇帖子中寫了這些)
我真的應該把這些寫成文章而不是論壇帖子。真是的。有人想幫我把這個轉成文章嗎?我現在太懶了。
好吧 Disch,我已經幫你把這個轉成文章了!希望大家喜歡!