天天看點

C++中使用boost庫存取ini結構化文本檔案

作者:海洋餅幹叔叔

包含如下内容的檔案dora.ini存儲了學号為20210426的某同學的姓名、年齡、以及已修三門課程的名稱和分數。這種名為ini的檔案格式可以很友善地存儲結構化的對象資訊。相較于自行設計文本檔案的内容結構,直接使用ini格式既友善,擴充性又好。本實踐中,我們借助于大名鼎鼎的boost庫來解析ini檔案。

[basic]
sNo=20210426
sName=Dora Chen
iAge=17
[scores]
size=3
sName_0=C++
iScore_0=97
sName_1=Calculus
iScore_1=70
sName_2=Economics
iScore_2=65           

知識産權協定

允許以教育/教育訓練為目的向學生或閱聽人進行免費引用,展示或者講述,無須取得作者同意。

不允許以電子/紙質出版為目的進行摘抄或改編。

在實踐中,我們經常需要借助類和對象來表示一個個的實體,例如學籍管理系統中的學生、醫療檔案管理系統中的病人。請看如下資料結構:

class Score {
public:
    string sName;            //課程名稱
    int iScore;              //分數
};

class Student {
    string sNo;              //學号
    string sName;            //姓名
    int iAge;                //年齡
    vector<Score> scores;    //成績表
}           

在這個資料結構中,一個Student對象代表一個學生,其有學号、姓名、年齡等屬性;另外還有一個類型為向量的屬性scores,該屬性存儲了學生0到多門已修課程的成績對象,該對象有課程名稱及分數兩個屬性。

現在考慮将Student對象序列化(儲存)到一個文本檔案裡。在這個資料結構裡,一個學生有多少門已修課程是不确定的。對于這種帶有不确定性的甚至預期可能發生改變(比如增加性别屬性)的資料結構,程式設計者自行組織檔案的存儲格式面臨諸多不便:①繁瑣;②未來資料結構改變時,調整困難。

有一種稱之為ini的文本檔案結構特别适合存儲此種資料結構。ini是initialization(初始化)的簡寫,這種檔案本來的用途是用于存儲軟體的配置資訊,但有也人(比如作者)喜歡借用這個結構來序列化對象。

接下來,我們通過boost庫的ini_parser子產品來完成ini檔案的存儲和解析。在介紹C++程式StudentInfo之前,我們先展示StudentInfo所儲存出來的dora.ini檔案的内容。

[basic]
sNo=20210426
sName=Dora Chen
iAge=17
[scores]
size=3
sName_0=C++
iScore_0=97
sName_1=Calculus
iScore_1=70
sName_2=Economics
iScore_2=65           

容易看出,ini檔案最基本的資訊形式為key=value。等号左邊為鍵(key),右邊為值(value)。dora.ini分為兩個部分,[basic]部分用于存儲學号、姓名和年齡,[scores]部分則用于存儲全部已修課程的成績資訊。鍵size=3表明存儲了三門課的成績,由于每門課都有課程名稱和分數,為消除歧義,故使用sName_i來表示第i門課的課程名稱,iScore_i來表示第i門課的分數。

  C++程式StudentInfo先是建立了用于表示Dora Chen的學生對象dora1,并為其添加了C++、微積分、經濟學三門課程的成績;然後将該對象序列化存儲至檔案dora.ini;然後再從dora.ini讀取其内容至學生對象dora2并列印出來。完整代碼如下:

//Project - StudentInfo
#include <iostream>
#include <vector>
#include <iomanip>
#include <boost/property_tree/ini_parser.hpp>
#include <boost/property_tree/ptree.hpp>
using namespace std;

class Score {
public:
    string sName;           //課程名稱
    int iScore;             //分數
    Score(const string& name, const int score){
        sName = name;
        iScore = score;
    }
};

class Student {
    string sNo;             //學号
    string sName;           //姓名
    int iAge;               //年齡
    vector<Score> scores;   //成績表

public:
    Student(){}
    Student(const string& no, const string& name, const int age){
        sNo = no; sName = name; iAge = age;
    }

    void addScore(const string& name, const int score){
        scores.emplace_back(name,score);
    }

