内存管理一直是令C++程序员最头疼的工作。C++继承了C高效而又灵活的指针,使用起来稍微不小心就会导致内存泄漏、悬挂指针、访问越界等问题—曾几何时,C++程序员曾经无限地向往Java、C#等语言的垃圾回收机制……
阅读本章,你会了解到高效的内存管理方法,彻底忘记“栈”、“堆”等内存分配相关的术语,并且还会发现,这些解决方案可能要比 Java、C#等语言更好。
3.1 smart_ptr库概述
计算机系统中资源有很多种,内存是我们最常用到的,此外还有文件描述符、socket、操作系统、handle、数据库连接等,程序里申请这些资源后必须及时归还系统,否则就会产生难以预料的后果。
3.3.1 RAII机制
为了管理内存等资源,C++程序员通常采用 RAII 机制(资源获取即初始化,Resource Acquisition Is Initialization),在类的构造函数里申请资源,然后使用,最终在析构函数中释放资源。
如果对象是用声明的方式在栈上创建的(一个局部对象),那么 RAII 机制会工作正常,当离开作用域时对象会自动销毁从而调用析构函数释放资源。但如果对象是用new操作符在堆上创建的,那么它的析构函数不会自动调用,程序员必须明确地用对应的delete操作符销毁它才能释放资源。这就存在着资源泄漏的隐患,因为这时没有任何对象对已经获取的资源负责,如果因某些意外导致程序未能执行delete语句,那么内存等资源就永久地丢失了。例如:
auto p = new class_need_resource; // 对象创建,获取资源... // 可能发生异常导致资源泄露delete p; // 删除对象,调用析构函数释放资源
new、delete以及指针的不恰当运用是C++中造成资源获取/释放问题的根源,能否正确而明智地运用delete是区分 C++新手与熟手的关键所在。但很多人—即使是熟练的C++程序员,也经常会忘记调用delete。
3.1.2 智能指针
智能指针(smart pointer)是C++群体中热门的议题,围绕它有很多有价值的讨论和结论。它实践了推荐书目[1]中的代理模式,代理了原始“裸”指针的行为,为它添加了更多更有用的特性。
C++引入异常机制后,智能指针由一种技巧升级为一种非常重要的技术,因为如果没有智能指针,程序员必须保证new对象能在正确的时机delete,必须到处编写异常捕获代码以释放资源,而智能指针则可以在退出作用域时—不管是正常流程离开或是因异常离开—总调用delete来析构在堆上动态分配的对象。
存在很多种智能指针,其中最早的一个的应该是C++98标准中的“自动指针”auto_ptr,它部分地解决了获取资源自动释放的问题,例如:
{
auto_ptr<class_need_resource> p1(new class_need_resource);
auto_ptr<demo_class> p2(factory.create());
...
} // 离开作用域,p1、p2自动析构从而释放内存等资源
auto_ptr的构造函数接受new操作符或者对象工厂创建出的对象指针作为参数,从而代理了原始指针。虽然它是一个对象,但因为重载了operator* 和 opreator->,其行为非常类似指针,可以把它用在大多数普通指针可用的地方。当退出作用域时(离开函数main或者发生异常),C++语言会保证auto_ptr对象销毁,调用auto_ptr的析构函数,进而使用delete操作符删除原始指针释放资源。auto_ptr很好用,被包含在C++标准库中令它在世界范围内被广泛使用,使智能指针的思想和用法深入人心。但auto_ptr存在一些缺陷,所以新的C++标准提供了更完善的unique_ptr、shared_ptr和weak_ptr,而它们正是基于我们接下来要介绍的boost.smart_ptr库。
boost.smart_ptr库提供了六种智能指针:scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr和intrusive_ptr。它们是轻量级的对象,速度与原始指针相差无几,都是异常安全的(exceptionsafe),而且对于所指向的类型T也仅有一个很小且很合理的要求:类型T的析构函数不能抛出异常。
由于scoped_array和shared_array应用的局限性较大,故本书只介绍scoped_ptr、shared_ptr、weak_ptr和intrusive_ptr这四种智能指针。
3.2 scoped_ptr
scoped_ptr是一个很类似auto_ptr/unique_ptr的智能指针,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确地删除。但scoped_ptr的所有权更加严格,不能转让,一旦scoped_ptr获取了对象的管理权,我们就无法再从它那里取回来。
scoped_ptr拥有一个很好的名字,它向代码的阅读者传递了明确的信息:这个智能指针只能在本作用域里使用,不希望被转让。
3.2.1 类摘要
scoped_ptr的类摘要如下:
template<class T>
class scoped_ptr{ // noncopyable
private:
T *px; // 原始指针
scoped_ptr(scoped_ptr const&); // 拷贝构造函数私有化
scoped_ptr &operator=(scoped_ptr const &); // 赋值操作私有化
void operator==(scoped_ptr const&) const; // 相等操作私有化
void operator!=(scoped_ptr const&) const; // 不等操作私有化
public:
explicit scoped_ptr(T *p=0); // 显示构造函数
~scoped_ptr(); // 析构函数
void reset(T *p = 0); // 重置智能指针
T& operator*() const; // 操作符重载
T* operator->() const; // 操作符重载
T* get() const; // 获得原始指针
explicit operator bool() const; // 显式bool值转型
void swap(scoped_ptr &b); // 交换指针
};
template<class T> inline // 与空指针比较
bool operator==(scoped_ptr<T> const &p, boost::detail::sp_nullptr_t);
// 为了兼容不同的编译器, Boost没有直接使用标准里的std::nullptr_t,
// 而是在 boost::detail 名字空间里用typedef定义了等价的类型 sp_nullptr_t.
3.2.2 操作函数
scoped_ptr的构造函数接受一个类型为T的指针p,创建出一个scoped_ptr对象,并在内部保存指针参数p。p必须是一个new表达式动态分配的结果,或者是个空指针(nullptr)。当scoped_ptr对象的生命期结束时,析构函数会使用delete操作符自动销毁所保存的指针对象,从而正确地回收资源。①
scoped_ptr同时把拷贝构造函数和赋值操作符都声明为私有的,禁止对智能指针的拷贝操作(原理可参考4.1节noncopyable),保证了被它管理的指针不能被转让所有权。
成员函数reset()的功能是重置scoped_ptr:它删除原来保存的指针,再保存新的指针值p。如果p是空指针,那么scoped_ptr将不持有任何指针。一般情况下reset()不应该被调用,因为它违背了scoped_ptr的本意—资源应该一直由scoped_ptr自己自动管理。scoped_ptr用operator()和operator->()重载了解引用操作符“*”和箭头操作符“->”,以模仿被代理的原始指针的行为,因此可以把scoped_ptr对象如同指针一样使用。如果scoped_ptr保存的是空指针,那么这两个操作的行为未定义。②
scoped_ptr提供了一个可以在bool语境(如if的条件表达式)中自动转换成bool值的功能,用来测试scoped_ptr是否持有一个有效的指针(非空)。它可以代替与空指针的比较操作,而且写法更简单。
成员函数get()返回scoped_ptr内部保存的原始指针,可以用在某些要求必须是原始指针的场景(如底层的C接口)。但使用时必须小心,这将使原始指针脱离scoped_ptr的控制!不能对这个指针做delete操作,否则scoped_ptr析构时会对已经删除的指针再进行删除操作,发生未定义行为(通常是程序崩溃,这可能是最好的结果,因为它说明你的程序存在bug)。
scoped_ptr支持有限的比较操作,不能在两个scoped_ptr之间进行相等或者不等测试,默认仅支持与nullptr进行比较(也可以是NULL或者0,因为这两者可以隐式转换为nullptr)。③
① 实际上调用的是
boost::check_delete()函数。 ② scoped_ptr 内部使用了BOOST_ASSERT来断言指针非空,但它仅工作在debug模式下。 ③ 我们可以为它编写额外的比较函数,但这样做通常意义不大,因为使用成员函数get()就可以获得原始指针进行比较。
3.2.3 用法
scoped_ptr的用法很简单:在原本使用指针变量接受new表达式结果的地方改成用scoped_ptr对象,然后去掉哪些多余的try/catch和delete操作就可以了。像这样:
scoped_str<string> sp(new string("text")); // 构造一个scoped_ptr对象
assert(sp); // 使用显式bool转换
assert(sp != nullptr); // 空指针比较操作
scoped_ptr是一种“智能指针”,因此其行为与普通指针基本相同,可以使用非常熟悉的“*”和“->”操作符:
cout << *sp << endl; // operator*取字符串的内容
cout << sp->size() << endl; // operator->取字符串的长度
但记住:不再需要delete操作,scoped_ptr会自动地帮助我们释放资源。如果我们对scoped_ptr执行delete会得到一个编译错误:因为scoped_ptr是一个行为类似指针的对象实例,而不是指针,对一个对象应用delete是不允许的。
scoped_ptr把拷贝构造函数和赋值函数都声明为私有的,不允许拷贝或赋值,拒绝了指针所有权的转让,只能在scoped_ptr被声明的作用域内使用—除了scoped_ptr自己,其他任何人都无权访问被管理的指针,从而保证了指针的绝对安全。
scoped_ptr<string> sp2 = sp; // 错误,scoped_str不能拷贝构造
如果代码编写者企图从一个scoped_ptr构造或赋值另一个scoped_ptr,那么编译器会报出一个错误,阻止他这么做,从而保护了我们的代码,而且是早在运行之前。scoped_ptr明确地表明了代码原始编写者的意图:只能在定义的作用域内使用,不可转让,这在代码后续的维护生命周期中很重要。由此也引出了另外一个结论:如果一个类持有scoped_ptr成员变量,那么它也会是不可拷贝和赋值的。例如:
class ptr_owned final { // 一个持有scoped_ptr成员的类是不可拷贝和赋值的
scoped_ptr<int> m_ptr; // scoped_ptr成员
};
ptr_owned p; // 类的一个实例
ptr_owned p2(p); // 编译错误,不能拷贝构造
在“*”和“->”之外scoped_ptr没有定义其他的操作符,所以不能对scoped_ptr进行“++”或者“—”等指针算术操作。与普通指针相比,它只有很小的接口,这一点使指针的使用更加安全,更容易使用同时更不容易被误用。下面的代码都是scoped_ptr的错误用法:
sp++; // 错误,scoped_ptr 未定义递增操作符
std::prev(sp); // 错误,scoped_ptr 未定义递减操作符
使用scoped_ptr会带来两个好处:一是使代码变得清晰简单,而简单意味着更少的错误;二是它并没有增加多余的操作,安全的同时保证了效率,可以获得与原始指针同样的速度。
示范 scoped_ptr 用法的另一段代码如下:
#include <boost/smart_ptr.hpp>
using namespace boost;
struct posix_file { // 一个示范性质的文件类
posix_file(const char *file_name) { // 构造函数打开文件
cout << "open file:" << file_name << endl;
}
~posix_file() { // 析构函数关闭文件
cout << "close file" << endl;
}
};
int main() {
// 文件类的 scoped_ptr,将在离开作用域时自动析构,从而关闭文件释放资源
scoped_ptr<posix_file> fp(new posix_file("/tmp/a.txt"));
scoped_ptr<int> p(new int); // 一个int指针的scoped_ptr
if (p) { // 在bool语境中测试指针是否有效
*p = 100; // 可以像普通指针一样使用解引用操作符 *
cout << *p << endl;
}
p.reset(); // 置空 scoped_ptr, 仅仅是演示
assert(p == 0); // 与 0 比较, p不持有任何指针
if (!p) { // 在bool语境中测试,可以用!操作符
cout << "scoped_ptr == nullptr" << endl;
}
} // 在这里发生scoped_ptr的析构, p和fp管理的指针自动被删除
/*
open file:/tmp/a.txt
100
scoped_ptr == nullptr
close file
*/
3.2.4 对比标准
unique_ptr是在 C++标准中定义的新的智能指针,用来取代曾经的auto_ptr。根据C++标准定义(C++11.20.7.1),unique_ptr不仅能够代理new创建的单个对象,也能够代理new[]创建的数组对象,在这里我们简单介绍它的单个对象用法。
unique_ptr
C++标准中对unique_ptr的定义如下(做了适当的简化,其中的部分新关键字可参考附录 B):
template<class T, class D = default_delete<T>> // 使用删除器
class unique_ptr {
public:
typedef some_define pointer; // 内部类型定义
typedef T element_type;
constexpr unique_ptr() noexcept; // 构造函数
explicit unique_ptr(pointer p) noexcept;
~unique_ptr(); // 析构函数
unique_ptr &operator=(unique_ptr &&u) noexcept; // 转移语义赋值
element_type &operator*() const; // 操作符重载
pointer operator->() const noexcept; // 操作符重载
pointer get() const noexcept; // 获得原始指针
explicit operator bool() const noexcept; // bool值转型
pointer release() noexcept; // 释放指针的管理权
void reset(pointer p) noexcept; // 重置智能指针
void swap(unique_ptr &u) noexcept; // 交换指针
unique_ptr(const unique_ptr &) = delete; // 使用delete禁用拷贝
unique_ptr &operator=(const unique_ptr &) = delete;
};
bool operator==(const unique_ptr &x, const unique_ptr &y);
...
unique_ptr的基本能力与scoped_ptr相同,同样可以在作用域内管理指针,也不允许拷贝构造和拷贝赋值,例如:①
unique_ptr<int> up(new int); // 声明一个 unique_ptr,管理 int 指针
assert(up); // bool 语境测试指针是否有效
*up = 10; // 使用 operator*操作指针
cout << *up << endl;
up.reset(); // 释放指针
assert(!up); // 此时不管理任何指针
但unique_ptr要比scoped_ptr有更多的功能:可以像原始指针一样进行比较,可以像shared_ptr一样定制删除器,也可以安全地放入标准容器。因此,如果读者使用的编译器支持C++11标准,那么可以毫不犹豫地使用unique_ptr来代替scoped_ptr。
当然,scoped_ptr也有它的优点,“少就是多”永远是一句至理名言,它只专注于做好作用域内的指针管理工作,含义明确,而且不允许转让指针所有权。
make_unique
C++11 标准虽然定义了unique_ptr,但却“遗忘”了对应的工厂函数make_unique()(C++14 标准补上了这个“漏洞”),于是boost.smart_ptr库特意在头文件
template<class T, class... Args> // 使用可变参数模板
inline typename boost::detail::up_if_not_array<T>::type
make_unique(Args&&... args) { // 使用可变参数模板
return std::unique_ptr<T>(new T(...)); // C++的完美转发
}
需要注意两点:其一,它不含在头文件
① 但支持新的转移语义,如unique_ptr
up = std::move(another_ptr),解决了auto_ptr的在拷贝构造时微妙的转移语义问题,本书不做过多介绍。 ② 实际上 make_unique.hpp 里有两个头文件,使用模板元编程技术分别创建单个对象和数组对象。
boost::make_unique()的用法与 C++14 标准是一样的,示范代码如下:
auto p = boost::make_unique<int>(10); // 使用auto创建unique_ptr<int>对象
assert(p && *p == 10); // 访问指针内容
scoped_ptr不需要也不可能有make_scoped()函数,因为它不能拷贝不能转移。
3.3 shared_ptr
shared_ptr是一个最像指针的“智能指针”,是boost.smart_ptr库中最有价值、最重要的组成部分,也是最有用的,Boost库的许多组件—甚至还包括其他一些领域的智能指针都使用了shared_ptr,所以它被毫无悬念地收入了C++11标准。shared_ptr与scoped_ptr一样包装了new操作符在堆上分配的动态对象,但它实现的是引用计数型的智能指针①,可以被自由地拷贝和赋值,在任意的地方共享它,当没有代码使用(引用计数为0)它时才删除被包装的动态分配的对象。
shared_ptr也可以安全地放到标准容器中,是在STL容器中存储指针的最标准解法.
① shared_ptr的早期名字就叫做counted_ptr。
3.3.1 类摘要
shared_ptr要比同为智能指针的scoped_ptr复杂许多,它的类摘要如下:
template<class T>
class shared_ptr
{
public:
typedef T element_type; // 内部类型定义
shared_ptr(); // 构造函数
template<class Y> explicit shared_ptr(Y * p);
template<class Y, class D> shared_ptr(Y * p, D d);
~shared_ptr(); // 析构函数
shared_ptr(shared_ptr const & r); // 拷贝构造
shared_ptr & operator=(shared_ptr const & r); // 赋值操作
template<class Y> shared_ptr & operator=(shared_ptr<Y> const & r);
void reset(); // 重置智能指针
template<class Y> void reset(Y * p);
template<class Y, class D> void reset(Y * p, D d);
T & operator*() const; // 操作符重载
T * operator->() const; // 操作符重载
T * get() const; // 获得原始指针
bool unique() const; // 是否唯一
long use_count() const; // 引用计数
explicit operator bool() const; // 显式bool值转型
void swap(shared_ptr & b); // 交换指针
};
3.3.2 操作函数
shared_ptr与scoped_ptr同样是用于管理new动态分配对象的智能指针,因此功能上有很多相似之处:它们都重载了“*”和“->”操作符以模仿原始指针的行为,提供显式bool类型转换以判断指针的有效性,get()可以得到原始指针,并且没有提供指针算术操作,也不能管理new[]产生的动态数组指针。
shared_ptr<int> spi(new int); // 一个int的shared_ptr
assert(spi); // 在bool语境中转换为bool值
*spi = 253; // 使用解引用操作符*
shared_ptr<string> sps(new string("smart")); // 一个string的shared_ptr
assert(sps->size() == 5); // 使用箭头操作符->
shared_ptr<int> dont_do_this(new int[10]); // 危险!不能正确释放内存
例如:但shared_ptr的名字表明了它与scoped_ptr的主要不同:它是可以被安全共享的。shared_ptr是一个“全功能”的类,有着正常的拷贝、赋值语义,也可以进行shared_ptr间的比较,是“最智能”的智能指针。
shared_ptr 有多种形式的构造函数,应用于各种可能的情形:
- 无参的shared_ptr()创建一个持有空指针的shared_ptr;
- shared_ptr(Y * p)获得指向类型T的指针p的管理权,同时引用计数置为 1。这个构造函数要求Y类型必须能够转换为T类型;
- shared_ptr(shared_ptr const & r)从另外一个shared_ptr获得指针的管理权,同时引用计数加 1,结果是两个shared_ptr共享一个指针的管理权;
- operator=赋值操作符可以从另外一个shared_ptr获得指针的管理权,其行为同拷贝构造函数;
- shared_ptr(Y p, D d)行为类似shared_ptr(Y p),但使用参数d指定了析构时的定制删除器,而不是简单的delete。这部分将在 3.3.8 节详述。
- 别名构造函数(aliasing)是不增加引用计数的特殊用法,在 3.3.9 节讲解。
shared_ptr的reset()函数的行为与scoped_ptr也不尽相同,它的作用是将引用计数减1,停止对指针的共享,除非引用计数为0,否则不会发生删除操作。带参数的reset()则类似相同形式的构造函数,原指针引用计数减1的同时改为管理另一个指针。
shared_ptr有两个专门的函数来检查引用计数。unique()在shared_ptr是指针的唯一所有者时返回true(这时shared_ptr的行为类似scoped_ptr或unique_ptr),use_count()返回当前指针的引用计数。要小心,use_count()应该仅仅用于测试或者调试,它不提供高效率的操作,而且有的时候可能是不可用的(极少数情形)。而unique()则是可靠的,任何时候都可用,而且比use_count()==1速度更快。
shared_ptr还支持比较运算,可以测试两个shared_ptr的相等或不相等,比较基于内部保存的指针,相当于a.get()==b.get()。shared_ptr还可以使用operator<比较大小,但不提供除operator<以外的比较操作符,这使得shared_ptr可以被用于标准关联容器(set和map):
typedef shared_ptr<string> sp_t; // shared_ptr类型定义
map<sp_t, int> m; // 标准映射容器
sp_t sp(new string("one")); // 一个shared_ptr对象
m[sp] = 111; // 关联数组用法
此外,shared_ptr还支持流输出操作符operator<<,输出内部的指针值,方便调试。
3.3.3 用法
shared_ptr的“高智能”使其行为最接近原始指针,因此它比scoped_ptr的应用范围更广。几乎是100%可以在任何new出现的地方接受new的动态分配结果,然后被任意使用,从而完全消灭delete的使用和内存泄漏,而它的用法与scoped_ptr同样的简单。
shared_ptr也提供基本的线程安全保证,一个shared_ptr可以被多个线程安全读取,但其他的访问形式结果是未定义的。
示范shared_ptr基本用法的例子如下:
shared_ptr<int> sp(new int(10)); // 一个指向整数的shared_ptr
assert(sp.unique()); // 现在shared_ptr是指针的唯一持有者
shared_ptr<int> sp2 = sp; // 第二个shared_ptr,拷贝构造函数
assert(sp == sp2 && // 两个shared_ptr相等
sp.use_count() == 2); // 指向同一个对象, 引用计数为 2
*sp2 = 100; // 使用解引用操作符修改被指对象
assert(*sp == 100); // 另一个shared_ptr也同时被修改
sp.reset(); // 停止shared_ptr的使用
assert(!sp); // sp不再持有任何指针(空指针)
第二个例子示范了shared_ptr较复杂的用法:
#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace boost;
class shared { // 一个拥有shared_ptr的类
private:
shared_ptr<int> p; // shared_ptr成员变量
public:
shared(shared_ptr<int> p_) : p(p_) {} // 构造函数初始化shared_ptr
void print() { // 输出 shared_ptr 的引用计数和指向的值
std::cout << "count:" << p.use_count() << " v=" << *p << std::endl;
}
};
void print_func(shared_ptr<int> p) { // 使用shared_ptr作为函数参数
// 同样输出引用计数和指向的值
std::cout << "count:" << p.use_count() << " v=" << *p << std::endl;
}
int main() {
shared_ptr<int> p(new int(100)); // shared_ptr持有整数指针
shared s1(p), s2(p); // 构造两个自定义类
s1.print();
s2.print();
*p = 20; // 修改shared_ptr所指的值
print_func(p);
s2.print();
std::cout << &p << "; " << &s1 << "; " << &s2;
}
/* count:3 v=100
count:3 v=100
count:4 v=20
count:3 v=20
0x7fff20dbbe30; 0x7fff20dbbe20; 0x7fff20dbbe10 */
这段代码定义了一个类和一个函数,两者都接受shared_ptr对象作为参数,特别注意的是我们没有使用引用的方式传递参数,而是直接拷贝,就像是在使用一个原始指针—shared_ptr支持这样的用法。
在声明了shared_ptr和两个shared类实例后,指针被它们所共享,因此引用计数为3。print_func()函数内部拷贝了一个shared_ptr对象,因此引用计数再增加1,但当退出函数时拷贝自动析构,引用计数又恢复为3。
3.4 工厂函数
shared_ptr很好地消除了显式的delete调用,如果读者掌握了它的用法,可以肯定delete将会在你的编程字典中彻底消失。
但这还不够,因为shared_ptr的构造还需要new调用,这导致了代码中的某种不对称性。虽然shared_ptr很好的包装了new表达式,但过多的显式new操作符也是个问题,显式new调用应该使用工厂模式来解决。
因此,smart_ptr库提供了一个工厂函数(位于 boost 名字空间)make_shared ()②,来消除显式的new调用,声明如下:
template<class T, class... Args> // C++可变参数模板
typename boost::detail::sp_if_not_array<T>::type // 模板元计算类型
make_shared( Args && ... args ); // C++的右值引用用法
make_shared()函数可以接受若干个参数,然后把它们传递给类型T的构造函数,创建一个shared_ptr
下面的代码示范了make_shared()函数的用法:
auto sp = make_shared<string>("make_shared"); // 创建string的共享指针
auto spv = make_shared<vector<int> >(10, 2); // 创建vector的共享指针
assert(spv->size() == 10);
如果C++编译器支持可变参数模板特性,那么make_shared()的参数数量没有限制,能够以任意多数量的参数构造对象,否则它只能接受最多10个参数,一般情况下这不会成为问题。实际上,很少有如此多的参数的函数接口,即使有,那也会是一个设计得不够好的接口,应该被重构。
除了make_shared(),smart_ptr库还提供一个allocate_shared(),它比make_shared()多接受一个定制的内存分配器类型参数,其他方面都相同。
① 作者已经在数年的实践开发工作中很久没有写过这个单词了,一直到写作本书。 ② 在 Boost 库的较早版本时make_shared()不含在
中,而是在
中,必须单独写出。
3.3.5应用于标准容器
有两种方式可以将shared_ptr应用于标准容器(或者容器适配器等其他容器)。一种用法是将容器作为shared_ptr管理的对象,如shared_ptr>,使容器可以被安全地共享,用法与普通shared_ptr没有区别,我们不再讨论。
另一种用法是将shared_ptr作为容器的元素,如vector
标准容器可以容纳原始指针,但这就丧失了容器的许多好处,因为标准容器无法自动管理类型为指针的元素,必须编写额外的大量代码来保证指针最终被正确删除,这通常很麻烦而且容易出错。
存储shared_ptr的容器与存储原始指针的容器功能几乎一样,但shared_ptr为程序员做了指针的管理工作,可以任意使用shared_ptr而不用担心资源泄漏。
下面的代码示范了将shared_ptr应用于标准容器的用法:
#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace boost;
int main(){
typedef std::vector<shared_ptr<int>> vs; // 一个持有 shared_ptr 的标准容器类型
vs vv(10); // 声明一个拥有 10 个元素的容器
// 元素被初始化为空指针
int i = 0;
for (auto pos = vv.begin(); pos != vv.end(); ++pos) {
(*pos) = make_shared<int>(++i); // 使用工厂函数赋值
std::cout << *(*pos) << ", "; // 输出值
}
std::cout << std::endl;
shared_ptr<int> pp = vv[9];
*pp = 100;
std::cout << *vv[9] << std::endl;
}
这段代码里需要注意的是迭代器和operator[]的用法,因为容器内存储的是shared_ptr,我们必须对迭代器pos使用一次解引用操作符“”以获得shared_ptr,然后再对shared_ptr使用解引用操作符“”才能操作真正的值。(pos)也可以直接写成*pos,但前者更清晰,后者很容易让人迷惑。
vector的operator[]用法与迭代器类似,也需要使用“”获取真正的值。使用boost.foreach库(8.1节)或者C++里的新式for循环可以避免迭代器到shared_ptr的两次解引用,直接取出容器里的shared_ptr,例如:
for (auto& ptr : v) { // range based for,注意是引用形式
ptr = make_shared<int>(++i); // 使用工厂函数赋值
cout << *ptr << ", "; // 输出值
}
Boost 还另外在boost.iterators库里提供了迭代器适配器 indirect_iterator来简化容纳shared_ptr 容器的使用,读者可参考推荐书目[3]。
3.3.6 应用于桥接模式
桥接模式(Bridge)是一种结构型设计模式,它把类的具体实现细节对用户隐藏起来,以达到类之间的最小耦合关系。在具体编程实践中桥接模式也被称为pimpl或者handle/body惯用法,它可以将头文件的依赖关系降到最小,减少编译时间,而且可以不使用虚函数实现多态。
scoped_ptr和shared_ptr都可以用来实现桥接模式,但shared_ptr通常更合适,因为它支持拷贝和赋值,这在很多情况下都是有用的,比如可以配合容器工作。
本节不可能完整讲述桥接模式和pimpl惯用法的所有细节,仅通过一个小例子来说明shared_ptr如何用于pimpl。
首先我们声明一个类sample,它仅向外界暴露了最小的细节,真正的实现在内部类impl,sample用一个shared_ptr来保存它的指针:
#include <boost/smart_ptr.hpp>
using namespace std;
class sample {
private:
class impl; // 不完整的内部类声明
shared_ptr<impl> p; // shared_ptr 成员变量
public:
sample(); // 构造函数
void print();
};
// 在sample的cpp中完整定义impl类和其他功能:
class sample::impl // 内部类的实现
{
public:
void print() {
std::cout << "impl print" << std::endl;
}
};
sample::sample() : p(new impl) {} // 构造函数初始化 shared_ptr
void sample::print() { // 调用 pimpl 实现 print()
p->print();
}
int main(){
// 桥接模式的使用
sample s;
s.print();
}
桥接模式非常有用,它可以任意改变具体的实现而外界对此一无所知,同时也减小了源文件之间的编译依赖,使程序获得了更多的灵活性。而shared_ptr是实现它的最佳工具之一,它解决了指针的共享和引用计数问题。(关于桥接模式更详细的讨论请见推荐书目[1])。
3.3.7 应用于工厂模式
工厂模式是一种创建型设计模式,这个模式包装了new操作符的使用,使对象的创建工作集中在工厂类或者工厂函数中,从而更容易适应变化,make_shared()就是工厂模式的一个很好的例子。
在程序中编写自己的工厂类或者工厂函数时通常需要在堆上使用new动态分配一个对象,然后返回对象的指针。这种做法很不安全,因为用户很容易忘记对指针调用delete,存在资源泄漏的隐患。
使用shared_ptr可以解决这个问题,只需要修改工厂方法的接口,不再返回一个原始指针,而是返回一个被shared_ptr包装的智能指针,这样可以很好地保护系统资源,而且会更好地控制对接口的使用。
接下来我们使用代码来解释shared_ptr应用于工厂模式的用法:
#include <iostream>
#include <boosta/smart_ptr.hpp>
using namespace boost;
// 首先实现一个纯抽象基类, 也就是接口类
class abstract // 接口类定义
{
public:
virtual void f() = 0;
virtual void g() = 0;
protected:
// 注意abstract的析构函数,被定义为保护的,意味着除了它自己和它的子类,
// 其他任何对象都无权调用delete来删除它
virtual ~abstract() = default; // 注意这里
};
// 然后再定义abstract的实现子类
class impl : public abstract {
public:
impl() = default;
virtual ~impl() = default;
public:
virtual void f() {
std::cout << "class impl f" << std::endl;
}
virtual void g() {
std::cout << "class impl g" << std::endl;
}
};
// 随后的工厂函数返回基类的shared_ptr
shared_ptr<abstract> create() {
return make_shared<impl>();
}
int main1() {
// 这样就完成全部工厂模式的实现,可以把这些组合起来
auto p = create(); // 工厂函数创建对象
p->f(); // 可以像普通指针一样使用
p->g(); // 不必担心资源泄漏,shared_ptr会自动管理指针
}
由于基类abstract的析构函数是保护的,所以用户不能做出任何对指针的破坏行为,即使是用get()获得了原始指针:
abstract *q = p.get(); // 正确
delete q; // 错误
这段代码不能通过编译,因为无法访问abstract的保护析构函数。
但这不是绝对的,使用“粗鲁”的方法也可以在shared_ptr外删除对象,因为impl的析构函数是公开的,所以:
impl *q = (impl*)(p.get()); // 强制转型
delete q; // ok but dangerous
这样就可以任意操作原本处于shared_ptr控制之下的原始指针了,但永远也不要这样做,因为这会使shared_ptr在析构时删除可能已经不存在的指针,引发未定义行为。
3.3.8 定制删除器
在3.3.2节我们特意没有讨论shared_ptr一种形式的构造函数shared_ptr(Yp, Dd),它涉及shared_ptr的另一个重要概念:删除器。
shared_ptr(Yp, Dd)的第一个参数是要被管理的指针,它的含义与其他构造函数的参数相同。而第二个删除器参数d则告诉shared_ptr在析构时不是使用delete来操作指针p,而要用d来操作,即把deletep换成d(p)。
在这里删除器d可以是一个函数对象,也可以是一个函数指针,只要它能够像函数那样被调用,使得d(p)成立即可。对删除器的要求是它必须可拷贝,行为必须也像delete那样,不能抛出异常。
为了配合删除器的工作,shared_ptr提供一个自由函数get_deleter(),它能够返回内部的删除器指针。
有了删除器的概念,我们就可以用shared_ptr实现管理任意资源。只要这种资源提供了它自己的释放操作,shared_ptr就能够保证自动释放。
假设我们有一组操作socket的函数,使用一个socket_t类:
class socket_t {...}; // socket 类
socket_t* open_socket() { // 打开 socket
cout << "open_socket" << endl;
return new socket_t;
}
void close_socket(socket_t *s) { // 关闭 socket
cout << "close_socket" << endl;
... //其他操作,释放资源
}
那么,socket资源对应的释放操作就是函数close_socket(),它符合shared_ptr对删除器的定义,可以用 shared_ptr这样管理socket 资源:
socket_t *s = open_socket();
shared_ptr<socket_t> p(s, close_socket); // 传入删除器
在这里删除器close_socket()是一个自由函数,因此只需要把函数名传递给 shared_ptr 就可以了。在函数名前也可以加上取地址操作符&,效果是等价的:
shared_ptr<socket_t> p(s, &close_socket); // 传入删除器
这样我们就使用shared_ptr配合定制的删除器管理了socket资源。当离开作用域时,shared_ptr会自动调用close_socket()函数关闭socket,再也不会有资源遗失的担心。
再例如,对于传统的使用struct FILE的C文件操作,也可以使用shared_ptr配合定制删除器自动管理,像这样:
shared_ptr<FILE> fp(fopen("./1.txt","r"), fclose);
离开作用域时shared_ptr就会自动调用fclose()函数关闭文件。
shared_ptr的删除器特性在处理某些特殊资源时非常有用,它使得用户可以定制、扩展shared_ptr的行为,使shared_ptr不仅仅能够管理内存资源,而是成为一个“万能”的资源管理工具。
3.3.9 高级议题
本节讨论关于shared_ptr的一些高级议题。
对比std::shared_ptr
C++标准(C++11.20.7.2)中定义了std::shared_ptr,功能与boost::shared_ptr基本相同,完全可以等价互换C++标准(C++11.20.7.2)中定义了std::shared_ptr,功能与boost::shared_ptr基本相同,完全可以等价互换
显示bool转型
早期版本的shared_ptr的bool转型函数是隐式转换,但后来为了与标准一致添加了explicit修饰,变成了显式转换。①
出于兼容性的考虑,C++标准规定在if/assert/for等逻辑判断语境下shared_ptr还是可以“隐式”转换的(比如之前的代码),但其他情形—如函数参数或者返回值—则必须显式转换,可以使用static_cast
bool bool_test() { // 测试shared_ptr的bool转型
auto p = make_shared<int>(776); // 创建一个shared_ptr
assert(p); // assert可以隐式转换
if(p) { // if判断可以隐式转换
std::cout << "explicit cast" << std::endl;
}
return static_cast<bool>(p); // 返回值必须显式转换
}
① 但如果编译器不支持 C++11,那么 shared_ptr 也不会使用显式 bool 转型。
指针转型函数
在编写基于虚函数的多态代码时指针的类型转换很有用,比如把一个基类指针转型为一个派生类指针或者反过来。但对于shared_ptr不能使用诸如static_cast
为了支持这样的用法,shared_ptr提供了类似的转型函数static_pointer_cast
例如,下面的代码使用dynamic_pointer_cast把一个shared_ptr
shared_ptr<std::exception> sp1(new bad_exception);
auto sp2 = dynamic_pointer_cast<bad_exception>(sp1);
auto sp3 = static_pointer_cast<std::exception>(sp2);
assert(sp3 == sp1);
shared_ptr
shared_ptr
但将指针存储为void同时也丧失了原来的类型信息,为了在需要的时候正确使用,可以用static_pointer_cast
*删除器的高级用法
基于shared_ptr
void any_func(void* p) { // 一个可执行任意功能的函数
cout << "some operate" << endl;
}
int main() {
shared_ptr<void> p(nullptr,any_func); // 容纳空指针,定制删除器
} // 退出作用域时将执行any_func()
shared_ptr
*别名构造函数(aliasing)
在之前介绍的构造函数之外,shared_ptr还有一种比较特殊的构造函数,形式是:
template< class Y >
shared_ptr( shared_ptr<Y> const & r, element_type * p );
它的作用是共享r的引用计数,但实际持有的却是另外一个可能毫无关系的指针p,而且并不负责p的自动销毁。
初看上去这种形式的构造函数非常的怪异,但它是有实际应用价值的,一个例子使用场景是指向已经被shared_ptr管理的对象的内部成员变量:
auto p1 = make_shared<std::pair<int, int>>(0,1); // 一个 pair 智能指针
shared_ptr<int> p2(p1, &p1->second); // 别名构造
assert(p1.use_count() == 2 && // 原引用计数增加
p1.use_count() == p2.use_count()); // 两者引用计数相同
assert((void*)p1.get() != (void*)p2.get()); // 但指向的内容不同
assert(&p1->second == p2.get()); // 指向的是另外的指针
owner_less
因为存在别名构造函数,所以某些情况下单纯地基于p.get()的指针值比较就不适用了,为此smart_ptr库提供了基于所有权的比较函数对象owner_less,定义了严格的弱序关系,可以用于关联容器。
下面的代码简单地示范了owner_less用于标准容器set:
#include <set>
#include <boost/smart_ptr/owner_less.hpp> //需单独包含头文件
using namespace boost;
int main() {
typedef shared_ptr<int> int_ptr; // 共享指针 typedef
typedef owner_less<int_ptr> int_ptr_less; // 函数对象 typedef
int_ptr p1(new int(10)); // 共享指针
int n = 20;
int_ptr p2(p1, &n); // 别名构造
assert(!int_ptr_less()(p1, p2) && // 两者即不小于
!int_ptr_less()(p2, p1)); // 也不大于,即等价
typedef std::set<int_ptr> int_set; // 关联容器 typedef
int_set s;
s.insert(p1); // 插入两个元素
s.insert(p2); // 因为等价所以不会被插入
assert(s.size() == 1); // 实际容器里只有一个元素
}
其他高级用法
shared_ptr的功能已经远远超出了智能指针的范围,除了以上的介绍外还有很多其他用途,如包装成员函数、延时释放等,限于篇幅本书不作详细介绍,读者可参考Boost说明文档。
3.4 weak_ptr
weak_ptr是为配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->。它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
template<class T>
class weak_ptr
{
public:
weak_ptr(); // 构造函数
template<class Y> weak_ptr(shared_ptr<Y> const & r);
weak_ptr(weak_ptr const & r);
~weak_ptr(); // 析构函数
weak_ptr & operator=(weak_ptr const & r); // 赋值
long use_count() const; // 引用计数
bool expired() const; // 是否失效指针
shared_ptr<T> lock() const; //获取 shared_ptr
void reset(); // 重置指针
void swap(weak_ptr<T> & b); // 交换指针
};
weak_ptr的接口很小,正如它的名字,是一个“弱”指针,但它能够完成一些特殊的工作,足以证明它的存在价值。
3.4.2 用法
weak_ptr被设计为与shared_ptr协同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。同样,weak_ptr析构时也不会导致引用计数减少,它只是一个静静的观察者。
使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是被shared_ptr管理的资源)已经不复存在。
weak_ptr没有重载operator*和->,这是特意的,因为它不共享指针,不能操作资源,这正是它“弱”的原因。但它可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,把弱关系转换为强关系,从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。下面的代码示范了weak_ptr的用法
#include <boost/smart_ptr/owner_less.hpp> //需单独包含头文件
using namespace boost;
#include <set>
int main() {
shared_ptr<int> sp(new int(10)); // 一个 shared_ptr
assert(sp.use_count() == 1);
weak_ptr<int> wp(sp); // 从 shared_ptr 创建 weak_ptr
assert(wp.use_count() == 1); // weak_ptr 不影响引用计数
if (!wp.expired()) { // 判断 weak_ptr 观察的对象是否失效
shared_ptr<int> sp2 = wp.lock(); // 获得一个 shared_ptr
*sp2 = 100;
assert(wp.use_count() == 2);
} // 退出作用域,sp2 自动析构,引用计数减 1
assert(wp.use_count() == 1);
sp.reset(); // shared_ptr 失效
assert(wp.expired());
assert(!wp.lock()); // weak_ptr 将获得一个空指针
}
3.4.3 对象自我管理
weak_ptr的一个重要用途是获得this指针的shared_ptr,使对象自己能够生产shared_ptr管理自己:对象使用weak_ptr观测this指针,这并不影响引用计数,在需要的时候就调用lock()函数,返回一个符合要求的shared_ptr供外界使用。
这个解决方案被实现为一个惯用法,在头文件
template<class T>
class enable_shared_from_this // 辅助类,需要继承使用
{
public:
shared_ptr<T> shared_from_this(); // 工厂函数,产生this的shared_ptr
}
使用的时候只需要让想被sharedptr管理的类继承它即可,成员函数 shared_from this()会返回this的 shared_ptr。例如:
#include <boost/smart_ptr.hpp>
using spacename std;
class self_shared : // 一个需要用shared_ptr自我管理的类
public enable_shared_from_this<self_shared> {
public:
self_shared(int n) : x(n) {}
int x;
void print() {
cout << "self_shared:" << x << endl;
}
};
int main() {
auto sp = make_shared<self_shared>(313);
sp->print();
auto p = sp->shared_from_this();
p->x = 1000;
p->print();
}
需要注意的是千万不能对一个普通对象(非sharedptr管理的对象)使用 shared from_this()获取shared_ptr,例如:
self_shared ss;
auto p = ss.shared_from_this(); // 错误!
这样虽然语法上正确,编译也无问题,但在运行时会导致shared_ptr析构时企图删除一个栈上分配的对象,发生未定义行为。
3.4.4 打破循环引用
有的时候代码中可能会出现“循环引用”,这时shared_ptr的引用计数机制就会失效,导致不能正确释放资源,例如:
#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace boost;
class node // 一个用于链表节点的类
{
public:
~node() { // 析构函数输出信息
std::cout << "deleted" << std::endl;
}
typedef shared_ptr<node> ptr_type; // 指针类型使用 shared_ptr
ptr_type next; //后继指针
};
int main() {
auto p1 = make_shared<node>(); // 两个节点对象
auto p2 = make_shared<node>();
p1->next = p2; // 形成循环链表
p2->next = p1;
assert(p1.use_count() == 2); // 每个 shared_ptr 的引用计数都是 2
assert(p2.use_count() == 2);
}
上面的代码中两个节点对象互相持有对方的引用,每一个shared_ptr的引用计数都是2,因此在析构时引用计数没有减至0,不会调用删除操作,导致内存泄漏。
这个时候我们就可以使用weak_ptr,因为它不会增加智能指针的引用计数,这样就把原来的强引用改变为弱引用,在可能存在循环引用的地方打破了循环,而在真正需要shared_ptr的时候调用weak_ptr的lock()函数:
#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace boost;
class node //链表节点的类
{
public:
~node() { // 析构函数输出信息
std::cout << "deleted" << std::endl;
}
typedef weak_ptr<node> ptr_type; // 指针类型使用 weak_ptr
ptr_type next; // 后继指针
};
int main() {
auto p1 = make_shared<node>(); // 两个节点对象
auto p2 = make_shared<node>();
p1->next = p2; // 形成循环链表
p2->next = p1; // 引用使用了 weak_ptr 所以正常
assert(p1.use_count() == 1); // 每个 shared_ptr 的引用计数是 1
assert(p2.use_count() == 1); // 没有了循环引用
if (!p1->next.expired()) { // 检查弱引用是否有效
auto p3 = p1->next.lock(); // 调用 lock()获得强引用
}
} // 退出作用域, shared_ptr 均正确析构
3.5 intrusive_ptr
intrusive_ptr也是一种引用计数型智能指针,但与之前介绍的scoped_ptr、shared_ptr不同,需要额外增加一些的代码才能 使用。它的名字可能会给人造成误解,实际上它并不一定要修改代理对象的内部数据。
如果现存代码已经有了引用计数机制管理的对象,那么intrusive_ptr是一个非常好的选择,可以包装已有对象从而得到与shared_ptr类似的智能指针。
3.5.1 类摘要
template<class T>
class intrusive_ptr {
public:
typedef T element_type; // 被代理的对象
intrusive_ptr(); // 构造函数
intrusive_ptr(T *p, bool add_ref = true);
intrusive_ptr(intrusive_ptr const &r);
template<class Y> intrusive_ptr(intrusive_ptr<Y> const &r);
~intrusive_ptr();
intrusive_ptr &operator=(intrusive_ptr const &r);
template<class Y> intrusive_ptr &operator=(intrusive_ptr<Y> const &r);
intrusive_ptr &operator=(T *r);
void reset(); // 重置指针
void reset(T *r);
void reset(T *r, bool add_ref);
T &operator*() const; // 操作符重载
T *operator->() const;
explicit operator bool() const;
T *get() const;
T *detach();
void swap(intrusive_ptr &b);
};
因为intrusive_ptr也是引用计数型指针,所以它的接口与shared_ptr很像,也同样支持比较和static_pointer_cast()、dynamic_pointer_cast()等转型操作,但它自己不直接管理引用计数,而是调用下面两个函数来间接管理:
void intrusive_ptr_add_ref(T * p); // 增加引用计数
void intrusive_ptr_release(T * p); // 减少引用计数
intrusive_ptr的构造函数和reset()还多出一个add_ref参数,表示是否增加引用计数,如果add_ref==false,那么它就相当于weak_ptr,只是简单地观察对象。
3.5.2 用法
假设我们已经有了一个自己实现引用技术的类counted_data:
struct counted_data { // 自己实现引用计数
int m_count = 0; // 引用计数
... // 其他数据成员
};
为了让intrusive_ptr能够正常工作,我们需要实现它要求的两个回调函数:①
void intrusive_ptr_add_ref(counted_data* p) { // 增加引用计数
++p->m_count;
}
void intrusive_ptr_release(counted_data* p) { // 减少引用计数
if(--p->m_count == 0) {
delete p; // 引用计数为0则删除指针
}
}
注意:在intrusive_ptr_release()函数里必须检查引用计数,因为intrusive_ptr不负责实例的销毁,这个工作必须由我们自己完成。
实现intrusive_ptr_release()和intrusive_ptr_release()后intrusive_ptr就可以管理counted_data了,示范代码如下:
int main() {
typedef intrusive_ptr<counted_data> counted_ptr; // 类型定义
counted_ptr p(new counted_data); // 创建智能指针
assert(p); // bool 转型
assert(p->m_count == 1); // operator->
counted_ptr p2(p); // 指针拷贝构造
assert(p->m_count == 2); // 引用计数增加
counted_ptr weak_p(p.get(), false); // 弱引用
assert(weak_p->m_count == 2); // 引用计数不增加
p2.reset(); // 复位指针
assert(p->m_count == 1); // 引用计数减少
} // 对象被正确析构
可以看到,只需要编写少量的代码,我们就可以复用既存的数据结构,获得一个与shared_ptr用法几乎一样的智能指针,而且并没有增加多余的开销,这在某些对性能要求比较苛刻的场景里非常有用。
但大多数情况下shared_ptr完全不必增加新代码,而且提供了更多的灵活性,使用intrusive_ptr前必须要确定它能够带来足够多的好处。
3.5.3 引用计数器
为了进一步简化实现引用计数的工作,intrusive_ptr在头文件
template< typename DerivedT, typename CounterPolicyT = thread_safe_counter>
class intrusive_ref_counter {
private:
typedef typename CounterPolicyT::type counter_type;
mutable counter_type m_ref_counter;
public:
intrusive_ref_counter();
unsigned int use_count() const;
protected:
~intrusive_ref_counter() = default;
friend void intrusive_ptr_add_ref(const intrusive_ref_counter* p);
friend void intrusive_ptr_release(const intrusive_ref_counter* p);
};
intrusive_ref_counter内部定义了一个计数器变量m_ref_counter,使用模板参数CounterPolicyT配置策略类实现了计数的增减,默认的策略是线程安全的thread_safe_counter。
intrusive_ref_counter需要被继承使用,这样子类就会自动获得引用计数的能力,之前的counted_data可以简化如下:
struct counted_data : public intrusive_ref_counter<counted_data>
{ ... };
int main() {
typedef intrusive_ptr<counted_data> counted_ptr; // 类型定义
counted_ptr p(new counted_data); // 创建智能指针
assert(p); // bool 转型
assert(p->use_count() == 1); // operator->
} // 对象被正确析构
本书的12.1.4节使用atomic实现了一个更好的辅助类ref_count,读者可参考。
3.6 pool库概述
如果读者学习过操作系统的内存管理机制和内存分配算法等知识,那么就可能了解“内存池”的概念。简单来说,内存池预先分配了一块大的内存空间,然后就可以在其中使用某种算法实现高效快速的自定制内存分配。
boost.pool库基于简单分隔存储思想实现了一个快速、紧凑的内存池库,不仅能够管理大量的对象,还可以被用做STL的内存分配器。某种程度上讲,它近似于一个小型的垃圾回收机制,在需要大量地分配/释放小对象时很有效率,而且完全不需要考虑delete。
pool库包含四个组成部分:最简单的pool、分配类实例的object_pool、单件内存池singleton_pool和可用于标准库的pool_alloc。
3.7 pool
pool是最简单也最容易使用的内存池类,可以返回一个简单数据类型(POD)①的内存指针。它位于名字空间boost,需要包含头文件
① POD是C++标准中的技术术语,是“普通旧式数据”(Plain Old Data)的缩写。
3.7.1 类摘要
template <typename UserAllocator = default_user_allocator_new_delete >
class pool {
public:
explicit pool(size_type requested_size); // 构造函数
~pool(); // 析构函数
size_type get_requested_size() const; // 分配内存块的大小
void * malloc(); // 分配内存
void * ordered_malloc();
void * ordered_malloc(size_type n);
bool is_from(void * chunk) const;
void free(void * chunk); // 归还内存
void ordered_free(void * chunk);
void free(void * chunks, size_type n);
void ordered_free(void * chunks, size_type n);
bool release_memory(); // 释放内存
bool purge_memory();
};
3.7.2 操作函数
pool的模板类型参数UserAllocator是一个用户定义的内存分配器,它实现了特定的内存分配算法,通常可以直接用默认的default_user_allocator_new_delete,它内部使用new[]和delete[]分配内存。
pool的构造函数接受一个size_type类型的整数requested_size,指示每次分配内存块的大小(而不是内存池的大小),这个值可以用get_requested_size()获得。pool会根据需要自动地向系统申请或归还使用的内存,在析构时,pool将自动释放它所持有的所有内存。
成员函数malloc()和ordered_malloc()的行为很类似C中的全局函数malloc(),用void*指针返回从内存池中分配的内存块,大小为构造函数中指定的requested_size。如果内存分配失败,函数会返回0(即空指针),不会抛出异常。malloc()从内存池中任意分配一个内存块,而ordered_malloc()则在分配的同时合并空闲块链表。ordered_malloc()带参数的形式还可以连续分配n块的内存。分配后的内存块可以用is_from()函数测试是否是从这个内存池分配出去的。
与malloc()对应的一组函数是free(),用来手工释放之前分配的内存块,这些内存块必须是从这个内存池分配出去的(is_from(chunk)==true)。一般情况内存池会自动管理内存分配,不应该调用free(),除非你认为内存池的空间已经不足,必须释放已经分配的内存。
最后还有两个成员函数:release_memory()让内存池释放所有未被分配的内存,但已分配的内存块不受影响;purge_memory()则强制释放pool持有的所有内存,不管内存块是否被使用。实际上,pool的析构函数就是调用的purge_memory()。这两个函数一般情况下也不应该由程序员手工调用。
3.7.3 用法
pool很容易使用,可以像C中的malloc()一样分配内存,然后随意使用。除非有特殊要求,否则不必对分配的内存调用free()释放,pool会很好地管理内存。例如:
int main(){
pool<> p1(sizeof(int)); // 一个可分配int的内存池
int *p = static_cast<int*>(p1.malloc()); // 必须把void*转换成需要的类型
assert(p1.is_form(p));
p1.free();
for(int i=0;i<100;i++){ // 释放内存池分配的内存卡
p1.ordered_malloc(100); // 连续分配大量的内存
}
} // 内存池对象析构,所以分配的内存在这里都被释放
因为pool在分配内存失败的时候不会抛出异常,所以实际编写的代码应该检查malloc()函数返回的指针,以防止空指针错误,不过通常这种情况极少出现:
int *p = static_cast<int *>(p1.malloc());
if (p != nullptr)
...
关于pool没有更多的解释,因为它真的很容易使用,只需要注意一点:它只能作为普通数据类型如int、double等的内存池,不能应用于复杂的类和对象,因为它只分配内存,不调用构造函数,这个时候我们需要用object_pool。
3.8 object_pool
object_pool是用于类实例(对象)的内存池,它的功能与pool类似,但会在析构时对所有已经分配的内存块调用析构函数,从而正确地释放资源。
object_pool位于名字空间boost,需要包含头文件
3.8.1 类摘要
template <typename T, typename UserAllocator>
class object_pool :protected pool<UserAllocator>
{
public:
typedef T element_type;
public:
object_pool(); // 构造函数
~object_pool(); // 析构函数
element_type * malloc(); // 分配内存
void free(element_type * p); // 归还内存
bool is_from(element_type * p) const; // 测试内存块的归属
element_type * construct(...); // 创建对象
void destroy(element_type * p); // 销毁对象
};
3.8.2 操作函数
object_pool是pool的子类,但它使用的是保护继承,因此不能使用pool的接口,但基本操作还是很相似的。
object_pool的模板类型参数T指定了object_pool要分配的元素类型,要求其析构函数不能抛出异常。一旦在模板中指定了类型,object_pool实例就不能再用于分配其他类型的对象。
malloc()和free()函数分别分配和释放一块类型为T*的内存块,同样,可以用is_from()来测试内存块的归属,只有是本内存池分配的内存才能被free()释放。但它们被调用时并不调用类的构造函数和析构函数,也就是说,操作的是一块原始内存块,里面的值是未定义的,因此我们应当尽量少使用malloc()和free()。
object_pool的特殊之处是construct()和destroy()函数,这两个函数是object_pool的真正价值所在。construct()实际上是一组函数,有多个参数的重载形式(目前最多支持3个参数,但可以扩展),它先调用malloc()分配内存,然后再在内存块上使用传入的参数调用类的构造函数,返回的是一个已经初始化的对象指针。destory()则先调用对象的析构函数,然后再用free()释放内存块。
这些函数都不会抛出异常,如果内存分配失败,将返回0(即空指针)。
3.8.3 用法
object_pool的用法也很简单,我们既可以像pool那样分配原始内存块,也可以使用construct()来直接在内存池中创建对象。当然,后一种使用方法是最方便的,也是本书所推荐的。
下面的代码示范了object_pool的用法:
#include <iostream>
#include <string>
#include <boost/pool/object_pool.hpp>
using namespace std;
using namespace boost;
struct demo_class // 一个示范用的类
{
public:
int a, b, c;
demo_class(int x = 1, int y = 2, int z = 3) :
a(x), b(y), c(z) {}
};
int main() {
object_pool<demo_class> pl; // 对象内存池
auto p = pl.malloc(); // 分配一个原始内存块
assert(pl.is_from(p));
// p指向的内存未经过初始化
assert(p->a != 1 || p->b != 2 || p->c != 3);
p = pl.construct(7, 8, 9); // 构造一个对象,可以传递参数
assert(p->a == 7);
object_pool<string> pls; // 定义一个分配 string 对象的内存池
for (int i = 0; i < 10; ++i) { // 连续分配大量 string 对象
string *ps = pls.construct("hello object_pool");
cout << *ps << endl;
}
} // 所有创建的对象在这里都被正确析构、释放内存
3.8.4 更多的构造参数
默认情况下,在使用object_pool的construct()的时候我们只能最多使用3个参数来创建对象。大多数情况下这都是足够的,但有的时候我们可能会定义3个以上参数的构造函数,此时construct()的默认重载形式就不能用了。
很遗憾,construct()并没有及时跟进C++标准使用可变参数模板支持任意数量的参数构造。它基于宏预处理m4(通常UNIX系统自带,也有Windows的版本)实现了一个变通的扩展机制,可以生成接受任意数量参数的construct()函数代码。
pool库在目录/boost/pool/detail下提供了一个名为pool_construct.m4和pool_construct_simple.m4的脚本,并同时提供可在UNIX和Windows下运行的同名sh和bat可执行脚本文件。只需要简单地向批处理脚本传递一个整数的参数N,m4就会自动生成能够创建具有N个参数的construct()函数源代码。
例如,在Linux下,执行命令:
./pool_construct_simple.sh 5;./pool_construct.sh 5
将生成两个同名的.ipp文件,里面包含了新的construct()函数定义,能够支持最多传递5个参数创建对象。
m4的解决方案显得比较笨拙,使用可变参数模板特性,我们可以定义一个辅助模板函数,支持任意数量的参数,彻底解决这个问题:
template<typename P, typename ... Args> // 可变参数模板
inline typename P::element_type*
construct(P& p, Args&& ... args) {
typename P::element_type* mem = p.malloc();
assert(mem != 0);
new (mem) typename P::element_type(
std::forward<Args>(args)...); // 完美转发
return mem;
}
自由函数construct()接受任意多个参数,第一个是object_pool对象,其后是创建对象所需参数,要创建的对象类型可以使用object_pool的内部类型定义element_type来获得。函数中首先调用malloc()分配一块内存,然后调用不太常见的“定位new表达式”(placementnewexpression)创建对象。
假设我们有如下的一个4参数构造函数的类:
struct demo_class {
demo_class(int, int, int, int) // 构造函数接受4个参数
{
cout << "demo_class ctor" << endl;
}
~demo_class() {
cout << "demo_class dtor" << endl;
}
};
那么使用自定义的 construct()创建对象的代码就是:
object_pool<demo_class> pl;
auto d = construct(pl, 1, 2, 3, 4); // 使用自定义扩展
3.9 singleton_pool
singleton_pool与pool的接口完全一致,可以分配简单数据类型(POD)的内存指针,但它是一个单件。
singleton_pool位于名字空间boost,需要包含头文件
singleton_pool默认使用boost.thread库提供线程安全保证,所以需要链接boost_thread库,如果不使用多线程,那么可以在头文件前定义宏BOOST_POOL_NO_MT。
3.9.1 类摘要
singleton_pool 的类摘要如下:
class singleton_pool {
public:
static bool is_from(void *ptr);
static void *malloc(); // 分配内存
static void *ordered_malloc();
static void *ordered_malloc(size_type n);
static void free(void *ptr); // 归还内存
static void ordered_free(void *ptr);
static void free(void *ptr, std::size_t n);
static void ordered_free(void *ptr, size_type n);
static bool release_memory(); // 释放内存
static bool purge_memory();
};
3.9.2 用法
singleton_pool主要有两个模板类型参数(其余的可以使用默认值)。第一个Tag仅仅是用于标记不同的单件,可以是空类,甚至只是声明(这个形式还被用于4.7节的boost.exception)。第二个参数RequestedSize等同于pool构造函数中的整数requested_size,指示pool分配内存块的大小。
singleton_pool的接口与pool完全一致,但成员函数均是静态的,所以不需要声明singleton_pool的实例①,直接用域操作符“::”来调用静态成员函数。因为singleton_pool是单件,所以它的生命周期与整个程序同样长,除非手动调用release_memory()或purge_memory(),否则singleton_pool不会自动释放所占用的内存。除了这两点,singleton_pool的用法与pool完全相同。
下面的代码示范了singleton_pool的用法:
struct pool_tag{}; // 仅仅用于标记的空类
typedef singleton_pool<pool_tag, sizeof(int)> spl; // 内存池定义
int main() {
int *p = (int*)spl::malloc(); // 分配一个整数内存块
assert(spl::is_from(p));
spl::release_memory(); // 释放所有未被分配的内存
} // spl的内存直到程序结束才完全释放,而不是退出作用域
singleton_pool在使用时最好使用typedef来简化名称,否则会使得类型名过于冗长而难以使用。如代码中所示:
typedef singleton_pool<pool_tag, sizeof(int)> spl;
用于标记的类pool_tag可以再进行简化,直接在模板参数列表中声明tag类,这样可以在一条语句中完成对singleton_pool的类型定义,例如:
typedef singleton_pool<struct pool_tag, sizeof(int)> spl;
① 因为使用了单件模式,用户也无法创建singleton_pool的实例。
3.10 pool_alloc
pool_alloc提供了两个可以用于标准容器模板参数的内存分配器,分别是pool_alloc和fast_pool_allocator,它们的行为与之前的内存池类有一点不同—当内存分配失败时会抛出异常std::bad_alloc。它们位于名字空间boost,需要包含头文件
除非有特别的需求,我们应该总使用标准库实现自带的内存分配器,使用pool_alloc需要经过仔细的测试,以保证它与容器可以共同工作。下面的代码示范了pool_alloc的用法:
vector<int, pool_allocator<int> > v; // pool_allocator代替默认的内存分配器
v.push_back(10); // vector将使用新的分配器良好工作
cout << v.size()
3.11 总结
内存管理是C++程序开发中永恒的话题,因为没有垃圾回收机制,小心谨慎地管理内存等系统资源是每一个C++程序员都必须面对的问题。C++标准提供了unique_ptr、shared_ptr和weak_ptr,较好地减轻了程序员的内存管理负担,但没有解决所有问题。本章讨论了Boost里关于内存管理的两个库:smart_ptr和pool。
smart_ptr库
smart_ptr库提供了数种新型智能指针,非常接近C++标准的定义,可以有效地消除new和delete的显式使用,减少甚至杜绝代码资源泄漏。scoped_ptr是smart_ptr库中最容易学习和使用的一个,它的行为类似unique_ptr,但所有权更明确,清晰地表明了这种智能指针只能在声明的作用域中使用,不能转让,任何对它的复制企图都会失败。这个特点对代码的后期维护工作非常有用。
shared_ptr可能是最有用的智能指针,也是这些智能指针中最“智能”的一个,不仅可以管理内存,也可以管理其他系统资源,能够应用于许多场合。它可以自动地计算指针的引用计数,其行为最接近原始指针。几乎可以用在任何原始指针可以使用的地方,并且不用承担资源泄漏的风险。shared_ptr不仅可以保存指针,通过配置删除器也可以自动释放指针关联的资源。在基本的用法之外,我们还讨论了shared_ptr的很多其他用法,如实现pimpl惯用法、应用于工厂模式、别名构造、持有任意对象的指针等,这些用法进一步展示了它的强大功能。
为了方便智能指针的使用,smart_ptr库提供了工厂函数make_unique()和make_shared(),进一步消除了代码中的new操作符。我们还简要讨论了smart_ptr里的另两个组件:weak_ptr是一个弱引用,能够“静态”地观察shared_ptr而不影响引用计数,intrusive_ptr则为自行实现引用计数智能指针提供了通用技术方案。
pool库
pool库是Boost程序库在内存管理方面提供的另一个有用工具,它实现了高效的内存池,用于管理内存资源。pool库提供了pool、object_pool、singleton_pool和pool_alloc四种形式的内存池,适合于各种情形的应用。可以完全把它们当做是一个小型的垃圾回收机制,在内存池中随意地动态创建对象,而完全不用关心它的回收,也无需对原有类做任何形式的修改。
pool库的四个内存池类中前三个都很有用,尤其是object_pool,它可以统一地管理各种对象的创建与销毁,能够很好地应用在各种规模的面向对象软件系统中。至于pool_alloc,它是符合C++标准的一个内存分配器实现,快速且高效,但通常标准库自带的内存分配器会更好地与容器配合工作,使用pool_alloc时需要仔细地评估可能带来的影响。
pool库还提供一个底层的实现类simple_segregated_storage,它实现了简单分隔存储的管理机制,是pool库其他类的基础。它不适合大多数库用户,但可以作为自行实现内存池类的一个很好的起点。
