条款13:以对象管理资源
动态分配对象时,对象存储在heap上,若不及时或者忘了delete对象指针,会造成内存泄露
即便最后没有忘记delete对象指针,在函数运行到delete语句之前,可能会遇到以下状况使得delete语句不被执行:
- new和delete之间有一个过早的return
- new和delete位于某个循环内,该循环由于某个continue、break或者goto过早退出
- delete语句之前抛出异常,直接跳转到异常处理函数
为了防止资源泄露,主要有两点要注意:
- 获得资源后立即放进资源的管理对象内
- 资源的管理对象执行析构函数来释放资源
常用的资源管理对象有auto_ptr、shared_ptr
其中auto_ptr允许资源剥夺,即同一时刻只有一个auto_ptr指向资源,那么就不符合传统的copy理念
class A {
...
}
A* createA(); //工厂函数,返回一个指向动态创建class A对象的指针
void f() {
auto_ptr<A> pA1(createA()); //pA1指向class A对象
auto_ptr<A> pA2(pA1); //pA2剥夺pA1占有的资源,pA1为null
}
因此,为了实现一般意义上的对象资源拷贝,可以使用shared_ptr来替换auto_ptr
shared_ptr使用引用计数的机制,计数器持续追踪有多少shared_ptr对象指向这份资源,当计数器=0的时候才会释放资源,每一次拷贝动作,就是计数器+1
class A {
...
}
A* createA(); //工厂函数,返回一个指向动态创建class A对象的指针
void f() {
shared_ptr<A> pA1(createA()); //pA1指向class A对象
shared_ptr<A> pA2(pA1); //pA2与pA1指向同一个class A对象
}
注意,auto_ptr和shared_ptr在析构时都是delete而不是delete[],因此如下形式虽然能通过编译,但是与原本目的并不相同
auto_ptr<string> pStr(new string[10]);
条款14:在资源管理类中小心copying行为
有时候,一些资源并不是存储在heap上的,也就不能使用auto_ptr或者shared_ptr来管理资源了,这时候需要自定义一个资源管理class
自定义的资源管理class需要遵循:调用资源管理class的构造函数时,应该传入需要管理的资源(如指向资源的指针),在调用资源管理class的析构函数时,确保管理的资源被正确释放。这也就是**{资源在构造期间获得,在析构期间释放}——RAII**的解释
那么当一个资源管理class的对象被复制时,一般有如下几种策略:
- 禁止复制。通过条款06中的父类unCopyable来实现对copying函数(拷贝构造函数、拷贝赋值运算符)的禁用。
- 底层资源引用计数。也就是允许RAII对象指向的底层资源的浅copy操作。只需要内含一个shared_ptr成员变量即可,注意shared_ptr当引用计数=0的时候默认是释放资源,但是这里的“释放资源”操作是可以自定义的(比如替换为释放互斥锁)
- 复制底部资源。也就是进行深copy。
- 转移底部资源所有权。也就是新的RAII对象可以剥夺旧的RAII对象占有的资源
条款15:在资源管理类中提供对原始资源的访问
资源管理class除了具备RAII的性质之外,最好对外提供能够访问原始资源的手段:提供一种方法能够将RAII class对象转换为其所含有的原始资源
这里的手段主要有(以shared_ptr为例):
-
显式转换:shared_ptr提供一个成员函数get(),来执行显示转换
shared_ptr<A> pA(createA()); A* pAOri = pA.get(); //显示转换为原始资源的指针
-
隐式转换:shared_ptr重载了operator->和operator*
int a = pA->a; //a是class A的成员变量
条款16:成对使用mew和delete时要采取相同形式
使用new会做两件事:
- 调用operator new函数分配内存
- 在分配出的内存上调用构造函数
同样,使用delete也会做两件事:
- 在已分配的内存上调用析构函数
- 调用operator delete函数释放内存
而使用delete的问题在于:在即将被释放的内存中,有多少个对象,这决定了调用多少个析构函数
当使用delete[]时,delete认为将要操作的指针指向一个数组,否则delete认为将要操作的指针指向单个对象
条款17:以独立语句将newed对象置入智能指针
newed对象,看时态,就是在独立语句(该语句只完成这一功能)中使用智能指针指向new出来的对象
为什么要用独立语句呢,因为:
void func1(shared_ptr<A> pA, int b);
int func2();
//传入实参
func1(shared_ptr<A>(new A), func2());
这里其实有三件事,按照理想顺序应该是:
- 执行new A
- 调用shared_ptr构造函数
- 调用func2()
但是,C++编译器并不一定会按照这个顺序完成这三件事,实际的顺序很可能是:
- 执行new A
- 调用func2()
- 调用shared_ptr构造函数
如果在这个过程中,在第2步出现了异常,那么new出来的class A对象没有置入智能指针,并且没有delete,就会造成内存泄漏