复杂的东西写多了,如今写点简单的好了。由于功能上的需要, 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, "VCZH'S 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   .//不重复那些声明

5   Student():chinese(0),math(0),english(0)
6   {
7   }
8 };

这样就容易多了。每当我们定义一个Student变量的时候,所有的成员都初始化好了。name和id因为string的构造函数也自己初始化了,因此所有的成员也都初始化了。加入Student用了一半我们想再初始化一下怎么办呢?也很容易:

1 Student vczh;
.//各种使用
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)
 2 {
 3   char* str=new char[MAX_PATH];
 4   bool result;
 5   try
 6   {
 7     result=GetXXX(str, MAX_PATH);
 8     if(result)s=str;
 9   }
10   catch()
11   {
12     delete[] str;
13     throw;
14   }
15   delete[] str;
16   return result;
17 }

借助这个函数我们可以看到,因为有了GetXXX这种C的东西,导致我们多了多少麻烦。不过这总是一劳永逸的,有了GetXXX2和修改之后的DoSomething2之后,我们就可以用更简单的方法来做了:

1 string a,b;
2 if(GetXXX2(a) && GetXXX2(b))
3 {
4   DoSomething2(a, b);
5 }

多么简单易懂。这个代码在任何地方发生了异常,所有new的东西都会被delete。这就是析构函数的一个好处。一个变量的析构函数在这个变量超出了作用域的时候一定会被调用,无论代码是怎么走出去的。

今天就说到这里了。说了这么多还是想让大家不要小看构造函数和析构函数。那种微不足道的因为一小部分不是瓶颈的性能问题而放弃构造函数和析构函数的做法,终究是要为了修bug而加班的。只要明白并用好了构造函数、析构函数和异常,那么C++的特性也可以跟C一样清楚明白便于理解,而且写出来的代码更好看的。大家期待第三篇哈。

from: http://www.cppblog.com/vczh/archive/2010/06/24/118603.html

C++实用技巧(二)相关推荐

  1. 麒麟子Cocos Creator实用技巧二:微信名字截断(支持表情)

    在我们日常游戏开发中,经常会面临将玩家名字截断的需求. 假如玩家是在我们游戏中创建的名字,那么可以简单粗暴地禁止玩家使用手机表情输入即可. 但如果我们是第三方账号登录,且使用了第三方账号的用户昵称,那 ...

  2. 麒麟子Cocos Creator实用技巧

    麒麟子Cocos Creator实用技巧 大家好,我是麒麟子, 开源棋牌<幼麟棋牌-四川麻将>(泄漏版叫 <达达麻将>)作者,成都幼麟科技创始人. 自09年进入游戏行业以来,不 ...

  3. 大小球实用技巧:程序实现“惊天反转”你想要的这里都有!

    大小球分析技巧程序实现 前言 一.大小球实用技巧 二.公式预估 1.程序化处理 2.电脑呈现界面 使用电脑下载打开 前言 卡塔尔世界杯为期不远,对于喜欢看球玩球又期待营收的朋友来说,上一届俄罗斯世界杯 ...

  4. 微信小小屠龙攻略服务器,小小屠龙实用技巧攻略合集

    小小屠龙无限元宝版是微信上非常火爆好玩的传奇手游,复古怀旧经典画风,可乐小小屠龙游戏承袭了经典三职业对战,在指尖暴走中疯狂击杀敌人,喜欢的小伙伴可以去体验哦~今天小编将系统地给大家讲解一下可乐小小屠龙 ...

  5. abaqus python二次开发攻略_Abaqus有限元分析常见问题解答与实用技巧 12天后上架...

    Abaqus有限元分析常见问题解答与实用技巧已印刷完毕,1-2天后上架,先睹为快.现在某些网站上的售卖信息,不靠谱.温馨提示:封底无防伪标均为盗版! 序  言 Abaqus是是国际上先进的大型通用非线 ...

  6. numpy实用技巧(二)

    numpy实用技巧(一) 一维 ⇒ 二维 >> x = np.array([1, 2, 3]) >> x[:, None] array([[1],[2],[3]]) >& ...

  7. python实用技巧(二)

    Python实用技巧(一) python实用技巧(二) python实用技巧(三) python实用技巧(四) code 解释 补充 list.reverse() 返回值类型为NoneType l.r ...

  8. Flutter完整开发实战详解(十七、 实用技巧与填坑二)

    作为系列文章的第十七篇,本篇再一次带来 Flutter 开发过程中的实用技巧,让你继续弯道超车,全篇均为个人的日常干货总结,以实用填坑为主,让你少走弯路狂飙车. Flutter 完整实战实战系列文章专 ...

  9. 深度学习11个实用技巧

    深度学习11个实用技巧 深度学习工程师George Seif发表了一篇博文,总结了7个深度学习的技巧,本文增加了几个技巧,总结了11个深度学习的技巧,主要从提高深度学习模型的准确性和速度两个角度来分析 ...

  10. 深度学习七个实用技巧

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 深度学习已经成为解决许多具有挑战性的现实世界问题的方法.对目标检测 ...

最新文章

  1. 深入了解line-height
  2. 写了一个在线流程设计器,效果很炫[Siverlight版本]
  3. Zabbix 4.2 发布:支持Prometheus数据收集,可扩展性大大提升
  4. 导入工程后编译不过,报错: apply plugin: 'com.github.dcendents.android-maven'
  5. Spring Boot:构建一个RESTful Web应用程序
  6. 云服务器cvm只能弄网站么,云服务器cvm与建站主机区别
  7. 用__asm写c函数[秋镇菜]
  8. Python工作笔记-使用Py遍历文件夹及其子文件夹内容
  9. Nordic Collegiate Programming Contest (NCPC) 2016
  10. Kontakt 6 for Mac(强大的音频采样器软件)
  11. 条形码类型及其字符集和长度的说明
  12. Typecho 插件开发基础
  13. 17所大学!考研计算机/软件专业课变化的大学名单!【20考研】
  14. multisim变压器反馈式_基于Multisim的电压串联型负反馈电路的仿真与分析
  15. 深度解析京东个性化推荐系统
  16. 第七届科技节微电影比赛须知
  17. Flutter:1个人,100天业余时间,能开发出什么?
  18. 数字逻辑综合工具实践-DC-10——CDC约束
  19. grin中pow算法-cuckoo cycle的lean算法简单分析
  20. 程序员也应该多花时间多读书

热门文章

  1. 互联网女皇”Mary Meeker 2015互联网趋势报告说了些什么
  2. 七牛技术总监陈超:记Spark Summit China 2015
  3. php引擎,PHP内核探索:Zend引擎
  4. Chromium:安装depot_tools及获取Chromium源代码
  5. Redis进阶-bind参数详解
  6. Spring-注入参数详解-[简化配置方式]
  7. Oracle-等待事件解读
  8. Java学习笔记(六)--字符串String类
  9. Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理
  10. java json太长_修复Long类型太长,而Java序列化JSON丢失精度问题的方法