天天看点

两起变量初始化问题的排查过程

【文章摘要】

        变量初始化看似很简单,但如果初始化位置不当或忘记初始化,则会导致程序行为异常。

       本文基于作者的实际项目经验,对近期遇到的两起变量初始化问题进行了详细的分析,为相关软件问题的分析及解决提供了有益的参考。

【关键词】

       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,重要的是我们要有解决问题的决心和毅力,不要因为程序出现了问题而自乱阵脚。只要采用正确的解决问题的方法,程序的任何“疑难杂症”都是可以根除的。