天天看點

兩起變量初始化問題的排查過程

【文章摘要】

        變量初始化看似很簡單,但如果初始化位置不當或忘記初始化,則會導緻程式行為異常。

       本文基于作者的實際項目經驗,對近期遇到的兩起變量初始化問題進行了詳細的分析,為相關軟體問題的分析及解決提供了有益的參考。

【關鍵詞】

       C語言  變量  初始化  開發

一、問題1的排查過程

在對某程式版本進行自測的過程中,發現在程式運作一段時間之後,某指針(pDBConn)就一直為空(NULL),使得該程式的正常流程無法執行。
        我們找到了對該指針(pDBConn)進行指派操作的代碼,程式的架構如下:
……
void *pDBConn = NULL;    
int  iFlag     = 0;
……
while (1)
{
    pDBConn = NULL;
    ……
    if (判斷條件)
    {
        iFlag = 1;
    }
    ……
    if (iFlag != 1)
    {
        // 對pDBConn進行指派
    }
    ……
}
 
        我們可以看到,隻有在iFlag不等于1的時候,pDBConn指針才會被指派。那麼現在pDBConn一直為NULL,就表示iFlag的值一直為1。什麼原因呢?
        我們又分析了一下判斷條件1,應該是不滿足的,也就是說,不會對iFlag進行指派1的操作。
        為了排查問題原因,我們在代碼中添加了詳細的調試日志。添加調試日志後的代碼架構如下所示:
……
void *pDBConn = NULL;    
int  iFlag     = 0;
……
// 調試日志1,列印pDBConn和iFlag的值
while (1)
{
    pDBConn = NULL;    // 對pDBConn進行初始化
    ……
    // 調試日志2,列印pDBConn和iFlag的值,并列印判斷條件1的取值
    if (判斷條件1)
    {
        iFlag = 1;
        // 調試日志3,列印Flag的值
    }
    ……
    // 調試日志4,列印pDBConn和iFlag的值
    if (iFlag != 1)
    {
        // 對pDBConn進行指派
        // 調試日志5,列印pDBConn的值
    }
    // 調試日志6,列印pDBConn和iFlag的值
    ……
}
 
       日志添加完畢之後,我們重新開機了程式并設計了多組測試用例。判斷條件1不滿足的幾組測試都是完全正常的,某一個測試用例使得判斷條件1滿足,iFlag被指派為1,接下來pDBConn不被指派,仍然為NULL,這也是正常的。
       接下來一組測試用例使得判斷條件1不滿足,即iFlag不被指派,但從調試日志2可以看出,iFlag在if語句之前已經為1了,是以也不會進入對pDBConn指派的流程。這就奇怪了,我們的本意是要讓iFlag為0,可它為什麼為1呢?之後,我們又執行了幾組測試用例,iFlag的值一直為1。
       難道有另外的程式在對iFlag進行指派操作嗎?我們仔細閱讀了代碼和日志,發現調試日志1隻出現了1次,之後就“銷聲匿迹”了。這也就是說,隻對iFlag進行一次初始化操作,之後程式一直在while循環中運作。由于沒有初始化為0的操作,某次對iFlag指派1之後,它就一直保持1這個值不變,也就使得程式永遠都進不了對pDBConn進行指派的流程中。
        而程式重新開機之後,在進入while循環之前,對iFlag定義的同時進行了初始化,是以隻要判斷條件1不滿足,程式就是正常執行的。隻要有一次iFlag被指派為1了,之後想要它為0都是不可能的了(除非程式重新開機)。這就是程式異常的原因。
        根據以上的分析,我們在while循環中添加了對iFlag賦初值的語句,之後再對修改後的程式進行了多次測試,發現異常就消除了,程式執行正常。
        修改之後的程式架構如下所示:
……
void *pDBConn = NULL;    
int  iFlag     = 0;
……
while (1)
{
    pDBConn = NULL;
    iFlag    = 0;      // 對iFlag進行初始化
    ……
    if (判斷條件1)
    {
        iFlag = 1;
    }
    ……
    if (iFlag != 1)
    {
        // 對pDBConn進行指派
    }
    ……
}
 
二、問題2的排查過程
        某程式需要将資料庫中掃描出來的資料按照一定的格式寫入到檔案中。在某次測試的過程中,發現生成了空檔案,而原本滿足條件的資料是存在的。也就是說,本來應該寫入檔案的資料不知道到哪兒去了。
        我們找到了拼湊檔案内容和将内容寫入檔案的代碼,程式的架構如下:
