C++何时发明的爱问为什么的大发明家发明C++

本帖子已过去太久远了,不再提供回复功能。C++相关问答:123456789101112131415++是什么时候发明的
美国AT&T贝尔实验室的本贾尼·斯特劳斯特卢普(Bjarne Stroustrup)博士在20世纪80年代初期发明并实现了C++(最初这种语言被称作“C with Classes”)。
一开始C++是作为C语言的增强版出现的,从给C语言增加类开始,不断的增加新特性。虚函数(virtual function)、运算符重载(operator overloading)、多重继承(multiple inheritance)、模板(template)、异常(exception)、RTTI、命名空间(name space)逐渐被加入标准。
1998年国际标准组织(ISO)颁布了C++程序设计语言的国际标准ISO/IEC 。C++是具有国际标准的编程语言,通常称作ANSI/ISOC++。
其他答案(共2个回答)
相关问答:12<a href="/b/1oNytom83...
丹尼斯·里奇, C语言之父,UNIX之父。曾担任朗讯科技公司贝尔实验室 下属的计算机科学研究中心系统软件研究部的主任一职。1978年与布莱恩·科尔尼干(Bria...
1.一个C语言源程序可以由一个或多个源文件组成。
2.每个源文件可由一个或多个函数组成。
3.一个源程序不论由多少个文件组成,都有一个且只能有一个main函数,...
双音多频信号(DTMF),电话系统中电话机与交换机之间的一种用户信令,通常用于发送被叫号码。
在使用双音多频信号之前,电话系统中使用一连串的断续脉冲来传送被叫号...
不知道是谁,但肯定是机器语言
答: 怎么通过孕囊数据看男女?谁会?帮我看看。
答: cisco网络技术是一个为CTO、IT技术经理、网络工程师、安全工程师、数据库工程师、网络管理员、系统工程师、开发工程师、项目管理人员等IT技术人员搭建的有关先...
答: 我想学网页制作、网络维护、电脑维护
可有比较好的学习网站或者论坛推选一下?
大家还关注
确定举报此问题
举报原因(必选):
广告或垃圾信息
激进时政或意识形态话题
不雅词句或人身攻击
侵犯他人隐私
其它违法和不良信息
报告,这不是个问题
报告原因(必选):
这不是个问题
这个问题分类似乎错了
这个不是我熟悉的地区
相关问答:123456789101112131415C++内存管理 - CSDN博客
C++内存管理
内存管理是C&#43;&#43;最令人切齿痛恨的问题,也是C&#43;&#43;最有争议的问题,C&#43;&#43;高手从中获得了更好的性能,更大的自由,C&#43;&#43;菜鸟的收获则是一遍一遍的检查代码和对C&#43;&#43;的痛恨,但内存管理在C&#43;&#43;中无处不在,内存泄漏几乎在每个C&#43;&#43;程序中都会发生,因此要想成为C&#43;&#43;高手,内存管理一关是必须要过的,除非放弃C&#43;&#43;,转到Java或者.NET,他们的内存管理基本是自动的,当然你也放弃了自由和对内存的支配权,还放弃了C&#43;&#43;超绝的性能。本期专题将从内存管理、内存泄漏、内存回收这三个方面来探讨C&#43;&#43;内存管理问题。
1&内存管理
伟大的Bill Gates&曾经失言:
  640K ought to be enough for everybody&—&Bill Gates 1981
程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。
1.1 C&#43;&#43;内存管理详解
1.1.1&内存分配方式
1.1.1.1&分配方式简介
  在C&#43;&#43;中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
  栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相&#20284;的,不过它是用free来结束自己的生命的。
  全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C&#43;&#43;里面没有这个区分了,他们共同占用同一块内存区。
  常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
1.1.1.2&明确区分堆与栈
  在bbs上,堆与栈的区分问题,&#20284;乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。
  首先,我们举一个例子:
void f() { int* p=new int[5]; }
  这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator
new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
0040102A call operator new ()
0040102F add esp,4
mov dword ptr [ebp-8],eax
mov eax,dword ptr [ebp-8]
mov dword ptr [ebp-4],eax
  这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。
1.1.1.3&堆和栈究竟有什么区别?
  好了,我们回到我们的主题:堆和栈究竟有什么区别?
  主要的区别由以下几点:
  1、管理方式不同;
  2、空间大小不同;
  3、能否产生碎片不同;
  4、生长方向不同;
  5、分配方式不同;
  6、分配效率不同;
  管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
  空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:
  打开工程,依次操作菜单如下:Project-&Setting-&Link,在Category&中选中Output,然后在Reserve中设定堆栈的最大&#20540;和commit。
  注意:reserve最小&#20540;为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的&#20540;,可能增加内存的开销和启动时间。
  碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
  生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
  分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
  分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C&#43;&#43;函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
  从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
  虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)
1.1.2&控制C&#43;&#43;的内存分配
  在嵌入式系统中使用C&#43;&#43;的一个常见问题是内存分配,即对new&和&delete&操作符的失控。
  具有讽刺意味的是,问题的根源却是C&#43;&#43;对内存的管理非常的容易而且安全。具体地说,当一个对象被消除时,它的析构函数能够安全的释放所分配的内存。
  这当然是个好事情,但是这种使用的简单性使得程序员们过度使用new&和&delete,而不注意在嵌入式C&#43;&#43;环境中的因果关系。并且,在嵌入式系统中,由于内存的限制,频繁的动态分配不定大小的内存会引起很大的问题以及堆破碎的风险。
  作为忠告,保守的使用内存分配是嵌入式环境中的第一原则。
  但当你必须要使用new&和delete时,你不得不控制C&#43;&#43;中的内存分配。你需要用一个全局的new&和delete来代替系统的内存分配符,并且一个类一个类的重载new&和delete。
  一个防止堆破碎的通用方法是从不同固定大小的内存持中分配不同类型的对象。对每个类重载new&和delete就提供了这样的控制。
1.1.2.1&重载全局的new和delete操作符
  可以很容易地重载new&和&delete&操作符,如下所示:
void * operator new(size_t size)
void *p = malloc(size);
return (p);
void operator delete(void *p);
  这段代码可以代替默认的操作符来满足内存分配的请求。出于解释C&#43;&#43;的目的,我们也可以直接调用malloc()和free()。
  也可以对单个类的new&和&delete&操作符重载。这是你能灵活的控制对象的内存分配。
