C++内存管理机制

内存管理是C++最使人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中得到了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每一个C++程序中都会发生,所以要想成为C++高手,内存管理一关是必需要过的,除非放弃C++,转到Java或者.NET,他们的内存管理基本是自动的,固然你也放弃了自由和对内存的支配权,还放弃了C++超绝的性能。本期专题将从内存管理、内存泄漏、内存回收这三个方面来探讨C++内存管理问题。ios

1 内存管理
c++

程序员们常常编写内存管理程序,每每提心吊胆。若是不想触雷,惟一的解决办法就是发现全部潜伏的地雷而且排除它们,躲是躲不了的。本文的内容比通常教科书的要深刻得多,读者需细心阅读,作到真正地通晓内存管理。程序员

1.1 C++内存管理详解
1.1.1 内存分配方式
1.1.1.1 分配方式简介
  在C++中,内存分红5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。算法

  栈,在执行函数时,函数内局部变量的存储单元均可以在栈上建立,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,可是分配的内存容量有限。编程

  堆,就是那些由new分配的内存块,他们的释放编译器不去管,由咱们的应用程序去控制,通常一个new就要对应一个delete。若是程序员没有释放掉,那么在程序结束后,操做系统会自动回收。小程序

  自由存储区,就是那些由malloc等分配的内存块,他和堆是十分类似的,不过它是用free来结束本身的生命的。设计模式

  全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在之前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。数组

  常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不容许修改。安全

1.1.1.2 明确区分堆与栈
  在bbs上,堆与栈的区分问题,彷佛是一个永恒的话题,因而可知,初学者对此每每是混淆不清的,因此我决定拿他第一个开刀。服务器

  首先,咱们举一个例子:

void f() { int* p=new int[5]; }

  这条短短的一句话就包含了堆与栈,看到new,咱们首先就应该想到,咱们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,因此这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先肯定在堆中分配内存的大小,而后调用operator new分配内存,而后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码以下:

00401028 push 14h

0040102A call operator new (00401060)

0040102F add esp,4

00401032 mov dword ptr [ebp-8],eax

00401035 mov eax,dword ptr [ebp-8]

00401038 mov dword ptr [ebp-4],eax

  这里,咱们为了简单并无释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工做。

1.1.1.3 堆和栈究竟有什么区别?
  好了,咱们回到咱们的主题:堆和栈究竟有什么区别?

  主要的区别由如下几点:

  一、管理方式不一样;

  二、空间大小不一样;

  三、可否产生碎片不一样;

  四、生长方向不一样;

  五、分配方式不一样;

  六、分配效率不一样;

  管理方式:对于栈来说,是由编译器自动管理,无需咱们手工控制;对于堆来讲,释放工做由程序员控制,容易产生memory leak。

  空间大小:通常来说在32位系统下,堆内存能够达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。可是对于栈来说,通常都是有必定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。固然,咱们能够修改:

  打开工程,依次操做菜单以下:Project->Setting->Link,在Category 中选中Output,而后在Reserve中设定堆栈的最大值和commit。

  注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增长内存的开销和启动时间。

  碎片问题:对于堆来说,频繁的new/delete势必会形成内存空间的不连续,从而形成大量的碎片,使程序效率下降。对于栈来说,则不会存在这个问题,由于栈是先进后出的队列,他们是如此的一一对应,以致于永远都不可能有一个内存块从栈中间弹出,在他弹出以前,在他上面的后进的栈内容已经被弹出,详细的能够参考数据结构,这里咱们就再也不一一讨论了。

  生长方向:对于堆来说,生长方向是向上的,也就是向着内存地址增长的方向;对于栈来说,它的生长方向是向下的,是向着内存地址减少的方向增加。

  分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,好比局部变量的分配。动态分配由alloca函数进行分配,可是栈的动态分配和堆是不一样的,他的动态分配是由编译器进行释放,无需咱们手工实现。

  分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照必定的算法(具体的算法能够参考数据结构/操做系统)在堆内存中搜索可用的足够大小的空间,若是没有足够大小的空间(多是因为内存碎片太多),就有可能调用系统功能去增长程序数据段的内存空间,这样就有机会分到足够大小的内存,而后进行返回。显然,堆的效率比栈要低得多。

  从这里咱们能够看到,堆和栈相比,因为大量new/delete的使用,容易形成大量的内存碎片;因为没有专门的系统支持,效率很低;因为可能引起用户态和核心态的切换,内存的申请,代价变得更加昂贵。因此栈在程序中是应用最普遍的,就算是函数的调用也利用栈去完成,函数调用过程当中的参数,返回地址,EBP和局部变量都采用栈的方式存放。因此,咱们推荐你们尽可能用栈,而不是用堆。

  虽然栈有如此众多的好处,可是因为和堆相比不是那么灵活,有时候分配大量的内存空间,仍是用堆好一些。

不管是堆仍是栈,都要防止越界现象的发生(除非你是故意使其越界),由于越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程当中,没有发生上面的问题,你仍是要当心,说不定何时就崩掉,那时候debug但是至关困难的:)

1.1.2 控制C++的内存分配
  在嵌入式系统中使用C++的一个常见问题是内存分配,即对new 和 delete 操做符的失控。

  具备讽刺意味的是,问题的根源倒是C++对内存的管理很是的容易并且安全。具体地说,当一个对象被消除时,它的析构函数可以安全的释放所分配的内存。

  这固然是个好事情,可是这种使用的简单性使得程序员们过分使用new 和 delete,而不注意在嵌入式C++环境中的因果关系。而且,在嵌入式系统中,因为内存的限制,频繁的动态分配不定大小的内存会引发很大的问题以及堆破碎的风险。

  做为忠告,保守的使用内存分配是嵌入式环境中的第一原则。

  但当你必需要使用new 和delete时,你不得不控制C++中的内存分配。你须要用一个全局的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);

{

free(p);

}

  这段代码能够代替默认的操做符来知足内存分配的请求。出于解释C++的目的,咱们也能够直接调用malloc() 和free()。

  也能够对单个类的new 和 delete 操做符重载。这是你能灵活的控制对象的内存分配。

class TestClass {

public:

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++将对象数组的内存分配做为一个单独的操做,而不一样于单个对象的内存分配。为了改变这种方式,你一样须要重载new[ ] 和 delete[ ]操做符。

class TestClass {

public:

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)

{

free(p);

}

int main(void)

{

TestClass *p = new TestClass[10];

// ... etc ...

delete[ ] p;

}

可是注意:对于多数C++的实现,new[]操做符中的个数参数是数组的大小加上额外的存储对象数目的一些字节。在你的内存分配机制重要考虑的这一点。你应该尽可能避免分配对象数组,从而使你的内存分配策略简单。

1.1.3 常见的内存错误及其对策
发生内存错误是件很是麻烦的事情。编译器不能自动发现这些错误,一般是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增长了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发做了。 常见的内存错误及其对策以下:

  * 内存分配未成功,却使用了它。

  编程新手常犯这种错误,由于他们没有意识到内存分配会不成功。经常使用解决办法是,在使用内存以前检查指针是否为NULL。若是指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行

  检查。若是是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。

  * 内存分配虽然成功,可是还没有初始化就引用它。

  犯这种错误主要有两个原由:一是没有初始化的观念;二是误觉得内存的缺省初值全为零,致使引用初值错误(例如数组)。 内存的缺省初值到底是什么并无统一的标准,尽管有些时候为零值,咱们宁肯信其无不可信其有。因此不管用何种方式建立数组,都别忘了赋初值,即使是赋零值也不可省略,不要嫌麻烦。

  * 内存分配成功而且已经初始化,但操做越过了内存的边界。

  例如在使用数组时常常发生下标“多1”或者“少1”的操做。特别是在for循环语句中,循环次数很容易搞错,致使数组操做越界。

  * 忘记了释放内存,形成内存泄露。

  含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序忽然死掉,系统出现提示:内存耗尽。

  动态内存的申请与释放必须配对,程序中malloc与free的使用次数必定要相同,不然确定有错误(new/delete同理)。

  * 释放了内存却继续使用它。

  有三种状况:

