Effective C++ - Resource Management

前言:如何有效运用C++,包括一般性的设计策略,以及带有具体细节的特定语言特性。知道细节很重要,否则如果疏忽几乎总是导致不可预期的程序行为(undefined behavior)。本文总结对于如何使用C++的一些建议,从而让你成为一个有战斗力的C++程序员。



资源,就是一旦用了它,将来必须还给系统。C++中最常使用的资源就是动态分配内存,但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述符,互斥锁,数据库连接,网络sockets等。

不论哪一种资源,重要的是,当你不再使用它时,必须将它还给系统。但是,难点是,当你考虑到异常、函数内多重回传路径、程序维护不正确的改动等情况,就很难做到上面的保证。

1 以对象管理资源

Use objects to manage resources.

void f()
{
    Investment* pInv = createInvestment();  // 调用factory函数,返回一个动态对象
    // do something
    // ...
    
    delete pInv;  // 释放动态对象
}

问题:在若干情况下(例如,中途过早返回,或抛出异常等),f可能无法删除动态对象。当然,谨慎地编写程序可以防止上面的错误,但是随着代码的修改和维护,这种保障总是显得吃力。

好的做法:为确保返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。许多资源被动态分配于heap内,而后被用于单一区块或函数内。它们应该在控制流离开那个区域或函数时被释放。

解决方案,使用auto_ptr

标准程序库提供的auto_ptr正是针对这种形势而设计的,auto_ptr是个类指针对象(智能指针),其析构函数自动对其所指对象调用delete。

void f()
{
    std::auto_ptr<Investment> pInv(createInvestment());

    // do something
    // ...

    // 最后由auto_ptr的析构函数自动删除pInv
}

想法:

  • 获得资源后立刻放进管理对象内。

