1. 程式人生 > >Qt 學習 之 二進位制檔案讀寫

Qt 學習 之 二進位制檔案讀寫

在上一章中,我們介紹了有關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();
重新執行一下程式,就OK了。

我們已經獲得宇宙終極問題的答案了,下面,我們要將這個答案讀取出來:

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_8);

上面一句是檔案的版本號,但是,Qt 不同版本之間的讀取方式可能也不一樣。這樣,我們就得指定 Qt 按照哪個版本去讀。這裡,我們指定以 Qt 4.8 格式去讀取內容。

當我們這樣寫入檔案之後,我們在讀取的時候就需要增加一系列的判斷:

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;