“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 砸的日子也沒有那么糟糕 …… …… ……