#(36):二進制文件讀寫
在上一章中,我們介紹了有關`QFile`和`QFileInfo`兩個類的使用。我們提到,`QIODevice`提供了`read()`、`readLine()`等基本的操作。同時,Qt 還提供了更高一級的操作:用于二進制的流`QDataStream`和用于文本流的`QTextStream`。本節,我們將講解有關`QDataStream`的使用以及一些技巧。下一章則是`QTextStream`的相關內容。
`QDataStream`提供了基于`QIODevice`的二進制數據的序列化。數據流是一種二進制流,這種流**完全不依賴**于底層操作系統、CPU 或者字節順序(大端或小端)。例如,在安裝了 Windows 平臺的 PC 上面寫入的一個數據流,可以不經過任何處理,直接拿到運行了 Solaris 的 SPARC 機器上讀取。由于數據流就是二進制流,因此我們也可以直接讀寫沒有編碼的二進制數據,例如圖像、視頻、音頻等。
`QDataStream`既能夠存取 C++ 基本類型,如 int、char、short 等,也可以存取復雜的數據類型,例如自定義的類。實際上,`QDataStream`對于類的存儲,是將復雜的類分割為很多基本單元實現的。
結合`QIODevice`,`QDataStream`可以很方便地對文件、網絡套接字等進行讀寫操作。我們從代碼開始看起:
~~~
QFile file("file.dat");
file.open(QIODevice::WriteOnly);
QDataStream out(&file);
out << QString("the answer is");
out << (qint32)42;
~~~
在這段代碼中,我們首先打開一個名為 file.dat 的文件(注意,我們為簡單起見,并沒有檢查文件打開是否成功,這在正式程序中是不允許的)。然后,我們將剛剛創建的`file`對象的指針傳遞給一個`QDataStream`實例`out`。類似于`std::cout`標準輸出流,`QDataStream`也重載了輸出重定向`<<`運算符。后面的代碼就很簡單了:將“the answer is”和數字 42 輸出到數據流(如果你不明白這句話的意思,這可是宇宙終極問題的答案 ;-P 請自行搜索《銀河系漫游指南》)。由于我們的 out 對象建立在`file`之上,因此相當于將宇宙終極問題的答案寫入`file`。
需要指出一點:最好使用 Qt 整型來進行讀寫,比如程序中的`qint32`。這保證了在任意平臺和任意編譯器都能夠有相同的行為。
我們通過一個例子來看看 Qt 是如何存儲數據的。例如`char *`字符串,在存儲時,會首先存儲該字符串包括 \0 結束符的長度(32位整型),然后是字符串的內容以及結束符 \0。在讀取時,先以 32 位整型讀出整個的長度,然后按照這個長度取出整個字符串的內容。
但是,如果你直接運行這段代碼,你會得到一個空白的 file.dat,并沒有寫入任何數據。這是因為我們的`file`沒有正常關閉。為性能起見,數據只有在文件關閉時才會真正寫入。因此,我們必須在最后添加一行代碼:
~~~
file.close(); // 如果不想關閉文件,可以使用 file.flush();
~~~
重新運行一下程序,你就得到宇宙終極問題的答案了。
我們已經獲得宇宙終極問題的答案了,下面,我們要將這個答案讀取出來:
~~~
QFile file("file.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
QString str;
qint32 a;
in >> str >> a;
~~~
這段代碼沒什么好說的。唯一需要注意的是,你必須按照寫入的順序,將數據讀取出來。也就是說,程序數據寫入的順序必須預先定義好。在這個例子中,我們首先寫入字符串,然后寫入數字,那么就首先讀出來的就是字符串,然后才是數字。順序顛倒的話,程序行為是不確定的,嚴重時會直接造成程序崩潰。
由于二進制流是純粹的字節數據,帶來的問題是,如果程序不同版本之間按照不同的方式讀取(前面說過,Qt 保證讀寫內容的一致,但是并不能保證不同 Qt 版本之間的一致),數據就會出現錯誤。因此,我們必須提供一種機制來確保不同版本之間的一致性。通常,我們會使用如下的代碼寫入:
~~~
QFile file("file.dat");
file.open(QIODevice::WriteOnly);
QDataStream out(&file);
// 寫入魔術數字和版本
out << (quint32)0xA0B0C0D0;
out << (qint32)123;
out.setVersion(QDataStream::Qt_4_0);
// 寫入數據
out << lots_of_interesting_data;
~~~
這里,我們增加了兩行代碼:
~~~
out << (quint32)0xA0B0C0D0;
~~~
用于寫入魔術數字。所謂魔術數字,是二進制輸出中經常使用的一種技術。二進制格式是人不可讀的,并且通常具有相同的后綴名(比如 dat 之類),因此我們沒有辦法區分兩個二進制文件哪個是合法的。所以,我們定義的二進制格式通常具有一個魔術數字,用于標識文件的合法性。在本例中,我們在文件最開始寫入 0xA0B0C0D0,在讀取的時候首先檢查這個數字是不是 0xA0B0C0D0。如果不是的話,說明這個文件不是可識別格式,因此根本不需要去繼續讀取。一般二進制文件都會有這么一個魔術數字,例如 Java 的 class 文件的魔術數字就是 0xCAFEBABE,使用二進制查看器就可以查看。魔術數字是一個 32 位的無符號整型,因此我們使用`quint32`來得到一個平臺無關的 32 位無符號整型。
接下來一行,
~~~
out << (qint32)123;
~~~
是標識文件的版本。我們用魔術數字標識文件的類型,從而判斷文件是不是合法的。但是,文件的不同版本之間也可能存在差異:我們可能在第一版保存整型,第二版可能保存字符串。為了標識不同的版本,我們只能將版本寫入文件。比如,現在我們的版本是 123。
下面一行還是有關版本的:
~~~
out.setVersion(QDataStream::Qt_4_0);
~~~
上面一句是文件的版本號,但是,Qt 不同版本之間的讀取方式可能也不一樣。這樣,我們就得指定 Qt 按照哪個版本去讀。這里,我們指定以 Qt 4.0 格式去讀取內容。
當我們這樣寫入文件之后,我們在讀取的時候就需要增加一系列的判斷:
~~~
QFile file("file.dat");
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
// 檢查魔術數字
quint32 magic;
in >> magic;
if (magic != 0xA0B0C0D0) {
return BAD_FILE_FORMAT;
}
// 檢查版本
qint32 version;
in >> version;
if (version < 100) {
return BAD_FILE_TOO_OLD;
}
if (version > 123) {
return BAD_FILE_TOO_NEW;
}
if (version <= 110) {
in.setVersion(QDataStream::Qt_3_2);
} else {
in.setVersion(QDataStream::Qt_4_0);
}
// 讀取數據
in >> lots_of_interesting_data;
if (version >= 120) {
in >> data_new_in_version_1_2;
}
in >> other_interesting_data;
~~~
這段代碼就是按照前面的解釋進行的。首先讀取魔術數字,檢查文件是否合法。如果合法,讀取文件版本:小于 100 或者大于 123 都是不支持的。如果在支持的版本范圍內(100 <= version <= 123),則當是小于等于 110 的時候,按照`Qt_3_2`的格式讀取,否則按照`Qt_4_0`的格式讀取。當設置完這些參數之后,開始讀取數據。
至此,我們介紹了有關`QDataStream`的相關內容。那么,既然`QIODevice`提供了`read()`、`readLine()`之類的函數,為什么還要有`QDataStream`呢?`QDataStream`同`QIODevice`有什么區別?區別在于,`QDataStream`提供流的形式,性能上一般比直接調用原始 API 更好一些。我們通過下面一段代碼看看什么是流的形式:
~~~
QFile file("file.dat");
file.open(QIODevice::ReadWrite);
QDataStream stream(&file);
QString str = "the answer is 42";
QString strout;
stream << str;
file.flush();
stream >> strout;
~~~
在這段代碼中,我們首先向文件中寫入數據,緊接著把數據讀出來。有什么問題嗎?運行之后你會發現,`strout`實際是空的。為什么沒有讀取出來?我們不是已經添加了`file.flush();`語句嗎?原因并不在于文件有沒有寫入,而是在于我們使用的是“流”。所謂流,就像水流一樣,它的游標會隨著輸出向后移動。當使用`<<`操作符輸出之后,流的游標已經到了最后,此時你再去讀,當然什么也讀不到了。所以你需要在輸出之后重新把游標設置為 0 的位置才能夠繼續讀取。具體代碼片段如下:
~~~
stream << str;
stream.device()->seek(0);
stream >> strout;
~~~
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)