“zero 帮帮忙吧 ~~ ”
听到这个充满诚意的声音,zero 垮下了双肩,感觉自己处于彻底崩溃的边缘 ———— 几个月来,每次 pisces 遇到什么不能解决的问题,总是用这个开场白来求 zero,而后 zero 就不得不面对各式各样匪夷所思的古怪问题。
“她怎么就能弄出那么多错误来呢?” zero 在心中哀叹。他苦着脸,问道:“你就不能放过我去找 Solmyr 么?”
“可是 Solmyr 指定你来帮助我们的呀!”
“ …… 好吧,说说是什么问题”,zero 一边在心里再次痛骂了 Solmyr 一百遍,一边做好了再次面对奇怪错误的准备。
“嗯,这里有一段代码,是读取配置文件信息的。现在只是个框架,将来可能需要读好些配置文件,而且可能放在不同目录下,所以这里我用一些 string 对象保存路径和文件名,然后用 fopen 打开。” 说着,pisces 调出了一个 cpp 文件:
#include <cstdio>
#include <string>
#include "config.h" // 头文件,定义了存放配置信息的结构
// 并包含 read_cfg 函数的声明
using namespace std;
const string path = "./cfg/";
const string name = "system.cfg";
// 参数为指向保存配置信息结构的指针
// 返回值为成功标志,true 代表成功,false 代表失败
bool read_cfg(CFG_DATA* p_data)
{
const string path_name = path+name;
FILE* fp = fopen(path_name.c_str(), "r");
if( fp == NULL )
return false;
// 使用 fp 读取配置文件,放入 CFG_DATA 结构
... ...
... ...
return true;
}
... ....
“在我这里运行的好好的,但提交给测试组做单元测试的时候却总是出毛病,说是找不到配置文件,我去他们那里看过了,配置文件的名字和路径都是对的呀,真是太奇怪了。”
zero 斜着眼看了看这段代码,对其中的风格大为不满:“我说 pisces,错误先不提,你这段代码的风格实在不太象 C++ 啊。首先参数应该用引用,它的安全性可比指针高多了,而且你既然用了指针,就应该用断言做检查么;其次你干吗不用流来读取文件呢?非要用 fopen ……”
“我知道我知道!” pisces 忙不迭的打断了 zero,“我知道这里我的风格不太好,可是无论怎样这段打开文件的代码应该没错呀?可现在问题是打开文件的时候出错,明明文件在那儿的,可函数总是返回 false 。帮我先把这个 bug 找出来吗,帮帮忙了 ~~~~~ ”
“好好!”,zero 摇了摇头,开始寻找错误。5 分钟过去,zero 的眉头渐渐皱了起来,这段只有三五行的代码看起来一目了然,根本没有隐藏错误的地方。zero 把这段代码引进他用来测试的工程里,编译连接,测试运行,程序一切正常,正确的找到了配置文件。这是怎么回事?zero 迷惑了。
“zero?” pisces 将探询的眼光投向 zero。“呃 …… 别急,我们去测试组那里看看究竟怎么回事。” zero 心存侥幸,没准是测试组弄错了呢?10 分钟之后,zero 垂头丧气的回来了,后面跟着 pisces。测试组没有弄错,在他们那里这段代码确实不能正常工作,调试器显示,作为文件名传入 fopen 的是个空字符串。这是怎么回事?zero 一边想着,一边往自己的座位走去 ———— 哎?那个站在自己计算机前的人不是 …… 不是 …… Solmyr 么?他手上拿的是 ……
zero 本能的感到了危险,猛的一偏头!一个文件夹从离他的脸只有 0.01 公分的地方唰的一下擦了过去!可惜后面的 pisces 就没有这么幸运了,被打个正着!
Solmyr 看了一眼正捂着脸的 pisces,对吓出一身冷汗的 zero 问道:“屏幕上那段代码不是你写的吧?”
“对,是她写的。” zero 同情的看了看 pisces ,后者好象还没有从打击中恢复过来 ……
“有因就有果,真是一点也不会错啊 ……” Solmyr 耸耸肩,“知道这段代码为什么会出错么?”
zero 苦笑:“不知道。”
“把眼光放开一点,你看一下调用这个函数的地方就知道了。”
zero 调出了整个工程。按照文档上的说明,read_cfg 是整个系统初始化过程的步骤之一,当系统启动时会读取配置文件确定一些初始化的参数。据此,zero 很容易的找到了调用处,在另外一个 cpp 文件中:
#include "config.h" // 其中声明了 read_cfg 这个函数
class system
{
public:
// 完成系统启动时的初始化工作
system()
{
CFG_DATA data;
read_cfg(&data);
// 使用 data 中的信息配置系统
... ...
}
// 完成系统退出时的清理工作
~system()
{
... ...
}
... ...
};
system theSystem; // 代表整个系统的全局对象
Solmyr 清了清嗓子:“这是个看起来很干净的手法。system 这个类只有这里一个全局对象,这个全局对象代表了整个系统,它构造,系统做初始化工作;它析构,系统开始做退出时的清理工作。全局对象的身份保证了它会在进入 main 函数之前构造,在 main 函数退出之后析构。这一招是你教 pisces 的吧?”Solmyr 看了看 zero 。
zero 点点头:“没错,上次她问我如何能够比较好的处理初始化和清理的代码,我想起了上次关于‘成对出现 ’的讨论(注一),就给她出了这个主意。”
“思路是对头的,但是实现的方式不妥,毛病就出在全局对象上面。我问你,一个 cpp 文件,或者说得正式一点,一个编译单元中的全局对象构造析构的顺序是怎样的?”
“ …… 应该是按照定义它们的顺序。” zero 努力的回忆一阵,很肯定的说道。
“正确,那么不同编译单元之间全局对象的构造顺序呢?”
“ …… 好象没有明确的规则,这个应该属于标准未定义对吧?”
“正确,所以 ……”
“所以 …… …… 啊!我明白了!哎呀!我怎么这么迟钝!看到 fopen 传入的是空字符串的时候我就应该想到的!” zero 露出了恍然大悟的表情,“在打开配置文件的代码段中,保存文件名的 path 和 name 也是全局对象,换句话说,这两个 string 对象和 theSystem 对象的构造次序是无法确定的。在测试组那里,theSystem 先于 path 和 name 构造,所以当 theSystem 的构造函数调用 read_cfg 函数的时候,path 和 name 这两个 string 对象根本还没有来得及构造!当然无法取出文件名和路径来!而在我和 pisces 的计算机上,构造次序与之相反,这段代码就可以正确运行。”
“很好,那么如何解决呢?”
“嗯 …… …… 我想只要尽可能避开全局对象就行了,一方面 theSystem 这个对象可以放到 main 函数里,一样可以保证正确完成初始化工作和清理工作;另一方面,read_cfg 那边最好也不要用全局的 string 对象了,一样可以用局部对象。这样是能够解决这个问题 ……” zero 皱起了眉头,显然对这个解法还不太满意,“那如果我有一些全局性质的对象,而且希望精确的定义它们的构造次序,该怎么办呢?”
Solmyr 点了点头:“没错,有时候这确实是个合理的要求。对此最简单的解法是‘被函数包装的 static 对象’(注二),象这样:”
system& theSystem()
{
static system instance;
return instance;
}
“instance 是个 static 对象,这保证了它的生存期,然后它会在第一次调用这个函数的时候构造。针对你的问题,只要你声明多个这样的函数,然后保证它们第一次调用的次序,就可以保证这些对象构造的次序了。”
zero 若有所思的点了点头。
“说起来,次序问题绝对不只这一个,C++ 中类似的问题相当多。和这个问题最接近的,是类的成员和基类的构造次序,其他的还有表达式求值的次序、函数参数求值的次序,(注三)等等。遇到次序问题,千万不要想当然,问自己一句:这个次序有定义吗?有定义的要遵守,无定义的要避开。好了,这个问题大概就是这样,接下来你的任务是 …… ”
“我知道,把这些讨论整理成文档对吧?”
“不,是想办法让 pisces 搞懂这个问题。”
“…… …… …… …… …… ……”
望着 Solmyr 甩手匆匆离去的背影,再看看身边正用最拿手的“诚恳”眼神看着自己的 pisces ,zero 突然泛起了一种奇怪的感觉:好象以前只是单纯被 Solmyr 砸的日子也没有那么糟糕 …… …… ……
=====================================
注一:关于“成对出现”,请参阅本人“Solmyr 的小品文系列之六:成对出现”
注二:可以参阅《C++ 程序设计语言(特别版)》的 10.4.9 节 ———— 其实你若看了那一节,这一篇东西就大多是废话了,^_^
注三:这些问题的讨论可以见《Effective C++ 2/e》条款 13 ,GotW 的条款 12 ,以及 …… 其他各处 …… ^_^b