一、引子
為什么要浪費時間去設計一個算法來實現數據的文件存儲還要費勁地調試代碼呢?Boost庫可以為你做這些事情。借助于串行化模板,你可以容易地把數據存儲到你自己定制格式的文件中。本文將教給你如何輕松地存儲數據并回讀數據。
二、概述
當你開發一個軟件包時,你總是想集中精力于軟件的功能。而最讓你擔心的是,花費大量的時間寫代碼,而該代碼有可能會應用在另外大量的其他程序中。那正是重用的含義所在,你會希望另外某人已經為你編寫出這樣現成的代碼。
這類問題的一個很好的例子是給予你的程序存檔的能力。例如,你可能在寫最偉大的天文學程序-在該程序中,你的用戶可以輕易地輸入時間和坐標,你的程序負責繪制當前天空的地圖。但是,假定你賦予你的用戶能夠高亮某些星星,這樣以來它們可以容易地突出在地圖上。最后,你讓用戶能夠保存他們的配置以備后用。
你的程序集中于天文學編程。你并不是在寫一個通用庫來保存文檔,所以你不必把大量的時間花在存儲功能上,因為你要專注于程序的天文學特性。如果你是用C++編程,你可以從Boost重用庫得到幫助。為了保存文件,Boost庫包括一個串行化類,正是你需要的。
如果你成功地創建了你的程序工程,很可能有一個類來包含用戶信息或文檔。例如,你可能有一個類,該類列舉用戶們最喜歡的星星的名字和位置。(請原諒這里的簡化)。這就是你希望用戶能夠保存到磁盤上的數據。畢竟,幾乎所有的程序都有文件保存功能。微軟的Word保存文本和格式化數據,而Excel保存工作單數據。一個優秀的地圖程序可以用戶保存喜歡的位置,GPS路線,旅程,等等。
借助于Boost串行化庫的幫助,實現保存很容易-所要做是僅僅是設置好你的類,而由庫來負責其它一切-使你專注于真正的工作。
其思想是很簡單的:你創建了一個包含用戶數據的對象。當準備保存信息時,用戶選擇File|Save As,然后從文件對話框中選擇一個文件名即可。借助于Boost,你的程序就把數據保存到選定的文件中。以后,當用戶重新啟動該程序時,選擇File|Open,選定已保存的文件,你的程序再一次使用Boost-但是這一次重新裝入數據,因此,重新產生了該對象。瞧,用戶數據被回復了!或者,從用戶的角度來看,文檔已被打開。
下面的例子只是簡單地演示保存和加載一些圖形類。第一個類,Vertex,描述了一個二維的點。第二個類,Polygon,包含一個Vertex實例的容器。第三個類,Drawing,包含一個Polygon的容器。
想把所有這些都保存到一個文檔中去無疑是一個惡夢-這不是花費時間的地方-你要實現最好的圖形程序設計,因為這是你的專長。好了,讓Boost庫為你做其它一切吧。
三、串行化一個類
首先,考慮一下Vertex類。該類是最容易串行化的一個,因為它不包含其它對象。該類包含兩個值,x和y,且都是double型。我還給該類定義了幾個存取x和y的函數,還有一個dump函數,它負責把x和y的值輸送到控制臺。最后,我包含了兩個構造器,一個是缺省的,另一個用作輸入參數。(為了簡化起見,該例程并沒有做任何實際的繪圖。抱歉?。?BR>
下面最吸引人的部分是必需的代碼行以串行化該類。下面就是該類(注意粗體部分):
class Vertex {
private:
friend class boost::serialization::aclearcase/" target="_blank" >ccess;
template
void serialize(Archive & ar, const unsigned int version)
{
ar & x;
ar & y;
}
double x;
double y;
public:
Vertex() {} // 串行化需要一個缺省的構造器
Vertex(double newX, double newY) : x(newX), y(newY) {}
double getX() const { return x; }
double getY() const { return y; }
void dump() {
cout << x << " " << y << endl;
}
};
注意在程序的最后,我沒有實際地使用缺省的構造器Vertex(),但是串行化庫的確調用了它,因此我需要把它包含進去。
串行化部分首先串行化庫存取私有成員,特別是接下來的串行化函數。串行化庫的創建者Robert Ramey指出,你不需要任何的函數,包括在派生類中的那些,調用你的串行化方法;只需由串行化庫來調用即可。因此,為了保護你的類,需要把串行化功能聲明為私有的,然后允許有限制地存取該串行化庫,這通過把類boost::serialization::access聲明為你的類的友元來實現,見代碼。
接下去是串行化函數,它是一個模板函數。如果你對模板還不太熟悉的話,不要緊:你不需要理解模板部分而照舊可以使之工作。然而,必須確保你理解了串行化功能的核心:
ar & x;
ar & y;
首先,讓我聲明一下:這兩行代碼并不是聲明參照引用變量,雖然形式上看上去相似。代之的是,它們調用一個&操作符,并且把你的類成員寫入到文件中或者把它們讀進來。是的,你已經正確地認出了;該功能實現了一石二鳥(或者更準確地說,用一套代碼完成了兩件任務)的功效。當你在把一個Vertex對象保存到一個文件中去時,串行化庫調用這個串行化功能;第一行把x的值寫入到文件中,第二行把y的值寫入到文件中。后來,當你把一個Vertex對象從文件中讀回時,第一行實現從文件中讀回x值,第二行實現從文件中讀回y值。
這是某種特別的操作符重載!事實上,&字符是一個在串行化庫內部定義的一個操作符。幸好你不需要理解它是如何工作的。
好,就是那么簡單。下面是一些示例代碼,你可以試著把一個Vertex 對象保存到一個文件中:
Vertex v1(1.5, 2.5);
std::ofstream ofs("myfile.vtx");
boost::archive::text_oarchive oa(ofs);
oa << v1;
ofs.close();
就是這樣!第一行產生Vertex對象。下面的四行打開一個文件,把一個特定的串行化類與文件相結合,然后寫向文件,最后關閉文件。下面是一段把一個Vertex 對象從文件中讀入的代碼:
Vertex v2;
std::ifstream ifs("myfile.vtx", std::ios::binary);
boost::archive::text_iarchive ia(ifs);
ia >>v2;
ifs.close();
v2.dump();
這段代碼生成一個Vertex的實例,然后再打開一個文件(這次是為讀取的目的),把一個串行化類與文件相關聯,把對象讀進來,然后關閉文件。最后,代碼輸出Vertex的值。如果你把前面的這兩個程序段放在一個main函數中并運行,你會看到輸出兩個原始值:1.5和2.5。
注意
注意我使用的文件擴展名是:.vtx。這并不是一個專門的擴展名;它是我自己定制的擴展名。這聽起來有點愚蠢和瑣碎,但是實際上,我們是在創建自己的文件格式。為了指出這一特殊的文件格式,我使用了擴展名叫.vtx,其意指Vertex。
四、串行化容器
在我的示例中,一個繪圖對象可以包含多個多邊形對象(我把它們存儲在一個向量中,該向量是標準庫容器模板的一員),每一個多邊形對象可以包含多個對象Vertex(我也用向量存儲它們)。
串行化庫包括保存數組和容器的功能。因為你可以把指針存儲到數組中,串行化庫也支持指針。請考慮一下:如果你有一個包含Vertex指針的數組,而且你直接把該數組寫入一個文件中,你就會有一堆指針存儲在文件中,而不是實際的Vertex 數據。那些指針僅是些數字(內存位置),當后面接著回讀數據時它們是毫無意義的。所以,該庫十分聰明地從對象中抓取了數據而不是指針。
考慮到存儲作為容器的對象,你要把每一個類串行化。在串行化方法中,你可以讀取和寫入容器,就象你操作另外一個成員一樣。你的容器可以是簡單的語言本身內存的數組(如Vertex *vertices[10];),或者是來自于標準庫的容器。因為現在是21世紀,我喜歡緊跟時代的步伐,所以我在本例中選擇使用標準庫。
盡管你可以在你的串行化類中編寫代碼,針對容器和每一個成員;然而,你不必這樣做。作為代勞,庫已十分聰明地自動遍歷容器了。你所有要做是僅是寫出容器,如下,其中vertices是一個容器:
ar & vertices;
讓庫來做其余的工作吧。相信嗎?下面是類Polygon的代碼,串行化部分以粗體標出:
class Polygon {
private:
vectorvertices;
friend class boost::serialization::access;
template
void serialize(Archive & ar, const unsigned int version)
{
ar & vertices;
}
public:
void addVertex(Vertex *v) {
vertices.push_back(v);
}
void dump() {
for_each(vertices.begin(), vertices.end(), mem_fun(&Vertex::dump));
}
};
首先,請注意我用一個矢量來存儲布點。(如果你對模板還是個新手,不要緊,只需要把vector當作是存儲指向Vertex 實例的指針的矢量就行,因為其實際上就是如此。)。下一步,在串行化函數中,我不想遍歷該矢量-寫每一個成員。相反,我只是讀寫整個矢量即可:
ar & vertices;
兩個公共方法的建立,可以用來十分方便地操作該多邊形。第一個addVertex方法,讓你把另外一個結點添加到該多邊形上;它使用了push_back方法,這是把一項加到一個矢量上去的標準方法。Dump函數遍歷該矢量,把每一個矢量寫到標準輸出設備上去。即使對一些很有經驗的C++老手,也可能對下面這一行不太熟悉:
for_each(vertices.begin(), vertices.end(), mem_fun(&Vertex::dump));
這里用了點小技巧。它不是串行化庫的一部分;它是標準庫的一部分,對于今天的C++程序可以放心使用,沒有任何多余的副作用和經濟問題。單詞for_each實際上是一個函數,它有三個參數:在容器中的起始位置,結束條件以及一個操作容器中每一項都要調用的函數(依賴于你的C++程序實現,你可以要包括頭文件如,’#include ’來得到for_each函數。我使用的是GNU庫,所以用’#include ’語句)。在我的例子中,我用的for_each函數的第三個參數是Vertex 類的dump成員函數。但是有一個問題:你不能只調用一個成員函數本身;你要從一個具體的對象中調用才行。這正是成員函數mem_fun的來源所在。它是一個專門函數(標準庫的一部分),在此與函數for_each一起工作,負責調用具體對象的dump函數。也就是說,它把dump()綁定到for_each當前操縱的Vertex對象上。
為簡化起見,這里的for_each調用遍歷整個列表中的每一個Vertex,并調用Vertex::dump-所有這些只有一行代碼!
接下來,Drawing類實際上與Polygon類很相似,除了它包含一些Polygon 對象,而不是包含一個Vertex對象的容器。不是一個大問題。
下面是完整的程序,包含一些額外的析構器以用于清理內存:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class Vertex {
private:
//串行化代碼開始
friend class boost::serialization::access;
template
void serialize(Archive & ar, unsigned int version)
{
ar & x;
ar & y;
}
//結束串行化代碼
double x;
double y;
public:
Vertex() {} //串行化需要一個缺省的構造器
~Vertex() {}
Vertex(double newX, double newY) : x(newX), y(newY) {}
double getX() const { return x; }
double getY() const { return y; }
void dump() {
cout << x << " " << y << endl;
}
};
void delete_vertex(Vertex *v) { delete v; }
class Polygon {
private:
vectorvertices;
friend class boost::serialization::access;
template
void serialize(Archive & ar, const unsigned int version)
{
ar & vertices;
}
public:
~Polygon() {
for_each(vertices.begin(), vertices.end(), delete_vertex);
}
void addVertex(Vertex *v) {
vertices.push_back(v);
}
void dump() {
for_each(vertices.begin(), vertices.end(), mem_fun(&Vertex::dump));
}
};
void delete_poly(Polygon *p) { delete p; }
class Drawing {
private:
vectorpolygons;
friend class boost::serialization::access;
template
void serialize(Archive & ar, const unsigned int version)
{
ar & polygons;
}
public:
~Drawing() {
for_each(polygons.begin(), polygons.end(), delete_poly);
}
void addPolygon(Polygon *p) {
polygons.push_back(p);
}
void dump() {
for_each(polygons.begin(), polygons.end(), mem_fun(&Polygon::dump));
}
};
string getFileOpen() {
//在實際開發中,這將調用一個各種樣的FileOpen 對話框
return "c:/myfile.grp";
}
string getFileSaveAs() {
//在實際開發中,這將調用一個各種樣的FileSave 對話框
return "c:/myfile.grp";
}
void saveDocument(Drawing *doc, const string &filename) {
ofstream ofs(filename.c_str());
boost::archive::text_oarchive oa(ofs);
oa << *doc;
ofs.close();
}
Drawing *openDocument(const string &filename) {
Drawing *doc = new Drawing();
std::ifstream ifs(filename.c_str(), std::ios::binary);
boost::archive::text_iarchive ia(ifs);
ia >>*doc;
ifs.close();
return doc;
}
int main()
{
Polygon *poly1 = new Polygon();
poly1->addVertex(new Vertex(0.1,0.2));
poly1->addVertex(new Vertex(1.5,1.5));
poly1->addVertex(new Vertex(0.5,2.9));
Polygon *poly2 = new Polygon();
poly2->addVertex(new Vertex(0,0));
poly2->addVertex(new Vertex(0,1.5));
poly2->addVertex(new Vertex(1.5,1.5));
poly2->addVertex(new Vertex(1.5,0));
Drawing *draw = new Drawing();
draw->addPolygon(poly1);
draw->addPolygon(poly2);
//演示保存一個文檔
saveDocument(draw, getFileSaveAs());
// 演示打開一個文檔
string filename2 = getFileOpen();
Drawing *doc2 = openDocument(getFileOpen());
doc2->dump();
delete draw;
return 0;
}
記?。何冶M力脫離開把繪圖對象寫入到文件中去的思想。代之的是,我只是在概念上把繪制對象當作我的文檔,然后存儲文檔文件和讀回它們。那些文檔文件都具有我為我的程序創立的專門格式,而且我給予它們唯一的文件擴展名.grp,其含義指圖形。
另外,我創建了幾個幫助函數:getFileSaveAs和getFileOpen。在本例中這些函數僅返回一個硬編碼的字符串形式的文件名。在實際開發中,這些函數一般會分別在菜單項File|Save As和File|Open中被調用;并將會調用系統的File|Open和File|Save對話框。這些對話框將返回一個用戶想使用的字符串形式的文件名。這樣,用戶的看法就同我們一樣了:打開和保存文檔,而不是讀取和寫繪圖對象數據到文檔中。要看清它們在概念上的區別,雖然它們在功能上是相同的。
五、小結
借助于Boost庫,給你的軟件增加文件的保存/打開功能相當容易。如果你想自己試驗這些代碼,你可以在官方站點找到該Boost庫,下載最新版本試驗。
六、備注
要使用串行化庫,你最少需要得到該庫的1.32.0版本(早期的版本不包括串行化庫)。注意,在此我不是向你介紹如何安裝Boost庫;網站上提供詳細步驟說明如何安裝該庫。如果你使用該串行化庫,你還需要編譯另外幾個該庫需要的.cpp源文件-你可以在boost_1_32_0\libs\serialization\src文件夾下找到它們。還有一個boost_1_32_0\libs\serialization\build 庫,它使用了一種新的創建(build)系統,稱為jamfile,你可以用它來把源文件創建成一個庫?;蛘?,你可以僅把這些源文件添加到你的工程的src目錄下。