一
我原本想把标题叫做《一个memset引起的血案》,后来一想,似乎过于惊世骇俗,而且也尚未到“血案”的悲壮程度,因此就取了个比较中庸一点的标题。
虽然造成的影响没有预想的严重,但也是得益于现在将这个问题挖出来了,试想如果带着问题将程序投产,那足以用“血案”来形容。
每每想到这里,我都感到一阵后怕。
事情从头说起,这是6月下旬来的一个需求,只是在原有的程序基础上进行一些代码改造,从需求设计,技术实现,再到核心逻辑编写,都是在我的主导下完成的,得益于项目小组一贯的高效率,涉及改造的8个交易一个构件不到半个月全部完成,后续的测试不可谓不细致,一轮单元自测,一轮交叉测试,一轮系统测试,涉及测试案例达到近四五百条,代码也经过了两轮以上的评审。
按理说,这样的结果足以让人放心了,事实上因为近来开发任务比较重,我也就将这件事搁置下了。直到近期这个需求即将上线,我在做例行的验证性回归测试时,才发现了一个问题。
这就是上文提到的,一个由memset引起的bug。
说起来,能够发现这个问题实在有些运气的成分,测试用的12条数据,有9条的结果都是符合预期的,但有三条不符,我正是在这三条不符预期的测试记录中挖出了这个隐藏的bug。因为一个变量未进行初始化,所以在测试的时候,意外送进来了一个奇怪的值,恰巧这个奇怪的值能让程序走进另外一段逻辑。
所以你现在能理解我为什么说主要靠运气成分了。
设若它送进来的奇怪的值是一个乱码或者其他的值,程序既不会报错,也不会走进其他的逻辑(拿这个变量值去查数据库,如果有记录则走进其他逻辑,没有记录则走正常逻辑),这也正是之前测试了那么多轮都没有发现这个bug的主要原因所在。从现有的测试结果来看,这个问题的出现概率仅仅是0.6%不到。
这才是我真正感到后怕的原因。
从刚刚接触C语言开始,到企业单位的代码规范,都在不断的强调变量初始化的重要性,但实际开发过程中,似乎很少有因为没有初始化而造成的问题,于是乎很多人就淡化了初始化的重要性,觉得不过是耸人听闻而已。这算是比较典型的一例。
因为很多时候,即使是一个局部变量,即使你不去初始化,它也恰巧是个空值,正是因为存在着这些巧合,才导致很多人写代码的时候没有对变量的初始化引起足够的重视。
特别是在某个(全局)变量在反复使用的时候,在频繁使用break,goto的时候,有没有跳过关键变量的初始化,这些浅显的道理本该是C语言的入门素养,我却不得不在这里再次强调一番。
当然我也见过一些人的程序,在构件里不管三七二十一,先来一段初始化,结果将程序的入参都给清掉了的。所以,过度初始化也未见得是什么好事。关键还是要理解,要读懂代码。
二
借这个机会,科普一下C语言程序的一些初始化的方法,算是班门弄斧,若有说得不对的地方,欢迎补充指正。
数值类变量初始化
整型、浮点型的变量可以在定义的同时进行初始化,一般都初始化为0。
int inum = 0;
float fnum = 0.00f;
double dnum = 0.00;
字符型变量初始化
字符型变量也可在定义的同时进行初始化,一般初始化为'\0'。
char ch = '\0';
字符串初始化
字符串初始化的方法比较多,我这里简单介绍三种,因为字符串本质上是由一个个字符组成的字符数组,所以其初始化的最终目的,就是将字符数组里面的一个个字符都初始化为'\0'。
方法一:使用空的字符串 ""。
char str[10] = "";
方法二:使用memset。
char str[10];
memset(str, 0, sizeof(str));
方法三:写一个循环。
char str[10];
for(int i = 0; i < 10; i++)
{
str[i] = '\0';
}
这里比较推荐的是第二种初始化方法。也即使用memset进行初始化。
很多人对memset这个函数一知半解,只知道它可以初始化很多数据类型的变量,却不知道其原理是什么样的,这里做一下简要的说明:memset是按照字节进行填充的。
先看下面的一段代码:
int num;
memset(&num, 0, sizeof(int));
printf("step1=%d\n", num);
memset(&num, 1, sizeof(int));
printf("step2=%d\n", num);
在讨论之前,我们先看一下运行结果
chenyc@DESKTOP-IU8FEL6:~/src$ gcc -o memset memset.c -g
chenyc@DESKTOP-IU8FEL6:~/src$ ./memset
step1 = 0
step2 = 16843009
chenyc@DESKTOP-IU8FEL6:~/src$
看到这个运行结果,是不是和你想象中的不一样呢?
step1 = 0 相信大家都好理解,可 step2 = 16843009 很多人就不能理解了。按照一般的惯性思维,不是应该 = 1 才对么?
这就是我要说的,memset是按照字节进行填充的。
我们知道,int 型是4个字节(每个字节有8位),按二进制表示出来就应该是:
00000000 00000000 00000000 00000000
按照按字节填充的原则,step1 的结果就是将4个字节全部填充0,所以得到的结果仍然是0:
00000000 00000000 00000000 00000000
而 step2 则是将每个字节都填充为1(注意是每个字节,而不是每个byte位),所以相对应的结果就应该是:
00000001 00000001 00000001 00000001
大家可以自己将上面那个二进制数转换成十进制看看,看看是不是16843009。
所以严格来说,memset函数本身并不具有初始化的功能,而是一个单纯的按字节填充函数,只是人们在使用的过程中,扩展出了初始化的作用。
字符串初始化有一个小窍门,我们知道字符串本质上是字符数组,因此它具有两个特性,一,字符串在内存里是连续的,二,字符串遇'\0'结束。所以我们在初始化的时候,总是愿意给字符串本身长度加1的长度的内存进行初始化。
char year[4+1];
memset(year, 0, sizeof(year));
strcpy(year,"2018");
指针初始化
一般来说,指针都是初始化为NULL。
int *pnum = NULL;
int num = 0;
pnum = #
指针是个让人又爱又恨的东西,一般的整形、字符串等,初始化之后就可以直接拿来用了,可指针如果初始化为NULL后,没有给该指针重新分配内存,则会出现难以预料的错误(最最常见的就是操作空指针引起的段错误)。
在动态内存管理中,由于变量的内存是分配在堆中的,所以一般用malloc、calloc等函数申请过动态内存,在使用完后需要及时释放,一般释放掉动态内存后要及时将指针置空,这也是很多人容易忽略的。
char *p = NULL;
p=(char *)malloc(100);
if(NULL == p)
{
printf("Memory Allocated at: %x\n",p);
}
else
{
printf("Not Enough Memory!\n");
}
free(p);
p = NULL; //这一行给指针置空必不可少,否则很可能后面操作了这个野指针而不自知,从而导致出现严重的问题
很多人经常会犯的一个错误,我们知道,在指针作为实参进行参数传递时,该指针就已经退化成了数组,所以很多人就想到用memset来对该指针进行初始化:
void fun(char *pstr)
{
memset(pstr, 0, sizeof(pstr));
...
}
这种写法是不正确的。我们姑且不管指针能不能用memset来进行初始化,指针首先保存的是一个4字节的地址,所以sizeof(pstr)永远只能 = 4,这样的初始化就毫无意义。
结构体初始化
结构体的初始化就比较简单了,基本也都是采用memset的方式。
typedef struct student
{
int id;
char name[20];
char sex;
}STU;
STU stu1;
memset((char *)&stu1, 0, sizeof(stu1));
关于初始化结构体的长度问题,也即memset的第三个参数,一般来说,传入数据类型和变量名效果是一样的,上例中,下面写法是等价的效果:
memset((char *)&stu1, 0, sizeof(STU));
但是对于结构体数组的初始化,长度就需要注意一下了,还是以上例来做说明:
STU stus[10];
memset((char *)&stus, 0, sizeof(stus)); //正确,数组本身在内存里就是连续的,sizeof取出的就是数组的字节长度
memset((char *)&stus, 0, sizeof(STU)); //错误,只会初始化第一个STU结构体,后面还有9个STU元素并未初始化
memset((char *)&stus, 0, sizeof(STU)*10); //正确,效果与第一个是一样的
有些人习惯将memset的第二个参数写成以下形式:
memset((char *)&stu1, 0x00, sizeof(stu1));
只要理解了memset是按字节进行填充的,就知道这样写也是正确的,完全没有问题。
三
前段时间给项目组的同事review代码的时候,发现了很多细节上的不规范,比如变量名定义过于随意,从别的地方拷贝来的代码不加以理解,直接照搬,导致很多代码冗余,张冠李戴,将A字段的值赋到B字段中,测试居然没有发现出来……等等问题不一而足。
项目组的很多成员都不是做纯C出身的(我也不是),基本上都是边学便用,而对于我们项目本身的性质,业务层面要大于技术层面,所以很难有比较系统的时间去学习、钻研C语言的语法及原理。导致大多数时候写代码要么照搬之前现成的例子,要么花在调试上的时间远远大于写代码的时间。这些往往都是没有对C知识深入理解所导致的。
其实学习是一个不断输入的过程,很多代码规范都是前人通过经验的不断积累出来的宝贵经验,遵守一定的代码规范,错误就会规避掉很大一部分。尤其对于我们做开发的人来说,不学习就意味着原地踏步,在如今科技高速发展的如今,昨天PHP还是世界上最好的语言,今天Python就以王者的姿态君临天下,谁知道明天又会发生什么呢?原地踏步,等于不断退步。
其实也并不是说编程语言掌握得越多越好,但总要有一门自己比较精通的,相对来说,C语言面向过程,比较偏底层实现和算法,历经三十多年而不衰,自有其独特之处。如果能真正掌握了这一门技术,其他的编程语言也是一法通万法通的事。
网友评论