  (1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象到底是否已经释放了内存,此时应该从新设计数据结构,从根本上解决对象管理的混乱局面。

  (2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,由于该内存在函数体结束时被自动销毁。

  (3)使用free或delete释放了内存后,没有将指针设置为NULL。致使产生“野指针”。

  【规则1】用malloc或new申请内存以后,应该当即检查指针值是否为NULL。防止使用指针值为NULL的内存。

  【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存做为右值使用。

  【规则3】避免数组或指针的下标越界,特别要小心发生“多1”或者“少1”操做。

  【规则4】动态内存的申请与释放必须配对,防止内存泄漏。

  【规则5】用free或delete释放了内存以后,当即将指针设置为NULL,防止产生“野指针”。

1.1.4 指针与数组的对比
  C++/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 << endl;

char *p = “world”; // 注意p指向常量字符串

p[0] = ‘X’; // 编译器不能发现该错误

cout << p << endl;

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)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。

// 数组…

char a[] = "hello";

char b[10];

strcpy(b, a); // 不能用 b = a;

if(strcmp(b, a) == 0) // 不能用 if (b == a)

// 指针…

int len = strlen(a);

char *p = (char *)malloc(sizeof(char)*(len+1));

strcpy(p,a); // 不要用 p = a;

if(strcmp(p, a) == 0) // 不要用 if (p == a)

1.1.4.3 计算内存容量
用运算符sizeof能够计算出数组的容量(字节数)。以下示例中,sizeof(a)的值是12(注意别忘了’’)。指针p指向a,可是sizeof(p)的值倒是4。这是由于sizeof(p)获得的是一个指针变量的字节数,至关于sizeof(char*),而不是p所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。

char a[] = "hello world";

char *p = a;

cout<< sizeof(a) << endl; // 12字节

cout<< sizeof(p) << endl; // 4字节

注意当数组做为函数的参数进行传递时,该数组自动退化为同类型的指针。以下示例中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。

void Func(char a[100])

{

 cout<< sizeof(a) << endl; // 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 << endl;

 free(str);

}

因为“指向指针的指针”这个概念不容易理解,咱们能够用函数返回值来传递动态内存。这种方法更加简单,见示例:

char *GetMemory3(int num)

{

 char *p = (char *)malloc(sizeof(char) * num);

 return p;

}

void Test3(void)

{

 char *str = NULL;

 str = GetMemory3(100);

 strcpy(str, "hello");

 cout<< str << endl;

 free(str);

}

用函数返回值来传递动态内存这种方法虽然好用,可是经常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,由于该内存在函数结束时自动消亡,见示例:

char *GetString(void)

{

 char p[] = "hello world";

 return p; // 编译器将提出警告

}

void Test4(void)

{

 char *str = NULL;

 str = GetString(); // str 的内容是垃圾

 cout<< str << endl;

}

用调试器逐步跟踪Test4,发现执行str = GetString语句后str再也不是NULL指针,可是str的内容不是“hello world”而是垃圾。

若是把上述示例改写成以下示例,会怎么样?

char *GetString2(void)

{

 char *p = "hello world";

 return p;

}

void Test5(void)

{

 char *str = NULL;

 str = GetString2();

 cout<< str << endl;

}

函数Test5运行虽然不会出错,可是函数GetString2的设计概念倒是错误的。由于GetString2内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。不管何时调用GetString2,它返回的始终是同一个“只读”的内存块。

1.1.6 杜绝“野指针”
  “野指针”不是NULL指针,是指向“垃圾”内存的指针。人们通常不会错用NULL指针,由于用if语句很容易判断。可是“野指针”是很危险的,if语句对它不起做用。 “野指针”的成因主要有两种:

(1)指针变量没有被初始化。任何指针变量刚被建立时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。因此,指针变量在建立的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

char *p = NULL;

char *str = (char *) malloc(100);

(2)指针p被free或者delete以后,没有置为NULL,让人误觉得p是个合法的指针。

(3)指针操做超越了变量的做用域范围。这种状况让人防不胜防,示例程序以下:

class A

{

 public:

  void Func(void){ cout << “Func of class A” << endl; }

};

void Test(void)

{

 A *p;

 {

  A a;

  p = &a; // 注意 a 的生命期

 }

 p->Func(); // p是“野指针”

}

函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,因此p就成了“野指针”。但奇怪的是我运行这个程序时竟然没有出错,这可能与编译器有关。

1.1.7 有了malloc/free为何还要new/delete?
  malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们均可用于申请动态内存和释放内存。

  对于非内部数据类型的对象而言,光用maloc/free没法知足动态对象的要求。对象在建立的同时要自动执行构造函数,对象在消亡以前要自动执行析构函数。因为malloc/free是库函数而不是运算符,不在编译器控制权限以内,不可以把执行构造函数和析构函数的任务强加于malloc/free。

所以C++语言须要一个能完成动态内存分配和初始化工做的运算符new,以及一个能完成清理与释放内存工做的运算符delete。注意new/delete不是库函数。咱们先看一看malloc/free和new/delete如何实现对象的动态内存管理,见示例:

class Obj

{

 public :

  Obj(void){ cout << “Initialization” << endl; }

  ~Obj(void){ cout << “Destroy” << endl; }

  void Initialize(void){ cout << “Initialization” << endl; }

  void Destroy(void){ cout << “Destroy” << endl; }

};

void UseMallocFree(void)

{

 Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存

 a->Initialize(); // 初始化

 //…

 a->Destroy(); // 清除工做

 free(a); // 释放内存

}

void UseNewDelete(void)

{

 Obj *a = new Obj; // 申请动态内存而且初始化

 //…

 delete a; // 清除而且释放内存

}

  类Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,因为malloc/free不能执行构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成初始化与清除工做。函数UseNewDelete则简单得多。

  因此咱们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。因为内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。

  既然new/delete的功能彻底覆盖了malloc/free,为何C++不把malloc/free淘汰出局呢?这是由于C++程序常常要调用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)

 {

  return;

 }

 …

}

 (2)判断指针是否为NULL,若是是则立刻用exit(1)终止整个程序的运行。例如:

void Func(void)

