《Effective C++》读书笔记(3):资源管理

2023-08-10 08:30:27 浏览数 (2)

所谓资源就是,一旦用了它,将来必须还给系统,包括最常使用的动态分配内存、文件描述符、互斥锁等等。由于异常、函数内多重回传路径、版本更改时遗漏等原因,任何时候都确保这一点是很难的。

本章总结了基于对象的一般化资源管理办法以及一些专属条款。严守这些做法几乎可以消除资源管理问题。

条款13、以对象管理资源

假设你有一个工厂函数用来获取一个动态分配对象,那么任何调用它的用户有责任删除这个对象:

代码语言:javascript复制
Widget* create_Widget() { ... }

void some_function(){
  Widget* p_Widget = create_Widget();
  ...
  delete p_Widget;
}

上述some_function函数看似没有问题,实际上仍有很多情况会导致不会执行删除操作,例如某个过早的return、某个异常、多个版本后维护人员遗忘了这一点等等。

为了确保动态获取的资源一定会被释放,可以用对象来管理资源,将获取资源的行为放在构造函数中,将释放资源的行为放在析构函数中;那么,不论程序如何运行,一定会执行析构函数,一定会释放资源。这种做法称为RAII(Resource Acquisition Is Initialization,资源获取即初始化)。

标准库中的智能指针可以辅助管理资源,其中shared_ptr的资源可以共享,通过引用计数来控制行为,引用计数归零时删除资源,而unique_ptr独享资源。更多的使用与实现可以查阅cppreference。

代码语言:javascript复制
void some_function(){
  //资源获取即初始化
  unique_ptr<Widget> p_Widget(create_Widget());
  ...
  //退出局部作用域,unique_ptr调用Widget的析构函数
}

条款14、在资源管理类中小心coping行为

对于管理堆对象来说,上文的智能指针已经足够。但是很多资源并非基于堆,需要自己实现一个RAII类来管理,这时就需要考虑一个问题:怎么处理拷贝? 通常有两种思路:

1、禁止拷贝。很多资源被复制是不合理的,因此可以用条款6中的方法来禁止拷贝构造/拷贝运算符。

2、对底层资源使用引用计数法。有时我们希望保有资源直到最后一个用户使用完,这时就可以用shared_ptr代替裸指针来管理底层资源,用shared_ptr的删除器来控制资源的析构行为。

假设需要包装Mutex互斥器,目前只有lock、unlock两个函数。因为资源通过这两个函数来获取与释放,不是通过堆,所以需要自己实现RAII类。

代码语言:javascript复制
void lock(Mutex* pm);
void unlock(Mutex* pm);

class Lock{
public:
  explicit Lock(Mutex* pm):sp_mutex(pm,unlock){
    lock(sp_mutex.get());
  }
private:
  std::shared_ptr<Mutex> sp_mutex;
};

不过也有一些其他做法,例如将拷贝操作实现为深拷贝、将拷贝操作实现为转移资源的拥有权等。

条款15、在资源管理类中提供对原始资源的访问

各类API往往要求访问原始资源,只提供了裸指针的接口,因此对于RAII类来说也应该提供一个“取得其所管理之资源”的方法。 或许有些破坏了类的封装性质,但对于RAII类来说问题不大,因为根本上来说它只是为了管理资源的获取与释放。

至于如何访问原始资源,一般分为显式转换与隐式转换。

1、显式转换,例如shared_ptr的get函数。因为需要明确指定,所以比隐式转换更安全。

2、隐式转换,用法更自然,但可能出错

代码语言:javascript复制
class WidgetHandle {};

class Widget {
public:
  operator WidgetHandle() const { return w; }
private:
  WidgetHandle w;
};

void f(WidgetHandle w) { ... }

int main() {
  Widget w;
  f(w);
}

条款16、成对使用new和delete时要采取相同形式

当删除指针时,为了让delete知道要处理的是单个对象还是数组,如果new表达式使用[]则delete表达式也应使用,如果new没有使用则delete也不应使用。

此外,为了避免失误,最好不要为数组形式进行typedef/using。事实上,STL中的vector、array基本可以替代原生数组。

条款17、以独立语句将newed对象置入智能指针

在函数传参时new一个指针再初始化智能指针是不安全的:

代码语言:javascript复制
some_function(std::shared_ptr<Widget>(new Widget), f());

考虑上面这个函数调用,假设f()出现异常,则前面申请的内存将没处释放了。这种内存泄露的本质是当申请数据指针后,没有马上传给std::shared_ptr。

解决方法有两个:1、在函数调用前先用独立语句初始化shared_ptr,再传给函数。2、函数传参时使用make_shared来初始化智能指针,它只执行一次内存申请,更加异常安全。

代码语言:javascript复制
std::shared_ptr<Widget> pw(new Widget);
some_function(pw,f());

some_function(make_shared<Widget>(),f());

0 人点赞