C++实用技巧(二)
复杂的东西写多了,如今写点简单的好了。由于功能上的需要,Vczh Library++3.0被我搞得很离谱。为了开发维护的遍历、减少粗心犯下的错误以及增强单元测试、回归测试和测试工具,因此记录下一些开发上的小技巧,以便抛砖引玉,造福他人。欢迎高手来喷,菜鸟膜拜。
上一篇文章讲到了如何检查内存泄露。其实只要肯用C++的STL里面的高级功能的话,内存泄露是很容易避免的。我在开发Vczh Library++ 3.0的时候,所有的测试用例都保证跑完了没有内存泄露。但是很可惜有些C++团队不能使用异常,更甚者不允许写构造函数析构函数之类,前一个还好,后一个简直就是在用C。当然有这些易做图规定的地方STL都是用不了的,所以我们更加需要扎实的基础来开发C++程序。
今天这一篇主要还是讲指针的问题。因为上一篇文章一笔带过,今天就来详细讲内存泄漏或者野指针发生的各种情况。当然我不可能一下子举出全部的例子,只能说一些常见的。
一、错误覆盖内存。
之前提到的不能随便乱memset其实就是为了避免这个问题的。其实memcpy也不能乱用,我们来看一个例子,最简单的:
1 #define MAX_STRING 20;
2
3 struct Student
4 {
5 char name[MAX_STRING];
6 char id[MAX_STRING];
7 int chinese;
8 int math;
9 int english;
10 };
大家对这种结构肯定十分熟悉,毕竟是大学时候经常要写的作业题……好了,大家很容易看得出来这其实是C语言的经典写法。我们拿到手之后,一般会先初始化一下,然后赋值。
1 Student vczh;
2 memset(&vczh, 0, sizeof(vczh));
3 strcpy(vczh.name, "vczh");
4 strcpy(vczh.id, "VCZHS ID");
5 vczh.chinese=70;
6 vczh.math=90;
7 vczh.english=80;
为什么要在这里使用memset呢?memset的用处是将一段内存的每一个字节都设置成同一个数字。这里是0,因此两个字符串成员的所有字节都会变成0。因此在memset了Student之后,我们通过正常方法来访问name和id的时候都会得到空串。而且如果Student里面有指针的话,0指针代表的是没有指向任何有效对象,因此这个时候对指针指向的对象进行读写就会立刻崩溃。对于其他数值,0一般作为初始值也不会有什么问题(double什么的要小心)。这就是我们写程序的时候使用memset的原因。
好了,如今社会进步,人民当家做主了,死程们再也不需要受到可恶的C语言剥削了,我们可以使用C++!因此我们借助STL的力量把Student改写成下面这种带有C++味道的形式:
1 struct Student
2 {
3 std::string name;
4 std::string id;
5 int chinese;
6 int math;
7 int english;
8 };
我们仍然需要对Student进行初始化,不然三个分数还是随机值。但是我们又不想每一次创建的时候都对他们分别进行赋值初始化城0。这个时候你心里可能还是想着memset,这就错了!在memset的时候,你会把std::string内部的不知道什么东西也给memset掉。假如一个空的std::string里面存放的指针指向的是一个空的字符串而不是用0来代表空的时候,一下子内部的指针就被你刷成0,等下std::string的析构函数就没办法delete掉指针了,于是内存泄露就出现了。有些朋友可能不知道上面那句话说的是什么意思,我们现在来模拟一下不能memset的std::string要怎么实现。
为了让memset一定出现内存泄露,那么std::string里面的指针必须永远都指向一个有效的东西。当然我们还需要在字符串进行复制的时候复制指针。我们这里不考虑各种优化技术,用最简单的方法做一个字符串出来:
1 class String
2 {
3 private:
4 char* buffer;
5
6 public:
7 String()
8 {
9 buffer=new char[1];
10 buffer[0]=0;
11 }
12
13 String(const char* s)
14 {
15 buffer=new char[strlen(s)+1];
16 strcpy(buffer, s);
17 }
18
19 String(const String& s)
20 {
21 buffer=new char[strlen(s.buffer)+1];
22 strcpy(buffer, s.buffer);
23 }
24
25 ~String()
26 {
27 delete[] buffer;
28 }
29
30 String& operator=(const String& s)
31 {
32 delete[] buffer;
33 buffer=new char[strlen(s.buffer)+1];
34 strcpy(buffer, s.buffer);
35 }
36 };
于是我们来做一下memset。首先定义一个字符串变量,其次memset掉,让我们看看会发生什么事情:
1 string s;
2 memset(&s, 0, sizeof(s));
第一行我们构造了一个字符串s。这个时候字符串的构造函数就会开始运行,因此strcmp(s.buffer, "")==0。第二行我们把那个字符串给memset掉了。这个时候s.buffer==0。于是函数结束了,字符串的析构函数尝试delete这个指针。我们知道delete一个0是不会有问题的,因此程序不会发生错误。我们活生生把构造函数赋值给buffer的new char[1]给丢了!铁定发生内存泄露!
好了,提出问题总要解决问题,我们不使用memset的话,怎么初始化Student呢?这个十分好做,我们只需要为Student加上构造函数即可:
1 struct Student
2 {
3 .//不重复那些声明
4
5 Student():chinese(0),math(0),english(0)
6 {
7 }
8 };
这样就容易多了。每当我们定义一个Student变量的时候,所有的成员都初始化好了。name和id因为string的构造函数也自己初始化了,因此所有的成员也都初始化了。加入Student用了一半我们想再初始化一下怎么办呢?也很容易:
1 Student vczh;
2 .//各种使用
3 vczh=Student();
经过一个等号操作符的调用,旧Student的所有成员就被一个新的初始化过的Student给覆盖了,就如同我们对一个int变量重新赋值一样常见。当然因为各种复制经常会出现,因此我们也要跟上面贴出来的string的例子一样,实现好那4个函数。至此我十分不理解为什么某些团队不允许使用构造函数,我猜就是为了可以memset,其实是很没道理的。
二、异常。
咋一看内存泄露跟异常好像没什么关系,但实际上这种情况更容易发生。我们来看一个例子:
1 char* strA=new char[MAX_PATH];
2 if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
3 char* strB=new char[MAX_PATH];
4 if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
5
6 DoSomething(strA, strB);
7
8 RELEASE_STRB:
9 delete[] strB;
10 RELEASE_STRA:
11 delete[] strA;
相信这肯定是大家的常用模式。我在这里也不是教唆大家使用goto,不过对于这种例子来说,用goto是最优美的解决办法了。但是大家可以看出来,我们用的是C++,因为这里有new。如果DoSomething发生了异常怎么办呢?如果GetXXX发生了异常怎么办呢?我们这里没有任何的try-catch,一有异常,函数里克结束,两行可怜的delete就不会被执行到了,于是内存泄漏发生了!
那我们如何避免这种情况下的内存泄露呢?一些可爱的小盆友可能会想到,既然是因为没有catch异常才发生的内存泄露,那我们来catch吧:
1 char* strA=new char[MAX_PATH];
2 try
3 {
4 if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
5 char* strB=new char[MAX_PATH];
6 try
7 {
8 if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
9 DoSomething(strA, strB);
10 }
11 catch()
12 {
13 delete[] strB;
14 throw;
15 }
16 }
17 catch()
18 {
19 delete[] strA;
20 throw;
21 }
22
23 RELEASE_STRB:
24 delete[] strB;
25 RELEASE_STRA:
26 delete[] strA;
你能接受吗?当然是不能的。问题出在哪里呢?因为C++没有try-finally。你看这些代码到处都是雷同的东西,显然我们需要编译器帮我们把这些问题搞定。最好的解决方法是什么呢?显然还是构造函数和析构函数。总之记住,如果想要事情成对发生,那么使用构造函数和析构函数。
第一步,GetXXX显然只能支持C模式的东西,因此我们要写一个支持C++的:
1 bool GetXXX2(string& s)
&nbs
补充:软件开发 , C++ ,