{

 A *a = new A;

 if(a == NULL)

 {

  cout << “Memory Exhausted” << endl;

  exit(1);

 }

 …

}

  (3)为new和malloc设置异常处理函数。例如Visual C++能够用_set_new_hander函数为new设置用户本身定义的异常处理函数,也可让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。

  上述(1)(2)方式使用最广泛。若是一个函数内有多处须要申请动态内存,那么方式(1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。

  不少人不忍心用exit(1),问:“不编写出错处理程序,让操做系统本身解决行不行?”

  不行。若是发生“内存耗尽”这样的事情,通常说来应用程序已经无药可救。若是不用exit(1) 把坏程序杀死,它可能会害死操做系统。道理如同:若是不把歹徒击毙,歹徒在老死以前会犯下更多的罪。

  有一个很重要的现象要告诉你们。对于32位以上的应用程序而言,不管怎样使用malloc与new,几乎不可能致使“内存耗尽”。我在Windows 98下用Visual C++编写了测试程序,见示例7。这个程序会无休止地运行下去,根本不会终止。由于32位操做系统支持“虚存”,内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98已经累得对键盘、鼠标毫无反应。

  我能够得出这么一个结论:对于32位以上的应用程序,“内存耗尽”错误处理程序毫无用处。这下可把Unix和Windows程序员们乐坏了:反正错误处理程序不起做用,我就不写了,省了不少麻烦。

我不想误导读者,必须强调:不加错误处理将致使程序的质量不好,千万不可因小失大。

void main(void)

{

 float *p = NULL;

 while(TRUE)

 {

  p = new float[1000000];

  cout << “eat memory” << endl;

  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返回值的类型是void *,因此在调用malloc时要显式地进行类型转换,将void * 转换成所须要的指针类型。

* malloc函数自己并不识别要申请的内存是什么类型,它只关心内存的总字节数。咱们一般记不住int, float等数据类型的变量的确切字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。最好用如下程序做一次测试:

cout << sizeof(char) << endl;

cout << sizeof(int) << endl;

cout << sizeof(unsigned int) << endl;

cout << sizeof(long) << endl;

cout << sizeof(unsigned long) << endl;

cout << sizeof(float) << endl;

cout << sizeof(double) << endl;

cout << sizeof(void *) << endl;

在malloc的“()”中使用sizeof运算符是良好的风格,但要小心有时咱们会昏了头,写出 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的语句也能够有多种形式。例如

class Obj

{

 public :

  Obj(void); // 无参数的构造函数

  Obj(int x); // 带一个参数的构造函数

  …

}

void Test(void)

{

 Obj *a = new Obj;

 Obj *b = new Obj(1); // 初值为1

 …

 delete a;

 delete b;

}

若是用new建立对象数组,那么只能使用对象的无参数构造函数。例如:

Obj *objects = new Obj[100]; // 建立100个动态对象

不能写成:

Obj *objects = new Obj[100](1);// 建立100个动态对象的同时赋初值1

在用delete释放对象数组时,留意不要丢了符号‘[]’。例如:

delete []objects; // 正确的用法

delete objects; // 错误的用法

后者有可能引发程序崩溃和内存泄漏。

1.2 C++中的健壮指针和资源管理
  我最喜欢的对资源的定义是:"任何在你的程序中得到并在此后释放的东西?quot;内存是一个至关明显的资源的例子。它须要用new来得到,用delete来释放。同时也有许多其它类型的资源文件句柄、重要的片段、Windows中的GDI资源,等等。将资源的概念推广到程序中建立、释放的全部对象也是十分方便的,不管对象是在堆中分配的仍是在栈中或者是在全局做用于内生命的。

  对于给定的资源的拥有着,是负责释放资源的一个对象或者是一段代码。全部权分立为两种级别——自动的和显式的(automatic and explicit),若是一个对象的释放是由语言自己的机制来保证的,这个对象的就是被自动地全部。例如,一个嵌入在其余对象中的对象,他的清除须要其余对象来在清除的时候保证。外面的对象被看做嵌入类的全部者。   相似地,每一个在栈上建立的对象(做为自动变量)的释放(破坏)是在控制流离开了对象被定义的做用域的时候保证的。这种状况下,做用于被看做是对象的全部者。注意全部的自动全部权都是和语言的其余机制相容的,包括异常。不管是如何退出做用域的——正常流程控制退出、一个break语句、一个return、一个goto、或者是一个throw——自动资源均可以被清除。

  到目前为止,一切都很好!问题是在引入指针、句柄和抽象的时候产生的。若是经过一个指针访问一个对象的话,好比对象在堆中分配,C++不自动地关注它的释放。程序员必须明确的用适当的程序方法来释放这些资源。好比说,若是一个对象是经过调用new来建立的,它须要用delete来回收。一个文件是用CreateFile(Win32 API)打开的,它须要用CloseHandle来关闭。用EnterCritialSection进入的临界区(Critical Section)须要LeaveCriticalSection退出,等等。一个"裸"指针,文件句柄,或者临界区状态没有全部者来确保它们的最终释放。基本的资源管理的前提就是确保每一个资源都有他们的全部者。

1.2.1 第一条规则(RAII)
  一个指针,一个句柄,一个临界区状态只有在咱们将它们封装入对象的时候才会拥有全部者。这就是咱们的第一规则:在构造函数中分配资源,在析构函数中释放资源。

  当你按照规则将全部资源封装的时候,你能够保证你的程序中没有任何的资源泄露。这点在当封装对象(Encapsulating Object)在栈中创建或者嵌入在其余的对象中的时候很是明显。可是对那些动态申请的对象呢?不要急!任何动态申请的东西都被看做一种资源,而且要按照上面提到的方法进行封装。这一对象封装对象的链不得不在某个地方终止。它最终终止在最高级的全部者,自动的或者是静态的。这些分别是对离开做用域或者程序时释放资源的保证。

  下面是资源封装的一个经典例子。在一个多线程的应用程序中,线程之间共享对象的问题是经过用这样一个对象联系临界区来解决的。每个须要访问共享资源的客户须要得到临界区。例如,这多是Win32下临界区的实现方法。

class CritSect

{

 friend class Lock;

 public:

  CritSect () { InitializeCriticalSection (&_critSection); }

  ~CritSect () { DeleteCriticalSection (&_critSection); }

 private:

  void Acquire ()

  {

   EnterCriticalSection (&_critSection);

  }

  void Release ()

  {

   LeaveCriticalSection (&_critSection);

  }

 private:

  CRITICAL_SECTION _critSection;

};

  这里聪明的部分是咱们确保每个进入临界区的客户最后均可以离开。"进入"临界区的状态是一种资源,并应当被封装。封装器一般被称做一个锁(lock)。

class Lock

{

 public:

  Lock (CritSect& critSect) : _critSect (critSect)

  {

   _critSect.Acquire ();

  }

  ~Lock ()

  {

   _critSect.Release ();

  }

 private

  CritSect & _critSect;

};

  锁通常的用法以下:

void Shared::Act () throw (char *)

{

 Lock lock (_critSect);

 // perform action —— may throw

 // automatic destructor of lock

}

  注意不管发生什么,临界区都会借助于语言的机制保证释放。

  还有一件须要记住的事情——每一种资源都须要被分别封装。这是由于资源分配是一个很是容易出错的操做,是要资源是有限提供的。咱们会假设一个失败的资源分配会致使一个异常——事实上,这会常常的发生。因此若是你想试图用一个石头打两只鸟的话,或者在一个构造函数中申请两种形式的资源,你可能就会陷入麻烦。只要想一想在一种资源分配成功但另外一种失败抛出异常时会发生什么。由于构造函数尚未所有完成,析构函数不可能被调用,第一种资源就会发生泄露。

这种状况能够很是简单的避免。不管什么时候你有一个须要两种以上资源的类时,写两个小的封装器将它们嵌入你的类中。每个嵌入的构造均可以保证删除,即便包装类没有构造完成。

1.2.2 Smart Pointers
  咱们至今尚未讨论最多见类型的资源——用操做符new分配,此后用指针访问的一个对象。咱们须要为每一个对象分别定义一个封装类吗?(事实上,C++标准模板库已经有了一个模板类,叫作auto_ptr,其做用就是提供这种封装。咱们一下子在回到auto_ptr。)让咱们从一个极其简单、呆板但安全的东西开始。看下面的Smart Pointer模板类,它十分坚固,甚至没法实现。

template <class T>

class SmartPointer

{

 public:

  ~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>

{

 public:

  explicit SmartItem (int i)

  : SmartPointer<Item> (new Item (i)) {}

};

  为每个类提供一个Smart Pointer真的值得吗?说实话——不!他颇有教学的价值,可是一旦你学会如何遵循第一规则的话,你就能够放松规则并使用一些高级的技术。这一技术是让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;

_p = 0;

return pTmp;

}

  注意在Release调用之后,Smart Pointer就再也不是对象的全部者了——它内部的指针指向空。如今,调用了Release都必须是一个负责的人而且迅速隐藏返回的指针到新的全部者对象中。在咱们的例子中,容器调用了Release,好比这个Stack的例子:

void Stack::Push (SmartPointer <Item> & item) throw (char *)

{

if (_top == maxStack)

throw "Stack overflow";

_arr [_top++] = item.Release ();

};

  一样的,你也能够再你的代码中用增强Release的可靠性。

相应的Pop方法要作些什么呢?他应该释放了资源并祈祷调用它的是一个负责的人并且当即做一个资源传递它到一个Smart Pointer?这听起来并很差。

1.2.4 Strong Pointers
  资源管理在内容索引(Windows NT Server上的一部分,如今是Windows 2000)上工做,而且,我对这十分满意。而后我开始想……这一方法是在这样一个完整的系统中造成的,若是能够把它内建入语言的自己岂不是一件很是好?我提出了强指针(Strong Pointer)和弱指针(Weak Pointer)。一个Strong Pointer会在许多地方和咱们这个SmartPointer类似--它在超出它的做用域后会清除他所指向的对象。资源传递会以强指针赋值的形式进行。也能够有Weak Pointer存在,它们用来访问对象而不须要全部对象--好比可赋值的引用。

  任何指针都必须声明为Strong或者Weak,而且语言应该来关注类型转换的规定。例如,你不能够将Weak Pointer传递到一个须要Strong Pointer的地方,可是相反却能够。Push方法能够接受一个Strong Pointer而且将它转移到Stack中的Strong Pointer的序列中。Pop方法将会返回一个Strong Pointer。把Strong Pointer的引入语言将会使垃圾回收成为历史。

  这里还有一个小问题--修改C++标准几乎和竞选美国总统同样容易。当我将个人注意告诉给Bjarne Stroutrup的时候,他看个人眼神好像是我刚刚要向他借一千美圆同样。

而后我忽然想到一个念头。我能够本身实现Strong Pointers。毕竟,它们都很想Smart Pointers。给它们一个拷贝构造函数并重载赋值操做符并非一个大问题。事实上,这正是标准库中的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 ();

}

}

  使这整个想法迅速成功的缘由之一是我能够以值方式传递这种封装指针!我有了个人蛋糕,而且也能够吃了。看这个Stack的新的实现:

class Stack

{

enum { maxStack = 3 };

public:

Stack ()

: _top (0)

{}

void Push (SmartPointer<Item> & item) throw (char *)

{

if (_top >= maxStack)

throw "Stack overflow";

_arr [_top++] = item;

}

SmartPointer<Item> Pop ()

{

if (_top == 0)

return SmartPointer<Item> ();

return _arr [--_top];

}

private

int _top;

SmartPointer<Item> _arr [maxStack];

};

  Pop方法强制客户将其返回值赋给一个Strong Pointer,SmartPointer<Item>。任何试图将他对一个普通指针的赋值都会产生一个编译期错误,由于类型不匹配。此外,由于Pop以值方式返回一个Strong Pointer(在Pop的声明时SmartPointer<Item>后面没有&符号),编译器在return时自动进行了一个资源转换。他调用了operator =来从数组中提取一个Item,拷贝构造函数将他传递给调用者。调用者最后拥有了指向Pop赋值的Strong Pointer指向的一个Item。

我立刻意识到我已经在某些东西之上了。我开始用了新的方法重写原来的代码。

1.2.5 Parser
我过去有一个老的算术操做分析器,是用老的资源管理的技术写的。分析器的做用是在分析树中生成节点,节点是动态分配的。例如分析器的Expression方法生成一个表达式节点。我没有时间用Strong Pointer去重写这个分析器。我令Expression、Term和Factor方法以传值的方式将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 { ('+' | '-') Term }

SmartPointer<MultiNode> pMultiNode = new SumNode (pNode);

do

{

_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 pNode; // by value!

}

  最开始,Term方法被调用。他传值返回一个指向Node的Strong Pointer而且马上把它保存到咱们本身的Strong Pointer,pNode中。若是下一个符号不是加号或者减号,咱们就简单的把这个SmartPointer以值返回,这样就释放了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的指针,并被赋值为_p,一个指向T的指针。因此若是U不是一个T的话,赋值会致使一个编译时刻错误。

std::auto_ptr

后来我意识到在STL中的auto_ptr模板,就是个人Strong Pointer。在那时候还有许多的实现差别(auto_ptr的Release方法并不将内部的指针清零--你的编译器的库极可能用的就是这种陈旧的实现),可是最后在标准被普遍接受以前都被解决了。

1.2.6 Transfer Semantics
  目前为止,咱们一直在讨论在C++程序中资源管理的方法。宗旨是将资源封装到一些轻量级的类中,并由类负责它们的释放。特别的是,全部用new操做符分配的资源都会被储存并传递进Strong Pointer(标准库中的auto_ptr)的内部。

  这里的关键词是传递(passing)。一个容器能够经过传值返回一个Strong Pointer来安全的释放资源。容器的客户只可以经过提供一个相应的Strong Pointer来保存这个资源。任何一个将结果赋给一个"裸"指针的作法都当即会被编译器发现。

auto_ptr<Item> item = stack.Pop (); // ok

Item * p = stack.Pop (); // Error! Type mismatch.

  以传值方式被传递的对象有value semantics 或者称为 copy semantics。Strong Pointers是以值方式传递的--可是咱们能说它们有copy semantics吗?不是这样的!它们所指向的对象确定没有被拷贝过。事实上,传递事后,源auto_ptr不在访问原有的对象,而且目标auto_ptr成为了对象的惟一拥有者(可是每每auto_ptr的旧的实现即便在释放后仍然保持着对对象的全部权)。天然而然的咱们能够将这种新的行为称做Transfer Semantics。

  拷贝构造函数(copy construcor)和赋值操做符定义了auto_ptr的Transfer Semantics,它们用了非const的auto_ptr引用做为它们的参数。

auto_ptr (auto_ptr<T> & ptr);

auto_ptr & operator = (auto_ptr<T> & ptr);

  这是由于它们确实改变了他们的源--剥夺了对资源的全部权。

经过定义相应的拷贝构造函数和重载赋值操做符,你能够将Transfer Semantics加入到许多对象中。例如,许多Windows中的资源,好比动态创建的菜单或者位图,能够用有Transfer Semantics的类来封装。

1.2.7 Strong Vectors
  标准库只在auto_ptr中支持资源管理。甚至连最简单的容器也不支持ownership semantics。你可能想将auto_ptr和标准容器组合到一块儿可能会管用,可是并非这样的。例如,你可能会这样作,可是会发现你不可以用标准的方法来进行索引。

vector< auto_ptr<Item> > autoVector;

  这种建造不会编译成功;

Item * item = autoVector [0];

  另外一方面,这会致使一个从autoVect到auto_ptr的全部权转换:

auto_ptr<Item> item = autoVector [0];

  咱们没有选择,只可以构造咱们本身的Strong Vector。最小的接口应该以下:

template <class T>

class auto_vector

{

public:

explicit auto_vector (size_t capacity = 0);

T const * operator [] (size_t i) const;

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的左值索引的访问,取而代之,若是你想设定(set)一个值的话,你必须用assign或者assign_direct方法。个人观点是,资源管理不该该被忽视,同时,也不该该在全部的地方滥用。在个人经验里,一个strong vector常常被许多push_back方法充斥着。

  Strong vector最好用一个动态的Strong Pointers的数组来实现:

template <class T>

class auto_vector

{

private

void grow (size_t reqCapacity);

auto_ptr<T> *_arr;

size_t _capacity;

size_t _end;

};

  grow方法申请了一个很大的auto_ptr<T>的数组,将全部的东西从老的书组类转移出来,在其中交换,而且删除原来的数组。

  auto_vector的其余实现都是十分直接的,由于全部资源管理的复杂度都在auto_ptr中。例如,assign方法简单的利用了重载的赋值操做符来删除原有的对象并转移资源到新的对象:

void assign (size_t i, auto_ptr<T> & p)

{

_arr [i] = p;

}

  我已经讨论了push_back和pop_back方法。push_back方法传值返回一个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 *>

{

public:

auto_iterator () : _pp (0) {}

auto_iterator (auto_ptr<T> * pp) : _pp (pp) {}

bool operator != (auto_iterator<T> const & it) const

{ return it._pp != _pp; }

auto_iterator const & operator++ (int) { return _pp++; }

auto_iterator operator++ () { return ++_pp; }

T * operator * () { return _pp->get (); }

private

auto_ptr<T> * _pp;

};

咱们给auto_vect提供了标准的begin和end方法来找回iterator:

class auto_vector

{

public:

typedef auto_iterator<T> iterator;

iterator begin () { return _arr; }

iterator end () { return _arr + _end; }

};

  你也许会问咱们是否要利用资源管理从新实现每个标准的容器?幸运的是,不;事实是strong vector解决了大部分全部权的需求。当你把你的对象都安全的放置到一个strong vector中,你能够用全部其它的容器来从新安排(weak)pointer。

设想,例如,你须要对一些动态分配的对象排序的时候。你将它们的指针保存到一个strong vector中。而后你用一个标准的vector来保存从strong vector中得到的weak指针。你能够用标准的算法对这个vector进行排序。这种中介vector叫作permutation vector。类似的,你也能够用标准的maps, priority queues, heaps, hash tables等等。

1.2.8 Code Inspection
  若是你严格遵守资源管理的条款,你就不会再资源泄露或者两次删除的地方遇到麻烦。你也下降了访问野指针的概率。一样的,遵循原有的规则,用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

{

public:

RefCounted () : _count (1) {}

int GetRefCount () const { return _count; }

void IncRefCount () { _count++; }

int DecRefCount () { return --_count; }

private

int _count;

};

  按照资源管理,一个引用计数是一种资源。若是你遵照它,你须要释放它。当你意识到这一事实的时候,剩下的就变得简单了。简单的遵循规则--再构造函数中得到引用计数,在析构函数中释放。甚至有一个RefCounted的smart pointer等价物:

template <class T>

class RefPtr

{

public:

RefPtr (T * p) : _p (p) {}

RefPtr (RefPtr<T> & p)

{

_p = p._p;

_p->IncRefCount ();

}

~RefPtr ()

{

if (_p->DecRefCount () == 0)

delete _p;

}

private

T * _p;

};

  注意模板中的T不比成为RefCounted的后代,可是它必须有IncRefCount和DecRefCount的方法。固然,一个便于使用的RefPtr须要有一个重载的指针访问操做符。在RefPtr中加入转换语义学(transfer semantics)是读者的工做。

1.2.10 全部权网络
  链表是资源管理分析中的一个颇有意思的例子。若是你选择表成为链(link)的全部者的话,你会陷入实现递归的全部权。每个link都是它的继承者的全部者,而且,相应的,余下的链表的全部者。下面是用smart pointer实现的一个表单元:

class Link

{

// ...

private

auto_ptr<Link> _next;

};

  最好的方法是,将链接控制封装到一个弄构进行资源转换的类中。

  对于双链表呢?安全的作法是指明一个方向,如forward:

class DoubleLink

{

// ...

private

DoubleLink *_prev;

auto_ptr<DoubleLink> _next;

};

  注意不要建立环形链表。

  这给咱们带来了另一个有趣的问题--资源管理能够处理环形的全部权吗?它能够,用一个mark-and-sweep的算法。这里是实现这种方法的一个例子:

template<class T>

class CyclPtr

{

public:

CyclPtr (T * p)

:_p (p), _isBeingDeleted (false)

{}

~CyclPtr ()

{

_isBeingDeleted = true;

if (!_p->IsBeingDeleted ())

delete _p;

}

void Set (T * p)

{

_p = p;

}

bool IsBeingDeleted () const { return _isBeingDeleted; }

private

T * _p;

bool _isBeingDeleted;

};

  注意咱们须要用class T来实现方法IsBeingDeleted,就像从CyclPtr继承。对特殊的全部权网络普通化是十分直接的。

  将原有代码转换为资源管理代码

若是你是一个经验丰富的程序员,你必定会知道找资源的bug是一件浪费时间的痛苦的经历。我没必要说服你和你的团队花费一点时间来熟悉资源管理是十分值得的。你能够当即开始用这个方法,不管你是在开始一个新项目或者是在一个项目的中期。转换没必要当即所有完成。下面是步骤。

(1)       首先,在你的工程中创建基本的Strong Pointer。而后经过查找代码中的new来开始封装裸指针。

(2)       最早封装的是在过程当中定义的临时指针。简单的将它们替换为auto_ptr而且删除相应的delete。若是一个指针在过程当中没有被删除而是被返回,用auto_ptr替换并在返回前调用release方法。在你作第二次传递的时候,你须要处理对release的调用。注意,即便是在这点,你的代码也可能更加"精力充沛"--你会移出代码中潜在的资源泄漏问题。

(3)       下面是指向资源的裸指针。确保它们被独立的封装到auto_ptr中,或者在构造函数中分配在析构函数中释放。若是你有传递全部权的行为的话,须要调用release方法。若是你有容器全部对象,用Strong Pointers从新实现它们。

(4)       接下来,找到全部对release的方法调用而且尽力清除全部,若是一个release调用返回一个指针,将它修改传值返回一个auto_ptr。

(5)       重复着一过程,直到最后全部new和release的调用都在构造函数或者资源转换的时候发生。这样,你在你的代码中处理了资源泄漏的问题。对其余资源进行类似的操做。

(6)       你会发现资源管理清除了许多错误和异常处理带来的复杂性。不只仅你的代码会变得精力充沛,它也会变得简单并容易维护。

2 内存泄漏
2.1 C++中动态内存分配引起问题的解决方案
假设咱们要开发一个String类,它能够方便地处理字符串数据。咱们能够在类中声明一个数组,考虑到有时候字符串极长,咱们能够把数组大小设为200,但通常的状况下又不须要这么多的空间,这样是浪费了内存。对了,咱们能够使用new操做符,这样是十分灵活的,但在类中就会出现许多意想不到的问题,本文就是针对这一现象而写的。如今,咱们先来开发一个String类,但它是一个不完善的类。的确,咱们要刻意地使它出现各类各样的问题,这样才好对症下药。好了,咱们开始吧!

/* String.h */

#ifndef STRING_H_

#define STRING_H_

class String

{

private:

char * str; //存储数据

int len; //字符串长度

public:

String(const char * s); //构造函数

String(); // 默认构造函数

~String(); // 析构函数

friend ostream & operator<<(ostream & os,const String& st);

};

#endif

/*String.cpp*/

#include <iostream>

#include <cstring>

#include "String.h"

using namespace std;

String::String(const char * s)

{

len = strlen(s);

str = new char[len + 1];

strcpy(str, s);

}//拷贝数据

String::String()

{

len =0;

str = new char[len+1];

str[0]='"0';

}

String::~String()

{

cout<<"这个字符串将被删除:"<<str<<'"n';//为了方便观察结果,特留此行代码。

delete [] str;

}

ostream & operator<<(ostream & os, const String & st)

{

os << st.str;

return os;

}

/*test_right.cpp*/

#include <iostream>

#include <stdlib.h>

#include "String.h"

using namespace std;

int main()

{

String temp("天极网");

cout<<temp<<'"n';

system("PAUSE");

return 0;

}

  运行结果:

  天极网

  请按任意键继续. . .

  你们能够看到,以上程序十分正确,并且也是十分有用的。但是,咱们不能被表面现象所迷惑!下面,请你们用test_String.cpp文件替换test_right.cpp文件进行编译,看看结果。有的编译器可能就是根本不能进行编译!

test_String.cpp:

#include <iostream>

#include <stdlib.h>

#include "String.h"

using namespace std;

void show_right(const String&);

void show_String(const String);//注意,参数非引用,而是按值传递。

int main()

{

String test1("第一个范例。");

String test2("第二个范例。");

String test3("第三个范例。");

String test4("第四个范例。");

cout<<"下面分别输入三个范例:"n";

cout<<test1<<endl;

cout<<test2<<endl;

cout<<test3<<endl;

String* String1=new String(test1);

cout<<*String1<<endl;

delete String1;

cout<<test1<<endl; //在Dev-cpp上没有任何反应。

cout<<"使用正确的函数:"<<endl;

show_right(test2);

cout<<test2<<endl;

cout<<"使用错误的函数:"<<endl;

show_String(test2);

cout<<test2<<endl; //这一段代码出现严重的错误!

String String2(test3);

cout<<"String2: "<<String2<<endl;

String String3;

String3=test4;

cout<<"String3: "<<String3<<endl;

cout<<"下面,程序结束,析构函数将被调用。"<<endl;

return 0;

}

void show_right(const String& a)

{

cout<<a<<endl;

}

void show_String(const String a)

{

cout<<a<<endl;

}

  运行结果:

  下面分别输入三个范例:

  第一个范例。

  第二个范例。

  第三个范例。

  第一个范例。

  这个字符串将被删除:第一个范例。

  使用正确的函数:

  第二个范例。

  第二个范例。

  使用错误的函数:

  第二个范例。

  这个字符串将被删除:第二个范例。

  这个字符串将被删除:?=

  ?=

  String2: 第三个范例。

  String3: 第四个范例。

  下面,程序结束,析构函数将被调用。

  这个字符串将被删除:第四个范例。

  这个字符串将被删除:第三个范例。

  这个字符串将被删除:?=

  这个字符串将被删除:x =

  这个字符串将被删除:?=

  这个字符串将被删除:

如今,请你们本身试试运行结果,或许会更加惨不忍睹呢!下面,我为你们一一分析缘由。

首先,你们要知道,C++类有如下这些极为重要的函数:

一:复制构造函数。

二:赋值函数。

咱们先来说复制构造函数。什么是复制构造函数呢?好比,咱们能够写下这样的代码:String test1(test2);这是进行初始化。咱们知道,初始化对象要用构造函数。可这儿呢?按理说,应该有声明为这样的构造函数:String(const String &);但是,咱们并无定义这个构造函数呀?答案是,C++提供了默认的复制构造函数,问题也就出在这儿。

(1):何时会调用复制构造函数呢?(以String类为例。)

  在咱们提供这样的代码:String test1(test2)时,它会被调用;当函数的参数列表为按值传递,也就是没有用引用和指针做为类型时,如:void show_String(const String),它会被调用。其实,还有一些状况,但在这儿就不列举了。

(2):它是什么样的函数。

它的做用就是把两个类进行复制。拿String类为例,C++提供的默认复制构造函数是这样的:

String(const String& a)

{

str=a.str;

len=a.len;

}

在平时,这样并不会有任何的问题出现,但咱们用了new操做符,涉及到了动态内存分配,咱们就不得不谈谈浅复制和深复制了。以上的函数就是实行的浅复制,它只是复制了指针,而并无复制指针指向的数据,可谓一点儿用也没有。打个比方吧!就像一个朋友让你把一个程序经过网络发给他,而你大大咧咧地把快捷方式发给了他,有什么用处呢?咱们来具体谈谈:

假如,A对象中存储了这样的字符串:“C++”。它的地址为2000。如今,咱们把A对象赋给B对象:String B=A。如今,A和B对象的str指针均指向2000地址。看似能够使用,但若是B对象的析构函数被调用时,则地址2000处的字符串“C++”已经被从内存中抹去,而A对象仍然指向地址2000。这时,若是咱们写下这样的代码:cout<<A<<endl;或是等待程序结束,A对象的析构函数被调用时,A对象的数据可否显示出来呢?只会是乱码。并且,程序还会这样作:连续对地址2000处使用两次delete操做符,这样的后果是十分严重的!

本例中,有这样的代码:

String* String1=new String(test1);

cout<<*String1<<endl;

delete String1;

  假设test1中str指向的地址为2000,而String中str指针一样指向地址2000,咱们删除了2000处的数据,而test1对象呢?已经被破坏了。你们从运行结果上能够看到,咱们使用cout<<test1时,一点反应也没有。而在test1的析构函数被调用时,显示是这样:“这个字符串将被删除:”。

再看看这段代码:

cout<<"使用错误的函数:"<<endl;

show_String(test2);

cout<<test2<<endl;//这一段代码出现严重的错误!

show_String函数的参数列表void show_String(const String a)是按值传递的,因此,咱们至关于执行了这样的代码:String a=test2;函数执行完毕,因为生存周期的缘故,对象a被析构函数删除,咱们立刻就能够看到错误的显示结果了:这个字符串将被删除:?=。固然,test2也被破坏了。解决的办法很简单,固然是手工定义一个复制构造函数喽!人力能够胜天!

String::String(const String& a)
{
len=a.len;
str=new char(len+1);
strcpy(str,a.str);
}

  咱们执行的是深复制。这个函数的功能是这样的:假设对象A中的str指针指向地址2000,内容为“I am a C++ Boy!”。咱们执行代码String B=A时,咱们先开辟出一块内存,假设为3000。咱们用strcpy函数将地址2000的内容拷贝到地址3000中,再将对象B的str指针指向地址3000。这样,就互不干扰了。

你们把这个函数加入程序中,问题就解决了大半,但尚未彻底解决,问题在赋值函数上。咱们的程序中有这样的段代码:

String String3;

String3=test4;

  通过我前面的讲解,你们应该也会对这段代码进行寻根摸底:凭什么能够这样作:String3=test4???缘由是,C++为了用户的方便,提供的这样的一个操做符重载函数:operator=。因此,咱们能够这样作。你们应该猜获得,它一样是执行了浅复制,出了一样的毛病。好比,执行了这段代码后,析构函数开始大展神威^_^。因为这些变量是后进先出的,因此最后的String3变量先被删除:这个字符串将被删除:第四个范例。很正常。最后,删除到test4的时候,问题来了:这个字符串将被删除:?=。缘由我不用赘述了,只是这个赋值函数怎么写,还有一点儿学问呢!你们请看:

平时,咱们能够写这样的代码:x=y=z。(均为整型变量。)而在类对象中,咱们一样要这样,由于这很方便。而对象A=B=C就是A.operator=(B.operator=(c))。而这个operator=函数的参数列表应该是:const String& a,因此,你们不难推出,要实现这样的功能,返回值也要是String&,这样才能实现A=B=C。咱们先来写写看:

String& String::operator=(const String& a)

{

delete [] str;//先删除自身的数据

len=a.len;

str=new char[len+1];

strcpy(str,a.str);//此三行为进行拷贝

return *this;//返回自身的引用

}

是否是这样就好了呢?咱们假如写出了这种代码:A=A,那么你们看看,岂不是把A对象的数据给删除了吗?这样可谓引起一系列的错误。因此,咱们还要检查是否为自身赋值。只比较两对象的数据是不行了,由于两个对象的数据颇有可能相同。咱们应该比较地址。如下是无缺的赋值函数:

String& String::operator=(const String& a)

{

if(this==&a)

return *this;

delete [] str;

len=a.len;

str=new char[len+1];

strcpy(str,a.str);

return *this;

}

把这些代码加入程序,问题就彻底解决,下面是运行结果:

  下面分别输入三个范例:

  第一个范例

  第二个范例

  第三个范例

  第一个范例

  这个字符串将被删除:第一个范例。

  第一个范例

   使用正确的函数:

  第二个范例。

  第二个范例。

   使用错误的函数:

  第二个范例。

  这个字符串将被删除:第二个范例。

  第二个范例。

  String2: 第三个范例。

  String3: 第四个范例。

  下面,程序结束,析构函数将被调用。

  这个字符串将被删除:第四个范例。

  这个字符串将被删除:第三个范例。

  这个字符串将被删除:第四个范例。

  这个字符串将被删除:第三个范例。

  这个字符串将被删除:第二个范例。

  这个字符串将被删除:第一个范例。

2.2 如何对付内存泄漏?
写出那些不会致使任何内存泄漏的代码。很明显,当你的代码中处处充满了new 操做、delete操做和指针运算的话,你将会在某个地方搞晕了头,致使内存泄漏,指针引用错误,以及诸如此类的问题。这和你如何当心地对待内存分配工做其实彻底没有关系:代码的复杂性最终老是会超过你可以付出的时间和努力。因而随后产生了一些成功的技巧,它们依赖于将内存分配(allocations)与从新分配(deallocation)工做隐藏在易于管理的类型以后。标准容器(standard containers)是一个优秀的例子。它们不是经过你而是本身为元素管理内存,从而避免了产生糟糕的结果。想象一下,没有string和vector的帮助,写出这个:

#include<vector>

#include<string>

#include<iostream>

#include<algorithm>

using namespace std;

int main() // small program messing around with strings

{

 cout << "enter some whitespace-separated words:"n";

 vector<string> v;

 string s;

 while (cin>>s) v.push_back(s);

 sort(v.begin(),v.end());

 string cat;

 typedef vector<string>::const_iterator Iter;

 for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";

 cout << cat << ’"n’;

}

  你有多少机会在第一次就获得正确的结果?你又怎么知道你没有致使内存泄漏呢?

  注意,没有出现显式的内存管理,宏,造型,溢出检查,显式的长度限制,以及指针。经过使用函数对象和标准算法(standard algorithm),我能够避免使用指针——例如使用迭代子(iterator),不过对于一个这么小的程序来讲有点小题大做了。

  这些技巧并不完美,要系统化地使用它们也并不老是那么容易。可是,应用它们产生了惊人的差别,并且经过减小显式的内存分配与从新分配的次数,你甚至能够使余下的例子更加容易被跟踪。早在1981年,我就指出,经过将我必须显式地跟踪的对象的数量从几万个减小到几打,为了使程序正确运行而付出的努力从可怕的苦工,变成了应付一些可管理的对象,甚至更加简单了。

  若是你的程序尚未包含将显式内存管理减小到最小限度的库,那么要让你程序完成和正确运行的话,最快的途径也许就是先创建一个这样的库。

  模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年之前。异常的使用使之更加完善。

  若是你实在不能将内存分配/从新分配的操做隐藏到你须要的对象中时,你能够使用资源句柄(resource handle),以将内存泄漏的可能性降至最低。这里有个例子:我须要经过一个函数,在空闲内存中创建一个对象并返回它。这时候可能忘记释放这个对象。毕竟,咱们不能说,仅仅关注当这个指针要被释放的时候,谁将负责去作。使用资源句柄,这里用了标准库中的auto_ptr,使须要为之负责的地方变得明确了。

#include<memory>

#include<iostream>

using namespace std;

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"; }

};

S* f()

{

 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,尼安德特人,旧石器时代普遍分布在欧洲的猿人)写的,如此等等),那么注意使用一个内存泄漏检测器做为开发过程的一部分,或者插入一个垃圾收集器(garbage collector)。

