一、引言
在項目過程中,難免會需要一個友善的配置檔案讀寫類,它可以像遊戲的存檔檔案一樣,記錄着我們目前項目的配置資訊,以至于友善我們每次初始化運作的時候可以從這個配置檔案讀取上一次的配置資訊,當然也可以在程式運作過程中記錄使用者的配置設定資訊。
我們理想中的這個配置檔案讀寫類,它要有以下這些方法:
1. 支援讀入一個指定配置檔案的能力
2. 支援随時加入一個配置項的能力
3. 足夠強大,能夠寫入各種資料結構的配置資訊
滿足以上條件的配置檔案讀寫類才是我們想要的。這篇部落格顯然不是一步一步介紹如何寫出這樣一個類的文章(這不是一件容易的事情,即使寫出來也會千瘡百孔),而是一篇在網上已有的流傳久遠的一個短小精悍的配置檔案 Config 讀寫類的基礎上的解讀分析文章。
我找到的這個短小精悍的配置檔案 Config 讀寫類是來自于這篇部落格:
C++編寫Config類讀取配置檔案
這篇部落格的作者僅僅粘貼出了這個 Config 類的代碼以及簡簡單單的幾行測試代碼,對于大多數新手來說并不友好,是以我特意在閱讀了相關代碼并且自行建立測試項目進行了測試之後,萌發了想要寫一篇部落格來好好解讀這個 Config 類的原理和用法的想法。
由于不喜歡在部落格裡面大篇幅的粘貼代碼,是以想要擷取到這個配置檔案 Config 讀寫類的同學可以在我的 GitHub 上閱讀這個類(也就簡簡單單三個檔案:一個 Config.h,另一個是 Config.cpp,testconfig.cpp 則是用來測試的檔案而已):
wangying2016/Config
那麼接下來,就讓我們一步一步分析這個配置檔案 Config 讀寫類的設計與實作吧!
二、Config 設計之:資料結構
要想了解一個類的設計與實作,最好的方法就是去了解它的設計目标,即需要滿足的需求。
這裡我們需要了解的就是,一個配置檔案的内容究竟是什麼樣子的:
由上圖可知,我們的一個配置檔案,是由兩部分組成的:
1. 注釋内容:在示例檔案中是由
#
來單行注釋表示的,用來解釋一些必要内容
2. 配置項内容:配置内容其實就是一個一個的鍵值對的記錄,左側是 key 值,比如這裡的
name
值,右側是 value 值,對應這裡的
wangying
。而在鍵值對中間,間插了一個符号
=
(當然可以自定義的)來分割 key 值和 value 值。
可知,其實配置檔案的内容是非常簡單明了的。接下來,我們則需要将這種看似簡單的檔案結構抽象成我們熟悉的程式設計領域的資料結構。
如果你學過主流程式設計語言的話,這裡的鍵值對應該會讓你想起那麼幾個詞:
map
、
hash
、
dictionary
等等
在程式員的世界裡,鍵值對其實就是我們的映射。比如在 C++ 裡,我們要存儲這樣的資料就使用 std::map 即可。
也就是說,我們的 Config 類中,需要有一個最基本最基本的存儲配置檔案鍵值對資訊的 std::map 成員,這個成員用來将配置檔案中的每個 key 值和其對應的 value 值記錄下來。
那麼另外一個問題也就來了,我們的 std::map 究竟應該是什麼類型的呢?
哈哈,這個問題其實非常簡單,因為我們的鍵值對資訊都是要讀出寫入到檔案的,那麼 std::map 不論是 key 值還是 value 值都将會是字元串類型,即 C++ STL 的 std::string (Config 類不支援中文編碼)類即可。
那麼有人就會問了,如果 value 值隻是一個簡簡單單的 std::string 類的話,我想要存儲一個非常複雜的資料結構怎麼辦,比如一個
phone
key 值,對應了一個電話号碼清單呢?
這個問題其實也非常簡單,這裡的 std::map 成員隻是 Config 類中的最基本最基本存儲到檔案裡的字元串鍵值對記錄,而 Config 為了支援使用者存儲多種複雜的 value 值,還提供了模闆支援。是以,這裡隻需要你提供的 value 值的結構可以被轉化為 std::string 類型,就可以使用 Config 類來存儲你的資料結構了。
是以,讓我們看看 Config 類的代碼:
std::string m_Delimiter; //!< separator between key and value
std::string m_Comment; //!< separator between value and comments
std::map<std::string, std::string> m_Contents; //!< extracted keys and values
這三個内部的屬性,
m_Delimiter
是我們之前提到的 key 值和 value 值的分隔符
=
的設定,
m_Comment
是我們之前提到的注釋内容開頭
#
字元的設定,
m_Contents
就是我們上面讨論的 std::map 對象,并且以 key 值和 value 值均為 std::string 類型存儲。
此外,我們在 Config 類中看到的那麼多的模闆函數,其歸根結底想要實作的,就是支援使用者自定義的 value 資料結構的讀取和寫入:
//!<Search for key and read value or optional default value, call as read<T>
template<class T> T Read(const std::string& in_key) const;
// Modify keys and values
template<class T> void Add(const std::string& in_key, const T& in_value);
這裡截取了兩個重要的函數,一個用來讀取 key 值對應的 value 值,一個用來添加一個鍵值對。可以看到,這裡的 key 值永遠都是一個 std::string 類型的對象,而相應的 value 值則是模闆定義的類型,支援使用者自定義傳入任何的可以轉成 std::string 類型的資料結構。
三、Config 設計之:暴露方法
接下來讓我們想想這樣一個問題,在我們看到了配置檔案的内容之後,并且将其抽象成了 std::map 的資料結構,之後我們需要做的,就是給類的調用者暴露方法的方法即可。
那麼應該有哪些方法呢:
1. 一個可以跟某個具體的配置檔案綁定起來的構造函數
2. 擷取指定 key 值的 value 值
3. 加入一對鍵值對
4. 修改指定 key 值的 value 值
5. 删除一對鍵值對
暫時就想到了這些比較重要的,那麼 Config 類中提供了這些方法了嗎?
哈哈,提供了,讓我們一個一個來看:
1. 一個可以跟某個具體的配置檔案綁定起來的構造函數
Config::Config(string filename, string delimiter, string comment)
: m_Delimiter(delimiter), m_Comment(comment)
{
// Construct a Config, getting keys and values from given file
std::ifstream in(filename.c_str());
if (!in) throw File_not_found(filename);
in >> (*this);
}
作者使用 std::ifstream 打開了一個本地檔案,注意,調用這個方法之前必須保證該檔案存在。我們要注意到作者調用了
in >> (*this)
,調用了本類的 operator>> 重載函數,用來讀取檔案内容(此函數過于冗長,可以自行檢視源碼)并将其存儲到 std::map
//!<Search for key and read value or optional default value, call as read<T>
template<class T> T Read(const std::string& in_key) const;
template<class T> T Read(const std::string& in_key, const T& in_value) const;
template<class T> bool ReadInto(T& out_var, const std::string& in_key) const;
這三個都是模闆函數,主要是用來擷取使用者自定義資料結構的 value 值。需要注意的是,這三個函數的用法,第一個是傳回 value 值;第二個是可以将 value 值在參數中傳回;第三個直接将 value 值寫入到傳入的 var 對象中。
3. 加入一對鍵值對
4. 修改指定 key 值的 value 值
作者直接使用了一個函數即完成了第 3 點和第 4 點的工作:
template<class T>
void Config::Add(const std::string& in_key, const T& value)
{
// Add a key with given value
std::string v = T_as_string(value);
std::string key = in_key;
Trim(key);
Trim(v);
m_Contents[key] = v;
return;
}
這裡使用了 C++ 的 std::map 的特性,如果 key 值在 std::map 中存在,則更新 value 值,否則就新增一對鍵值對。需要注意的是,這裡調用了這行代碼:
std::string v = T_as_string(value);
其中
T_as_string
函數将使用者傳入的自定義模闆類轉化為 std::string 類型進行存儲,而該方法的實作如下:
/* static */
template<class T>
std::string Config::T_as_string(const T& t)
{
// Convert from a T to a string
// Type T must support << operator
std::ostringstream ost;
ost << t;
return ost.str();
}
這個類直接調用了使用者自定義模闆類的 operator<< 重載操作符函數,也就是說,隻要使用者自定義資料結構自定義重載了 operator<< 操作符函數,就可以用 Config 類來進行 value 值的讀寫操作了。
5. 删除一對鍵值對
void Config::Remove(const string& key)
{
// Remove key and its value
m_Contents.erase(m_Contents.find(key));
return;
}
幸而有 C++ STL 強大的功能,删除一對鍵值對就是這麼簡單。
6. 另外的一些方法
作者為了友善使用者使用,還提供了諸如查詢檔案是否存在、鍵值是否存在、讀入檔案、設定擷取鍵值分隔符、設定擷取注釋辨別符等等方法。都是比較簡單并且易用的,感興趣的同學可以自行檢視源碼。
四、Config 的使用 Demo
這裡,我自行編寫了一個 Demo 來測試 Config 類的功能:
#include <iostream>
#include <cstdlib>
#include <string>
#include <fstream>
#include "Config.h"
int main()
{
// 打開一個寫檔案流指向 config.ini 檔案
std::string strConfigFileName("config.ini");
std::ofstream out(strConfigFileName);
// 初始化寫入注釋
out << "# test for config read and write\n";
// 寫入一對配置記錄: name = wangying
out << "name = wangying\n";
out.close();
// 初始化 Config 類
Config config(strConfigFileName);
// 讀取鍵值
std::string strKey = "name";
std::string strValue;
strValue = config.Read<std::string>(strKey);
std::cout << "Read Key " << strKey << "'s Value is "
<< strValue << std::endl;
// 寫入新鍵值對
std::string strNewKey = "age";
std::string strNewValue = "23";
config.Add<std::string>(strNewKey, strNewValue);
// 将 Config 類的修改寫入檔案
out.open(strConfigFileName, std::ios::app);
if (out.is_open()) {
// 利用 Config 類的 << 重載運算符
out << config;
std::cout << "Write config content success!" << std::endl;
}
out.close();
system("pause");
return ;
}
幸而有強大的 Config 類,讓我操作配置檔案變成了一件這麼簡單的事情!
然而這裡需要注意的是,我們在使用 Config 類進行了 Add() 操作之後,我們僅僅隻是在 Config 類中操作了 std::map 類型的 m_Contens 對象内容而已,我們還需要将其寫入到檔案中去,是以這裡我最後調用了寫檔案流進行寫入操作,注意這行代碼:
// 利用 Config 類的 << 重載運算符
out << config;
這裡隐含調用了 Config 類的 operator<< 重載運算符:
std::ostream& operator<<(std::ostream& os, const Config& cf)
{
// Save a Config to os
for (Config::mapci p = cf.m_Contents.begin();
p != cf.m_Contents.end();
++p)
{
os << p->first << " " << cf.m_Delimiter << " ";
os << p->second << std::endl;
}
return os;
}
哈哈哈,看吧,就這麼簡單!
至此,完結撒花 ^_^
五、總結
這是一個非常非常強大而又異常短小的配置檔案讀寫類,細細品之反而又覺得意味無窮。
回想我們引言裡說到的三點:
1. 支援讀入一個指定配置檔案的能力
2. 支援随時加入一個配置項的能力
3. 足夠強大,能夠寫入各種資料結構的配置資訊
Config 類無一不一一滿足甚至提供了更加人性化的方法供使用者使用。
閱讀他人的代碼并且了解其設計思路本身就是一個很快樂的事情:)
To be Stronger!