• 文章
  • Disch 的優秀二進位制檔案教程
釋出者:
2012 年 8 月 8 日 (最後更新:2012 年 8 月 8 日)

Disch 的優秀二進位制檔案教程

評分:4.3/5 (264 票)
*****
****=== 正確的二進位制檔案 I/O 方法 ===****

1) 定義你的構建塊
二進位制檔案,在其核心,不過是一系列位元組。這意味著任何大於位元組(注意:幾乎所有東西)的東西都需要以位元組為單位來定義。對於大多數基本型別來說,這很簡單。

C++ 提供了一些常用的整數型別。有 charshortintlong(以及其他)。

這些型別的問題在於它們的大小沒有被很好地定義。`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,我已經幫你把這個轉成文章了!希望大家喜歡!