2.3浅谈C/C++内存泄漏及其检测工具
  对于一个c/c++程序员来讲,内存泄漏是一个常见的也是使人头疼的问题。已经有许多技术被研究出来以应对这个问题,好比Smart Pointer,Garbage Collection等。Smart Pointer技术比较成熟,STL中已经包含支持Smart Pointer的class,可是它的使用彷佛并不普遍,并且它也不能解决全部的问题;Garbage Collection技术在Java中已经比较成熟,可是在c/c++领域的发展并不畅,虽然很早就有人思考在C++中也加入GC的支持。现实世界就是这样的,做为一个c/c++程序员,内存泄漏是你心中永远的痛。不过好在如今有许多工具可以帮助咱们验证内存泄漏的存在,找出发生问题的代码。

2.3.1 内存泄漏的定义
通常咱们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小能够在程序运行期决定),使用完后必须显示释放的内存。应用程序通常使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,不然,这块内存就不能被再次使用,咱们就说这块内存泄漏了。如下这段小程序演示了堆内存发生泄漏的情形:

void MyFunction(int nSize)

{

 char* p= new char[nSize];

 if( !GetStringFrom( p, nSize ) ){

  MessageBox(“Error”);

  return;

 }

 …//using the string pointed by p;

 delete p;

}

  当函数GetStringFrom()返回零的时候,指针p指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,可是c函数能够在任何地方退出,因此一旦有某个出口处没有释放应该释放的内存,就会发生内存泄漏。

  广义的说,内存泄漏不只仅包含堆内存的泄漏,还包含系统资源的泄漏(resource leak),好比核心态HANDLE,GDI Object,SOCKET, Interface等,从根本上说这些由操做系统分配的对象也消耗内存,若是这些对象发生泄漏最终也会致使内存的泄漏。并且,某些对象消耗的是核心态内存,这些对象严重泄漏时会致使整个操做系统不稳定。因此相比之下,系统资源的泄漏比堆内存的泄漏更为严重。

