• 文章
  • (Disch 著) 不要向二進位制檔案寫入任何變數
釋出
2013年11月8日 (最後更新:2013年11月8日)

(Disch 著) 不要向二進位制檔案寫入任何大於 1 位元組的變數

評分:3.5/5 (43 票)
*****
大家好!
我在二進位制檔案方面遇到了一些問題,並建立了一個帖子,Disch 提供了很大的幫助,我覺得最好不要讓那個帖子僅僅停留在那個帖子裡。(帖子連結:文章底部)
這篇文章是對這篇文章的背景介紹
Disch 的良好二進位制檔案教程
在這篇文章中,你不會看到“如何向二進位制檔案寫入資料”,而是會看到“為什麼我們不應該向二進位制檔案寫入大於 1 位元組的變數和資料。”
開始吧




當你對記憶體塊進行原始寫入時,`write()` 會檢視你給它的指標,並盲目地將 X 位元組複製到檔案中。對於 POD(純舊資料)型別,這 sort of 有效……但對於複雜型別(如字串),則完全無效。

讓我們看看為什麼。

****為什麼你不應該讀/寫複雜的非 POD 結構/類****

原因 #1:複雜型別可能包含動態分配的記憶體或其他指標

這裡有一個簡化的例子

1
2
3
4
5
6
7
8
9
class Foo
{
private:
    int* data;

public:
    Foo() { data = new int[10]; }
    ~Foo() { delete[] data; }
};


在這裡……我們的 `Foo` 類在概念上包含 10 個 int(約 40 位元組)的資訊。但如果你執行 `sizeof(Foo)`……它可能會給你一個指標的大小(約 4 位元組)。

這是因為 `Foo` 類不包含它所指向的資料……它只包含一個指向它的指標。因此……對檔案進行一次簡單寫入只會寫入指標,而不是實際資料。

之後嘗試讀取該資料只會導致得到一個指向隨機記憶體的指標。

這與字串的運作方式類似。字串資料實際上不在字串類中……而是動態分配的。

#2:非 POD 型別可能包含 VTables 和其他你絕對不能觸碰的“隱藏”資料

簡化的例子

1
2
3
4
5
6
class Foo
{
public:
    virtual ~Foo() { }
    int x;
};



`sizeof(Foo)` 可能會比 `sizeof(int)` 大,因為 `Foo` 現在是多型的……這意味著它有一個 VTable。VTables 是黑魔法,你絕對不能隨意擺弄它們,否則你就有可能毀掉你的程式。

但同樣……一次簡單的讀/寫並不會注意到這一點……只會嘗試讀/寫整個物件……包括 vtable。結果就是嚴重的錯誤。





所以是的。簡單的讀/寫對複雜型別無效,除非它們是 POD。

但如果你注意到,我之前說過 POD 型別只“sort of”有效。這是什麼意思?

****為什麼你不應該讀/寫 POD 結構/類****

讓我們再看一個簡單的例子

1
2
3
4
5
6
struct Foo
{
    char a;  // 1 byte
    int b;   // 4 bytes
    char c;  // 1 byte
};



這裡我們有一個 POD 結構。它不會出現前面提到的任何問題。我添加了註釋來說明每個單獨變數可能佔用的位元組數(技術上這可能有所不同,但這是典型的)。

所以,如果一個結構只是所有這些變數的集合……你期望結構的大小等於它們的總和……對嗎?那麼 `sizeof(Foo)` 應該是 6?

嗯……在我的機器上 `sizeof(Foo)` 是 12。驚喜!

發生的情況是,編譯器正在為結構新增填充,以便變數在某些記憶體邊界上對齊。這使得訪問它們更快。

所以,當你對檔案進行一次簡單的、原始的寫入時,它也會寫入填充位元組。當然,當你讀取它時……你會讀取填充位元組,並且如你所料,它會起作用。

那麼為什麼我說它只 sort of 有效呢?

嗯,考慮以下情況。

- 你執行你的程式並儲存了一些檔案。
- 你將你的程式移植到另一個平臺和/或更改或更新你的編譯器
- 這個新的編譯器碰巧為該結構分配了不同的填充
- 你執行新編譯的程式,並嘗試載入你儲存在舊版本程式中的檔案


由於填充已更改,資料將被以不同的方式讀取(讀取的資料更多或更少,或者填充在不同的位置)——因此讀取失敗,你會得到垃圾資料。


有一些方法可以告訴編譯器去掉填充。但這會引起其他我現在不想深入探討的問題。我們只說記憶體對齊很重要。


所以,好的……簡單來說……一次性讀/寫結構並不是一個好主意。所以只讀/寫單個變數可以……對吧?

嗯……

****為什麼你不應該讀/寫任何大於 1 位元組的變數****
有 2 件事情你需要注意。

#1:變數大小定義不明確。`int` 可能根據你的平臺/編譯器是 4 位元組……或者可能是 2 位元組,或者可能是 8 位元組。

所以讀/寫一個完整的`int` 會遇到與上面“填充”場景相同的問題。如果你有一個用你的程式 X 版本儲存的檔案,然後在 Y 版本中重新構建,其中 int 的大小突然改變了……你的檔案將不再載入。

這可以透過使用`` 型別,如 `uint8_t`、`uint16_t` 等來解決,這些型別都保證具有特定的位元組大小。


#2:位元組序。記憶體由一系列位元組組成。一個 int 在記憶體中如何儲存,當你進行原始寫入時,它在檔案中就是如何儲存的。但是 int 在記憶體中如何儲存,取決於你執行的機器。

x86/x64 機器是小端序。所以如果你有一個 `int foo = 1;`,在記憶體中 foo 會是這樣的
01 00 00 00
所以,如果你在你的 x86 機器上將 `foo` 儲存到檔案中……然後將該檔案交給你的朋友,他執行的是大端序機器……他會以同樣的方式讀回它。

然而……在大端序機器上……`01 00 00 00` 不是 1……而是 0x1000000……或者 **16777216**
所以是的……你的載入失敗了,你的程式也崩潰了。



這就是為什麼我堅持永遠不要向二進位制檔案讀/寫任何大於單個位元組的內容。這樣做可以確保你的檔案始終有效。




考慮到這一點……我寫了一篇文章,解釋瞭如何只透過讀/寫單個位元組來完成所有的二進位制檔案 IO。這包括如何讀/寫字串。

文章在這裡

http://www.cplusplus.com/articles/DzywvCM9/




這是 Disch 最初的論壇帖子
http://www.cplusplus.com/forum/beginner/108114/#msg587223