Effective C++ 03 资源管理

3. 资源管理

所谓资源就是,一旦用了它,将来必须还给系统。C++ 程序中最常使用的资源就是动态分配内存(如果你分配内存却从来不曾归还它,会导致内存泄漏)。除了内存外,其他常见的资源还包括文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接以及网络 sockets。


条款 13:以对象管理资源

参考下面的例子:

class Investment { ... };  // 投资类型继承体系中的 root class
// 通过一个工厂函数(见条款 7)供应我们某特定的的Ivestment 对象
Investment* creatInvestment();  // 返回指针,指向 Investment 继承体系内的动态分配对象,调用者有责任删除它。

现考虑函数 f 调用了 creatInvestment,并且要删除它:

void f() {
	Investment* pInv = creatInvestment();  // 调用 factory 函数
	...
	delete pInv;  // 释放 pInv 所指对象
}

如果程序因 ”…“ 区域一个过早的 return 语句而返回,或者在 ”…“ 区域内抛出异常,都可能会导致 delete 无法被执行。无论 delete 如何被忽略过去,我们都会造成资源泄露。因此单纯依赖 f 执行其 delete 语句是行不通的。

为确保 creatInvestment 返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开 f,该对象的析构函数会自动释放那些资源。标准库提供的 auto_ptr 正是针对这种形式而设计的特制产品。auto_ptr 是个类指针对象,即指针指针,其析构函数自动对齐所指对象调用 delete。

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

这个简单的例子示范”以对象管理资源“的两个关键想法:

  • 获得资源后立即放进管理对象内。上述代码中 creatInvestment 返回的资源被当作其管理者 auto_ptr 的初值。实际上”以对象管理资源“的观念常被称为”资源取得时机便是初始化实际“(RAII),因此我们几乎在获得资源后同一语句内以它初始化某个管理对象。
  • 管理对象运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁,其析构函数自然会被自动调用,于是资源被释放。
改进

由于 auto_ptr 被销毁时会自动删除它所指之物,所以一定要注意别让多个 auto_ptr 同时指向同一对象。否则会产生一个对象被删除多次,这会导致未定义的行为。但实际上 auto_ptr 有一个性质:若通过 copy 构造函数或 copy assignment 运算符复制它们,它们会变成 null (有点类似移动构造函数),而复制所得的指针将取得资源的唯一拥有权:

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

auto_ptr 的替代方案是“引用计数型智慧指针(RCSP)”。它会记录有多少指针共同指向该对象,并在无人指向它时自动删除该资源。

void f() {
	std::shared_ptr<Investment> pInv(creatInvestment());  // 调用 factory 函数
}

关于 shared_ptr 内容见条款 14,18 和 54。

注意事项

需要注意的是,auto_ptr 和 shared_ptr 两者在其析构函数内做 delete 而不是 delete[](条款 16 对两者的不同有些描述)。这意味着在动态分配而得的 array 身上使用 auto_ptr 和 shared_ptr 是个不好的尝试,即使可以通过编译:

std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<int> spi(new int[1024]);

并没有针对 C++ 动态分配数组而设计的类似 auto_ptr 和 shared_ptr 的操作,这是因为 vector 和 string 已经可以取代动态分配而得到的数组。

请记住:

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

条款 14: 在资源管理类中小心 copying 行为

假设我们使用 C API 函数处理类型为 Mutex 的互斥器对象,共有 lock 和 unlock 两个函数可用:

void lock(Mutex* pm);  // 锁定 pm 所指的互斥器
void unlock(Mutex* pm);  // 将互斥器解除锁定

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

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

客户对 Lock 的用法符合 RAII 方式:

Mutex m;  // 定义互斥器
...
{
	Lock m1(&m);  // 锁定互斥器
	...
}  // 区块末尾,自动解除互斥器锁定

现在思考,当一个 RAII 对象被复制时,会发生什么?大多数适合会选择下面两种可能:

  • 禁止复制

许多时候允许 RAII 对象被复制并不合理。如果复制 RAII 对象不合理,应该禁止此操作。如条款 6 所示,将 copying 操作声明为 private:

class Lock : private Uncopyable {  // 禁止复制,见条款 6
public:
	...
};
  • 对底层资源使用“引用计数法”

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

shared_ptr 允许指定删除器,那是一个函数或函数对象,当引用次数为 0 时便被调用(此既能不存在于 auto_ptr,它总是将其指针删除)。删除器对于 shared_ptr 来说是可选的:

class Lock {
public:
	explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) {  // 以 unlock 函数为删除器
		lock(mutexPtr.get());  // get 见条款 15 
	}
private:
	std:tr1::shared_ptr<Mutex> mutexPtr;  // 使用 shared_ptr
};

注意本例中的 Lock class 没有声明析构函数,mutexPtr 的析构函数会在互斥器引用次数为 0 时自动调用 shared_ptr 的删除器。

  • 复制底部资源

复制资源管理类对象,应该同时也复制其所包含的资源、也就是说,复制资源管理类对象时,进行的是“深度拷贝”。

  • 转移底部资源的拥有权

某些情况下你可能希望确保永远只有一个 RAII 对象指向一个未加工资源,即使 RAII 对象被复制依然如此。此时,资源的拥有权会从被复制对象转移到目标对象。这是 auto_ptr 复制的意义。

