《Effective C++》第三章笔记
条款13:以对象管理资源
获得资源后立即放入对象内:当申请一块动态内存之后,往往要记得释放内存。但是,有些情况(1.执行到释放资源语句之前return;2.continue或者goto提前退出;3.发生异常抛出异常)会导致无法执行到释放资源的语句,这就导致内存资源没有归还给系统。因此需要把资源放到对象中,因为对象一旦离开生命周期,就会被销毁,其资源也就释放。(RAII)
资源获取即初始化:20230412:意思就是资源获得之后要进行初始化,初始化指的是一个对象创建时赋予一个值,因此意思就是资源获取之后就放到一个对象中。
管理对象运用析构函数确保资源被释放:不管控制流如何离开区块,只要对象被销毁,其析构函数自动执行,资源也就被释放了。当然过程有可能发生异常,这就是前面所提到的“析构函数如何处理异常”的话题了。
智能指针auto_ptr
auto_ptr会在它自动销毁时释放其指向的资源。所以不要让两个指针同时指向同一对象,否则会释放一个资源两次,这种行为是未定义的。
特性:若被copy构造函数或者copy assignment运算符复制,它们就会变成nullptr,而复制所得的指针具有所有权。
因此其缺点就是不能同时指向同一个对象,但是对于STL容器来说,需要进行复制操作。
注:现在一般不再使用auto_ptr指针,而是使用unique_ptr指针。详细可参考《C++.Primer.Plus》(中文第六版)16.2.3
智能指针shared_ptr
引用计数型智能指针(Reference-Counting Smart Pointer),即会追踪一共有多少指针指向资源,并在无指针指向时释放资源。
RCSP的行为类似于垃圾回收,但是它无法打破环状引用,比如两个指针互相指向。
**需要注意的是:以上两种指针在其析构函数中调用的时delete而不是delete[],**因此不能指向类似于数字类型的连续空间。
条款14:在资源管理类中注意copy行为
当一个RAII对象被复制时,应该采用哪种策略:
禁止复制:如果复制并不合理,可以拒绝复制,即在资源管理类中显式拒绝copying操作(C++11之后可以使用delete关键字);
引用计数法:即采用类似shared_ptr智能指针的方法,记录一共有多少个指针指向资源,当指针数为0时释放资源。如果指针数为0时不想释放资源,shared_ptr提供了删除器(deleter),即一个函数对象,当引用次数为零时被调用。例如解除互斥锁而不是删除它。
1
2
3
4
5
6
7
8
9
10
11
12
13void lock(Mutex *pm);
void unlock(Mutex *pm);
class Lock {
private:
std::shared_ptr<Mutex> mutexPtr;
public:
// 使用pm作为智能指针指向的空间
// 并使用unlock函数作为当引用计数为0时的删除器
Lock(Mutex *pm) : mutexPtr(pm, unlock) {
lock(pm);
}
};复制底部资源:(深复制)就是说复制资源管理对象的时候把其资源也复制一份。
转移底部资源的所有权:即像auto_ptr指针那样把资源所有权从被复制物转移到复制物,被复制的指针指向空。
条款15:在资源管理类中提供对于资源的访问功能
虽然使用资源管理类去管理资源的方式降低了资源未被归还的可能,但是在使用时往往是需要使用资源的,比如申请了一块char类型的内存,在使用时是希望使用char类型而不是使用资源管理对象,因此资源管理类需要对外提供直接访问资源的方式。
显式转换:提供一个接口(成员函数),比如get()方法,返回值是资源。
隐式转换:定义隐式转换函数,将资源管理类的类型转换成资源的类型。比如auto_ptr和shared_ptr都提供了隐式转换为所指向资源类型指针的隐式转换函数,可以直接隐式转换为底部资源的指针类型,并且重载了指针取值操作符->和*。
1
2
3
4
5
6
7
8class Test {
public:
// 隐式类型转换函数
// 将管理的资源转换为int类型返回
operator int() const {
return ...;
}
};
但是需要注意的是:隐式转换可能会导致某些问题,可能被误用(比如上面的例子中如果将资源转赋值给一个int类型,就是导致Test类对象隐式转换为int对象,这不一定是用户想要做的)。
另外其实RAII对象不算是严格意义上的封装,设计它仅仅是为了管理资源,因此向外提供了访问底部资源的接口和转换。
条款16:成对使用new和delete时要采取相同的形式
当使用new时:1.内存被分配出来(通过名为operator new的函数,条款49和51);2.针对此内存有一个或者更多构造函数被调用(内置类型也看作具有构造函数)。
当使用delete时:1.针对此内存有一个或更多析构函数被调用;2.内存释放(通过名为operator delete的函数,条款51)。
具体有多少个构造函数和析构函数被调用,需要关注内存中究竟有多少个对象(一种理解内存布局的方式):
- 单一内存布局:即使用new Type申请出来的空间,可以看作 [object],只有一块内存;
- 对象数组布局:即使用new Type []申请出来的空间,需要包含这块内存的数组大小的记录 [n][object 1]…[object n]。
虽然对于对象数组的布局并非一定是上面描述,但是可以看到两种形式的内存布局实现应该是不一样的,因此需要使用不同的delete形式:
- 针对单一内存布局:即new,需要使用delete;
- 针对对象数组布局:即new type [],需要使用delete [] type;
条款17:以独立语句将newed对象放入智能指针内
非独立语句形式:
1 | int other(); // 一个其他函数的声明 |
为什么有可能引发内存泄露呢?主要原因是对于一个语句中多项操作C++未定义顺序:
1.调用other函数;2.new Type;3.调用shared_ptr<Type>的构造函数。
在一条语句中这3个操作的顺序是未被定义的,可以确定的是2一定在3之前,因为2的返回值作为3的参数。
如果操作顺序为:2.new Type –> 1.调用other函数 –> 3.调用shared_ptr<Type>的构造函数,那么假设调用other函数时发生异常,那么new出来的内存就没有存储到智能指针中,也就无法实现资源管理,从而导致内存泄露。
因此,为了保证将new出来的资源一定能存储到资源管理类中,应该把操作放到一条语句中:
1 | shared_ptr<Type> pt(new Type); |
对于不同语句的操作,C++是不允许调整顺序的。