程序需要管理的资源有哪些?
- 动态分配的内存
- 文件描述符
- 互斥锁
- UI中的字型和笔刷
- 数据库、socket连接
资源获取即初始化(RAII,Resource Acquisition Is Initialization)是C++管理资源避免内存泄漏的方法。书中提到了共享指针shared_ptr和auto_ptr(从C++17开始被移除)。
RAII简单来说就是资源在构造期间获得,在析构期间释放,自动管理资源的一种方式。
智能指针分类:std::shared_ptr、std::unique_ptr、std::weak_ptr和std::auto_ptr。注意事项:
- 循环引用问题
- 混合适用原始指针和智能指针
这个条款是RAII 于heap-based资源上的应用,其实现的基础就是三个智能指针:std::shared_ptr std::unique_ptr和std::weak_ptr。
条款14 在资源管理类中小心copying行为事实上,利用智能指针还可以实现在一些非heap-based上利用RAII思想完成自动化释放资源。以C++的互斥锁为例说明这个RAII思想是如何发挥作用的。
假设C API使用Mutex的互斥锁,
void lock(Mutex *pm);
void unlock(Mutex * pm);
lock方法进行加锁,unlock进行解锁。下面我们定义一个RAII对象来管理这个互斥锁:
class Mylock
{
public:
explict Mylock(Mutex * lo):m_lo(lo)
{
lock(m_lo);
}
~Mylock()
{
unlock(m_lo);
}
private:
Mutex m_lo;
}
int main()
{
Mutex lolo;
{
Mylock(&lolo);
}
}
这样以来就不需要再担心忘记解锁的问题了。但是这会带来一个新的问题,当RAII对象被复制时,会发生什么?作者给出的两种解决方法是:
- 禁止复制
- 引用计数法(借助std::shared_ptr)
有些RAII对象被拷贝是不合理的,条款6告诉我们最简单的就是使用delete关键字。
引用计数的方法不需要和之前提到的那样写析构函数:
class Mylock
{
public:
explicit Mylock(Mutex lo):m_loPtr(lo,unlock)
{
lock(m_loPtr.get());
}
private:
std::shared_ptr m_loPtr;
}
int main()
{
Mutex lolo;
{
Mylock(&lolo);
}
}
这里没有必要使用析构函数,因为shared_ptr已经传递了一个删除器,离开作用域时将会自动调用对应的unlock。如果你想要拷贝是控制权转移,那么应该用unique_ptr代替shared_ptr。
资源管理类(heap-based)拷贝行为需要区分深拷贝和浅拷贝,应该根据你的需求来选择拷贝类型。
条款15 在资源管理中提供对原始资源的访问智能指针有很多优点,在某些情况下,智能指针调用非常麻烦。
Investment * createInvestment();//创建一个资源句柄
int dayHeld(const Investment * pi);//原始指针作为资源的参数
上面是一种C-like API,一切资源操作都是通过一个内置指针操作的。我们可以用一个智能指针来管理这个资源:
std::shared_ptr pInv(createInvestment());//使用函数返回原始指针,无论是new运算符还是函数返回都可以作为智能指针的参数
int days=dayHeld(pInv.get());
智能指针get()方法可以返回这个内置指针,实现对C-like API的兼容。智能指针是通过隐式转换成底层指针来实现与内置指针一样的功能的-> * bool
。下面是一个RAII对象:
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font
{
public:
explicit Font(FontHandle fh)
:f(fh)
{}
~Font() { releaseFont(f); }
private:
FontHandle f;
};
因为大多数接口都是C-API,将会非常频繁的请求Font到FontHanle之间的转换,有两种方法解决这个问题:
- get方法显式要求转换
- 隐式类型转换运算符
对于前者:
class FontHandle;
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font
{
public:
explicit Font(FontHandle fh)
:f(fh)
{}
~Font() { releaseFont(f); }
FontHandle get() { return f; }
private:
FontHandle f;
};
当需要使用内置指针(句柄)时,就可以通过get方法实现了:
changeFontSize(f.get(),newFontSize);
某些程序员可能会认为每一个地方都要求这样的转换,足以让人倒胃口,宁可不使用这个类。所以才有了第二个方法:
class Font
{
public:
...
operator FontHandle()const
{return f;}
...
}
使用的时候再也不用带上这个恼人的get了:
changeFontSize(f,newFontSize);
这样做有缺点,如:
FontHandle f2=f1;//f1资源被多个FontHandle管理,当f1析构时,f2可能导致二次析构
使用显示转换更加安全,使用隐式对使用者更加友好。
条款16 成对使用new和delete时要采取相同形式这个条款相对简单,堆上分配空间和删除空间,数组和普通对象的底层结构不同,前者在开始位置会有关于数组大小的说明。因此,如果你在new表达式中使用[],必须在相应的delete中也使用[],如果你在new表达式中没有使用[],那也一定不要在delete表达式使用[]。
条款17 以独立语句将newed对象置于智能指针这个条款的主要目的是防止因为执行顺序导致的内存泄露。
int priority();
void processWidget(std::shared_ptr pw,int priority);
一个错误调用processWidget的语句:
processWidget(new Widget,priority());
这个是不能通过编译的,因为智能指针将内置指针到智能指针的转换设置为删除。
可以改成这样:
processWidget(std::shared_ptr(new Widget),priority());
实际调用过程,程序参数调用的顺序是不确定的,但是唯一能保证的是,new Widget一定早于std::shared_ptr。假如调用顺序如下:
- 执行new Widget
- 调用priority
- 调用std::shared_ptr
假设步骤2出现异常,程序提前退出,new Widget的资源尚未被shared_ptr接管就退出了程序,就会出现内存泄漏。
如何解决?
将std::shared_ptr用单独的语句进行构造后传入这样的使用智能指针的语句:
std::shared_ptr pw(new Widget);
processWidget(pw,priority());