GDI Object的泄漏是一种常见的资源泄漏:

void CMyView::OnPaint( CDC* pDC )

{

 CBitmap bmp;

 CBitmap* pOldBmp;

 bmp.LoadBitmap(IDB_MYBMP);

 pOldBmp = pDC->SelectObject( &bmp );

 …

 if( Something() ){

  return;

 }

 pDC->SelectObject( pOldBmp );

 return;

}

  当函数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. 隐式内存泄漏。程序在运行过程当中不停的分配内存,可是直到结束的时候才释放内存。严格的说这里并无发生内存泄漏,由于最终程序释放了全部申请的内存。可是对于一个服务器程序,须要运行几天,几周甚至几个月,不及时释放内存也可能致使最终耗尽系统的全部内存。因此,咱们称这类内存泄漏为隐式内存泄漏。举一个例子:

class Connection

{

 public:

  Connection( SOCKET s);

  ~Connection();

  …

 private:

  SOCKET _socket;

  …

};

class ConnectionManager

{

 public:

  ConnectionManager(){}

  ~ConnectionManager(){

   list::iterator it;

   for( it = _connlist.begin(); it != _connlist.end(); ++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 );

   delete pconn;

  }

 private:

  list _connlist;

};

  假设在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的,因此只要截获前面一组便可)。对于其余的泄漏,能够采用相似的方法,截获住相应的分配和释放函数。好比,要检测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 0x00881710, 200 bytes long.

