# 【CCG】资源管理
作者:wallace-lai
发布:2024-02-27
更新:2024-02-27
资源管理主题中的资源指的是需要在代码中(显式或者隐式地)获取并释放的东西,包括但不限于**内存**、**文件描述符**、**套接字**、**锁**等资源。资源管理的目的是:
(1)避免产生资源泄露;
(2)及时释放不再需要的资源;
## 一、资源管理原则
对于资源管理,有几个总的使用原则如下所示。稍后将针对这几个总的原则给出一些细则。
### (1)使用resource handles和RAII来自动地管理资源
```cpp
void send(X* x, string_view destination)
{
auto port = open_port(destination);
my_mutex.lock();
// ...
send(port, x);
// ...
my_mutex.unlock();
close_port(port);
delete x;
}
```
上面的send是很常见的写法,但是它没有使用RAII来自动管理资源(文件描述符port、锁my_mutex)。不要这样,请使用resource handle和RAII来管理这些资源,代码如下所示。
```cpp
void send(unique_ptr x, string_view destination) // x owns the X
{
Port port{destination}; // port owns the PortHandle
lock_guard guard{my_mutex}; // guard owns the lock
// ...
send(port, x);
// ...
} // automatically unlocks my_mutex and deletes the pointer in x
class Port {
PortHandle port;
public:
Port(string_view destination) : port{open_port(destination)} { }
~Port() { close_port(port); }
operator PortHandle() { return port; }
// port handles can't usually be cloned, so disable copying and assignment if necessary
Port(const Port&) = delete;
Port& operator=(const Port&) = delete;
};
```
和原始版本相比,有以下几点变化:
- 使用智能指针`unique_ptr`,避免了使用没有ownner的裸指针;
- 使用resource handler来处理文件描述符`PortHandle`,利用resource handler的自动构造和析构过程自动管理文件描述符port的打开与关闭;
- 使用`lock_guard`来处理锁,与智能指针类似;
这样做的好处是**程序员不再需要关心资源何时释放**的问题了,利用RAII完全可以实现资源的自动获取和释放,避免资源泄露问题的产生。
### (2)尽量不要使用原始指针进行传参
对于能用vecotr等容器替代的场景,尽量不使用原始指针。使用vector等容器,编译器可以更好地进行边界检查,避免读写越界问题的产生。
```cpp
void f1(int *p, int n)
{
// ...
p[2] = 7; // bad
// ...
}
void f2(vector &p)
{
// ...
p[2] = 7; // recommend
// ...
}
```
### (3)原始的指针或者引用对所引用的对象不具备所有权,尽量不要使用
```cpp
int *p1 = new int(7); // bad : raw owning pointer
auto p2 = make_unique(7); // good : the int is owned by a unique pointer
```
### (4)优先使用栈内存,尽量不使用动态内存分配
这条规则应该很好理解,因为动态内存分配需要支付额外的性能代价,能不用就不用
## 二、资源申请与释放规则
### 规则1:避免在C++中使用malloc和free申请和释放内存
`malloc()`和`free()`并不支持内存申请之后的额外的构造与析构操作,与C++中的`new`和`delete`不能够很好地兼容。因此,尽量避免在C++代码中使用它们。
```cpp
class Record {
int id;
string name;
};
// p1可能是空指针nullptr
// *p1没有被初始化,所以其中的name不是一个真正的string,只是包含了一定大小的二进制比特位
Record *p1 = static_cast(malloc(sizeof(Record)));
// p2默认就会被初始化,除非new过程中抛出了异常
auto p2 = new Record;
// p3可能为空指针nullptr(new抛出异常),否则它必定会被初始化
auto p3 = new(nothrow) Record;
// error : 不能delete由malloc所申请的内存
delete p1;
// error : 不能free由new创建的对象
free(p2);
```
### 规则2:避免显示地调用new和delete
当你写下一个“裸露”的`new`时,意味着你在别的什么地方需要有一个同样“裸露”的`delete`。在大型程序当中,过多“裸露”的`new`和`delete`调用,往往的是BUG的来源。不要这样,使用智能指针,比如`unique_ptr`来避免显示地调用`new`和`delete`。
### 规则3:所有申请得来的资源都必须立刻交给它的管理对象
如果不这样做,一个不经意抛出的异常或者return语句都可能导致资源泄露。
```cpp
void func(const string &name)
{
FILE *f = fopen(name, "r"); // open the file
vector buf(1024);
auto _ = finally([f] {fclose(f);}); // remember to close the file
// ...
}
```
上述代码中的`buf`在申请过程中可能抛出异常,于是导致文件描述符f被泄露。为了解决这个问题,可以使用file handle,即ifstream,来管理打开的文件描述符。
```cpp
void func(const string &name)
{
ifstream f{name};
vector buf(1024);
}
```
ifstream的使用更加安全、高效,这就是RAII的威力!
### 规则4:在一条语句中最多只允许一次显式的资源申请
比如有以下的函数,如果不注意,可能会写成以下的调用方式。
```cpp
void fun(shared_ptr sp1, shared_ptr sp2);
// BAD : potential leak
fun(shared_ptr(new Widget(a, b)), shared_ptr(new Widget(c, d)));
```
上述代码中,假如在new第二个Widget的过程中抛出异常,那么第一个Widget中的内存将会泄露!
常见的解决办法是**一条语句中只进行一次显式的资源申请**,如下所示。这个方法怎么说呢,可以但显得很蠢。
```cpp
shared_ptr sp1(new Widget(a, b));
fun(sp1, new Widget(c, d));
```
**最好的办法是使用智能指针**,根本不进行显式的资源申请操作,如下所示。
```cpp
fun(make_shared(a, b), make_shared(c, d));
```
哪怕第二次内存申请失败,第一次成功申请的内存依然可以由智能指针自动地释放,不会产生内存泄露。
### 规则5:避免对指针取下标,使用span代替
如下所示,建议使用`gsl::span`来代替传递数组指针的方式。
```cpp
void f(int[]); // not recommended
void f(int*); // not recommended for multiple objects
// (a pointer should point to a single object, do not subscript)
void f(gsl::span); // good, recommended
```
我认为,**最好的办法其实是用容器代替传递数组指针的方式**,直接避免指针的传递。
### 规则6:把申请与释放当做不可分割的一对接口来重载
```cpp
class X {
// ...
void *operator new(size_t s);
void operator delete(void *);
// ...
};
```
即使你不希望对象可以被析构,那你也是将delete操作置上delete标记,而不是只写new而不写delete。
```cpp
void operator delete(void*) = delete;
```
## 三、智能指针
### 规则1:使用unique_ptr或者shared_ptr表示(对象的)归属
如下所示,使用智能指针,不要使用裸指针。
```cpp
void f()
{
void f()
{
X *p1 {new X}; // bad : p1 will leak
auto p2 = make_unique(); // good : unique ownership
auto p3 = make_shared(); // good : shared ownership
}
}
```
### 规则2:优选unique_ptr,除非你有共享对象归属的需求才使用shared_ptr
从概念上来讲,unique_ptr要比shared_ptr更加简单,性能更高,行为更可预测。所以,优选unique_ptr。
```cpp
void f1()
{
// bad example
shared_ptr base = make_shared();
// 在代码局部使用base,base的引用计数不会超过1,因此应该使用unique_ptr
}
```
### 规则3:使用make_shared()创建shared_ptr
```cpp
shared_ptr p1 { new X{2}}; // bad
auto p = make_shared(2); // good
```
推荐使用make_shared来创建shared_ptr,具体原因没太理解
### 规则4:使用make_unique()创建unique_ptr
利用同规则3
### 规则5:使用weak_ptr打破shared_ptr循环
具体内容在智能指针章节中叙述。
### 规则6:仅在需要表示生命周期语义时使用智能指针作为参数来传递
> Passing a smart pointer transfers or shares ownership and should only be used when ownership semantics are intended. A function that does not manipulate lifetime should take raw pointers or references instead.
只在函数有操作对象生命周期时才使用智能指针作为参数传递,否则只应该传递原始裸指针。
> Passing by smart pointer restricts the use of a function to callers that use smart pointers. A function that needs a widget should be able to accept any widget object, not just ones whose lifetimes are managed by a particular kind of smart pointer.
给函数传递智能指针会限制函数的使用,如果一个函数需要接收一个widget对象,那么理应所有的widget都是可接受的,而不是仅接受一个生命周期由智能指针所管理的对象。
> Passing a shared smart pointer (e.g., std::shared_ptr) implies a run-time cost.
传递share_ptr会增加代码运行时的开销。
### 规则7:使用第三方智能指针时,请遵循std中的智能指针模式
这个一般用在代码中引用了第三方智能指针的场景。
### 规则8:使用`unique_ptr`作为入参来表示归属权的转移
```cpp
void sink(unique_ptr); // takes ownership of the Widget
void uses(widget*); // just uses the Widget
// bad example, usually not what you want
void thinko(const unique_ptr &);
```
上面的`sink`函数的入参表示Widget对象的归属权将转移到`sink`函数中;而`uses`的入参则表示`uses`函数只是使用了Wdiget对象,但它并不会造成Wdiget对象的归属权的转移。
使用`const unique_ptr &`作为入参无法达成归属权的转移,因为`unique_ptr`具有移动语义,归属权会被转移到`sink`函数的局部入参中。
### 规则9:使用`unique_ptr`作为入参来表示归属权的保留
```cpp
void reseat(unique_ptr&);
// bad example, usually not what you want
void thinko(const unique_ptr &);
```
因为使用引用的方式,所以`reseat`中不会创建智能指针的副本,所有对智能指针的修改都将反应的原始的指针上。
禁止使用`const unique_ptr &`的方式代替`unique_ptr &`。
### 规则10:Take a `shared_ptr` parameter to express shared ownership
### 规则11:Take a `shared_ptr&` parameter to express that a function might reseat the shared pointer
### 规则12:Take a `const shared_ptr&` parameter to express that it might retain a reference count to the object
### 规则13:Do not pass a pointer or reference obtained from an aliased smart pointer
越来越复杂了