……
int    iUserType     = 0;                    // 用于表示使用者類型,0-開戶,1-銷戶
char szUserNo[50] = {0};               // 用于表示使用者号碼
int     iReasonCode = 0;                 // 用于表示銷戶原因
char szContentLine[1024] = {0};    // 用于儲存需要寫入檔案的每行内容
……
_snprintf(szContentLine, sizeof(szContentLine) - 1, "%d%s\r\n",iUserType, szUserNo);
……
if (g_iControlFlag == 1)
{
    memset(szContentLine,0x00, sizeof(szContentLine));
    if (iUserType == 1)
    {
        _snprintf(szContentLine,sizeof(szContentLine) - 1, "%d%s%d\r\n", iUserType, szUserNo, iReasonCode);
    }
}
……
// 調用SaveDataToFile函數将szContentLine中的内容寫入檔案
……
 
        程式流程是這樣的,首先組裝寫入檔案的内容(使用者類型和使用者号碼),當全局變量g_iControlFlag為1時,對于銷戶(即iUserType為1)的資料,要在寫入檔案的内容後面添加銷戶原因(即iReasonCode字段),然後将内容寫入檔案中。
        現在出現了空檔案的問題,首先可以确定的是資料庫中滿足條件的資料是存在的。也就是說,檔案中是應該有内容的。那麼,一定是程式哪裡有問題。
        為了跟蹤程式的執行情況,我們同樣在代碼中添加了調試日志。添加調試日志後的代碼架構如下所示:
……
int     iUserType     = 0;                    // 用于表示使用者類型,0-開戶,1-銷戶
char szUserNo[50] = {0};                // 用于表示使用者号碼
int    iReasonCode = 0;                  // 用于表示銷戶原因
char szContentLine[1024] = {0};    // 用于儲存需要寫入檔案的每行内容
……
// 調試日志1,列印szContentLine的值
_snprintf(szContentLine, sizeof(szContentLine) - 1, "%d%s\r\n",iUserType, szUserNo);
// 調試日志2,列印g_iControlFlag和szContentLine的值
……
if (g_iControlFlag == 1)
{
    // 調試日志3,列印iUserType和szContentLine的值
    memset(szContentLine,0x00, sizeof(szContentLine));
    if (iUserType == 1)
    {
        _snprintf(szContentLine,sizeof(szContentLine) - 1, "%d%s%d\r\n", iUserType, szUserNo, iReasonCode);
        // 調試日志4,列印szContentLine的值
    }
    // 調試日志5,列印szContentLine的值
}
// 調試日志6,列印szContentLine的值
……
// 調用SaveDataToFile函數将szContentLine中的内容寫入檔案
……
 
        添加了調試日志之後,我們重新開機了程式,分g_iControlFlag為0和1兩種情況來測試。當g_iControlFlag為0時,一切正常。當g_iControlFlag為1時,我們發現了一個問題,生成的檔案要麼為空,要麼隻包含了銷戶的資料,那麼開戶的資料到哪裡去了呢?
        我們回過頭來詳細地分析了日志。對于開戶資料(iUserType為0),調試日志2和調試日志3列印出的szContentLine有具體的值,而調試日志5列印出的szContentLine就沒有具體值了,當然寫入檔案也就沒有具體值了。什麼原因呢?我們的目光落到了調試日志3後面的那行初始化代碼上,對于銷戶資料,即使szContentLine被初始化了,程式仍然會進入之後的if語句對szContentLine指派;但對于開戶資料,szContentLine被初始化之後,就沒有再被指派,是以一直為空。這也就是開戶資料丢失的原因,也是空檔案出現的原因。
       我們将對szContentLine進行初始化的語句放到緊随其後的if語句裡面,再對程式進行測試,就一切正常了。修改之後的程式架構如下所示:
……
int     iUserType     = 0;                     // 用于表示使用者類型,0-開戶,1-銷戶
char szUserNo[50] = {0};                 // 用于表示使用者号碼
int     iReasonCode = 0;                  // 用于表示銷戶原因
char szContentLine[1024] = {0};    // 用于儲存需要寫入檔案的每行内容
……
_snprintf(szContentLine, sizeof(szContentLine) - 1, "%d%s\r\n",iUserType, szUserNo);
……
if (g_iControlFlag == 1)
{
    if (iUserType == 1)
    {
        memset(szContentLine,0x00, sizeof(szContentLine));    // 初始化語句的正确位置
        _snprintf(szContentLine,sizeof(szContentLine) - 1, "%d%s%d\r\n", iUserType, szUserNo, iReasonCode);
    }
}
……
// 調用SaveDataToFile函數将szContentLine中的内容寫入檔案
……
 
三、總結
        在這兩起初始化問題的排查中,主要依靠程式分析和添加程式調試日志來定位問題。
        通過這兩起問題的排查,我們總結出的經驗有以下幾個:
        (1) 變量的初始化非常的重要,忘記初始化或初始化的位置不對都會導緻程式異常。
        (2) 為了更快地、準确地定位問題,我們可以在出問題的程式語句周圍添加詳細的調試日志,用以列印出某些關鍵變量的值。通過對日志的閱讀,我們能夠很快找到程式的問題所在。
        (3) 在排查問題的過程中,我們要細緻、有耐心,要不斷地縮小“搜查範圍”。在修改了代碼之後,要多對修改之後的程式進行測試,防止引入新的問題。
 
        本文對兩起變量初始化問題的排查過程進行了詳細的描述,為相關開發項目類似問題的排查提供了有益的參考。是程式就會有bug,重要的是我們要有解決問題的決心和毅力,不要因為程式出現了問題而自亂陣腳。隻要采用正确的解決問題的方法,程式的任何“疑難雜症”都是可以根除的。