Data: <abcdefghijklmnop> 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70

  第一行显示该内存块由TestDlg.cpp文件,第70行代码分配,地址在0x00881710,大小为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__;

#endif

有了这样的定义,在编译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++的标准,对于以上的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 pResult;

 …

}

  第二个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程序的例子相比,缺乏了文件名和行号。对于一个比较大的程序,没有这些信息,解决问题将变得十分困难。

  为了可以知道泄漏的内存块是在哪里分配的,你须要实现相似MFC的映射功能,把new,maolloc等函数映射到_malloc_dbg函数上。这里我再也不赘述,你能够参考MFC的源代码。

  因为Debug Function实如今MS C-RuntimeLibrary中,因此它只能检测到堆内存的泄漏,并且只限于malloc,realloc或strdup等分配的内存,而那些系统资源,好比HANDLE,GDI Object,或是不经过C-Runtime Library分配的内存,好比VARIANT,BSTR的泄漏,它是没法检测到的,这是这种检测法的一个重大的局限性。另外,为了能记录内存块是在哪里分配的,源代码必须相应的配合,这在调试一些老的程序很是麻烦,毕竟修改源代码不是一件省心的事,这是这种检测法的另外一个局限性。

对于开发一个大型的程序,MS C-Runtime Library提供的检测功能是远远不够的。接下来咱们就看看外挂式的检测工具。我用的比较多的是BoundsChecker,一则由于它的功能比较全面,更重要的是它的稳定性。这类工具若是不稳定,反而会忙里添乱。究竟是出自鼎鼎大名的NuMega,我用下来基本上没有什么大问题。

2.3.3.2 使用BoundsChecker检测内存泄漏
  BoundsChecker采用一种被称为 Code Injection的技术,来截获对分配内存和释放内存的函数的调用。简单地说,当你的程序开始运行时,BoundsChecker的DLL被自动载入进程的地址空间(这能够经过system-level的Hook实现),而后它会修改进程中对内存分配和释放的函数调用,让这些调用首先转入它的代码,而后再执行原来的代码。BoundsChecker在作这些动做的时,无须修改被调试程序的源代码或工程配置文件,这使得使用它很是的简便、直接。

  这里咱们以malloc函数为例,截获其余的函数方法与此相似。

  须要被截获的函数可能在DLL中,也可能在程序的代码里。好比,若是静态连结C-Runtime Library,那么malloc函数的代码会被连结到程序里。为了截获住对这类函数的调用,BoundsChecker会动态修改这些函数的指令。

如下两段汇编代码,一段没有BoundsChecker介入,另外一段则有BoundsChecker的介入:

126: _CRTIMP void * __cdecl malloc (

127: size_t nSize

128: )

129: {

00403C10 push ebp

00403C11 mov ebp,esp

130: return _nh_malloc_dbg(nSize, _newmode, _NORMAL_BLOCK, NULL, 0);

00403C13 push 0

00403C15 push 0

00403C17 push 1

00403C19 mov eax,[__newmode (0042376c)]

00403C1E push eax

00403C1F mov ecx,dword ptr [nSize]

00403C22 push ecx

00403C23 call _nh_malloc_dbg (00403c80)

00403C28 add esp,14h

131: }

如下这一段代码有BoundsChecker介入:

126: _CRTIMP void * __cdecl malloc (

127: size_t nSize

128: )

129: {

00403C10 jmp 01F41EC8

00403C15 push 0

00403C17 push 1

00403C19 mov eax,[__newmode (0042376c)]

00403C1E push eax

00403C1F mov ecx,dword ptr [nSize]

00403C22 push ecx

00403C23 call _nh_malloc_dbg (00403c80)

00403C28 add esp,14h

131: }

  当BoundsChecker介入后,函数malloc的前三条汇编指令被替换成一条jmp指令,原来的三条指令被搬到地址01F41EC8处了。当程序进入malloc后先jmp到01F41EC8,执行原来的三条指令,而后就是BoundsChecker的天下了。大体上它会先记录函数的返回地址(函数的返回地址在stack上,因此很容易修改),而后把返回地址指向属于BoundsChecker的代码,接着跳到malloc函数原来的指令,也就是在00403c15的地方。当malloc函数结束的时候,因为返回地址被修改,它会返回到BoundsChecker的代码中,此时BoundsChecker会记录由malloc分配的内存的指针,而后再跳转到到原来的返回地址去。

  若是内存分配/释放函数在DLL中,BoundsChecker则采用另外一种方法来截获对这些函数的调用。BoundsChecker经过修改程序的DLL Import Table让table中的函数地址指向本身的地址,以达到截获的目的。

截获住这些分配和释放函数,BoundsChecker就能记录被分配的内存或资源的生命周期。接下来的问题是如何与源代码相关,也就是说当BoundsChecker检测到内存泄漏,它如何报告这块内存块是哪段代码分配的。答案是调试信息(Debug Information)。当咱们编译一个Debug版的程序时,编译器会把源代码和二进制代码之间的对应关系记录下来,放到一个单独的文件里(.pdb)或者直接连结进目标程序,经过直接读取调试信息就能获得分配某块内存的源代码在哪一个文件,哪一行上。使用Code Injection和Debug Information,使BoundsChecker不但能记录呼叫分配函数的源代码的位置,并且还能记录分配时的Call Stack,以及Call Stack上的函数的源代码位置。这在使用像MFC这样的类库时很是有用,如下我用一个例子来讲明:

void ShowXItemMenu()

{

 …

 CMenu menu;

 menu.CreatePopupMenu();

 //add menu items.

 menu.TrackPropupMenu();

 …

}

void ShowYItemMenu( )

{

 …

 CMenu menu;

 menu.CreatePopupMenu();

 //add menu items.

 menu.TrackPropupMenu();

 menu.Detach();//this will cause HMENU leak

 …

}

BOOL CMenu::CreatePopupMenu()

{

 …

 hMenu = CreatePopupMenu();

 …

}

当调用ShowYItemMenu()时,咱们故意形成HMENU的泄漏。可是,对于BoundsChecker来讲被泄漏的HMENU是在class CMenu::CreatePopupMenu()中分配的。假设的你的程序有许多地方使用了CMenu的CreatePopupMenu()函数,如CMenu::CreatePopupMenu()形成的,你依然没法确认问题的根结到底在哪里,在ShowXItemMenu()中仍是在ShowYItemMenu()中,或者还有其它的地方也使用了CreatePopupMenu()?有了Call Stack的信息,问题就容易了。BoundsChecker会以下报告泄漏的HMENU的信息:

Function

File

Line

CMenu::CreatePopupMenu

E:"8168"vc98"mfc"mfc"include"afxwin1.inl

1009

ShowYItemMenu

E:"testmemleak"mytest.cpp

100

  这里省略了其余的函数调用

  如此,咱们很容易找到发生问题的函数是ShowYItemMenu()。当使用MFC之类的类库编程时,大部分的API调用都被封装在类库的class里,有了Call Stack信息,咱们就能够很是容易的追踪到真正发生泄漏的代码。

  记录Call Stack信息会使程序的运行变得很是慢,所以默认状况下BoundsChecker不会记录Call Stack信息。能够按照如下的步骤打开记录Call Stack信息的选项开关:

  1. 打开菜单:BoundsChecker|Setting…

  2. 在Error Detection页中,在Error Detection Scheme的List中选择Custom

  3. 在Category的Combox中选择 Pointer and leak error check

  4. 钩上Report Call Stack复选框

  5. 点击Ok

  基于Code Injection,BoundsChecker还提供了API Parameter的校验功能,memory over run等功能。这些功能对于程序的开发都很是有益。因为这些内容不属于本文的主题,因此不在此详述了。

尽管BoundsChecker的功能如此强大,可是面对隐式内存泄漏仍然显得苍白无力。因此接下来咱们看看如何用Performance Monitor检测内存泄漏。

