Effective C++(四): 资源管理
文章目录
- 一、智能指针驱动的RAII
- 二、shared_ptr 和 weak_ptr
- 三、如何复制 RAII 对象
- 四、在资源管理类中应该提供对原始资源的访问函数
为了防止忘记调用 delete 造成的内存泄露,我们应该尽可能让对象管理资源,并且采用 RAII 机制(Resource Acquisition is Initialize)机制,让析构函数负责资源的释放。
一、智能指针驱动的RAII
在cpp11中,可以使用unique_ptr 或者 shared_ptr两种智能指针来管理内存。其中 unique_ptr 通过专一所有权来管理 RAII 的对象,而shared_ptr通过引用计数来管理。
std::unique_ptr pUniqueInv1(CreateInvestment());
std::unique_ptr pUniqueInv2(std::move(pUniqueInv1));
std::shared_ptr pSharedInv1(CreateInvestment());
std::shared_ptr pSharedInv2(pSharedInv1); /
std::shared_ptr pSharedInv2(std::move(pSharedInv1))
std::move(pSharedInv1)返回的是pSharedInv1的右值引用,也就是一个std::shared_ptr&&类型,在执行完这句之后,pSharedInv1就变成了一个空指针 nullptr,而pSharedInv2现在拥有原本属于pSharedInv1 的对象。 请注意在调用std::shared_ptr 的移动构造函数的时候,shared_ptr的引用技术不变。
智能指针默认会自动 delete 所持有的对象,我们也可以为智能指针指定所管理对象的释放方式(删除器deleter):
// void GetRidOfInvestment(Investment*) {}
std::unique_ptr<Investment, decltype(GetRidOfInvestment)*> pUniqueInv(CreateInvestment(), GetRidOfInvestment);
std::shared_ptr pSharedInv(CreateInvestment(), GetRidOfInvestment);
在这里decltype的作用是:
二、shared_ptr 和 weak_ptr
一个很常见的面试问题是能否使用 weak_ptr来实现 RAII ? 答案显然是否定的。 首先介绍一下weak_ptr, weak_ptr是一种用于解决 shared_ptr的循环计数死锁的智能指针。一个例子如下:
#include <memory>
#include <vector>
class Child;
class Parent {
public:
std::vector<std::shared_ptr<Child>> children;
void addChild(const std::shared_ptr<Child>& child) {
children.push_back(child);
}
~Parent() {
// 析构函数
}
};
class Child {
public:
std::shared_ptr<Parent> parent;
Child(const std::shared_ptr<Parent>& p) : parent(p) {}
~Child() {
// 析构函数
}
};
可以看到 child 和 parent 互相持有对方的shared_ptr, 造成循环引用。 哪怕当这些对象超出作用御的时候他们的析构函数也不会被调用,从而导致内存泄露。
为了解决这个问题,我们可以使用 weak_ptr 解决循环引用,我们可以把 Child 中的shared_ptr 改成 weak_ptr :
class Child {
public:
std::weak_ptr<Parent> parent; // 使用 weak_ptr 而非 shared_ptr
Child(const std::shared_ptr<Parent>& p) : parent(p) {} //这里参数仍然是shapred_ptr 因为 1. shared_ptr => weak_ptr 转换是兼容的。 2.确保有一个 shared_ptr<Parent>存在保证 Parent 对象存活。
~Child() {
// 析构函数
}
};
三、如何复制 RAII 对象
-
引用计数
正如 shared_ptr一样,对于每一个资源对象的每一次复制就让引用计数 + 1, 每一个对象离开定义域调用析构函数使得引用计数 - 1, 直到引用计数为 0 就把资源销毁。 -
深拷贝
在拷贝的时候不但copy 对象,同时 copy 底层资源,比如:
请注意,在拷贝的时候,有三点是非常需要注意的:
1.是否拷贝了底层资源对象
2.是否 handle 了自赋值
3.是否 handle 了异常处理
#include<iostream>
#include<cstring>
class RAIIArray {
public:
RAIIArray(const char* str) {
if(str) {
len_ = std::strlen(str) + 1;
data_ = new char[len];
strcpy(data_, str);
} else {
data_ = nullptr;
len_ = 0;
}
}
//Deepcopy for Copy Constructor
RAIIArray(const RAIIArray& other) {
size_ = other.size_;
if(size_ > 0) {
data_ = new char[size_];
std::strcpy(data_, other.data_);
} else {
data_ = nullptr;
}
}
//Deepcopy for operator=
RAIIArray& operator=(const RAIIArray& other) {
//Identity Judgement
if(this != &other) {
delete data;
size = other.size;
if(size > 0) {
data = new char[size];
std::strcpy(data, other.data);
} else {
data = nullptr;
size = 0;
}
}
return *this;
}
//You can also use this : first explicit call copy constructor then copy & swap
RAIIArray& operator=(const RAIIArray& other) {
if(this != other) {
RAIIArray temp(other); //显shi 调用了拷贝构造函数
swap(*this, other);
}
return *this;
}
//use friend to access the private variables in first and second
friend void swap( RAIIArray& first, RAIIArray& second) {
using std::swap;
swap(first.size, second.size);
swap(first.data, second.data);
}
//Deepcopy with copy-and-swap
RAIIArray& operator=(RAIIArray other) {
swap(*this,other);
return *this;
}
}
- 转移底层资源所有权
和std::unique_ptr类似,永远保持只有一个对象拥有对资源的管理权,当需要复制对象的时候转移资源的管理权。
四、在资源管理类中应该提供对原始资源的访问函数
和所有的智能指针一样,stl 中的智能指针也提供了对原始资源的隐藏访问和显示访问
Investment* pRaw = pSharedInv.get(); // 显示访问
Investment raw = *pSharedInv; //隐式访问
这里,pSharedInv是一个 shared_ptr, 指向Investment类型
当我们自己在设计自己的资源管理类的时候,也要考虑在提供原始资源访问的同时,是使用显示还是隐式方法。
比如:
class Font