本章提出的不少思想和方法,在目前的modern C++中已经得到了实践和改进,因此,曾思考过是否要写本章笔记,后来觉得,了解一下来龙去脉也挺好的,所以有了本次总结。
一、以对象管理资源
所谓资源,便是使用之后须还给系统,C++常见的资源类型有:动态申请的内存,文件描述符,互斥锁,数据库连接,网络socket连接等等,由于可能存在的各种问题,推荐以对象管理资源的形式对资源进行管理。
为何以对象管理资源
通常情况下,我们可能会这样写一个程序:
1
2
3
4
5
6class Example{};
void func(){
Example *ptre = new Example;
......
delete ptre;
}但这样的内存管理具有一定缺陷,若是控制流由于异常,过早的return,或是被魔改而提前退出而抵达不了
delete ptre
语句,我们便可能将Example所保存的那部分资源泄露。故而,C++提出了以对象管理资源的方式,把资源放入对象中,当控制流离开func,该对象对应的析构函数便可以自动释放那些资源,这正是利用了C++析构函数自动调用机制来解决问题(即系统自动释放栈内资源)。
以对象管理资源
以对象管理资源应当遵循两个基本守则:
- 获得资源后立刻放入对象
- 管理对象调用析构函数时确保释放资源
C++的智能指针
遵循以对象管理资源的理念,C++提供了几个智能指针:
auto_ptr
,shared_ptr
,unique_ptr
以及weak_ptr
。首先须说明的是,
auto_ptr
由于并不符合copy语义,而更像是移动语义,所以C++1x中已被unique_ptr
代替,接下来康康auto和 shared两种ptr的用法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//智能指针使用示例
//auto_ptr
std::auto_ptr<int> ptr(new int(2));
std::auto_ptr<int> ptr2 = ptr; //ptr为null,根本不符合copy语义
//shared_ptr
std::shared_ptr<int> ptr = std::make_shared<int>(200);
std::cout << *ptr << std::endl; //输出200,隐式转换
std::shared_ptr<int> ptr2 = ptr; //二者指向同一个对象,计数+1
std::cout << ptr.use_count() << std::endl; //获得计数
ptr2.reset(); //指向空,此时ptr计数为1
int * t = ptr.get(); //从shared_ptr中获取资源,不推荐这么做
//当ptr使用reset或本段代码结束时,自动调用shared_ptr的delete,结束
二、在资源管理类中当心copying行为
当一个RAII对象被复制,常见有几种可能可供选择:
- 禁止复制【不拷贝】
- 对底层资源实行引用计数并写好它的deleter(删除器)【浅拷贝+计数】
- 复制底部资源【移动】
- 转移资源拥有权【深拷贝】
其中,auto_ptr总是会销毁对象,而其余智能指针可以自定义删除器,带自定义删除器的shared_ptr写法如下:
1 | struct FileCloser { |
使用lambda表达式定义删除器:
1 | std::shared_ptr<FILE> sptr( |
三、在资源管理类中提供对原始对象的访问
因为历史原因,总会有API的参数不是智能指针,这时需要我们对于RAII管理的原始资源提供访问。
一种方式是使用get函数,这种方法较为繁琐,但更为安全,另一种方式是采用()运算符重载来提供隐式转换,但有时会造成程序员写出不安全的代码,比如:
1 | //C API: |
值得一提的是,在C++1x的智能指针中,同时提供了隐式转换和显示转换,但自己写RAII类时,一般采用显式的get方法写更佳。
四、成对地采用new,delete
1 | int *ptr = new int(10); |
因为new出的空间排布很可能是这样的:
单对象: [Object]
多对象:[计数n]|[Object | Object|……]
调用单对象的new,多对象的delete [],可能会delete掉其它一些部分,造成错误;调用多对象的new T[n],单对象的delete,会导致内存没被释放完,造成内存泄漏
五、以独立语句将newed对象放入指针
1 | void handle(std::auto_ptr<int>, int); |
上述的handle使用起来可能会造成内存泄漏,因为编译器要调用handle函数,需要执行三个操作:
- nowsum()
- new int(10)
- 调用auto_ptr构造函数
我们无法确定编译器怎样完成这一过程,可能因为O3优化,它生成了如下步骤的代码:
- new int(10)
- nowsum()
- 调用auto_ptr函数
这种情况下,如果nowsum阶段异常,就铁定造成资源泄露,故而需要以独立语句将newed对象放入智能指针:
1 | std::auto_ptr<int> ptr(new int(10)); |