/****************************************************************/
/* 学习是合作和分享式的!
/* Author:Atlas
/****************************************************************/
上期内容回顾:传送门1,传送门2,传送门3
1.C++内存管理
1.1c语言和C++内存分配
1.2区分堆、栈、静态存储区
1.3控制C++的内存分配
1.4内存管理的基本要求
1.5常见的内存错误及对策
1.6数组和指针
1.7指针参数是如何传递内存的
1.8杜绝“野指针“
1.9malloc、free和new、delete的区别
1.10-1.12 malloc,free,new,delete使用
--------------------------------------------------------------------------------
2.C++中的健壮指针及资源管理
对于资源,就是一旦用了它,将来必须还给系统。我们最常见的资源是动态分配内存,其他常见的还有:文件描述器、互斥锁、图形界面中的字形和笔刷、数据库连接、以及网络socket等等。
2.1 引入
对于给定的资源的拥有着,是负责释放资源的一个对象或者是一段代码。所有权分立为两种级别——自动的和显式的(automatic and explicit),如果一个对象的释放是由语言本身的机制来保证的,这个对象的就是被自动地所有。例如,一个嵌入在其他对象中的对象,他的清除需要其他对象来在清除的时候保证。外面的对象被看作嵌入类的所有者。类似地,每个在栈上创建的对象(作为自动变量)的释放是在控制流离开了对象被定义的作用域的时候保证的。这种情况下,作用于被看作是对象的所有者。注意所有的自动所有权都是和语言的其他机制相容的,包括异常。无论是如何退出作用域的——正常流程控制退出、一个break语句、一个return、一个goto、或者是一个throw——自动资源都可以被清除。
OK!,在引入指针、句柄和抽象的时候产生了问题。如果通过一个指针访问一个对象的话,比如对象在堆中分配,C++不自动地关注它的释放。程序员必须明确的用适当的程序方法来释放这些资源。比如说,如果一个对象是通过调用new来创建的,它需要用delete来回收。一个文件是用CreateFile(Win32 API)打开的,它需要用CloseHandle来关闭。用EnterCritialSection进入的临界区(Critical Section)需要LeaveCriticalSection退出,等等。基本的资源管理的前提就是确保每个资源都有他们的所有者。
2.2 第一条规则RAII(Resource Acquisition Is Initialization)
RAII是指C++语言中的一个惯用法(idiom),它是“Resource Acquisition Is Initialization”的首字母缩写。中文可将其翻译为“资源获取就是初始化”。我们怎么理解这句话呢,在一本c++书中有这么一句:“使用局部对象管理资源的技术通常称为“资源获取就是初始化(RAII)”。这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作用。”通俗的说一下就是:一个指针,一个句柄,一个临界区状态只有在我们将它们封装入对象的时候才会拥有所有者。这就是我们的第一规则:在构造函数中分配资源,在析构函数中释放资源。
为什么会有这个规则?为什么资源要在构造函数中申请及初始化以及析构函数中释放?让我们一个例子来说明,
下面的UseFile函数中:
1: void UseFile(char const* fn) 2: { 3: FILE* f = fopen(fn, "r"); // 获取资源 4: // 在此处使用文件句柄f... // 使用资源 5: fclose(f); // 释放资源 6: }
调用fopen()打开文件就是获取文件句柄资源,操作完成之后,调用fclose()关闭文件就是释放该资源。资源的释放工作至关重要,如果只获取而不释放,那么资源最终会被耗尽。上面的代码是否能够保证在任何情况下都调用fclose函数呢?请考虑如下情况:
1: void UseFile(char const* fn) 2: { 3: FILE* f = fopen(fn, "r"); // 获取资源 4: // 使用资源 5: if (!g()) return; // 如果操作g失败! 6: // ... 7: if (!h()) return; // 如果操作h失败! 8: // ... 9: fclose(f); // 释放资源 10: }
在使用文件f的过程中,因某些操作失败而造成函数提前返回的现象经常出现。在操作g或h失败之后,UseFile函数必须首先调用fclose()关闭文件,然后才能返回其调用者,否则会造成资源泄漏。因此,需要将UseFile函数修改为:
1: void UseFile(char const* fn) 2: { 3: FILE* f = fopen(fn, "r"); 4: //获取资源 5: //使用资源 6: if (!g()) 7: {fclose(f);return; } 8: // ... 9: if (!h()) 10: {fclose(f);return; } 11: // ... 12: fclose(f); 13: //释放资源 14: }
现在的问题是:用于释放资源的代码fclose(f)需要在不同的位置重复书写多次。如果再加入异常处理,情况会变得更加复杂。
假设UseResources函数要用到n个资源,则进行资源管理的一般模式为:
1: void UseResources() 2: { 3: //获取资源1 4: // 5: ... 6: //获取资源n 7: //使用这些资源 8: //释放资源n 9: // 10: ... 11: //释放资源1 12: }
获取资源和释放资源要对应,这里就会面临上面示例的麻烦。释放的不彻底将会导致memory leak,致使程序臃肿、出错等。看到这里自然而然的可以想到C++中的一对特殊函数,构造函数和析构函数。在构造函数中申请资源,以及在析构函数中释放资源。类是C++中的主要抽象工具,那么就将资源抽象为类,用局部对象来表示资源,把管理资源的任务转化为管理局部对象的任务。这就是RAII惯用法,RAII有效地实现了C++资源管理的自动化。
当你按照RAII规则将所有资源封装的时候,可以保证你的程序中没有任何的资源泄露。这点在当封装对象(Encapsulating Object)在栈中建立或