    void save(const string& sFile){
        boost::property_tree::ptree s;

        s.put("basic.sNo",sNo);
        s.put("basic.sName",sName);
        s.put("basic.iAge",iAge);

        s.put("scores.size",scores.size());
        for (unsigned int i=0;i<scores.size();i++){
            auto& r = scores[i];
            s.put(string("scores.sName_")+std::to_string(i),r.sName);
            s.put(string("scores.iScore_")+std::to_string(i),r.iScore);
        }

        boost::property_tree::ini_parser::write_ini(sFile,s);
    }

    void load(const string& sFile){
        boost::property_tree::ptree s;
        boost::property_tree::ini_parser::read_ini(sFile,s);

        sNo = s.get("basic.sNo","");
        sName = s.get("basic.sName","");
        iAge = s.get("basic.iAge",0);

        scores.clear();
        auto size = s.get("scores.size",0);
        for (auto i=0;i<size;i++){
            auto sName = s.get(string("scores.sName_")+std::to_string(i),"");
            auto iScore = s.get(string("scores.iScore_")+std::to_string(i),0);
            scores.emplace_back(sName,iScore);
        }
    }

    void output(ostream& o){
        o << left;
        o << setw(10)<<"No."<<setw(15)<<"Name"<<setw(6)<<"Age"<<endl;
        o << "-------------------------------" << endl;
        o << setw(10)<<sNo<<setw(15)<<sName<<setw(6)<<iAge<<endl;
        o << "-------------------------------" << endl;
        for (auto& s:scores)
            o << setw(25) << s.sName << setw(6) << s.iScore << endl;
    }
};

int main() {
    Student dora1("20210426","Dora Chen",17);
    dora1.addScore("C++",97);
    dora1.addScore("Calculus",70);
    dora1.addScore("Economics",65);
    dora1.save("dora.ini");             //儲存對象dora1至檔案dora.ini

    Student dora2;
    dora2.load("dora.ini");             //從檔案dora.ini讀取内容至dora2
    dora2.output(cout);
    return 0;
}           

上述代碼的執行結果為:

No.       Name           Age
-------------------------------
20210426  Dora Chen      17
-------------------------------
C++                      97
Calculus                 70
Economics                65           

C++的标準模闆庫并不提供解析ini檔案的能力,本着“不要重新發明輪子”的原則,我們引用了大名鼎鼎的boost庫才完成相應任務。

  首先作者下載下傳了目前最新版本(v1.78.0)的boost庫壓縮包并将其解壓縮至D:/C2Cpp目錄下,如圖20-6所示。

圖20-6 解壓縮後的boost庫

接下來,作者在Qt Creator中編輯了項目檔案StudentInfo.pro,增加了下述内容中的第6行。該行内容将boost庫目錄納入項目的頭檔案包含目錄中。這樣,當cpp檔案通過#include宏指令引入boost中的頭檔案時,編譯器裡的預處理器可以在相應的目錄中找到它們。

  注意:在Qt Creator中建立項目時,其中的Build System項有cmake和qmake兩種,請務必選擇qmake,否則會找不到下述StudentInfo.pro檔案。

TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle
CONFIG -= qt

INCLUDEPATH += D:/C2Cpp/boost_1_78_0

SOURCES += \
        main.cpp           

第5 ~ 6行:引入boost庫中的屬性樹(ptree)以及ini解析器(ini_parser)頭檔案。屬性樹是一種樹形資料結構,對其内部工作原理的探讨超出來本書的範圍,在本書中,我們将其視為提供了可用功能接口的黑盒,而忽視其内部結構。

第80 ~ 91行:程式主體部分。main()首先構造了一個名為dora1的Student對象,随後的三行通過addScore()成員函數為dora1添加了C++、微積分、經濟學三門課程的成績。接下來,執行dora1的save()函數将對象内容序列化并存儲至ini格式的檔案dora.ini。然後,程式建立了一個新的Student對象dora2,通過執行dora2的load()函數從dora.ini讀取資料至dora2,最後通過dora2的output()函數将資訊列印至螢幕,以便确認dora2與dora1在内容上的一緻性。

本程式中Score、Student類型的資料成員聲明、構造函數定義等部分并無特别之處,我們重點解釋Student類型的save()和load()函數。

