一、引子

  为什么要浪费时间去设计一个算法来实现数据的文件存储还要费劲地调试代码呢?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的值输送到控制台。最后,我包含了两个构造器,一个是缺省的,另一个用作输入参数。(为了简化起见,该例程并没有做任何实际的绘图。抱歉!)

  下面最吸引人的部分是必需的代码行以串行化该类。下面就是该类(注意粗体部分):

class Vertex {
 private:
  friend class boost::serialization::access;
  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:
  vector vertices;
  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:
  vector vertices;
  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:
  vector polygons;
  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;
}

  记住:我尽力脱离开把绘图对象写入到文件中去的思想。代之的是,我只是在概念上把绘制对象当作我的文档,然后存储文档文件和读回它们。那些文档文件都具有我为我的程序创立的专门格式,而且我给予它们唯一的文件扩展名.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目录下。