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

【文章摘要

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

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

【关键词

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

(本人微博:http://weibo.com/zhouzxi?topnav=1&wvr=5,微信号:245924426,欢迎关注!)

时间: 2024-10-09 16:51:22

两起变量初始化问题的排查过程的相关文章

Java 类的实例变量初始化的过程 静态块、非静态块、构造函数的加载顺序

Java 类的实例变量初始化的过程 静态块.非静态块.构造函数的加载顺序 先看一道Java面试题: 1 public class Baset { 2 private String baseName = "base"; 3 // 构造方法 4 public Baset() { 5 callName(); 6 } 7 // 成员方法 8 public void callName() { 9 // TODO Auto-generated method stub 10 System.out.p

Java类加载及变量初始化过程

Java虚拟机如何把编译好的.class文件加载到虚拟机里面?加载之后如何初始化类?静态类变量和实例类变量的初始化过程是否相同,分别是如何初始化的呢?这篇文章就是解决上面3个问题的. 本文前面理论部分比较枯燥,但是如果耐心读完,结合后面的实例,我相信你以后绝对不会再遇到java类初始化这样的疑惑.若有不正之处,请多多谅解并欢迎各位能够给予批评指正,提前谢谢各位. 1. Java虚拟机加载.class过程 虚拟机把Class文件加载到内存,然后进行校验,解析和初始化,最终形成java类型,这就是虚

Java类变量和成员变量初始化过程

一.类的初始化 对于类的初始化:类的初始化一般只初始化一次,类的初始化主要是初始化静态成员变量. 类的编译决定了类的初始化过程. 编译器生成的class文件主要对定义在源文件中的类进行了如下的更改: 1)       先按照静态成员变量的定义顺序在类内部声明成员变量. 2)       再按照原java类中对成员变量的初始化顺序进行初始化. 一个java类和编译后的class对应的转换如下: 源文件: public class Person{ public static String name=

案例三:小明左右手分别拿两张纸牌:黑桃10和红心8,现在交换手中的牌。编写一个程序模拟这一个过程:两个整数分别保存在两个变量中,将这两个变量的值互换,并输出互换后的结果。

package project_03; /** * 2018-9-7 20:19:59 * @author Sauron XiaMen * */ public class ChangeCard { //将两个变量的值互换 public static void main(String[] args) { System.out.println("输出互换前手中的纸牌:"); int left=10; int right=8; int i=0; System.out.println(&quo

【Go语言】【5】变量初始化及赋值

在真正的编码过程中要使用一个变量,必须先声明然后才能使用,GO语言也不例外 1.声明变量 var postCode int    //声明一个整型变量postCode var phoneNum int    //声明一个整型变量phoneNum var name string     //声明一个字符串变量name var address string  //声明一个字符串变量address 接下来我们在main()方法直接打印一下各个值分别是多少: 从上面可以看到尽管我们只是声明了一个变量,但

Java实例变量初始化

由一道面试题所想到的--Java实例变量初始化 时间:2015-10-07 16:08:38      阅读:23      评论:0      收藏:0      [点我收藏+] 该题目源自微信公众号(程序员的那些事)的推送:携程 Java 工程师的一道面向对象面试题 题目是这样的:求下面程序的输出: public class Base { private String baseName = "base"; public Base() { callName(); } public v

Java变量初始化的时机

对于JAVA中变量的初始化是一个很基础的问题,其中的一些问题也是易被学习者所忽略.当在编写代码的时候碰到时,常被这些问题引发的错误,感觉莫名其妙.而且现在许多大公司的面试题,对于这方面的考查也是屡试不爽.以下是对java变量初始化的时机的分析. [java变量执行初始化的步骤] java是一门强类型语言,因此java语言规定每个变量必须先声明,然后才能使用,声明变量时必须指定该变量的数据类型.首先看下面这条语句的执行过程: int a = 5; 实际上面这条语句会被拆分成两个过程执行: (1)i

类内const static(static const)成员变量初始化问题

在查找const相关资料的过程中,又遇到了另外一个问题,就是C++类中const static(或者static const)成员变量应当如何初始化的问题. 查阅了许多资料,发现VC环境下,只允许const static成员变量在类外初始化,这个应该是编译器遗留下的bug(你也可以说是要求严格). 在其他编译器下,整型以及枚举类型的const static成员变量是允许在声明的同时进行初始的,其中整型包括int.short.long.char等,非整型是指浮点型 包括float.double等.

一起空指针引发的程序问题的排查过程

      [文章摘要] 在C程序中,指针操作是难点和精华所在.指针一旦使用不当,极有可能造成程序的崩溃. 本文对一空指针引发的程序问题的排查过程进行了详细的介绍,为相关软件问题的分析及解决提供了有益的参考. 一.问题描述 最近,某程序在测试过程中突然崩溃.日志中出现如下内容: #0  0xf64f2b3a in FunctionA(event=666,dlgindex=0, ucErrNo=1 '\001') at src/A.c:6838 #1  0xf64e3a4f in Function