2.3.3.3 使用Performance Monitor检测内存泄漏
  NT的内核在设计过程当中已经加入了系统监视功能,好比CPU的使用率,内存的使用状况,I/O操做的频繁度等都做为一个个Counter,应用程序能够经过读取这些Counter了解整个系统的或者某个进程的运行情况。Performance Monitor就是这样一个应用程序。

  为了检测内存泄漏,咱们通常能够监视Process对象的Handle Count,Virutal Bytes 和Working Set三个Counter。Handle Count记录了进程当前打开的HANDLE的个数,监视这个Counter有助于咱们发现程序是否有Handle泄漏;Virtual Bytes记录了该进程当前在虚地址空间上使用的虚拟内存的大小,NT的内存分配采用了两步走的方法,首先,在虚地址空间上保留一段空间,这时操做系统并无分配物理内存,只是保留了一段地址。而后,再提交这段空间,这时操做系统才会分配物理内存。因此,Virtual Bytes通常总大于程序的Working Set。监视Virutal Bytes能够帮助咱们发现一些系统底层的问题; Working Set记录了操做系统为进程已提交的内存的总量,这个值和程序申请的内存总量存在密切的关系,若是程序存在内存的泄漏这个值会持续增长,可是Virtual Bytes倒是跳跃式增长的。

  监视这些Counter可让咱们了解进程使用内存的状况,若是发生了泄漏,即便是隐式内存泄漏,这些Counter的值也会持续增长。可是,咱们知道有问题殊不知道哪里有问题,因此通常使用Performance Monitor来验证是否有内存泄漏,而使用BoundsChecker来找到和解决。

  当Performance Monitor显示有内存泄漏,而BoundsChecker却没法检测到,这时有两种可能:第一种,发生了偶发性内存泄漏。这时你要确保使用Performance Monitor和使用BoundsChecker时,程序的运行环境和操做方法是一致的。第二种,发生了隐式的内存泄漏。这时你要从新审查程序的设计,而后仔细研究Performance Monitor记录的Counter的值的变化图,分析其中的变化和程序运行逻辑的关系,找到一些可能的缘由。这是一个痛苦的过程,充满了假设、猜测、验证、失败,但这也是一个积累经验的绝好机会。

3 探讨C++内存回收
3.1 C++内存对象大会战
  若是一我的自称为程序高手,却对内存一无所知,那么我能够告诉你,他必定在吹牛。用C或C++写程序,须要更多地关注内存,这不只仅是由于内存的分配是否合理直接影响着程序的效率和性能,更为主要的是,当咱们操做内存的时候一不当心就会出现问题,并且不少时候,这些问题都是不易发觉的,好比内存泄漏,好比悬挂指针。笔者今天在这里并非要讨论如何避免这些问题,而是想从另一个角度来认识C++内存对象。

  咱们知道,C++将内存划分为三个逻辑区域:堆、栈和静态存储区。既然如此,我称位于它们之中的对象分别为堆对象,栈对象以及静态对象。那么这些不一样的内存对象有什么区别了?堆对象和栈对象各有什么优劣了?如何禁止建立堆对象或栈对象了?这些即是今天的主题。

3.1.1 基本概念
  先来看看栈。栈,通常用于存放局部变量或对象,如咱们在函数定义中用相似下面语句声明的对象:

Type stack_object ;

  stack_object即是一个栈对象,它的生命期是从定义点开始,当所在函数返回时,生命结束。

  另外,几乎全部的临时对象都是栈对象。好比,下面的函数定义:

Type fun(Type object);

  这个函数至少产生两个临时对象,首先,参数是按值传递的,因此会调用拷贝构造函数生成一个临时对象object_copy1 ,在函数内部使用的不是使用的不是object,而是object_copy1,天然,object_copy1是一个栈对象,它在函数返回时被释放;还有这个函数是值返回的,在函数返回时,若是咱们不考虑返回值优化(NRV),那么也会产生一个临时对象object_copy2,这个临时对象会在函数返回后一段时间内被释放。好比某个函数中有以下代码:

Type tt ,result ; //生成两个栈对象

tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2

  上面的第二个语句的执行状况是这样的,首先函数fun返回时生成一个临时对象object_copy2 ,而后再调用赋值运算符执行

tt = object_copy2 ; //调用赋值运算符

  看到了吗?编译器在咱们毫无知觉的状况下,为咱们生成了这么多临时对象,而生成这些临时对象的时间和空间的开销多是很大的,因此,你也许明白了,为何对于“大”对象最好用const引用传递代替按值进行函数参数传递了。

  接下来,看看堆。堆,又叫自由存储区,它是在程序执行的过程当中动态分配的,因此它最大的特性就是动态性。在C++中,全部堆对象的建立和销毁都要由程序员负责,因此,若是处理很差,就会发生内存问题。若是分配了堆对象,却忘记了释放,就会产生内存泄漏;而若是已释放了对象,却没有将相应的指针置为NULL,该指针就是所谓的“悬挂指针”,再度使用此指针时,就会出现非法访问,严重时就致使程序崩溃。

  那么,C++中是怎样分配堆对象的?惟一的方法就是用new(固然,用类malloc指令也可得到C式堆内存),只要使用new,就会在堆中分配一块内存,而且返回指向该堆对象的指针。

  再来看看静态存储区。全部的静态对象、全局对象都于静态存储区分配。关于全局对象,是在main()函数执行前就分配好了的。其实,在main()函数中的显示代码执行以前,会调用一个由编译器生成的_main()函数,而_main()函数会进行全部全局对象的的构造及初始化工做。而在main()函数结束以前,会调用由编译器生成的exit函数,来释放全部的全局对象。好比下面的代码:

void main(void)

{

 … …// 显式代码

}

  实际上,被转化成这样:

void main(void)

{

 _main(); //隐式代码,由编译器产生,用以构造全部全局对象

 … … // 显式代码

 … …

 exit() ; // 隐式代码,由编译器产生,用以释放全部全局对象

}

  因此,知道了这个以后,即可以由此引出一些技巧,如,假设咱们要在main()函数执行以前作某些准备工做,那么咱们能够将这些准备工做写到一个自定义的全局对象的构造函数中,这样,在main()函数的显式代码执行以前,这个全局对象的构造函数会被调用,执行预期的动做,这样就达到了咱们的目的。 刚才讲的是静态存储区中的全局对象,那么,局部静态对象了?局部静态对象一般也是在函数中定义的,就像栈对象同样,只不过,其前面多了个static关键字。局部静态对象的生命期是从其所在函数第一次被调用,更确切地说,是当第一次执行到该静态对象的声明代码时,产生该静态局部对象,直到整个程序结束时,才销毁该对象。

  还有一种静态对象,那就是它做为class的静态成员。考虑这种状况时,就牵涉了一些较复杂的问题。

  第一个问题是class的静态成员对象的生命期,class的静态成员对象随着第一个class object的产生而产生,在整个程序结束时消亡。也就是有这样的状况存在,在程序中咱们定义了一个class,该类中有一个静态对象做为成员,可是在程序执行过程当中,若是咱们没有建立任何一个该class object,那么也就不会产生该class所包含的那个静态对象。还有,若是建立了多个class object,那么全部这些object都共享那个静态对象成员。

  第二个问题是,当出现下列状况时:

class Base

{

 public:

  static Type s_object ;

}

class Derived1 : public Base / / 公共继承

{

 … …// other data

}

class Derived2 : public Base / / 公共继承

{

 … …// other data

}

Base example ;

Derivde1 example1 ;

Derivde2 example2 ;

example.s_object = …… ;

example1.s_object = …… ;

example2.s_object = …… ;

  请注意上面标为黑体的三条语句,它们所访问的s_object是同一个对象吗?答案是确定的,它们的确是指向同一个对象,这听起来不像是真的,是吗?但这是事实,你能够本身写段简单的代码验证一下。我要作的是来解释为何会这样? 咱们知道,当一个类好比Derived1,从另外一个类好比Base继承时,那么,能够看做一个Derived1对象中含有一个Base型的对象,这就是一个subobject。一个Derived1对象的大体内存布局以下:

  让咱们想一想,当咱们将一个Derived1型的对象传给一个接受非引用Base型参数的函数时会发生切割,那么是怎么切割的呢?相信如今你已经知道了,那就是仅仅取出了Derived1型的对象中的subobject,而忽略了全部Derived1自定义的其它数据成员,而后将这个subobject传递给函数(实际上,函数中使用的是这个subobject的拷贝)。

  全部继承Base类的派生类的对象都含有一个Base型的subobject(这是能用Base型指针指向一个Derived1对象的关键所在,天然也是多态的关键了),而全部的subobject和全部Base型的对象都共用同一个s_object对象,天然,从Base类派生的整个继承体系中的类的实例都会共用同一个s_object对象了。上面提到的example、example一、example2的对象布局以下图所示:

3.1.2 三种内存对象的比较
  栈对象的优点是在适当的时候自动生成,又在适当的时候自动销毁,不须要程序员操心;并且栈对象的建立速度通常较堆对象快,由于分配堆对象时,会调用operator new操做,operator new会采用某种内存空间搜索算法,而该搜索过程多是很费时间的,产生栈对象则没有这么麻烦,它仅仅须要移动栈顶指针就能够了。可是要注意的是,一般栈空间容量比较小,通常是1MB~2MB,因此体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,由于随着递归调用深度的增长,所需的栈空间也会线性增长,当所需栈空间不够时,便会致使栈溢出,这样就会产生运行时错误。

  堆对象,其产生时刻和销毁时刻都要程序员精肯定义,也就是说,程序员对堆对象的生命具备彻底的控制权。咱们经常须要这样的对象,好比,咱们须要建立一个对象,可以被多个函数所访问,可是又不想使其成为全局的,那么这个时候建立一个堆对象无疑是良好的选择,而后在各个函数之间传递这个堆对象的指针,即可以实现对该对象的共享。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,若是这时还须要生成新的堆对象,一般不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。

接下来看看static对象。

  首先是全局对象。全局对象为类间通讯和函数间通讯提供了一种最简单的方式,虽然这种方式并不优雅。通常而言,在彻底的面向对象语言中,是不存在全局对象的,好比C#,由于全局对象意味着不安全和高耦合,在程序中过多地使用全局对象将大大下降程序的健壮性、稳定性、可维护性和可复用性。C++也彻底能够剔除全局对象,可是最终没有,我想缘由之一是为了兼容C。

  其次是类的静态成员,上面已经提到,基类及其派生类的全部对象都共享这个静态成员对象,因此当须要在这些class之间或这些class objects之间进行数据共享或通讯时,这样的静态成员无疑是很好的选择。

  接着是静态局部对象,主要可用于保存该对象所在函数被多次调用期间的中间状态,其中一个最显著的例子就是递归函数,咱们都知道递归函数是本身调用本身的函数,若是在递归函数中定义一个nonstatic局部对象,那么当递归次数至关大时,所产生的开销也是巨大的。这是由于nonstatic局部对象是栈对象,每递归调用一次,就会产生一个这样的对象,每返回一次,就会释放这个对象,并且,这样的对象只局限于当前调用层,对于更深刻的嵌套层和更浅露的外层,都是不可见的。每一个层都有本身的局部对象和参数。

  在递归函数设计中,能够使用static对象替代nonstatic局部对象(即栈对象),这不只能够减小每次递归调用和返回时产生和释放nonstatic对象的开销,并且static对象还能够保存递归调用的中间状态,而且可为各个调用层所访问。

