3 资源管理
条款 13 :以对象管理资源
// 资源种类: 内存、文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接、网络套接字
// 当不再使用时,需归还系统
class Investment // 塑模投资行为
{
...
};
// 工厂函数生成 Investment对象
Investment* createInvestment(); // 返回指针,指向 Investment 继承体系内的动态分配对象
// 以下为使用示例
void f()
{
Investment* pInv = createInvestment();
...
delete pInv; // 释放pInv 所指对象
}
// 该函数在若干情况下可能无法删除 pInv 所指对象资源
// 在“...” 区域 出现return语句 或者该区域抛出异常等
// 策略是把资源放进对象内,依赖 C++ 的析构函数自动调用机制确保资源的释放
// 示例1 使用 auto_ptr 避免潜在的资源泄露可能性
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
... // 一如既往地使用 pInv, 经由 auto_ptr 析构函数自动删除 pInv
}
// 这个例子引出两个想法:
// 1. 获得资源后立刻放进管理对象内
// 2. 管理对象运用析构函数确保资源被释放
// RCSP(reference-counting smart pointer),即引用计数型智慧指针,持续追踪共有多少对象指向某笔资源,并在
// 无人指向它时自动删除该资源,但无法打破环状引用
// 示例2 使用 tr1::shared_ptr重写刚才用例
void f()
{
std::tr1:shared_ptr<Investment> pInv(createInvestment());
...
}
请记住:
- 为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
- 两个常被使用的RAII classes 分别是 tr1::shared_ptr 和 auto_ptr 。 前者通常是较佳选择,因为其copy行为比较直观。若选择 auto_ptr, 复制动作会使它(被复制物)指向null。
条款 14 :在资源管理类中小心 copying行为
// RAII : Resource Acquisition Is Initialization ,资源取得时机便是初始化时机
// C API 处理类型为 Mutex 互斥器对象
void lock(Mutex *pm);
void unlock(Mutex *pm);
// 建立 class 类管理机锁
class Lock
{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{ lock(mutexPtr); }
~Lock() { unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};
Mutex m;
...
{
Lock m1(&m); // 锁定互斥器
...
}
Lock ml1(&m); // 锁定 m
Lock ml2(ml1); // 将 ml1 复制到 ml2 身上,这会发生什么事?
// 可能的选择
// 1. 禁止复制
class Lock : private Uncopyable // 禁止复制,见条款 6
{
public: // 如前
...
};
// 2. 对底层资源祭出“引用计数法”
class Lock
{
public:
explicit Lock(Mutex *pm) // 以某个 Mutex 初始化 shared_ptr
: mutexPtr(pm, unlock) // 并以 unlock 函数为删除器
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
// 3. 复制底部资源: “深度拷贝”
// 4. 转移底部资源: 资源的拥有权从被复制物转移到目标物
请记住:
- 复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为。
- 普遍而常见的 RAII class copying 行为是: 抑制 copying、施行引用计数法。不过其他行为也都可能被实现。
条款 15 :在资源管理类中提供对原始资源的访问
std::tr1::shared_ptr<Investment> pInv(createInvestment()); // 见条款 13
// daysHeld 返回投资天数
int daysHeld(const Investment* pi);
// tr1::shared_ptr 和 auto_ptr 都提供一个 get 成员函数,用来执行显示转换,返回智能指针内部原始指针(的复件)
int days = daysHeld(pInv.get());
// RAII class 设计 提供一个隐式转换函数
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font {
public:
explicit Font()
: f(fh)
{}
~Font() { releaseFont(f); }
private:
FontHandle f;
};
// 提供显示转换函数,比如get,但此举较为麻烦
class Font
{
public:
...
FontHandle get() const {return f;}
...
};
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize); // 明白地将 Font 转换为 FontHandle
// 提供隐式转换函数,转型为 FontHandle
class Font
{
public:
...
operator FontHandle() const // 隐式转换函数
{ return f; }
...
};
Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize); // 将 Font 隐式转换为 FontHandle
// 但这个隐式转换可能增加错误发生机会
Font f1(getFont());
...
FontHandle f2 = f1; // 原意是要拷贝一个 Font 对象,却反而将 f1 隐式转换为底部 FontHandle,然后才复制它。
// 当 f1 被销毁,字体被释放,而 f2 因此成为“虚吊的”
请记住:
- APIs 往往要求访问原始资源,所以每个 RAII class 应该提供一个“取得其所管理之资源”的办法。
- 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式比较安全,但隐式专函对客户比较方便。
条款 16 :成对使用 new 和 delete 时要采取相同形式
/*
new : 1. 内存被分配(operator new 函数); 2. 针对此内存会有一个或多个构造函数被调用。
delete : 1. 针对此内存会有一个或多个析构函数被调用; 2. 内存被释放(operator delete 函数)
*/
// 示例1
std::string* stringArray = new std::string[100];
...
delete stringArray; // 结果程序行为未有定义
// 示例2
typedef std::string AddressLines[4]; // 每个人的地址有4行,每行是一个 string
// 由于 AddressLines 是一个数组,所以返回结果等价于 new string[4]
std::string* pal = new AddressLines;
//释放
delete pal; // 行为未有定义
delete [] pal; // 很好
// 正确示例3
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1;
delete [] stringptr2;
请记住:
- 如果你在 new 表达式中使用 [ ], 必须在相应的 delete 表达式中也使用 [ ]. 如果你在 new 表达式中不使用 [ ], 一定不要在相应的 delete 表达式中使用 [ ].
条款 17 :以独立语句将 newed 对象置入智能指针
// 举例说明处理程序的优先权
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
// 调用形式
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
/*
在调用 processWidget 之前,编译器必须创建代码,做以下三件事:
1. 调用 priority
2. 执行 “new Widget”
3. 调用 tr1::shared_ptr 构造函数
*/
// 问题: C++ 编译器以怎样的次序完成这些事情呢?
/*
其中可能的顺序:
1. 执行 “new Widget”
2. 调用 priority
3. 调用 tr1::shared_prt 构造函数
*/
// 如果对 priority 的调用导致异常,则会导致资源泄漏的可能
// 避免方法: 1. 创建 Widget 2. 将它置入一个智能指针内; 3. 传给 processWidget
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
请记住:
- 以独立语句将 newed 对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。