请记住:

  • 复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为。
  • 普通而常见的 RAII class 的 copying 行为是:抑制 copying、施行引用计数法。不过其他行为也都可能被实现。

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

参考下面的例子:

FontHandle getFont();  // 这是个 C API,为求简化咱略参数
void releaseFont(FontHandle fh);
class Font {  // RAII class
public:
	explicit Font(FontHandle fh) : f(fh) { }  // 获取资源
	~Font() { releaseFont(f); }  // 释放资源
private:
	FontHandle f;  // 原始字体资源
};

假设有大量与字体相关的 C API,它们处理的是 FontHandle,那么将 Font 转换为 FontHandle 会是一种很频繁的需求。Font class 可为此提供要给显式转换,像 get 那样:

class Font {
public:
	FontHandle get() const { return f; }  // 显式转换函数
};

但是这使得客户每当想要使用 API 时就必须调用 get:

void changeFontSize(FontHandle f, int newSize);  // C API
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize);  // 将Font 转换为 FontHandle

但是这样会非常繁琐,还有一个办法时令 Font 提供隐式转换函数:

class Font {
public:
	operator FontHandle() const {  // 隐式转换函数
		return f;
	}
};

这样客户调用 C API 时就会比较简单:

Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize);  // 将 Font 隐式转换为 FontHandle

但是这个隐式转换会增加错误发生的机会,例如客户可能会在需要 Font 时以外创建一个 FontHandle:

Font f1(getFont());
...
FontHandle f2 = f1;

这个愿意是要拷贝一个 Font 对象,但是反而将 f1 隐式转换为了一个 FontHandle 然后才拷贝它。

请记住:

  • API 往往要求访问原始资源,所以每一个 RAII class 应该体用一个“取得其所管理的资源”的方法。
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

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

当使用 new(即,通过 new 动态生成一个对象),有两件事情发生:第一,内存被分配出来(通过名为 operator new 的函数,见条款 49 和 51);第二,针对此内存会有一个(或多个)构造函数被调用。

当使用 delete,也有两件事发生:第一,针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为 operator delete 的函数,见条款 51)。

delete 的最大问题在于:即将被删除的内存之内究竟存有多少对象?这个问题的答案决定了有多少个析构函数必须被调用。这个问题也可以解释成,即将被删除的那个指针,所指的是单一对象还是对象数组?

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

std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1;  // 删除一个对象
delete [] stringPtr2;  // 删除一个由对象组成的数组

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

这个规则对于 typedef 十分重要,因为它意味 typedef 的作者必须说清楚,当程序员以 new 创建该种 typedef 类型对象时,该使用哪一种 delete 形式删除:

typedef std::string AddressLines[4];  // 每个地址有 4 行,每行是个 string

由于 AddressLines 是个数组,如果这样使用 new:

std::string* pal = new AddressLines;

就必须匹配数组形式的 delete:

delete pal;  // 错误 行为没有定义
delete []pal;  // 正确

为避免这类错误,最好不要对数组形式做 typedef 动作。

请记住:

  • 如果你在 new 表达式使用 [],必须在相应的 delete 表示中也是用 []。如果在表达式中不适用 [],一定不要再相应的 delete 表达式中使用 []。

条款 17:以独立语句将 new(ed) 对象置入智能指针

假设我们有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的 Widget 上进行某些优先权的处理:

int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priotiry);

参考条款 13,processWidget 决定对其动态分配的来电 Widget 运用智能指针:

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

虽然这样可以通过编译,但是上述调用却可能泄露资源。编译器产出一个 processWidget 调用码之前,必须首先核算即将被传递的各个实参。上述第二实参是一个对 priority 函数的调用,第一个实参 std::tr1::shared_ptr<Widget>(new Widget) 由两个部分组成:

  • 执行 new Widget 表达式;
  • 调用 std::tr1::shared_ptr 构造函数。

于是在调用 processWidget 之前,编译器必须创建代码,并做以下三件事:

  • 调用 priority;
  • 执行 new Widget;
  • 调用 std::tr1::shared_ptr 构造函数。

Java 和 C# 总是以特定次序完成函数参数的核算,但是 C++ 不同,C++ 完成次序的弹性很大。唯一可以确定的是 new Widget 一定在 std::tr1::shared_ptr 构造函数之前被调用,因为前者还要作为后者的实参,但对 priority 的调用则没有限制,如果编译器选择在第二顺位执行,则有:

  • 执行 new Widget;
  • 调用 priority;
  • 调用 std::tr1::shared_ptr 构造函数。

假设对 priority 的调用产生了异常,此时 new Widget 返回的指针会遗失,所以它并不会被置入 std::tr1::shared_ptr 内,后者是我们用来防范资源泄露而使用的,而此时将会引发资源泄露。

避免这类问题的办法就是:使用分离语句,分别写出(1)创建 Widget 语句,(2)将它置入智能指针内,然后再把那个智能指针传递给 processWidget:

std::shared_ptr<Widget> pw(new Widget);
processWidge(pw, priority());  // 这个调用动作绝不至于造成泄露

请记住:

  • 以独立语句将 new(ed) 对象存储于(置入)智能指针内。如果不这么做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值