35       void save(const string& sFile){
36           boost::property_tree::ptree s;
37   
38           s.put("basic.sNo",sNo);
39           s.put("basic.sName",sName);
40           s.put("basic.iAge",iAge);
41   
42           s.put("scores.size",scores.size());
43           for (unsigned int i=0;i<scores.size();i++){
44               auto& r = scores[i];
45               s.put(string("scores.sName_")+std::to_string(i),r.sName);
46               s.put(string("scores.iScore_")+std::to_string(i),r.iScore);
47           }
48   
49           boost::property_tree::ini_parser::write_ini(sFile,s);
50       }           

第35 ~ 50行:Student的save()函數負責将Student對象内容序列化并存儲至ini格式檔案sFile中,sFile為指定檔案名。

第36行:函數構造了一個空的屬性樹(ptree)對象s,類型ptree位于boost::property_tree名字空間之下。

第38 ~ 40行:接下來,通過s的put函數往屬性樹中添加鍵值對。如第38行所示,put()函數的第一個參數為鍵,第二個參數為值,其中鍵以S.K的形式提供,S表示分區(Section),K表示分區下的健。具體到本例,s.put(“basic.sNo”,sNo)的執行結果對應dora.ini中的下述内容:

[basic]
sNo=20210426           

讀者應注意到,屬性樹的put()函數是函數名重載的,因為其第2個參數既可以是字元串,也可以是整數或者其他類型的對象。

第42行:在scores分區下添加名為size的鍵,表示scores向量的元素數量。具體到本例,執行結果對應dora.ini中的下述内容:

[scores]
size=3           

第43 ~ 47行:對scores向量進行周遊,将課程名稱和分數逐一加入屬性樹s。為了區分不同序号的課程,在鍵名後附加整數序号。具體到本例,執行結果對應dora.ini中的下述内容:

[scores]
...
sName_0=C++
iScore_0=97
sName_1=Calculus
iScore_1=70
sName_2=Economics
iScore_2=65           

第49行:通過boost::property_tree::ini_parser名字空間下的write_ini()函數将屬性樹s中的資訊寫入檔案sFile,檔案格式為ini。

第52 ~ 67行:Student的load()函數負責從ini格式檔案sFile讀取内容并填入内部資料結構,其中,sFile為指定檔案名。

第54行:使用boost::property_tree::ini_parser名字空間下的read_ini()函數将指定的ini格式檔案sFile的全部内容讀入屬性樹s。

第56 ~ 66行:通過get()函數從屬性樹s擷取屬性并填入内部資料結構。屬性樹的get()函數用于讀取其内部的鍵值對。函數的第一個參數為形如S.K的鍵,S表示分區(Section),K表示分區下的健。第二個參數則為預設值,即當指定的鍵不存在時,直接傳回預設值。

容易看出,同put()函數一樣,get()函數也有多個函數名重載的版本,其第2個參數(預設值)的類型間接決定了get()函數的傳回值類型。

除ini格式之外,boost庫還支援對xml、json等結構化文本檔案的讀取。作者的建議是,對于那些結構化的資料,盡量使用現成的結構化的文本檔案格式來存取。除boost外,大部分第三方C++庫,比如Qt,也提供對ini等結構化文本檔案的直接支援,沒有必要設計“個性化”的文本檔案存儲結構。

練習鞏固
20-3(json檔案)修改20.3節中的示例程式,使用json格式存儲學生及成績資訊。

本案例節選自作者編寫的教材及配套實驗指導書。

《C++程式設計基礎及應用》(高等教育出版社,出版過程中)

《Python程式設計基礎及應用》,高等教育出版社

《Python程式設計基礎及應用實驗教程》,高等教育出版社

C++中使用boost庫存取ini結構化文本檔案

高校教師同行如果期望索取樣書,教學支援資料,加群,請私信作者,聯系時請提供學校及個人姓名為盼,各高校在讀學生勿擾為謝。

青少年讀者們如果期望系統性地學習Python及C/C++程式設計語言,歡迎嘗試下述今日頭條(西瓜)免費視訊課程。

C/C++從入門到放棄(重慶大學現場版)

Python程式設計基礎及應用(重慶大學現場版)

C++中使用boost庫存取ini結構化文本檔案