C++ 入門指南

單元 31 - 存檔與載入

本書已有新版,請參考 C++ 入門指南 4.01 - 單元 31 - 完成版的 encrypt_gui

我們要存檔,儲存的到底是什麼呢?物件 (object) 嗎?資料成員 (data member) 嗎?還有成員函數 (member function) 呢?

物件 → 資料成員
    ↘ 
      成員函數

如果某次編碼結果不錯,我們往後想要繼續利用同一個 Encrypt 物件,這時候就需要把 Encrypt 物件儲存下來。想一想我們存檔應該儲存什麼?把整個 Encrypt 物件都儲存下來,還是只要儲存編碼用的 code_array 就可以了呢?

問題很簡單,就是存檔究竟要儲存什麼,儲存整個 Encrypt 物件也不是不行,不過這樣存檔載入都變得有點複雜,物件還得序列化 (serialization) 等等。想一想,如果情況改成遊戲程式的話,儲存的不外是遊戲狀態或進度的資料表,資料表中大概都是數字或字串 (string) ,因此實際儲存的也會是數字跟字串

如果是要儲存數字或字串,解決問題的方式就更簡單了,因為數字或字串都是很常用的資料格式,程式庫針對很常用的資料格式都有直接套用的方式,因此我們可以直接儲存密碼表字串就可以了。

我們對 on_pushButton_save_clicked() 的實作如下

// 按下「儲存」按鈕的事件
void EncryptWindow::on_pushButton_save_clicked()
{
    // 先測試是否有按過「新建」按鈕
    if (e != nullptr) {
        // 有按過「新建」按鈕,建立檔名 encryptor 的 QFile 物件
        QFile file("encryptor");
        // 以寫入模式開啟檔案
        file.open(QIODevice::WriteOnly);
        // 建立 QDataStream 物件讀取檔案串流
        QDataStream out(&file);
        // 從 QDataStream 物件將密碼表輸出到檔案
        out << s2q(e->get_code_array());
        
        // 最後在 label_display 顯示提示訊息
        ui->label_display->setText("已儲存。");
    }
    else {
        // 沒按過「新建」按鈕,在 label_display 顯示提示訊息
        ui->label_display->setText("無編碼物件,無法儲存。");
    }
}

完整程式請參考 encryptwindow.cpp

這裡同樣做了個預防措施,如果 enullptr ,就是使用者沒有按過 新建 按鈕,因此沒有密碼表可以儲存,所以直接在 label_display 顯示提示訊息。

實際存檔是建立 QFile 型態 (type) 的物件 file ,並以檔名字串建構函數 (constructor) 的參數 (parameter) ,接著設定開啟方式為寫入,也就是 QIODevice::WriteOnly

// 有按過「新建」按鈕,建立檔名 encryptor 的 QFile 物件
QFile file("encryptor");
// 以寫入模式開啟檔案
file.open(QIODevice::WriteOnly);

接著用 QDataStream 型態的物件 out 進行寫入,需要以 QFile 物件的指標當參數,最後用輸出運算子將密碼表寫到 out 內,也就是存到檔案 encryptor

// 建立 QDataStream 物件讀取檔案串流
QDataStream out(&file);
// 從 QDataStream 物件將密碼表輸出到檔案
out << s2q(e->get_code_array());

這裡的變數 out 跟標準程式庫 iostream 中的 out 有一樣的名稱,兩者都表示輸出,前者輸出到檔案,後者則是輸出到標準輸出裝置。

Mac 系統裡 encryptor 檔案可能會儲存在編譯好的 encrypt_gui 執行檔套件內。

這樣就完成存檔了,倒是用了 QFileQDataStream ,就要先 #include 進來

#include <QFile>
#include <QDataStream>

下面我們繼續看到載入 on_pushButton_load_clicked() 的實作

// 按下「載入」按鈕的事件
void EncryptWindow::on_pushButton_load_clicked() 
{
    // 建立檔名 encryptor 的 QFile 物件
    QFile file("encryptor");
    // 以唯讀模式開啟,先測試檔案存不存在
    if (file.open(QIODevice::ReadOnly)) {
        // 檔案存在,建立 QDataStream 物件讀取檔案串流
        QDataStream in(&file);
        // 建立一個 QString 字串暫存密碼表
        QString temp;
        // 從 QDataStream 物件將密碼表輸出到 QString 字串
        in >> temp;
        
        // 如果使用者沒按過「新建」,先新建成員變數 e
        if (e == nullptr) {
            e = new Encrypt;
        }
        
        // 將密碼表寫入成員變數 e
        e->set_code_array(q2s(temp));
        
        // 最後在 label_display 顯示提示訊息
        ui->label_display->setText(""編碼物件已載入。"");
    }
    else {
        // 檔案不存在,在 label_display 顯示提示訊息
        ui->label_display->setText("編碼物件無法載入。");
    }
}

跟存檔比較不一樣的是這裡先建立 QFile 檔案物件 file ,然後用唯獨模式 QIODevice::ReadOnly 開啟, open() 會傳布林值,若存在檔案就是 true

// 建立檔名 encryptor 的 QFile 物件
QFile file("encryptor");
// 以唯讀模式開啟,先測試檔案存不存在
if (file.open(QIODevice::ReadOnly)) {

在其他程式語言中,有關檔案的部份可能要放到例外處理 (exception handling) 之中, C++ 也有例外處理的機制,倒是沒有強制限制要用例外處理檔案,因此直接測試檔案是否存在,如此可減少額外的處理時間。

接下來由 QDataStream 型態的 in 寫入資料到 QStringtemp

// 檔案存在,建立 QDataStream 物件讀取檔案串流
QDataStream in(&file);
// 建立一個 QString 字串暫存密碼表
QString temp;
// 從 QDataStream 物件將密碼表輸出到 QString 字串
in >> temp;

後續測試 e 是不是 nullptr ,如果是 nullptr 就新建 Encrypt ,不然可能會發生不能寫入的問題,最後就直接用 e 設定密碼表。

// 如果使用者沒按過「新建」,先新建成員變數 e
if (e == nullptr) {
    e = new Encrypt;
}

// 將密碼表寫入成員變數 e
e->set_code_array(q2s(temp));

咦?有參數版本的 set_code_array() 並無實作,留待練習來實作好了,下個單元我們繼續完成其他功能囉!

相關教學影片

中英文術語對照
物件 object
資料成員 data member
成員函數 member function
序列化 serialization
字串 string
型態 type
建構函數 constructor
參數 parameter
例外處理 exception handling
重點整理
1. 存檔機制採用儲存密碼表的方式,利用 QFileQDataStream 將密碼表寫進 encryptor 中。
2. 載入同樣利用 QFileQDataStream ,將密碼表讀取到暫存的 QString 字串,然後再寫入密碼表。
問題與討論
1. 比較儲存物件及儲存字串的優劣,列出兩者的優缺點。
2. 什麼是例外處理?什麼情況下要做例外處理?
練習
1. 實作 set_code_array(string) ,除了要在實作檔實作外,也要在標頭檔加入宣告。
2. 承接上一個單元的 guess_game 專案,想一想要不要有存檔的機制。

上一頁 單元 30 - QString 的問題
回 C++ 入門指南目錄
下一頁 單元 32 - 完成版的 encrypt_gui
回 C++ 教材
回程式語言教材首頁