class TestClass {
void * operator new(size_t size);
void operator delete(void *p);
// .. other members here ...
void *TestClass::operator new(size_t size)
void *p = malloc(size); // Replace this with alternative allocator
return (p);
void TestClass::operator delete(void *p)
free(p); // Replace this with alternative de-allocator
  所有TestClass&对象的内存分配都采用这段代码。更进一步,任何从TestClass&继承的类也都采用这一方式,除非它自己也重载了new&和&delete&操作符。通过重载new&和&delete&操作符的方法,你可以自由地采用不同的分配策略,从不同的内存池中分配不同的类对象。
1.1.2.2&为单个的类重载&new[ ]和delete[ ]
  必须小心对象数组的分配。你可能希望调用到被你重载过的new&和&delete&操作符,但并不如此。内存的请求被定向到全局的new[
]和delete[ ]&操作符,而这些内存来自于系统堆。
  C&#43;&#43;将对象数组的内存分配作为一个单独的操作,而不同于单个对象的内存分配。为了改变这种方式,你同样需要重载new[ ]&和&delete[
class TestClass {
void * operator new[ ](size_t size);
void operator delete[ ](void *p);
// .. other members here ..
void *TestClass::operator new[ ](size_t size)
void *p = malloc(size);
return (p);
void TestClass::operator delete[ ](void *p)
int main(void)
TestClass *p = new TestClass[10];
// ... etc ...
但是注意:对于多数C&#43;&#43;的实现,new[]操作符中的个数参数是数组的大小加上额外的存储对象数目的一些字节。在你的内存分配机制重要考虑的这一点。你应该尽量避免分配对象数组,从而使你的内存分配策略简单。
1.1.3&常见的内存错误及其对策
发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。&常见的内存错误及其对策如下:
  *&内存分配未成功,却使用了它。
  编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行
  检查。如果是用malloc或new来申请内存,应该用if(p==NULL)&或if(p!=NULL)进行防错处理。
  *&内存分配虽然成功,但是尚未初始化就引用它。
  犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初&#20540;全为零,导致引用初&#20540;错误(例如数组)。&内存的缺省初&#20540;究竟是什么并没有统一的标准,尽管有些时候为零&#20540;,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初&#20540;,即便是赋零&#20540;也不可省略,不要嫌麻烦。
  *&内存分配成功并且已经初始化,但操作越过了内存的边界。
  例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
  *&忘记了释放内存,造成内存泄露。
  含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。
  动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
  *&释放了内存却继续使用它。
  有三种情况:
  (<span style="color:#)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
  (<span style="color:#)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
  (3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
  【规则1】用malloc或new申请内存之后,应该立即检查指针&#20540;是否为NULL。防止使用指针&#20540;为NULL的内存。
  【规则2】不要忘记为数组和动态内存赋初&#20540;。防止将未被初始化的内存作为右&#20540;使用。
  【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
  【规则4】动态内存的申请与释放必须配对,防止内存泄漏。
  【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
1.1.4&指针与数组的对比
  C&#43;&#43;/C程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。
  数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
  指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。
  下面以字符串为例比较指针与数组的特性。
1.1.4.1&修改内容
下面示例中,字符数组a的容量是6个字符,其内容为hello。a的内容可以改变,如a[0]=&‘X’。指针p指向常量字符串“world”(位于静态存储区,内容为world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]=&‘X’有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。
char a[] = “hello”;
a[0] = ‘X’;
cout && a &&
char *p =&“world”; //&注意p指向常量字符串
p[0] =&‘X’; //&编译器不能发现该错误
cout && p &&
1.1.4.2&内容复制与比较
  不能对数组名进行直接复制与比较。若想把数组a的内容复制给数组b,不能用语句&b = a&,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b==a)&来判断,应该用标准库函数strcmp进行比较。
语句p = a&并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)&#43;1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a)&比较的不是内容而是地址,应该用库函数strcmp来比较。
char a[] = &hello&;
char b[10];
strcpy(b, a); //&不能用&b =
if(strcmp(b, a) == 0) //&不能用&if (b == a)
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len&#43;1));
strcpy(p,a); //&不要用&p =
if(strcmp(p, a) == 0) //&不要用&if (p == a)
1.1.4.3&计算内存容量
用运算符sizeof可以计算出数组的容量(字节数)。如下示例中,sizeof(a)的&#20540;是<span style="color:#(注意别忘了’’)。指针p指向a,但是sizeof(p)的&#20540;却是<span style="color:#。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。C&#43;&#43;/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
char a[] = &hello world&;
cout&& sizeof(a) && // 12字节
cout&& sizeof(p) && // 4字节
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。如下示例中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。
void Func(char a[100])
 cout&& sizeof(a) && // 4字节而不是100字节
1.1.5&指针参数是如何传递内存的?
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如下示例中,Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?
void GetMemory(char *p, int num)
 p = (char *)malloc(sizeof(char) * num);
void Test(void)
 char *str = NULL;
 GetMemory(str, 100); // str&仍然为&NULL
 strcpy(str, &hello&); //&运行错误
毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是&_p,编译器使&_p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。
如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例:
void GetMemory2(char **p, int num)
 *p = (char *)malloc(sizeof(char) * num);
void Test2(void)
 char *str = NULL;
 GetMemory2(&str, 100); //&注意参数是&&str,而不是str
 strcpy(str, &hello&);
 cout&& str &&
 free(str);
由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回&#20540;来传递动态内存。这种方法更加简单,见示例:
char *GetMemory3(int num)
 char *p = (char *)malloc(sizeof(char) * num);
void Test3(void)
 char *str = NULL;
 str = GetMemory3(100);
 strcpy(str, &hello&);
 cout&& str &&
 free(str);
用函数返回&#20540;来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示例:
char *GetString(void)
 char p[] = &hello world&;
  //&编译器将提出警告
void Test4(void)
 char *str = NULL;
 str = GetString(); // str&的内容是垃圾
 cout&& str &&
用调试器逐步跟踪Test4,发现执行str = GetString语句后str不再是NULL指针,但是str的内容不是“hello
world”而是垃圾。
如果把上述示例改写成如下示例,会怎么样?
char *GetString2(void)
 char *p = &hello world&;
void Test5(void)
 char *str = NULL;
 str = GetString2();
 cout&& str &&
函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错误的。因为GetString2内的“hello
world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。
1.1.6&杜绝“野指针”
  “野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。&“野指针”的成因主要有两种:
(1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省&#20540;是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如
char *p = NULL;
char *str = (char *) malloc(100);
(2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
(3)指针操作超越了变量的作用域范围。这种情况让人防不胜防,示例程序如下:
  void Func(void){ cout &&&“Func of class A”&&& }
void Test(void)
  p = &a; //&注意&a&的生命期
 p-&Func(); // p是“野指针”
函数Test在执行语句p-&Func()时,对象a已经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。
1.1.7&有了malloc/free为什么还要new/delete?
  malloc与free是C&#43;&#43;/C语言的标准库函数,new/delete是C&#43;&#43;的运算符。它们都可用于申请动态内存和释放内存。
  对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C&#43;&#43;语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们先看一看malloc/free和new/delete如何实现对象的动态内存管理,见示例:
 public :
  Obj(void){ cout &&&“Initialization”&&& }
  ~Obj(void){ cout &&&“Destroy”&&& }
  void Initialize(void){ cout &&&“Initialization”&&& }
  void Destroy(void){ cout &&&“Destroy”&&& }
void UseMallocFree(void)
 Obj *a = (obj *)malloc(sizeof(obj)); //&申请动态内存
 a-&Initialize(); //&初始化
 a-&Destroy(); //&清除工作
 free(a); //&释放内存
void UseNewDelete(void)
 Obj *a = new O //&申请动态内存并且初始化
  //&清除并且释放内存
  类Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于malloc/free不能执行构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成初始化与清除工作。函数UseNewDelete则简单得多。
  所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
  既然new/delete的功能完全覆盖了malloc/free,为什么C&#43;&#43;不把malloc/free淘汰出局呢?这是因为C&#43;&#43;程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。
1.1.8&内存耗尽怎么办?
  如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。
  (1)判断指针是否为NULL,如果是则马上用return语句终止本函数。例如:
void Func(void)
 A *a = new A;
 if(a == NULL)
 (2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。例如:
void Func(void)
 A *a = new A;
 if(a == NULL)
  cout &&&“Memory Exhausted”&&&
  exit(1);
  (3)为new和malloc设置异常处理函数。例如Visual C&#43;&#43;可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C&#43;&#43;使用手册。
  上述(1)(2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式(1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。
  很多人不忍心用exit(1),问:“不编写出错处理程序,让操作系统自己解决行不行?”
  不行。如果发生“内存耗尽”这样的事情,一般说来应用程序已经无药可救。如果不用exit(1)&把坏程序杀死,它可能会害死操作系统。道理如同:如果不把歹徒击毙,歹徒在老死之前会犯下更多的罪。
  有一个很重要的现象要告诉大家。对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致“内存耗尽”。我在Windows 98下用Visual
C&#43;&#43;编写了测试程序,见示例7。这个程序会无休止地运行下去,根本不会终止。因为32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98已经累得对键盘、鼠标毫无反应。
  我可以得出这么一个结论:对于32位以上的应用程序,“内存耗尽”错误处理程序毫无用处。这下可把Unix和Windows程序员们乐坏了:反正错误处理程序不起作用,我就不写了,省了很多麻烦。
我不想误导读者,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。
void main(void)
 float *p = NULL;
 while(TRUE)
  p = new float[1000000];
  cout &&&“eat memory”&&&
  if(p==NULL)
   exit(1);
1.1.9 malloc/free的使用要点
函数malloc的原型如下:
void * malloc(size_t size);
用malloc申请一块长度为length的整数类型的内存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。
* malloc返回&#20540;的类型是void *,所以在调用malloc时要显式地进行类型转换,将void *&转换成所需要的指针类型。
* malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int, float等数据类型的变量的确切字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。最好用以下程序作一次测试:
cout && sizeof(char) &&
cout && sizeof(int) &&
cout && sizeof(unsigned int) &&
cout && sizeof(long) &&
cout && sizeof(unsigned long) &&
cout && sizeof(float) &&
cout && sizeof(double) &&
cout && sizeof(void *) &&
在malloc的“()”中使用sizeof运算符是良好的风&#26684;,但要当心有时我们会昏了头,写出&p = malloc(sizeof(p))这样的程序来。
函数free的原型如下:
void free( void * memblock );
为什么free函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。
1.1.10 new/delete的使用要点
运算符new使用起来要比函数malloc简单得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如
 public :
  Obj(void); //&无参数的构造函数
  Obj(int x); //&带一个参数的构造函数
void Test(void)
 Obj *a = new O
 Obj *b = new Obj(1); //&初&#20540;为1
如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如:
Obj *objects = new Obj[100]; //&创建100个动态对象
不能写成:
Obj *objects = new Obj[100](1);//&创建100个动态对象的同时赋初&#20540;1
在用delete释放对象数组时,留意不要丢了符号‘[]’。例如:
delete [] //&正确的用法
//&错误的用法
后者有可能引起程序崩溃和内存泄漏。
1.2 C&#43;&#43;中的健壮指针和资源管理
  我最喜欢的对资源的定义是:&任何在你的程序中获得并在此后释放的东西?内存是一个相当明显的资源的例子。它需要用new来获得,用delete来释放。同时也有许多其它类型的资源文件句柄、重要的片断、Windows中的GDI资源,等等。将资源的概念推广到程序中创建、释放的所有对象也是十分方便的,无论对象是在堆中分配的还是在栈中或者是在全局作用于内生命的。
  对于给定的资源的拥有着,是负责释放资源的一个对象或者是一段代码。所有权分立为两种级别——自动的和显式的(automatic and explicit),如果一个对象的释放是由语言本身的机制来保证的,这个对象的就是被自动地所有。例如,一个嵌入在其他对象中的对象,他的清除需要其他对象来在清除的时候保证。外面的对象被看作嵌入类的所有者。&  类&#20284;地,每个在栈上创建的对象(作为自动变量)的释放(破坏)是在控制流离开了对象被定义的作用域的时候保证的。这种情况下,作用于被看作是对象的所有者。注意所有的自动所有权都是和语言的其他机制相容的,包括异常。无论是如何退出作用域的——正常流程控制退出、一个break语句、一个return、一个goto、或者是一个throw——自动资源都可以被清除。
  到目前为止,一切都很好!问题是在引入指针、句柄和抽象的时候产生的。如果通过一个指针访问一个对象的话,比如对象在堆中分配,C&#43;&#43;不自动地关注它的释放。程序员必须明确的用适当的程序方法来释放这些资源。比如说,如果一个对象是通过调用new来创建的,它需要用delete来回收。一个文件是用CreateFile(Win32
API)打开的,它需要用CloseHandle来关闭。用EnterCritialSection进入的临界区(Critical Section)需要LeaveCriticalSection退出,等等。一个&裸&指针,文件句柄,或者临界区状态没有所有者来确保它们的最终释放。基本的资源管理的前提就是确保每个资源都有他们的所有者。
1.2.1&第一条规则(RAII)
  一个指针,一个句柄,一个临界区状态只有在我们将它们封装入对象的时候才会拥有所有者。这就是我们的第一规则:在构造函数中分配资源,在析构函数中释放资源。
  当你按照规则将所有资源封装的时候,你可以保证你的程序中没有任何的资源泄露。这点在当封装对象(Encapsulating Object)在栈中建立或者嵌入在其他的对象中的时候非常明显。但是对那些动态申请的对象呢?不要急!任何动态申请的东西都被看作一种资源,并且要按照上面提到的方法进行封装。这一对象封装对象的链不得不在某个地方终止。它最终终止在最高级的所有者,自动的或者是静态的。这些分别是对离开作用域或者程序时释放资源的保证。
  下面是资源封装的一个经典例子。在一个多线程的应用程序中,线程之间共享对象的问题是通过用这样一个对象联系临界区来解决的。每一个需要访问共享资源的客户需要获得临界区。例如,这可能是Win32下临界区的实现方法。
class CritSect
 friend class L
  CritSect () { InitializeCriticalSection (&_critSection); }
  ~CritSect () { DeleteCriticalSection (&_critSection); }
 private:
  void Acquire ()
   EnterCriticalSection (&_critSection);
  void Release ()
   LeaveCriticalSection (&_critSection);
 private:
  CRITICAL_SECTION _critS
  这里聪明的部分是我们确保每一个进入临界区的客户最后都可以离开。&进入&临界区的状态是一种资源,并应当被封装。封装器通常被称作一个锁(lock)。
class Lock
  Lock (CritSect& critSect) : _critSect (critSect)
   _critSect.Acquire ();
  ~Lock ()
   _critSect.Release ();
  CritSect & _critS
  锁一般的用法如下:
void Shared::Act () throw (char *)
 Lock lock (_critSect);
 // perform action&——&may throw
 // automatic destructor of lock
  注意无论发生什么,临界区都会借助于语言的机制保证释放。
  还有一件需要记住的事情——每一种资源都需要被分别封装。这是因为资源分配是一个非常容易出错的操作,是要资源是有限提供的。我们会假设一个失败的资源分配会导致一个异常——事实上,这会经常的发生。所以如果你想试图用一个石头打两只鸟的话,或者在一个构造函数中申请两种形式的资源,你可能就会陷入麻烦。只要想想在一种资源分配成功但另一种失败抛出异常时会发生什么。因为构造函数还没有全部完成,析构函数不可能被调用,第一种资源就会发生泄露。
这种情况可以非常简单的避免。无论何时你有一个需要两种以上资源的类时,写两个小的封装器将它们嵌入你的类中。每一个嵌入的构造都可以保证删除,即使包装类没有构造完成。
1.2.2 Smart Pointers
  我们至今还没有讨论最常见类型的资源——用操作符new分配,此后用指针访问的一个对象。我们需要为每个对象分别定义一个封装类吗?(事实上,C&#43;&#43;标准模板库已经有了一个模板类,叫做auto_ptr,其作用就是提供这种封装。我们一会儿在回到auto_ptr。)让我们从一个极其简单、呆板但安全的东西开始。看下面的Smart
Pointer模板类,它十分坚固,甚至无法实现。
template &class T&
class SmartPointer
  ~SmartPointer () { delete _p; }
  T * operator-&() { return _p; }
  T const * operator-&() const { return _p; }
 protected:
  SmartPointer (): _p (0) {}
  explicit SmartPointer (T* p): _p (p) {}
  T * _p;
  为什么要把SmartPointer的构造函数设计为protected呢?如果我需要遵守第一条规则,那么我就必须这样做。资源——在这里是class T的一个对象——必须在封装器的构造函数中分配。但是我不能只简单的调用new
T,因为我不知道T的构造函数的参数。因为,在原则上,每一个T都有一个不同的构造函数;我需要为他定义个另外一个封装器。模板的用处会很大,为每一个新的类,我可以通过继承SmartPointer定义一个新的封装器,并且提供一个特定的构造函数。
class SmartItem: public SmartPointer&Item&
  explicit SmartItem (int i)
  : SmartPointer&Item& (new Item (i)) {}
  为每一个类提供一个Smart Pointer真的&#20540;得吗?说实话——不!他很有教学的价&#20540;,但是一旦你学会如何遵循第一规则的话,你就可以放松规则并使用一些高级的技术。这一技术是让SmartPointer的构造函数成为public,但是只是是用它来做资源转换(Resource
Transfer)我的意思是用new操作符的结果直接作为SmartPointer的构造函数的参数,像这样:
SmartPointer&Item& item (new Item (i));
  这个方法明显更需要自控性,不只是你,而且包括你的程序小组的每个成员。他们都必须发誓出了作资源转换外不把构造函数用在人以其他用途。幸运的是,这条规矩很容易得以加强。只需要在源文件中查找所有的new即可。
1.2.3 Resource Transfer
  到目前为止,我们所讨论的一直是生命周期在一个单独的作用域内的资源。现在我们要解决一个困难的问题——如何在不同的作用域间安全的传递资源。这一问题在当你处理容器的时候会变得十分明显。你可以动态的创建一串对象,将它们存放至一个容器中,然后将它们取出,并且在最终安排它们。为了能够让这安全的工作——没有泄露——对象需要改变其所有者。
  这个问题的一个非常显而易见的解决方法是使用Smart Pointer,无论是在加入容器前还是还找到它们以后。这是他如何运作的,你加入Release方法到Smart Pointer中:
template &class T&
T * SmartPointer&T&::Release ()
T * pTmp = _p;
  注意在Release调用以后,Smart Pointer就不再是对象的所有者了——它内部的指针指向空。现在,调用了Release都必须是一个负责的人并且迅速隐藏返回的指针到新的所有者对象中。在我们的例子中,容器调用了Release,比如这个Stack的例子:
void Stack::Push (SmartPointer &Item& & item) throw (char *)
if (_top == maxStack)
throw &Stack overflow&;
_arr [_top&#43;&#43;] = item.Release ();
  同样的,你也可以再你的代码中用加强Release的可靠性。
相应的Pop方法要做些什么呢?他应该释放了资源并祈祷调用它的是一个负责的人而且立即作一个资源传递它到一个Smart Pointer?这听起来并不好。
1.2.4 Strong Pointers
  资源管理在内容索引(Windows NT Server上的一部分,现在是Windows 2000)上工作,并且,我对这十分满意。然后我开始想……这一方法是在这样一个完整的系统中形成的,如果可以把它内建入语言的本身岂不是一件非常好?我提出了强指针(Strong Pointer)和弱指针(Weak
Pointer)。一个Strong Pointer会在许多地方和我们这个SmartPointer相&#20284;--它在超出它的作用域后会清除他所指向的对象。资源传递会以强指针赋&#20540;的形式进行。也可以有Weak Pointer存在,它们用来访问对象而不需要所有对象--比如可赋&#20540;的引用。
  任何指针都必须声明为Strong或者Weak,并且语言应该来关注类型转换的规定。例如,你不可以将Weak Pointer传递到一个需要Strong Pointer的地方,但是相反却可以。Push方法可以接受一个Strong
Pointer并且将它转移到Stack中的Strong Pointer的序列中。Pop方法将会返回一个Strong Pointer。把Strong Pointer的引入语言将会使垃圾回收成为历史。
  这里还有一个小问题--修改C&#43;&#43;标准几乎和竞选美国总统一样容易。当我将我的注意告诉给Bjarne Stroutrup的时候,他看我的&#30524;神好像是我刚刚要向他借一千美元一样。
然后我突然想到一个念头。我可以自己实现Strong Pointers。毕竟,它们都很想Smart Pointers。给它们一个拷贝构造函数并重载赋&#20540;操作符并不是一个大问题。事实上,这正是标准库中的auto_ptr有的。重要的是对这些操作给出一个资源转移的语法,但是这也不是很难。
template &class T&
SmartPointer&T&::SmartPointer (SmartPointer&T& & ptr)
_p = ptr.Release ();
template &class T&
void SmartPointer&T&::operator = (SmartPointer&T& & ptr)
if (_p != ptr._p)
delete _p;
_p = ptr.Release ();
  使这整个想法迅速成功的原因之一是我可以以&#20540;方式传递这种封装指针!我有了我的蛋糕,并且也可以吃了。看这个Stack的新的实现:
class Stack
enum { maxStack = 3 };
: _top (0)
void Push (SmartPointer&Item& & item) throw (char *)
if (_top &= maxStack)
throw &Stack overflow&;
_arr [_top&#43;&#43;] =
SmartPointer&Item& Pop ()
if (_top == 0)
return SmartPointer&Item& ();
return _arr [--_top];
SmartPointer&Item& _arr [maxStack];
  Pop方法强制客户将其返回&#20540;赋给一个Strong Pointer,SmartPointer&Item&。任何试图将他对一个普通指针的赋&#20540;都会产生一个编译期错误,因为类型不匹配。此外,因为Pop以&#20540;方式返回一个Strong
Pointer(在Pop的声明时SmartPointer&Item&后面没有&符号),编译器在return时自动进行了一个资源转换。他调用了operator
=来从数组中提取一个Item,拷贝构造函数将他传递给调用者。调用者最后拥有了指向Pop赋&#20540;的Strong Pointer指向的一个Item。
我马上意识到我已经在某些东西之上了。我开始用了新的方法重写原来的代码。
1.2.5 Parser
我过去有一个老的算术操作分析器,是用老的的资源管理技术写的。分析器的作用是在分析树中生成节点,节点是动态分配的。例如分析器的Expression方法生成一个表达式节点。我没有时间用Strong
Pointer去重写这个分析器。我令Expression、Term和Factor方法以传&#20540;的方式将Strong
Pointer返回到Node中。看下面的Expression方法的实现:
SmartPointer&Node& Parser::Expression()
// Parse a term
SmartPointer&Node& pNode = Term ();
EToken token = _scanner.Token();
if ( token == tPlus || token == tMinus )
// Expr := Term { ('&#43;' | '-') Term }
SmartPointer&MultiNode& pMultiNode = new SumNode (pNode);
_scanner.Accept();
SmartPointer&Node& pRight = Term ();
pMultiNode-&AddChild (pRight, (token == tPlus));
token = _scanner.Token();
} while (token == tPlus || token == tMinus);
pNode = up_cast&Node, MultiNode& (pMultiNode);
// otherwise Expr := Term
return pN // by value!
  最开始,Term方法被调用。他传&#20540;返回一个指向Node的Strong Pointer并且立刻把它保存到我们自己的Strong Pointer,pNode中。如果下一个符号不是加号或者减号,我们就简单的把这个SmartPointer以&#20540;返回,这样就释放了Node的所有权。另外一方面,如果下一个符号是加号或者减号,我们创建一个新的SumMode并且立刻(直接传递)将它储存到MultiNode的一个Strong
Pointer中。这里,SumNode是从MultiMode中继承而来的,而MulitNode是从Node继承而来的。原来的Node的所有权转给了SumNode。
  只要是他们在被加号和减号分开的时候,我们就不断的创建terms,我们将这些term转移到我们的MultiNode中,同时MultiNode得到了所有权。最后,我们将指向MultiNode的Strong
Pointer向上映射为指向Mode的Strong Pointer,并且将他返回调用着。
  我们需要对Strong Pointers进行显式的向上映射,即使指针是被隐式的封装。例如,一个MultiNode是一个Node,但是相同的is-a关系在SmartPointer&MultiNode&和SmartPointer&Node&之间并不存在,因为它们是分离的类(模板实例)并不存在继承关系。up-cast模板是像下面这样定义的:
template&class To, class From&
inline SmartPointer&To& up_cast (SmartPointer&From& & from)
return SmartPointer&To& (from.Release ());
  如果你的编译器支持新加入标准的成员模板(member template)的话,你可以为SmartPointer&T&定义一个新的构造函数用来从接受一个class U。
template &class T&
template &class U& SmartPointer&T&::SmartPointer (SPrt&U& & uptr)
: _p (uptr.Release ())
  这里的这个花招是模板在U不是T的子类的时候就不会编译成功(换句话说,只在U is-a T的时候才会编译)。这是因为uptr的缘故。Release()方法返回一个指向U的指针,并被赋&#20540;为_p,一个指向T的指针。所以如果U不是一个T的话,赋&#20540;会导致一个编译时刻错误。
std::auto_ptr
后来我意识到在STL中的auto_ptr模板,就是我的Strong Pointer。在那时候还有许多的实现差异(auto_ptr的Release方法并不将内部的指针清零--你的编译器的库很可能用的就是这种陈旧的实现),但是最后在标准被广泛接受之前都被解决了。
1.2.6 Transfer Semantics
  目前为止,我们一直在讨论在C&#43;&#43;程序中资源管理的方法。宗旨是将资源封装到一些轻量级的类中,并由类负责它们的释放。特别的是,所有用new操作符分配的资源都会被储存并传递进Strong Pointer(标准库中的auto_ptr)的内部。
  这里的关键词是传递(passing)。一个容器可以通过传&#20540;返回一个Strong Pointer来安全的释放资源。容器的客户只能够通过提供一个相应的Strong Pointer来保存这个资源。任何一个将结果赋给一个&裸&指针的做法都立即会被编译器发现。
auto_ptr&Item& item = stack.Pop (); // ok
Item * p = stack.Pop (); // Error! Type mismatch.
  以传&#20540;方式被传递的对象有value semantics&或者称为&copy semantics。Strong Pointers是以&#20540;方式传递的--但是我们能说它们有copy
semantics吗?不是这样的!它们所指向的对象肯定没有被拷贝过。事实上,传递过后,源auto_ptr不在访问原有的对象,并且目标auto_ptr成为了对象的唯一拥有者(但是往往auto_ptr的旧的实现即使在释放后仍然保持着对对象的所有权)。自然而然的我们可以将这种新的行为称作Transfer
Semantics。
  拷贝构造函数(copy construcor)和赋&#20540;操作符定义了auto_ptr的Transfer Semantics,它们用了非const的auto_ptr引用作为它们的参数。
auto_ptr (auto_ptr&T& & ptr);
auto_ptr & operator = (auto_ptr&T& & ptr);
  这是因为它们确实改变了他们的源--剥夺了对资源的所有权。
通过定义相应的拷贝构造函数和重载赋&#20540;操作符,你可以将Transfer Semantics加入到许多对象中。例如,许多Windows中的资源,比如动态建立的菜单或者位图,可以用有Transfer Semantics的类来封装。
1.2.7 Strong Vectors
  标准库只在auto_ptr中支持资源管理。甚至连最简单的容器也不支持ownership semantics。你可能想将auto_ptr和标准容器组合到一起可能会管用,但是并不是这样的。例如,你可能会这样做,但是会发现你不能够用标准的方法来进行索引。
vector& auto_ptr&Item& & autoV
  这种建造不会编译成功;
Item * item = autoVector [0];
  另一方面,这会导致一个从autoVect到auto_ptr的所有权转换:
auto_ptr&Item& item = autoVector [0];
  我们没有选择,只能够构造我们自己的Strong Vector。最小的接口应该如下:
template &class T&
class auto_vector
explicit auto_vector (size_t capacity = 0);
T const * operator [] (size_t i)
T * operator [] (size_t i);
void assign (size_t i, auto_ptr&T& & p);
void assign_direct (size_t i, T * p);
void push_back (auto_ptr&T& & p);
auto_ptr&T& pop_back ();
  你也许会发现一个非常防御性的设计态度。我决定不提供一个对vector的左&#20540;索引的访问,取而代之,如果你想设定(set)一个&#20540;的话,你必须用assign或者assign_direct方法。我的观点是,资源管理不应该被忽视,同时,也不应该在所有的地方滥用。在我的经验里,一个strong
vector经常被许多push_back方法充斥着。
  Strong vector最好用一个动态的Strong Pointers的数组来实现:
template &class T&
class auto_vector
void grow (size_t reqCapacity);
auto_ptr&T& *_
  grow方法申请了一个很大的auto_ptr&T&的数组,将所有的东西从老的书组类转移出来,在其中交换,并且删除原来的数组。
  auto_vector的其他实现都是十分直接的,因为所有资源管理的复杂度都在auto_ptr中。例如,assign方法简单的利用了重载的赋&#20540;操作符来删除原有的对象并转移资源到新的对象:
void assign (size_t i, auto_ptr&T& & p)
_arr [i] =
  我已经讨论了push_back和pop_back方法。push_back方法传&#20540;返回一个auto_ptr,因为它将所有权从auto_vector转换到auto_ptr中。
  对auto_vector的索引访问是借助auto_ptr的get方法来实现的,get简单的返回一个内部指针。
T * operator [] (size_t i)
return _arr [i].get ();
  没有容器可以没有iterator。我们需要一个iterator让auto_vector看起来更像一个普通的指针向量。特别是,当我们废弃iterator的时候,我们需要的是一个指针而不是auto_ptr。我们不希望一个auto_vector的iterator在无意中进行资源转换。
template&class T&
class auto_iterator: public
iterator&random_access_iterator_tag, T *&
auto_iterator () : _pp (0) {}
auto_iterator (auto_ptr&T& * pp) : _pp (pp) {}
bool operator != (auto_iterator&T& const & it) const
{ return it._pp != _ }
auto_iterator const & operator&#43;&#43; (int) { return _pp&#43;&#43;; }
auto_iterator operator&#43;&#43; () { return &#43;&#43;_ }
T * operator * () { return _pp-&get (); }
auto_ptr&T& * _
我们给auto_vect提供了标准的begin和end方法来找回iterator:
class auto_vector
typedef auto_iterator&T&
iterator begin () { return _ }
iterator end () { return _arr &#43; _ }
  你也许会问我们是否要利用资源管理重新实现每一个标准的容器?幸运的是,不;事实是strong vector解决了大部分所有权的需求。当你把你的对象都安全的放置到一个strong vector中,你可以用所有其它的容器来重新安排(weak)pointer。
设想,例如,你需要对一些动态分配的对象排序的时候。你将它们的指针保存到一个strong vector中。然后你用一个标准的vector来保存从strong vector中获得的weak指针。你可以用标准的算法对这个vector进行排序。这种中介vector叫做permutation
vector。相&#20284;的,你也可以用标准的maps, priority queues, heaps, hash tables等等。
1.2.8 Code Inspection
  如果你严&#26684;遵照资源管理的条款,你就不会再资源泄露或者两次删除的地方遇到麻烦。你也降低了访问野指针的几率。同样的,遵循原有的规则,用delete删除用new申请的德指针,不要两次删除一个指针。你也不会遇到麻烦。但是,那个是更好的注意呢?
  这两个方法有一个很大的不同点。就是和寻找传统方法的bug相比,找到违反资源管理的规定要容易的多。后者仅需要一个代码检测或者一个运行测试,而前者则在代码中隐藏得很深,并需要很深的检查。
  设想你要做一段传统的代码的内存泄露检查。第一件事,你要做的就是grep所有在代码中出现的new,你需要找出被分配空间地指针都作了什么。你需要确定导致删除这个指针的所有的执行路径。你需要检查break语句,过程返回,异常。原有的指针可能赋给另一个指针,你对这个指针也要做相同的事。
  相比之下,对于一段用资源管理技术实现的代码。你也用grep检查所有的new,但是这次你只需要检查邻近的调用:
  ●&这是一个直接的Strong Pointer转换,还是我们在一个构造函数的函数体中?
  ●&调用的返回知是否立即保存到对象中,构造函数中是否有可以产生异常的代码。?
  ●&如果这样的话析构函数中时候有delete?
  下一步,你需要用grep查找所有的release方法,并实施相同的检查。
  不同点是需要检查、理解单个执行路径和只需要做一些本地的检验。这难道不是提醒你非结构化的和结构化的程序设计的不同吗?原理上,你可以认为你可以应付goto,并且跟踪所有的可能分支。另一方面,你可以将你的怀疑本地化为一段代码。本地化在两种情况下都是关键所在。
  在资源管理中的错误模式也比较容易调试。最常见的bug是试图访问一个释放过的strong pointer。这将导致一个错误,并且很容易跟踪。
1.2.9&共享的所有权
  为每一个程序中的资源都找出或者指定一个所有者是一件很容易的事情吗?答案是出乎意料的,是!如果你发现了一些问题,这可能说明你的设计上存在问题。还有另一种情况就是共享所有权是最好的甚至是唯一的选择。
  共享的责任分配给被共享的对象和它的客户(client)。一个共享资源必须为它的所有者保持一个引用计数。另一方面,所有者再释放资源的时候必须通报共享对象。最后一个释放资源的需要在最后负责free的工作。
  最简单的共享的实现是共享对象继承引用计数的类RefCounted:
class RefCounted
RefCounted () : _count (1) {}
int GetRefCount () const { return _ }
void IncRefCount () { _count&#43;&#43;; }
int DecRefCount () { return --_ }
  按照资源管理,一个引用计数是一种资源。如果你遵守它,你需要释放它。当你意识到这一事实的时候,剩下的就变得简单了。简单的遵循规则--再构造函数中获得引用计数,在析构函数中释放。甚至有一个RefCounted的smart pointer等价物:
template &class T&
class RefPtr
RefPtr (T * p) : _p (p) {}
RefPtr (RefPtr&T& & p)
_p = p._p;
_p-&IncRefCount ();
~RefPtr ()
if (_p-&DecRefCount () == 0)
delete _p;
  注意模板中的T不比成为RefCounted的后代,但是它必须有IncRefCount和DecRefCount的方法。当然,一个便于使用的RefPtr需要有一个重载的指针访问操作符。在RefPtr中加入转换语义学(transfer
semantics)是读者的工作。
1.2.10&所有权网络
  链表是资源管理分析中的一个很有意思的例子。如果你选择表成为链(link)的所有者的话,你会陷入实现递归的所有权。每一个link都是它的继承者的所有者,并且,相应的,余下的链表的所有者。下面是用smart pointer实现的一个表单元:
class Link
auto_ptr&Link& _
  最好的方法是,将连接控制封装到一个弄构进行资源转换的类中。
  对于双链表呢?安全的做法是指明一个方向,如forward:
class DoubleLink
DoubleLink *_
auto_ptr&DoubleLink& _
  注意不要创建环形链表。
  这给我们带来了另外一个有趣的问题--资源管理可以处理环形的所有权吗?它可以,用一个mark-and-sweep的算法。这里是实现这种方法的一个例子:
template&class T&
class CyclPtr
CyclPtr (T * p)
:_p (p), _isBeingDeleted (false)
~CyclPtr ()
_isBeingDeleted =
if (!_p-&IsBeingDeleted ())
delete _p;
void Set (T * p)
bool IsBeingDeleted () const { return _isBeingD }
bool _isBeingD
  注意我们需要用class T来实现方法IsBeingDeleted,就像从CyclPtr继承。对特殊的所有权网络普通化是十分直接的。
  将原有代码转换为资源管理代码
如果你是一个经验丰富的程序员,你一定会知道找资源的bug是一件浪费时间的痛苦的经历。我不必说服你和你的团队花费一点时间来熟悉资源管理是十分&#20540;得的。你可以立即开始用这个方法,无论你是在开始一个新项目或者是在一个项目的中期。转换不必立即全部完成。下面是步骤。
(1)首先,在你的工程中建立基本的Strong Pointer。然后通过查找代码中的new来开始封装裸指针。
(2)最先封装的是在过程中定义的临时指针。简单的将它们替换为auto_ptr并且删除相应的delete。如果一个指针在过程中没有被删除而是被返回,用auto_ptr替换并在返回前调用release方法。在你做第二次传递的时候,你需要处理对release的调用。注意,即使是在这点,你的代码也可能更加&精力充沛&--你会移出代码中潜在的资源泄漏问题。
(3)下面是指向资源的裸指针。确保它们被独立的封装到auto_ptr中,或者在构造函数中分配在析构函数中释放。如果你有传递所有权的行为的话,需要调用release方法。如果你有容器所有对象,用Strong
Pointers重新实现它们。
(4)接下来,找到所有对release的方法调用并且尽力清除所有,如果一个release调用返回一个指针,将它修改传&#20540;返回一个auto_ptr。
(5)重复着一过程,直到最后所有new和release的调用都在构造函数或者资源转换的时候发生。这样,你在你的代码中处理了资源泄漏的问题。对其他资源进行相&#20284;的操作。
(6)你会发现资源管理清除了许多错误和异常处理带来的复杂性。不仅仅你的代码会变得精力充沛,它也会变得简单并容易维护。
2&内存泄漏
2.1 C&#43;&#43;中动态内存分配引发问题的解决方案
假设我们要开发一个String类,它可以方便地处理字符串数据。我们可以在类中声明一个数组,考虑到有时候字符串极长,我们可以把数组大小设为200,但一般的情况下又不需要这么多的空间,这样是浪费了内存。对了,我们可以使用new操作符,这样是十分灵活的,但在类中就会出现许多意想不到的问题,本文就是针对这一现象而写的。现在,我们先来开发一个String类,但它是一个不完善的类。的确,我们要刻意地使它出现各种各样的问题,这样才好对症下药。好了,我们开始吧!
/* String.h */
#ifndef STRING_H_
#define STRING_H_
class String
char * //存储数据
//字符串长度
String(const char * s); //构造函数
String(); //&默认构造函数
~String(); //&析构函数
friend ostream & operator<<(ostream & os,const String& st);
/*String.cpp*/
#include&<iostream>
#include&<cstring>
#include &String.h&
String::String(const char * s)
len = strlen(s);
str = new char[len &#43; 1];
strcpy(str, s);
}//拷贝数据
String::String()
str = new char[len&#43;1];
str[0]='&0';
String::~String()
cout<<&这个字符串将被删除:&<<str<<'&n';//为了方便观察结果,特留此行代码。
ostream & operator<<(ostream & os, const String & st)
os&<<&st.
/*test_right.cpp*/
#include&<iostream>
#include&<stdlib.h>
#include &String.h&
int main()
String temp(&天极网&);
cout<<temp<<'&n';
system(&PAUSE&);
  运行结果:
  天极网
  请按任意键继续. . .
  大家可以看到,以上程序十分正确,而且也是十分有用的。可是,我们不能被表面现象所迷惑!下面,请大家用test_String.cpp文件替换test_right.cpp文件进行编译,看看结果。有的编译器可能就是根本不能进行编译!
test_String.cpp:
#include&<iostream>
#include&<stdlib.h>
#include &String.h&
void show_right(const String&);
void show_String(const String);//注意,参数非引用,而是按&#20540;传递。
int main()
String test1(&第一个范例。&);
String test2(&第二个范例。&);
String test3(&第三个范例。&);
String test4(&第四个范例。&);
cout<<&下面分别输入三个范例:&n&;
cout<<test1<<
cout<<test2<<
cout<<test3<<
String* String1=new String(test1);
cout<<*String1<<
delete String1;
cout<<test1<< //在Dev-cpp上没有任何反应。
cout<<&使用正确的函数:&<<
show_right(test2);
cout<<test2<<
cout<<&使用错误的函数:&<<
show_String(test2);
cout<<test2<< //这一段代码出现严重的错误!
String String2(test3);
cout<<&String2: &<<String2<<
String String3;
String3=test4;
cout<<&String3: &<<String3<<
cout<<&下面,程序结束,析构函数将被调用。&<<
void show_right(const String& a)
cout<<a<<
void show_String(const String a)
cout<<a<<
  运行结果:
  下面分别输入三个范例:
  第一个范例。
  第二个范例。
  第三个范例。
  第一个范例。
  这个字符串将被删除:第一个范例。
  使用正确的函数:
  第二个范例。
  第二个范例。
  使用错误的函数:
  第二个范例。
  这个字符串将被删除:第二个范例。
  这个字符串将被删除:?=
  String2:&第三个范例。
  String3:&第四个范例。
  下面,程序结束,析构函数将被调用。
  这个字符串将被删除:第四个范例。
  这个字符串将被删除:第三个范例。
  这个字符串将被删除:?=
  这个字符串将被删除:x =
  这个字符串将被删除:?=
  这个字符串将被删除:
现在,请大家自己试试运行结果,或许会更加惨不忍睹呢!下面,我为大家一一分析原因。
首先,大家要知道,C++类有以下这些极为重要的函数:
一:复制构造函数。
二:赋&#20540;函数。
我们先来讲复制构造函数。什么是复制构造函数呢?比如,我们可以写下这样的代码:String test1(test2);这是进行初始化。我们知道,初始化对象要用构造函数。可这儿呢?按理说,应该有声明为这样的构造函数:String(const String &);可是,我们并没有定义这个构造函数呀?答案是,C++提供了默认的复制构造函数,问题也就出在这儿。
(1):什么时候会调用复制构造函数呢?(以String类为例。)
  在我们提供这样的代码:String test1(test2)时,它会被调用;当函数的参数列表为按&#20540;传递,也就是没有用引用和指针作为类型时,如:void show_String(const
String),它会被调用。其实,还有一些情况,但在这儿就不列举了。
(2):它是什么样的函数。
它的作用就是把两个类进行复制。拿String类为例,C++提供的默认复制构造函数是这样的:
String(const String& a)
在平时,这样并不会有任何的问题出现,但我们用了new操作符,涉及到了动态内存分配,我们就不得不谈谈浅复制和深复制了。以上的函数就是实行的浅复制,它只是复制了指针,而并没有复制指针指向的数据,可谓一点儿用也没有。打个比方吧!就像一个朋友让你把一个程序通过网络发给他,而你大大咧咧地把快捷方式发给了他,有什么用处呢?我们来具体谈谈:
假如,A对象中存储了这样的字符串:“C++”。它的地址为2000。现在,我们把A对象赋给B对象:String
B=A。现在,A和B对象的str指针均指向2000地址。看&#20284;可以使用,但如果B对象的析构函数被调用时,则地址2000处的字符串“C++”已经被从内存中抹去,而A对象仍然指向地址2000。这时,如果我们写下这样的代码:cout<<A<<或是等待程序结束,A对象的析构函数被调用时,A对象的数据能否显示出来呢?只会是乱码。而且,程序还会这样做:连续对地址2000处使用两次delete操作符,这样的后果是十分严重的!
本例中,有这样的代码:
String* String1=new String(test1);
cout<<*String1<<
delete String1;
  假设test1中str指向的地址为2000,而String中str指针同样指向地址2000,我们删除了2000处的数据,而test1对象呢?已经被破坏了。大家从运行结果上可以看到,我们使用cout<<test1时,一点反应也没有。而在test1的析构函数被调用时,显示是这样:“这个字符串将被删除:”。
再看看这段代码:
cout<<&使用错误的函数:&<<
show_String(test2);
cout<<test2<<//这一段代码出现严重的错误!
show_String函数的参数列表void show_String(const String a)是按&#20540;传递的,所以,我们相当于执行了这样的代码:String a=test2;函数执行完毕,由于生存周期的缘故,对象a被析构函数删除,我们马上就可以看到错误的显示结果了:这个字符串将被删除:?=。当然,test2也被破坏了。解决的办法很简单,当然是手工定义一个复制构造函数喽!人力可以胜天!
String::String(const String& a)
str=new char(len&#43;1);
strcpy(str,a.str);
  我们执行的是深复制。这个函数的功能是这样的:假设对象A中的str指针指向地址<span style="color:#00,内容为“I
am a C&#43;&#43; Boy!”。我们执行代码String B=A时,我们先开辟出一块内存,假设为<span style="color:#00。我们用strcpy函数将地址<span style="color:#00的内容拷贝到地址<span style="color:#00中,再将对象B的str指针指向地址<span style="color:#00。这样,就互不干扰了。
大家把这个函数加入程序中,问题就解决了大半,但还没有完全解决,问题在赋&#20540;函数上。我们的程序中有这样的段代码:
String String3;
String3=test4;
  经过我前面的讲解,大家应该也会对这段代码进行寻根摸底:凭什么可以这样做:String3=test4???原因是,C++为了用户的方便,提供的这样的一个操作符重载函数:operator=。所以,我们可以这样做。大家应该猜得到,它同样是执行了浅复制,出了同样的毛病。比如,执行了这段代码后,析构函数开始大展神威^_^。由于这些变量是后进先出的,所以最后的String3变量先被删除:这个字符串将被删除:第四个范例。很正常。最后,删除到test4的时候,问题来了:这个字符串将被删除:?=。原因我不用赘述了,只是这个赋&#20540;函数怎么写,还有一点儿学问呢!大家请看:
平时,我们可以写这样的代码:x=y=z。(均为整型变量。)而在类对象中,我们同样要这样,因为这很方便。而对象A=B=C就是A.operator=(B.operator=(c))。而这个operator=函数的参数列表应该是:const
String& a,所以,大家不难推出,要实现这样的功能,返回&#20540;也要是String&,这样才能实现A=B=C。我们先来写写看:
String& String::operator=(const String& a)
delete []//先删除自身的数据
str=new char[len&#43;1];
strcpy(str,a.str);//此三行为进行拷贝
return *//返回自身的引用
是不是这样就行了呢?我们假如写出了这种代码:A=A,那么大家看看,岂不是把A对象的数据给删除了吗?这样可谓引发一系列的错误。所以,我们还要检查是否为自身赋&#20540;。只比较两对象的数据是不行了,因为两个对象的数据很有可能相同。我们应该比较地址。以下是完好的赋&#20540;函数:
String& String::operator=(const String& a)
if(this==&a)
str=new char[len&#43;1];
strcpy(str,a.str);
把这些代码加入程序,问题就完全解决,下面是运行结果:
  下面分别输入三个范例:
  第一个范例
  第二个范例
  第三个范例
  第一个范例
  这个字符串将被删除:第一个范例。
  第一个范例
   使用正确的函数:
  第二个范例。
  第二个范例。
   使用错误的函数:
  第二个范例。
  这个字符串将被删除:第二个范例。
  第二个范例。
  String2:&第三个范例。
  String3:&第四个范例。
  下面,程序结束,析构函数将被调用。
  这个字符串将被删除:第四个范例。
  这个字符串将被删除:第三个范例。
  这个字符串将被删除:第四个范例。
  这个字符串将被删除:第三个范例。
  这个字符串将被删除:第二个范例。
  这个字符串将被删除:第一个范例。
2.2&如何对付内存泄漏?
写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了new&操作、delete操作和指针运算的话,你将会在某个地方搞晕了头,导致内存泄漏,指针引用错误,以及诸如此类的问题。这和你如何小心地对待内存分配工作其实完全没有关系:代码的复杂性最终总是会超过你能够付出的时间和努力。于是随后产生了一些成功的技巧,它们依赖于将内存分配(allocations)与重新分配(deallocation)工作隐藏在易于管理的类型之后。标准容器(standard
containers)是一个优秀的例子。它们不是通过你而是自己为元素管理内存,从而避免了产生糟糕的结果。想象一下,没有string和vector的帮助,写出这个:
#include&vector&
#include&string&
#include&iostream&
#include&algorithm&
int main() // small program messing around with strings
 cout && &enter some whitespace-separated words:&n&;
 vector&string&
 while (cin&&s) v.push_back(s);
 sort(v.begin(),v.end());
 typedef vector&string&::const_iterator I
 for (Iter p = v.begin(); p!=v.end(); &#43;&#43;p) cat &#43;= *p&#43;&&#43;&;
 cout && cat &&&’&n’;
  你有多少机会在第一次就得到正确的结果?你又怎么知道你没有导致内存泄漏呢?
  注意,没有出现显式的内存管理,宏,造型,溢出检查,显式的长度限制,以及指针。通过使用函数对象和标准算法(standard algorithm),我可以避免使用指针——例如使用迭代子(iterator),不过对于一个这么小的程序来说有点小题大作了。
  这些技巧并不完美,要系统化地使用它们也并不总是那么容易。但是,应用它们产生了惊人的差异,而且通过减少显式的内存分配与重新分配的次数,你甚至可以使余下的例子更加容易被跟踪。早在1981年,我就指出,通过将我必须显式地跟踪的对象的数量从几万个减少到几打,为了使程序正确运行而付出的努力从可怕的苦工,变成了应付一些可管理的对象,甚至更加简单了。
  如果你的程序还没有包含将显式内存管理减少到最小限度的库,那么要让你程序完成和正确运行的话,最快的途径也许就是先建立一个这样的库。
  模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年以前。异常的使用使之更加完善。
  如果你实在不能将内存分配/重新分配的操作隐藏到你需要的对象中时,你可以使用资源句柄(resource handle),以将内存泄漏的可能性降至最低。这里有个例子:我需要通过一个函数,在空闲内存中建立一个对象并返回它。这时候可能忘记释放这个对象。毕竟,我们不能说,仅仅关注当这个指针要被释放的时候,谁将负责去做。使用资源句柄,这里用了标准库中的auto_ptr,使需要为之负责的地方变得明确了。
#include&memory&
#include&iostream&
struct S {
 S() { cout && &make an S&n&; }
 ~S() { cout && &destroy an S&n&; }
 S(const S&) { cout && &copy initialize an S&n&; }
 S& operator=(const S&) { cout && &copy assign an S&n&; }
 return new S; //&谁该负责释放这个S?
auto_ptr&S& g()
 return auto_ptr&S&(new S); //&显式传递负责释放这个S
int main()
 cout && &start main&n&;
 S* p = f();
 cout && &after f() before g()&n&;
 // S* q = g(); //&将被编译器捕捉
 auto_ptr&S& q = g();
 cout && &exit main&n&;
 // *p产生了内存泄漏
 // *q被自动释放
  在更一般的意义上考虑资源,而不仅仅是内存。
如果在你的环境中不能系统地应用这些技巧(例如,你必须使用别的地方的代码,或者你的程序的另一部分简直是原始人类(译注:原文是Neanderthals,&#23612;安德特人,旧石器时代广泛分布在欧洲的猿人)写的,如此等等),那么注意使用一个内存泄漏检测器作为开发过程的一部分,或者插入一个垃圾收集器(garbage collector)。
2.3浅谈C/C&#43;&#43;内存泄漏及其检测工具
  对于一个c/c&#43;&#43;程序员来说,内存泄漏是一个常见的也是令人头疼的问题。已经有许多技术被研究出来以应对这个问题,比如Smart Pointer,Garbage Collection等。Smart Pointer技术比较成熟,STL中已经包含支持Smart
Pointer的class,但是它的使用&#20284;乎并不广泛,而且它也不能解决所有的问题;Garbage Collection技术在Java中已经比较成熟,但是在c/c&#43;&#43;领域的发展并不顺畅,虽然很早就有人思考在C&#43;&#43;中也加入GC的支持。现实世界就是这样的,作为一个c/c&#43;&#43;程序员,内存泄漏是你心中永远的痛。不过好在现在有许多工具能够帮助我们验证内存泄漏的存在,找出发生问题的代码。
2.3.1&内存泄漏的定义
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。以下这段小程序演示了堆内存发生泄漏的情形:
void MyFunction(int nSize)
 char* p= new char[nSize];
 if( !GetStringFrom( p, nSize ) ){
  MessageBox(“Error”);
 …//using the
  当函数GetStringFrom()返回零的时候,指针p指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,但是c函数可以在任何地方退出,所以一旦有某个出口处没有释放应该释放的内存,就会发生内存泄漏。
  广义的说,内存泄漏不仅仅包含堆内存的泄漏,还包含系统资源的泄漏(resource leak),比如核心态HANDLE,GDI
Object,SOCKET,&Interface等,从根本上说这些由操作系统分配的对象也消耗内存,如果这些对象发生泄漏最终也会导致内存的泄漏。而且,某些对象消耗的是核心态内存,这些对象严重泄漏时会导致整个操作系统不稳定。所以相比之下,系统资源的泄漏比堆内存的泄漏更为严重。
GDI Object的泄漏是一种常见的资源泄漏:
void CMyView::OnPaint( CDC* pDC )
 CBitmap* pOldB
 bmp.LoadBitmap(IDB_MYBMP);
 pOldBmp = pDC-&SelectObject( &bmp );
 if( Something() ){
 pDC-&SelectObject( pOldBmp );
  当函数Something()返回非零的时候,程序在退出前没有把pOldBmp选回pDC中,这会导致pOldBmp指向的HBITMAP对象发生泄漏。这个程序如果长时间的运行,可能会导致整个系统花屏。这种问题在Win9x下比较容易暴露出来,因为Win9x的GDI堆比Win2k或NT的要小很多。
2.3.2&内存泄漏的发生方式
  以发生的方式来分类,内存泄漏可以分为4类:
  1.&常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。比如例二,如果Something()函数一直返回True,那么pOldBmp指向的HBITMAP对象总是发生泄漏。
  2.&偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。比如例二,如果Something()函数只有在特定环境下才返回True,那么pOldBmp指向的HBITMAP对象并不总是发生泄漏。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
3.&一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,但是因为这个类是一个Singleton,所以内存泄漏只会发生一次。另一个例子:
char* g_lpszFileName = NULL;
void SetFileName( const char* lpcszFileName )
 if( g_lpszFileName ){
  free( g_lpszFileName );
 g_lpszFileName = strdup( lpcszFileName );
  如果程序在结束的时候没有释放g_lpszFileName指向的字符串,那么,即使多次调用SetFileName(),总会有一块内存,而且仅有一块内存发生泄漏。
4.&隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严&#26684;的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。举一个例子:
class Connection
  Connection( SOCKET s);
  ~Connection();
 private:
  SOCKET _
class ConnectionManager
  ConnectionManager(){}
  ~ConnectionManager(){
   list::
   for( it = _connlist.begin(); it != _connlist.end(); &#43;&#43;it ){
    delete&(*it);
   _connlist.clear();
  void OnClientConnected( SOCKET s ){
   Connection* p = new Connection(s);
   _connlist.push_back(p);
  void OnClientDisconnected( Connection* pconn ){
   _connlist.remove( pconn );
 private:
  list _
  假设在Client从Server端断开后,Server并没有呼叫OnClientDisconnected()函数,那么代表那次连接的Connection对象就不会被及时的删除(在Server程序退出的时候,所有Connection对象会在ConnectionManager的析构函数里被删除)。当不断的有连接建立、断开时隐式内存泄漏就发生了。
从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。
2.3.3&检测内存泄漏
  检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。截获住这两个函数,我们就能跟踪每一块内存的生命周期,比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存。这里只是简单的描述了检测内存泄漏的基本原理,详细的算法可以参见Steve
Maguire的&&Writing Solid Code&&。
  如果要检测堆内存的泄漏,那么需要截获住malloc/realloc/free和new/delete就可以了(其实new/delete最终也是用malloc/free的,所以只要截获前面一组即可)。对于其他的泄漏,可以采用类&#20284;的方法,截获住相应的分配和释放函数。比如,要检测BSTR的泄漏,就需要截获SysAllocString/SysFreeString;要检测HMENU的泄漏,就需要截获CreateMenu/
DestroyMenu。(有的资源的分配函数有多个,释放函数只有一个,比如,SysAllocStringLen也可以用来分配BSTR,这时就需要截获多个分配函数)
  在Windows平台下,检测内存泄漏的工具常用的一般有三种,MS C-Runtime Library内建的检测功能;外挂式的检测工具,诸如,Purify,BoundsChecker等;利用Windows
NT自带的Performance Monitor。这三种工具各有优缺点,MS C-Runtime Library虽然功能上较之外挂式的工具要弱,但是它是免费的;Performance Monitor虽然无法标示出发生问题的代码,但是它能检测出隐式的内存泄漏的存在,这是其他两类工具无能为力的地方。
  以下我们详细讨论这三种检测工具:
2.3.3.1 VC下内存泄漏的检测方法
  用MFC开发的应用程序,在DEBUG版模式下编译后,都会自动加入内存泄漏的检测代码。在程序结束后,如果发生了内存泄漏,在Debug窗口中会显示出所有发生泄漏的内存块的信息,以下两行显示了一块被泄漏的内存块的信息:
E:&TestMemLeak&TestDlg.cpp(70) : {59} normal block at 0x0 bytes long.
Data: &abcdefghijklmnop& 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70
  第一行显示该内存块由TestDlg.cpp文件,第70行代码分配,地址在0x,大小为200字节,{59}是指调用内存分配函数的Request
Order,关于它的详细信息可以参见MSDN中_CrtSetBreakAlloc()的帮助。第二行显示该内存块前16个字节的内容,尖括号内是以ASCII方式显示,接着的是以16进制方式显示。
  一般大家都误以为这些内存泄漏的检测功能是由MFC提供的,其实不然。MFC只是封装和利用了MS C-Runtime Library的Debug Function。非MFC程序也可以利用MS
C-Runtime Library的Debug Function加入内存泄漏的检测功能。MS C-Runtime Library在实现malloc/free,strdup等函数时已经内建了内存泄漏的检测功能。
注意观察一下由MFC Application Wizard生成的项目,在每一个cpp文件的头部都有这样一段宏定义:
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
有了这样的定义,在编译DEBUG版时,出现在这个cpp文件中的所有new都被替换成DEBUG_NEW了。那么DEBUG_NEW是什么呢?DEBUG_NEW也是一个宏,以下摘自afx.h,1632行
#define DEBUG_NEW new(THIS_FILE, __LINE__)
所以如果有这样一行代码:
char* p = new char[200];
经过宏替换就变成了:
char* p = new( THIS_FILE, __LINE__)char[200];
根据C&#43;&#43;的标准,对于以上的new的使用方法,编译器会去找这样定义的operator new:
void* operator new(size_t, LPCSTR, int)
我们在afxmem.cpp 63行找到了一个这样的operator new&的实现
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine)
 return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);
void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)
 pResult = _malloc_dbg(nSize, nType, lpszFileName, nLine);
 if (pResult != NULL)
  return pR
  第二个operator new函数比较长,为了简单期间,我只摘录了部分。很显然最后的内存分配还是通过_malloc_dbg函数实现的,这个函数属于MS C-Runtime Library&的Debug Function。这个函数不但要求传入内存的大小,另外还有文件名和行号两个参数。文件名和行号就是用来记录此次分配是由哪一段代码造成的。如果这块内存在程序结束之前没有那被释放,么这些信息就会输出到Debug窗口里。
  这里顺便提一下THIS_FILE,__FILE和__LINE__。__FILE__和__LINE__都是编译器定义的宏。当碰到__FILE__时,编译器会把__FILE__替换成一个字符串,这个字符串就是当前在编译的文件的路径名。当碰到__LINE__时,编译器会把__LINE__替换成一个数字,这个数字就是当前这行代码的行号。在DEBUG_NEW的定义中没有直接使用__FILE__,而是用了THIS_FILE,其目的是为了减小目标文件的大小。假设在某个cpp文件中有100处使用了new,如果直接使用__FILE__,那编译器会产生100个常量字符串,这100个字符串都是飧?/SPAN&cpp文件的路径名,显然十分冗余。如果使用THIS_FILE,编译器只会产生一个常量字符串,那100处new的调用使用的都是指向常量字符串的指针。
  再次观察一下由MFC Application Wizard生成的项目,我们会发现在cpp文件中只对new做了映射,如果你在程序中直接使用malloc函数分配内存,调用malloc的文件名和行号是不会被记录下来的。如果这块内存发生了泄漏,MS
C-Runtime Library仍然能检测到,但是当输出这块内存块的信息,不会包含分配它的的文件名和行号。
要在非MFC程序中打开内存泄漏的检测功能非常容易,你只要在程序的入口处加入以下几行代码:
int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );
tmpFlag |= _CRTDBG_LEAK_CHECK_DF;
_CrtSetDbgFlag( tmpFlag );
  这样,在程序结束的时候,也就是winmain,main或dllmain函数返回之后,如果还有内存块没有释放,它们的信息会被打印到Debug窗口里。
如果你试着创建了一个非MFC应用程序,而且在程序的入口处加入了以上代码,并且故意在程序中不释放某些内存块,你会在Debug窗口里看到以下的信息:
{47} normal block at 0x00C91C90, 200 bytes long.
Data: & & 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
  内存泄漏的确检测到了,但是和上面MFC程序的例子相比,缺少了文件名和行号。对于一个比较大的程序,没有这些信息,解决问题将变得十分困难。
  为了能够知道泄漏的内存块是在哪里分配的,你需要实现

我要回帖

更多关于 为什么发明计算机 的文章

 

随机推荐