即,资源取得时机便是初始化时机(Resource Acquisition Is Initialization; RAII

  • 管理对象运用析构函数确保资源被释放。

不论控制流如何离开区块,一旦对象被销毁,其析构函数会被自动调用,于是资源被释放。

注意:由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象,否则对象会被删除一次以上。为了预防这个问题,auto_ptr有一个特性是,若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权

// pInv1指向createInvestment返回物
std::auto_ptr<Investment> pInv1(createInvestment());

// 现在pInv2指向对象,pInv1被设为null
std::auto_ptr<Investment> pInv2(pInv1);

// 现在pInv1指向对象,pInv2被设为null
pInv1 = pInv2;

带来的问题:由于STL容器要求其元素发挥**“正常的”复制行为**,而auto_ptr的这种诡异的复制行为,导致其不符合STL的容器要求。

替代方案

auto_ptr的替代方案是**“引用计数型智能指针(reference-counting smart pointer; RCSP)**”。

所谓RCSP,也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSP提供的行为类似垃圾回收(garbage collection),不同的是,RCSP无法打破环状引用(cycles of references)。例如,两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用”的状态。

TR1的tr1::shared_ptr就是个RCSP,所以你可以这么写f:

void f()
{
    // 使用shared_ptr
    std::tr1::shared_ptr<Investment> pInv(createInvestment());
    
    // do something
    // ...

    // 经由shared_ptr析构函数自动删除pInv
}

// 看下复制行为
void f()
{
    // pInv1指向createInvestment返回物
    std::tr1::shared_ptr<Investment> pInv1(createInvestment());

    // pInv1和pInv2指向同一个对象
    std::tr1::shared_ptr<investment> pInv2(pInv1);

    // 同上,无任何改变
    pInv1 = pInv2;

    // pInv1和pInv2被销毁,它们所指的对象也就被自动销毁
}

解决auto_ptr的问题:由于tr1::shared_ptr的复制行为"一如预期",因此,它们可以被用于STL容器。

注意:
auto_ptrtr1::shared_ptr两者都在其析构函数内做delete,而不是delete[]动作,那么意味着,在动态分配而得的array身上使用auto_ptr或tr1::shared_ptr是个馊主意。

问题

#include <iostream>
#include <string>
#include <memory>

using namespace std;

int main()
{  
	// 馊主意,会用上错误的delete形式,而且编译器不是提示编译错误
	std::auto_ptr<std::string> aps(new std::string[10]);
	std::shared_ptr<int> spi(new int[1024]);
}

你会发现:并没有特别针对“C++动态分配数组”而设计的类似auto_ptr或tr1::shared_ptr那样的东西。那是因为,vecotrstring几乎总是可以取代动态分配而得的数组。

请记住:

  • 为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源,并在析构函数中释放资源。
  • 两个常被使用的RAII classes分别是tr1::shared_ptrauto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

TODO scope_ptr

2 在资源管理中小心coping行为

问题:
auto_ptrshared_ptr可以实现对heap-based资源的RAII,然后对于非heap-based的资源并不合适。因此,有时需要建立自己的资源管理类

例如:

void lock(Mutex* pm);     // 加锁
void unlock(Mutex* pm);   // 解锁

为了确保不会将一个被锁住的Mutex解锁,需要建立一个class用来管理锁。这样的class的基本结构由RAII守则支配,也就是“资源在构造期间获得,在析构期间释放”。

class Lock
{
    public:
        // 获得资源
        explicit Lock(Mutex* pm) : mutexPtr(pm)
        { lock(mutexPtr); }
        // 释放资源
        ~Lock()
        { unlock(mutexPtr); }
       
    private:
        Mutex *mutexPtr;
};

客户在使用时:

Mutex m; // 定义互斥器

// 建立一个区块用来定义critical section
{
    Lock m1(&m); // 锁定互斥器
                 // 执行critical section内的操作
                 // 在区块最末尾,自动解除互斥器锁定
}

问题:如果Lock对象被复制,会发生什么事情?

Lock m1(&m);  // 锁定m
Lock m2(m1);  // 将m1复制到m2身上,会发生什么?

一般有两种选择

  • 禁止复制。

许多时候允许RAII对象被复制并不合理。如果复制动作对RAII class并不合理,你便应该禁止之。

  • 对底层资源进行"引用计数法(reference-count)"

有时候,我们希望保有资源直到它的最后一个使用者被销毁。这种情况下,复制RAII对象时,应该将资源的“被引用数”递增。shared_ptr便是如此。

请记住:

  • 复制RAII对象,必须一并复制它所管理的资源。
  • 普通常见的RAII class copying行为是,抑制copying,使用引用计数。

3 在资源管理类中提供对原始资源的访问

Provide access to raw resources in resource-managing classes.

问题

我们期望通过使用resource-managing classes对抗资源泄露,但是许多APIs直接指涉资源,导致下面问题。

std::tr1::shared_ptr<Investment> pInv(createInvestment());

// 假设需要下面的函数处理Investment对象
int daysHeld(const Investment* pi);// 返回投资天数

// 正常需要这样调用
int days = daysHeld(pInv); // 错误,无法通过编译,因为daysHeld需要的是Investment* 指针,而我们传递的却是个类型为tr1::shared_ptr<Investment>的对象

解决方法

这个时候需要一个函数可将RAII class对象(tr1::shared_ptr)转换为其所内含之原始资源(Investment*)。有两个方法可以达到:

  1. 显示转换

shared_ptrauto_ptr都提供了一个get成员函数(它会返回智能指针内部的原始指针),用来执行显式转换。

int days = daysHeld(pInv.get()); // ok
  1. 隐式转换

shared_ptrauto_ptr也重载了指针取值操作符(operator->operator*),它们允许隐式转换至底部原始指针。

bool taxable1 = !(pInv->isTaxFree());   // 经由operator->访问资源
bool taxable2 = !((*pInv).isTaxFree()); // 经由operator*访问资源

最佳实践

是否应该提供一个显式转换函数RAII class转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAII class被设计执行的特定工作,以及它被使用的情况。最佳的设计原则是:让接口容易被正确使用,不易被误用

请记住

  • APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个"取得其所管理之资源"的办法。
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言,显式转换比较安全,但隐式转换对客户比较方便。

4 成对使用new和delete时要采取相同形式

问题

std::string* array = new std::string[100];
// ...
delete array;

上面array所含的100个string对象中的99个不太可能被适当删除,因为它们的析构函数很可能没有被调用。

当你使用new,有两件事情发生:

  1. 内存被分配出来(通过operator new的函数)
  2. 针对此内存会有一个(或更多)构造函数被调用

当你使用delete,也有两件事情发生:

  1. 针对此内存会有一个(或更多)构造函数被调用
  2. 内存被释放(通过operator delete的函数)

delete的最大问题在于:即将被删除的内存究竟存有多少对象,这个问题的答案决定了有多少个析构函数必须被调用。

单一对象 对象数组
object n Object Object Object …

当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个数组大小记录的办法就是:使用delete时加上中括号(方括号),delete便认定指针指向一个数组,否则,它便认定指针指向单一对象。

正确做法

std::string* ptr1 = new std::string;
std::string* ptr2 = new std::string[100];

// ...

delete ptr1;    // 删除一个对象
delete [] ptr2; // 删除一个由对象组成的数组

请记住
如果调用new时使用[],那么必须在对应调用delete时也使用[]。如果调用new时没有使用[],那么也不应该在对应调用delete时使用[]

5 以独立语句将newed对象置入智能指针

问题

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

new Widget一定执行于tr1::shared_ptr构造函数被调用之前,因为这个表达式的结果还要被传递作为tr1::shared_ptr构造函数的一个实参,但对priority的调用则可以排在第一或第二或第三执行。如果编译器选择以第二执行它,则操作序列为:

  1. 执行new Widget
  2. 调用priority
  3. 调用tr1:shared_ptr构造函数

如果对priority的调用导致异常,会发生什么?

在此情况下,new Widget返回的指针将会遗失,因为它尚未被置入tr1::shared_ptr内。因此,避免此类问题的办法是,使用分离语句

std::tr1::shared_ptr<Widget> pw(new Widget);
// 这个调用动作不会造成泄漏
processWidget(pw, priority());

请记住
以独立语句将new对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值