3.1.3 使用栈对象的意外收获
  前面已经介绍到,栈对象是在适当的时候建立,而后在适当的时候自动释放的,也就是栈对象有自动管理功能。那么栈对象会在什么会自动释放了?第一,在其生命期结束的时候;第二,在其所在的函数发生异常的时候。你也许说,这些都很正常啊,没什么大不了的。是的,没什么大不了的。可是只要咱们再深刻一点点,也许就有意外的收获了。

  栈对象,自动释放时,会调用它本身的析构函数。若是咱们在栈对象中封装资源,并且在栈对象的析构函数中执行释放资源的动做,那么就会使资源泄漏的几率大大下降,由于栈对象能够自动的释放资源,即便在所在函数发生异常的时候。实际的过程是这样的:函数抛出异常时,会发生所谓的stack_unwinding(堆栈回滚),即堆栈会展开,因为是栈对象,天然存在于栈中,因此在堆栈回滚的过程当中,栈对象的析构函数会被执行,从而释放其所封装的资源。除非,除非在析构函数执行的过程当中再次抛出异常――而这种可能性是很小的,因此用栈对象封装资源是比较安全的。基于此认识,咱们就能够建立一个本身的句柄或代理来封装资源了。智能指针(auto_ptr)中就使用了这种技术。在有这种须要的时候,咱们就但愿咱们的资源封装类只能在栈中建立,也就是要限制在堆中建立该资源封装类的实例。

3.1.4 禁止产生堆对象
  上面已经提到,你决定禁止产生某种类型的堆对象,这时你能够本身建立一个资源封装类,该类对象只能在栈中产生,这样就能在异常的状况下自动释放封装的资源。

  那么怎样禁止产生堆对象了?咱们已经知道,产生堆对象的惟一方法是使用new操做,若是咱们禁止使用new不就好了么。再进一步,new操做执行时会调用operator new,而operator new是能够重载的。方法有了,就是使new operator 为private,为了对称,最好将operator delete也重载为private。如今,你也许又有疑问了,难道建立栈对象不须要调用new吗?是的,不须要,由于建立栈对象不须要搜索内存,而是直接调整堆栈指针,将对象压栈,而operator new的主要任务是搜索合适的堆内存,为堆对象分配空间,这在上面已经提到过了。好,让咱们看看下面的示例代码:

#include <stdlib.h> //须要用到C式内存分配函数

class Resource ; //表明须要被封装的资源类

class NoHashObject

{

 private:

  Resource* ptr ;//指向被封装的资源

  ... ... //其它数据成员

  void* operator new(size_t size) //非严格实现,仅做示意之用

  {

   return malloc(size) ;

  }

  void operator delete(void* pp) //非严格实现,仅做示意之用

  {

   free(pp) ;

  }

 public:

  NoHashObject()

  {

   //此处能够得到须要封装的资源,并让ptr指针指向该资源

   ptr = new Resource() ;

  }

  ~NoHashObject()

  {

   delete ptr ; //释放封装的资源

  }

};

  NoHashObject如今就是一个禁止堆对象的类了,若是你写下以下代码:

NoHashObject* fp = new NoHashObject() ; //编译期错误!

delete fp ;

上面代码会产生编译期错误。好了,如今你已经知道了如何设计一个禁止堆对象的类了,你也许和我同样有这样的疑问,难道在类NoHashObject的定义不能改变的状况下,就必定不能产生该类型的堆对象了吗?不,仍是有办法的,我称之为“暴力破解法”。C++是如此地强大,强大到你能够用它作你想作的任何事情。这里主要用到的是技巧是指针类型的强制转换。

void main(void)

{

 char* temp = new char[sizeof(NoHashObject)] ;

 //强制类型转换,如今ptr是一个指向NoHashObject对象的指针

 NoHashObject* obj_ptr = (NoHashObject*)temp ;

 temp = NULL ; //防止经过temp指针修改NoHashObject对象

 //再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员

 Resource* rp = (Resource*)obj_ptr ;

 //初始化obj_ptr指向的NoHashObject对象的ptr成员

 rp = new Resource() ;

 //如今能够经过使用obj_ptr指针使用堆中的NoHashObject对象成员了

 ... ...

 delete rp ;//释放资源

 temp = (char*)obj_ptr ;

 obj_ptr = NULL ;//防止悬挂指针产生

 delete [] temp ;//释放NoHashObject对象所占的堆空间。

}

  上面的实现是麻烦的,并且这种实现方式几乎不会在实践中使用,可是我仍是写出来路,由于理解它,对于咱们理解C++内存对象是有好处的。对于上面的这么多强制类型转换,其最根本的是什么了?咱们能够这样理解:

  某块内存中的数据是不变的,而类型就是咱们戴上的眼镜,当咱们戴上一种眼镜后,咱们就会用对应的类型来解释内存中的数据,这样不一样的解释就获得了不一样的信息。

  所谓强制类型转换实际上就是换上另外一副眼镜后再来看一样的那块内存数据。

  另外要提醒的是,不一样的编译器对对象的成员数据的布局安排多是不同的,好比,大多数编译器将NoHashObject的ptr指针成员安排在对象空间的头4个字节,这样才会保证下面这条语句的转换动做像咱们预期的那样执行:

Resource* rp = (Resource*)obj_ptr ;

  可是,并不必定全部的编译器都是如此。

  既然咱们能够禁止产生某种类型的堆对象,那么能够设计一个类,使之不能产生栈对象吗?固然能够。

3.1.5 禁止产生栈对象
  前面已经提到了,建立栈对象时会移动栈顶指针以“挪出”适当大小的空间,而后在这个空间上直接调用对应的构造函数以造成一个栈对象,而当函数返回时,会调用其析构函数释放这个对象,而后再调整栈顶指针收回那块栈内存。在这个过程当中是不须要operator new/delete操做的,因此将operator new/delete设置为private不能达到目的。固然从上面的叙述中,你也许已经想到了:将构造函数或析构函数设为私有的,这样系统就不能调用构造/析构函数了,固然就不能在栈中生成对象了。

  这样的确能够,并且我也打算采用这种方案。可是在此以前,有一点须要考虑清楚,那就是,若是咱们将构造函数设置为私有,那么咱们也就不能用new来直接产生堆对象了,由于new在为对象分配空间后也会调用它的构造函数啊。因此,我打算只将析构函数设置为private。再进一步,将析构函数设为private除了会限制栈对象生成外,还有其它影响吗?是的,这还会限制继承。

  若是一个类不打算做为基类,一般采用的方案就是将其析构函数声明为private。

  为了限制栈对象,却不限制继承,咱们能够将析构函数声明为protected,这样就一箭双鵰了。以下代码所示:

class NoStackObject

{

 protected:

  ~NoStackObject() { }

 public:

  void destroy()

  {

   delete this ;//调用保护析构函数

  }

};

  接着,能够像这样使用NoStackObject类:

NoStackObject* hash_ptr = new NoStackObject() ;

... ... //对hash_ptr指向的对象进行操做

hash_ptr->destroy() ;

  呵呵,是否是以为有点怪怪的,咱们用new建立一个对象,却不是用delete去删除它,而是要用destroy方法。很显然,用户是不习惯这种怪异的使用方式的。因此,我决定将构造函数也设为private或protected。这又回到了上面曾试图避免的问题,即不用new,那么该用什么方式来生成一个对象了?咱们能够用间接的办法完成,即让这个类提供一个static成员函数专门用于产生该类型的堆对象。(设计模式中的singleton模式就能够用这种方式实现。)让咱们来看看:

class NoStackObject

{

 protected:

  NoStackObject() { }

  ~NoStackObject() { }

 public:

  static NoStackObject* creatInstance()

  {

   return new NoStackObject() ;//调用保护的构造函数

  }

  void destroy()

  {

   delete this ;//调用保护的析构函数

  }

};

  如今能够这样使用NoStackObject类了:

NoStackObject* hash_ptr = NoStackObject::creatInstance() ;

... ... //对hash_ptr指向的对象进行操做

hash_ptr->destroy() ;

hash_ptr = NULL ; //防止使用悬挂指针

如今感受是否是好多了,生成对象和释放对象的操做一致了。

3.2 浅议C++ 中的垃圾回收方法
  许多 C 或者 C++ 程序员对垃圾回收嗤之以鼻,认为垃圾回收确定比本身来管理动态内存要低效,并且在回收的时候必定会让程序停顿在那里,而若是本身控制内存管理的话,分配和释放时间都是稳定的,不会致使程序停顿。最后,不少 C/C++ 程序员坚信在C/C++ 中没法实现垃圾回收机制。这些错误的观点都是因为不了解垃圾回收的算法而臆想出来的。

  其实垃圾回收机制并不慢,甚至比动态内存分配更高效。由于咱们能够只分配不释放,那么分配内存的时候只须要从堆上一直的得到新的内存,移动堆顶的指针就够了;而释放的过程被省略了,天然也加快了速度。现代的垃圾回收算法已经发展了不少,增量收集算法已经可让垃圾回收过程分段进行,避免打断程序的运行了。而传统的动态内存管理的算法一样有在适当的时间收集内存碎片的工做要作,并不比垃圾回收更有优点。

  而垃圾回收的算法的基础一般基于扫描并标记当前可能被使用的全部内存块,从已经被分配的全部内存中把未标记的内存回收来作的。C/C++ 中没法实现垃圾回收的观点一般基于没法正确扫描出全部可能还会被使用的内存块,可是,看似不可能的事情实际上实现起来却并不复杂。首先,经过扫描内存的数据,指向堆上动态分配出来内存的指针是很容易被识别出来的,若是有识别错误,也只能是把一些不是指针的数据当成指针,而不会把指针当成非指针数据。这样,回收垃圾的过程只会漏回收掉而不会错误的把不该该回收的内存清理。其次,若是回溯全部内存块被引用的根,只可能存在于全局变量和当前的栈内,而全局变量(包括函数内的静态变量)都是集中存在于 bss 段或 data段中。

  垃圾回收的时候,只须要扫描 bss 段, data 段以及当前被使用着的栈空间,找到多是动态内存指针的量,把引用到的内存递归扫描就能够获得当前正在使用的全部动态内存了。