文章目录
内存管理一直是令c++程序员最头疼的工作,c++继承了c那高效而又灵活的指针,使用起来稍微不小心就会导致内存泄漏(
memory leak)、“野”指针(
wildpointer)、越界访问(
access denied)等问题。曾几何时,c++程序员曾经无限地向往Java、C#等语言的垃圾回收机制。虽然c++标准提供了智能指针
std::auto_ptr,但并没有解决所有问题。
阅读完本章,你会了解到高效的内存管理方法,彻底忘记"栈"(Stack)、“堆”(Heap)等内存分配相关的术语,并且还会发现,Boost为c++提供的解决方案可能要比Java和C#等其他语言更好。
1 smart_ptr 库概述
计算机系统中资源又很多种,内存是我们最常用到的,此外还有文件描述符、socket、操作系统handler、数据库连接等等,程序中申请这些资源后必须及时归还系统,否则就会产生难以预料的后果。
1.1 RAII机制
为了管理内存等资源,c++程序员通常采用RAII机制(资源获取即初始化,Resource Acquisition Is Initialization),在使用该资源的类的构造函数中申请资源,然后使用,最后在析构函数中释放资源。
如果对象是用声明的方式在栈上创建的(一个局部对象),那么RAII机制会工作正常,当离开作用域时对象会自动销毁从而调用析构函数释放资源。但如果对象是用new操作符在堆上创建的,那么它析构函数不会自动调用,程序员必须明确地用对应的delete操作符销毁它才能释放资源。这就存在着资源泄漏的隐患,因为这时没有任何对象对已经获取的资源负责,如果因某些以外导致程序未能执行delete语句,那么内存等资源就永久地丢失了。例如:
int *p = new class_need_resource; //对象创建,获取资源
.. //可能发生异常导致资源泄漏
delete p; //析构释放资源
new、delete 以及指针的不恰当运用是c++中造成资源获取/释放问题的根源,能够正确而明智地运用delete是区分c++新手与熟手的关键所在。但是很多人—即使是熟练的c++程序员,也经常会忘记调用delete。
1.2 智能指针
智能指针(smart pointer)是c++群体中热门的议题,围绕它有很多价值的讨论和结论。它实践了代理模式,代理了原始"裸"指针的行为,为它添加了更多更有用的特性。
c++引入异常机制后,智能指针由一种技巧升级为一种非常重要的技术,因为如果没有智能指针,程序员必须保证new对象能在正确的实际delete,四处编写异常捕获代码以释放资源,而智能指针则可以退出作用域时——不管是正常流程离开或是因异常离开——总调用delete来析构在堆上动态分配的对象。
存在很多种智能指针,其中最有名的应该是c++98标准中的“自动指针”std::auto_ptr,它部分地解决了获取资源自动释放的问题,例如:
int main()
{
//离开作用域,p1、p2自动析构从而释放内存等资源
auto_ptr<class_need_resource> p1(new class_need_resource);
auto_ptr<demo_class> p2(factory.create());
...
}
auto_ptr的构造函数接受new操作符或者对象工厂创建出的对象指针作为参数,从而代理了原始指针。虽然它是一个对象,但因为重载了operator*和operator->,其行为非常类似指针,可以把它用在大多数普通指针可用的地方。当退出作用域时(离开函数main()或者发生异常),c++语言会保证auto_ptr对象销毁,调用auto_ptr的析构函数,进而使用delete操作符删除原始指针释放资源。
auto_ptr很好用,被包含在c++标准库中令它在全世界范围内广泛使用,使智能指针的思想、用法深入人心。但标准库并没有覆盖智能指针的全部领域,尤其是最重要的引用计数型智能指针。
boost.smart_ptr库是对c++98标准的一个绝佳补充。它提供了六种智能指针,包括scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr和intrusive_ptr,从各个方面来增强std::auto_ptr,而且是异常安全的。库中的两个类——shared_ptr和weak_ptr已被收入到c++新标准的TR1库中。
接下来详细介绍scoped_ptr、scoped_array、shared_ptr和shared_array,简要介绍另两个组件weak_ptr和intrusive_ptr。它们都是很轻量级的对象,速度与原始指针相差无几,对于所指的类型T也有一个很小且很合里的要求:类型T的析构函数不能抛出异常。
这些智能指针都位于名字空间boost,为了使用smart_ptr组件,需要包含头文件<boost/smart_ptr.hpp>,即:
#include <boost/smart_ptr.hpp>
using namespace boost;
2 scoped_ptr
scoped_ptr是一个很类似auto_ptr的智能指针,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确地删除。但scoped_ptr的所有权更加严格,不能转让,一旦scoped_ptr获取了对象的管理权,你就无法再从它那里取回来。
scoped_ptr拥有一个很好的名字,它向代码的阅读者传递了明确的信息:这个智能指针只能在本作用域里使用,不希望被转让。
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 &);
public:
explicit scoped_ptr(T* p=0);
~scoped_ptr();
void reset(T* p = 0);
T& operator*() const;
T* operator->() const;
T* get() const;
operator unspecified-bool-type() const;
void swap(scoped_ptr& b);
};
2.2 操作函数
scoped_ptr的构造函数接受一个类型为T*的指针p,创建出一个scoped_ptr对象,并在内部保存指针参数p。p必须是一个new表达式动态分配的结果,或者是个空指针(0)。当scoped_ptr对象的生命期结束时,析构函数~scoped_ptr会使用delete操作符自动销毁所保存的指针对象,从而正确地回收资源。
scoped_ptr同时把拷贝构造函数和赋值操作符都声明为私有的,禁止对智能指针的复制操作,保证了被它管理的指针不能被转让所有权。
成员函数reset()的功能是重置scoped_ptr;它删除原来保存的指针,再保存新的指针值p。如果p是空指针,那么scoped_ptr将不会持有任何指针。一般情况下reset()不应该被调用,因为它违背了scoped_ptr的本意——资源应该一直有scoped_ptr自己自动管理。
scoped_ptr用operator*()和operator->()重载了解引用操作符*和箭头操作符->,以模仿被代理的原始指针的行为,因此可以把scoped_ptr对象如同指针一样使用。如果scoped_ptr保存空指针,那么这两个操作的行为未定义。
scoped_ptr不支持比较操作,不能在两个scoped_ptr之间、scoped_ptr和原始指针或空指针之间进行相等或者不相等测试,我们也无法为它编写额外的比较函数,因为它已经将operator==和operator!=两个操作符重载都声明为私有的。但scoped_ptr提供一个可以在bool语境(context)中自动转换成bool值(如if的条件表达是)的功能,用来测试scoped_ptr是否持有一个有效的指针(为空)。他可以代替与空指针的比较操作,而且写法更简单。
成员函数swap()可以交换两个scoped_ptr保存的原始指针。它是高效的操作,被用于实现reset()函数,也可以被boost:swap()所利用。
最后是成员函数get(),它返回scoped_ptr内部保存的原始指针,可以用在某些要求必须是原始指针的场景(如底层的c接口)。但是使用时必须小心,这将使原始指针脱离scoped_ptr的控制!不能对这个指针做delete操作,否则scoped_ptr析构时会对已经删除的指针在进行删除操作,发送未定义行为(通常是程序崩溃,这可能是最好的结果,因为它说明你的程序存在bug)。
2.3 用法
scoped_ptr的用法很简单:在原本使用指针变量接受new表达式结果的地方该用scoped_ptr对象,然后去掉哪些多余的try/catch和delete操作就可以了。像这样:
scoped_ptr<string> sp(new string("text"));
scoped_ptr是一种"智能指针",因此其行为与普通指针基本相同,可以使用非常熟悉的*和->操作符:
cout << *sp << endl; //取字符串的内容
cout << sp->size() << endl; //取字符串的长度
但记住:不再需要delete操作,scoped_ptr会自动地帮助我们释放资源。如果我们对scoped_ptr执行delete会得到一个编译错误:因为scoped_ptr是一个行为类似指针的对象,而不是指针,对一个对象应用delete是不允许的。
scoped_ptr不允许拷贝、赋值,只能在scoped_ptr被声明的作用域内使用。除了*和->外scoped_ptr也没有定义其他的操作符(不能对scoped_ptr进行++或者–等指针算术操作)。与普通指针相比它只有很小的接口,因此使指针的使用更加安全,更容易使用同时更不容易被误用。下面的代码都是scoped_ptr的错误用法:
sp++; //错误,scoped_ptr未定义递增操作符
scoped_ptr<string> sp2 = 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<int> p(new int); //一个int指针的scoped_ptr
if(p) //在bool语境中测试指针是否有效
{
*p = 100; //可以像普通指针一样使用解引用操作符*
cout << *p << endl;
}
p.reset(); //reset()置空scoped_ptr,仅仅是演示
assert(p == 0); //p不持有任何指针
if(!p) //在bool语境中测试,可以用!操作符
{cout << "scoped_ptr == null" << endl; }
//文件类的scoped_ptr
//将在离开作用域时自动析构,从而关闭文件释放资源
scoped_ptr<posix_file> fp(new posix_file("/tmp/a.txt"));
} //在这里发生scoped_ptr的析构
//p和fp管理的指针自动被删除
2.4 与auto_ptr的区别
scoped_ptr的用法与auto_ptr几乎一样,大多数情况下它可以与auto_ptr相互替换,它也可以从一个auto_ptr获得指针的管理权(同时auto_ptr失去管理权)。
scoped_ptr也具有auto_ptr同样的“缺陷”——不能用作容器的元素,但原因不同:
auto_ptr是因为它的转移语义scoped_ptr是因为不支持拷贝和赋值,不符合容器对元素类型的要求。
scoped_ptr与auto_ptr的根本性区别在于指针的所有权:
-
auto_ptr特意被设计为指针的所有权是可转移的,可以在函数之间传递,同一时刻只能有一个auto_ptr管理指针。它的用意是好的,但转移语义太过于微妙,不熟悉auto_ptr特性的初学者很容易误用引发错误。 -
scoped_ptr是把拷贝函数和赋值函数都声明为私有的,拒绝了指针所有权的转让——除了scoped_ptr自己,其他任何人都无权访问被管理的指针,从而保证了指针的绝对安全。
实例代码:
auto_ptr<int> ap(new int(10)); //一个int自动指针
scoped_ptr<int> sp(ap); //从auto_ptr获得原始指针
assert(ap.get == 0); //原auto_ptr不再拥有指针
ap.reset(new int(20)); //auto_ptr拥有新的指针
cout << *ap << "," << *sp << endl;
auto_ptr<int> ap2;
ap2 = ap; //ap2从ap获得原始指针,发送所有权转移
assert(ap.get() == 0); //ap不再拥有指针
scoped_ptr<int> sp2; //另一个scoped_ptr
sp2 = sp; //赋值操作,无法通过编译!!!
3 scoped_array
scoped_array很像scoped_ptr,它包装了new[]操作符(不是单纯的new)在堆上分配的动态数组,为动态数组提供了一个代理,保证可以正确的释放内存。
scoped_array弥补了标准库中没有指向数组的智能指针的缺憾。
3.1 类摘要
scoped_array的类摘要如下:
template<class T> class scoped_array //noncopyable
{
public:
explicit scoped_array(T* p = 0);
~scoped_array();
void reset(T* p = 0);
T& operator[](std::ptrdiff_t i) const;
T* get() const;
operator unspecified-bool-type() const;
void swap(scoped_array& b);
};
scoped_array的接口和功能几乎是与scoped_ptr是相同的(甚至还要少一些),主要特点如下:
- 构造函数接受的指针p必须是 new[] 的结果,而不能是new表达式的结果;
- 没有*、->操作符重载,因为
scoped_array特有的不是一个普通指针; - 析构函数使用 delete[] 释放资源,而不是 delete;
- 提供
operator[]操作符重载,可以像普通数组一样用下标访问元素; - 没有
begin()、end()等类似容器的迭代器操作函数。
3.2 用法
scoped_array与scoped_ptr源于相同的设计思想,故而用法非常相似:它只能在被声明的作用域内使用,不能拷贝、赋值。唯一不同的是scoped_array包装的是 new[] 产生的指针,并在析构时调用delete[],因为它管理的是动态数组,而不是单个动态对象。
通常scoped_array的创建方式是这样的:
scoped_array<int> sa(new int[100]); //包装动态数组
scoped_array重载了operator[],因此它用起来就像是一个普通的数组,但因为它不提供指针运算,所以不能用"数组首地址+N"的方式访问数组元素:
sa[0] = 10; //正确用法,使用operator[]
*(sa+1) = 20; //错误用法,不能通过编译
在使用重载的operator[]时要小心,scoped_array不提供数组索引的范围检查,如果使用了超过动态数组大小的索引或者是负数索引将引发未定义行为。
下面代码进一步示范了scoped_array的用法:
#include <boost/smart_ptr.hpp>
using namespace boost;
int main()
{
int *pArr = new int[100]; //一个整数的动态数组
scoped_array<int> sa(arr); //scoped_array 对象代理原始动态数组
fill_n(&sa[0],100,5); //可以使用标注库算法赋值数据
sa[10] = sa[20] + sa[30]; //用起来就像是个普通数组
} //这里 scoped_array 被自动析构,
//释放动态数组资源
3.3 使用建议
scoped_array没有给程序增加额外的负担,用起来很方便轻巧。它的速度与原始数组同样快,很适合哪些习惯于用new操作符在堆上分配内存的程序员。但scoped_array的功能很有限,不能动态增长,也没有迭代器支持,不能搭配STL算法,仅有一个纯粹的"裸"数组接口。而且,我们应该尽量避免使用new[]操作符,它比new更可怕,是许多错误的来源,因为
int *p = new int[10];
delete p;
这样的代码完全可以通过编译,无论是编译器还是程序员都很难区分new[]和new分配的空间,而错误的运用delete将导致资源异常。
在需要动态数组的情况下,我们应该使用std::vector,它比scoped_array提供了更多的灵活性,而只付出了很小的代价。使用std::vector,之前的例子可以写成:
vector<int> sa(100,5);
sa[10] = sa[20] + sa[30];
很明显,std::vector只用一条语句就完成了scoped_array三条语句的初始化和赋值工作。因为vector有丰富的成员函数来操纵数据,能够使代码更加简单明了,易于维护。
除非对性能有非常苛刻的要求,或者编译器标准库(比如某些嵌入式操作系统),否则不推荐使用scoped_array,它只是为了与老式C风格代码兼容而使用的类,它的出现往往意味着你的代码中存在着隐患。
4 shared_ptr
shared_ptr是一个最想指针的"智能指针",是boost.smart_ptr库中最有价值、最重要的组成部分,也是最有用的,Boost库的许多组件——甚至还包括其他一些领域的智能指针都使用了shared_ptr。
shared_ptr与scoped_ptr一样包装了new操作符在堆上分配的动态对象,但它实现的是引用计数型的智能指针,可以被自由地拷贝和赋值,在任意地方共享它,当没有代码使用(引用计数为0)它时,才删除被它包装的动态分配的对象。shared_ptr也可以安全地放到标准容器中,并弥补了auto_ptr因为转移语义而不能把指针作为STL容器元素的缺陷。
4.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);
template<class Y> explicit shared_ptr(std::auto_ptr<Y> & r);
shared_ptr & operator=(shared_ptr const & r);
template<class Y> shared_ptr & operator=(shared_ptr<Y> const & r);
template<class Y> shared_ptr & operator=(std::auto_ptr<Y> & 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;
operator unspecified-bool-type() const;
void swap(shared_ptr & b);
};
4.2 操作函数
shared_ptr与scoped_ptr同样是用于管理new动态分配对象的智能指针,因此功能上有很多相似之处:
- 都重载了*和->操作符以模仿原始指针的行为,
- 提供了隐式
bool类型转换以判断指针的有效性, get()可以得到原始指针,- 并且没有提供指针算术操作。
例如:
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的名字表明了它与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共享一个指针的管理权;shared_ptr(std::auto_ptr<Y> & r)从一个auto_ptr获得指针的管理权,引用计数置为1,同时auto_ptr自动失去管理权;operator=赋值操作符可以从另外一个shared_ptr或auto_ptr获得指针的管理权,其行为通构造函数;shared_ptr(Y * p,D d)行为类似shared_ptr(Y * p),但使用参数d指定了析构时的定制删除器,而不是简单的delete。
shared_ptr的reset() 函数的行为与scoped_ptr也不尽相同:
-
无参函数的作用是将引用计数减1,停止对指针的共享,除非引用计数为0,否则不会发生删除操作。
-
带参数的
reset()则类似于相同形式的构造函数,原指针引用计数减1的同时改为管理另一个指针。
shared_ptr有两个专门的函数来检查引用计数:
-
unique()在shared_ptr是指针的唯一所有者时返回true(这时shared_ptr的行为类似auto_ptr或scoped_ptr), -
use_count()返回当前指针的引用计数。要小心,use_count()应该仅仅用于测试或者调试,它不提供高效率的操作,而且有的时候可能是不可用的(极少数情形)。而unique()则是可靠的,任何时候都可以,而且比use_count()==1速度更快。
shared_ptr还支持比较运算符:
- 可以测试两个
shared_ptr的相等或不相等,比较基于内部保存的指针,相当于a.get()==b.get()。 - 还可以使用
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不能使用诸如static_cast<T*>(p.get())的形式,这将导致转型后的指针无法再被shared_ptr正确管理。为了支持这样的用法,shared_ptr提供了类似的转型函数:
static_pointer_cast<T>()<—>static_cast<T>()const_pointe_cast<T>()<—>const_cast<T>()dynamic_pointer_cast<T>()<—>dynamic_cast<T>
返回的是转型后的shared_ptr。
例如:
shared_ptr<std::exception> sp1(new bad_exception("error"));
shared_ptr<bad_exception> sp2 = dynamic_pointer_cast<bad_exception>(sp1);
shared_ptr<std::exception> sp3 = static_pointer_cast<std::exception>(sp2);
assert(sp3 == sp1);
此外,shared_ptr还支持流输出操作符operator<<,输出内部的指针值,方便调试。
4.3 用法
shared_ptr的智能使其行为最接近原始指针,因此它比auto_ptr和scoped_ptr的应用范围更广。几乎是100%可以在任何new出现的地方接受new的动态分配结果,然后被任意使用,从而完全消灭delete的使用和内存泄漏,而它的用法与auto_ptr和scoped_ptr一样简单。
shared_ptr也提供基本的线程安全保证,一个shared_ptr可以被多个线程安全读取,但其他的访问形式结果时未定义的。
示例1:
shared_ptr<int> sp(new int(10)); //一个指向整数的shared_ptr
assert(sp.unique()); //现在shared_ptr是指针的唯一持有者
shared_ptr<int> sp2 = sp; //第二个shared_ptr,拷贝构造函数
//两个shared_ptr相等,指向同一个对象,引用计数为2
assert(sp == sp2 && sp.use_count() == 2);
*sp2 = 100; //使用解引用操作符修改被指对象
assert(*sp == 100); //另一个shared_ptr也同时被修改
sp.reset(); //停止shared_ptr的使用
assert(!sp); //sp不再持有任何指针(空指针)
示例2:
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的引用计数和指向的值
{
cout << "count:" << p.use_count()
<< " v = " << *p << endl;
}
};
void print_func(shared_ptr<int> p)
{
//同样输出shared_ptr的引用计数和指向的值
cout << "count:" << p.use_count()
<< " v=" << *p << endl;
}
int main()
{
shared_ptr<int> p(new int(100));
shared s1(p),s2(p); //构造两个自定义类
s1.print();
s2.print();
*p = 20; //修改shared_ptr所指的值
print_func(p);
s1.print();
}
这段代码定义了一个类和一个函数,两者都接受shared_ptr对象作为参数,特别注意的是我们没有使用引用的方式传递参数,而是直接拷贝,就像是在使用一个原始指针。
在声明了shared_ptr和两个shared类实例后,指针被它们所共享,因此引用计数为3。print_func()函数内部拷贝了一个shared_ptr对象,因此引用计数再增加1,但当退出函数时自动析构,引用计数又恢复为3。
运行结果如下:
count:3 v=100
count:3 v=100
count:4 v=20
count:3 v=20
4.4 工厂函数
shared_ptr很好的消除了显示的delete调用,如果读者掌握了它的用法,可以肯定delete将会在你的编程字典中彻底消失。
但这还不够,因为shared_ptr的构造还需要new调用,这导致了代码中的某种不对称性。虽然shared_ptr很好的包装了new表达式,但过多的显示new操作符也是个问题,它应该使用工厂模式来解决。
因此,shared_ptr在头文件<boost/make_shared.hpp>中提供了一个自由工厂函数(位于boost名字空间)make_shared<T>(),来消除显示的new调用,它的名字模仿了标准库的make_pair(),声明如下:
template<class T,class... Args>
shared_ptr<T> make_shared(Args && ... args);
make_shared()函数可以接受最多10个参数,然后把它们传递给类型T的构造函数,创建一个shared_ptr<T>的对象并返回。make_shared()函数要比直接创建shared_ptr对象的方式快且高效,因为它内部仅分配一次内存,消除了shared_ptr构造时的开销。
用法示例:
#include <boost/make_shared.hpp>
int main()
{
shared_ptr<string> sp = make_shared<string>("make_shared"); //创建string的共享指针
shared_ptr<vector<int>> spv = make_shared<vector<int>>(10,2); //创建vector的共享指针
assert(spv->size() == 10);
}
make_shared()不能接受任意多数量的参数构造对象,一般情况下这个不会成为问题。实际上,很少有如此多的参数的函数接口,即使有,那也会是一个设计的不够好的接口,应该被重构。
除了make_shared(),smart_ptr库还提供一个allocate_shared(),它比make_shared()多接受一个定制的内存分配器类型参数,其他方面都相同。
4.5 应用于标准容器
有两种方式可以将shared_ptr应用于标准容器(或者容器适配器等其他容器)。
- 将容器作为
shared_ptr管理的对象,如shared_ptr<list<T>>,使容器可以被安全地共享,用法与普通shared_ptr没有区别。 - 将
shared_ptr作为容器的元素,如vector<shared_ptr<T>>,因为shared_ptr支持拷贝语义和比较操作,符合标准容器对元素的要求,所以可以实现在容器中安全地容纳元素的指针而不是拷贝。
对于其他类型指针:
- 标准容器不能容纳
auto_ptr,这是c++标准特别规定的; - 标准容器也不能容纳
scoped_ptr,因为scoped_ptr不能拷贝和赋值; - 标准容器可以容纳原始指针,但这就丧失了容器的许多好处,因为标准容器无法自动管理类型为指针的元素,必须编写额外的大量代码来保证指针最终被正确删除,这通常很麻烦很难实现。
存储shared_ptr的容器与存储原始指针的容器功能几乎一样,但shared_ptr为程序员做了指针的管理工作,可以任意使用shared_ptr而不用担心资源泄漏。
示例代码:
#include <boost/make_shared.hpp>
int main()
{
typedef vector<shared_ptr<int>> vs; //一个持有shared_ptr的标准容器类型
vs v(10); //声明一个拥有10个元素的容器,
//元素被初始化为空指针
int i = 0;
for(vs::iterator pos = v.begin();pos != v.end(); ++pos)
{
(*pos) = make_shared<int>(++i); //使用工厂函数赋值
cout << *(*pos) << ", "; //输出值
}
cout << endl;
shared_ptr<int> p = v[9];
*p = 100;
cout << *v[9] << endl;
}
这段代码需要注意的是迭代器和operator[]的用法,因为容器内存储的是shared_ptr,我们必须对迭代器pos使用一次解引用操作符 * 以获得shared_ptr,然后再对shared_ptr使用解引用操作符 * 才能操作真正的值。
*(*pos)也可以直接写成**pos,但前者更清晰,后者很容易让人迷惑。vector的operator[]用法与迭代器类似,也需要使用*获取真正的值。
4.6 应用于桥接模式
桥接模式(bridge)是一种结构型设计模式,它把类的具体实现细节对用户隐藏起来,以达到类之间的最小耦合关系。在具体编程实践中桥接模式也被成为pimpl或者handle/body惯用法,它可以将头文件的依赖关系降到最小,减少编译时间,而且可以不使用虚函数实现多态。
scoped_ptr和shared_ptr都可以用实现桥接模式,但shared_ptr通常更合适,因为它支持拷贝和赋值,这在很多情况下都是有用,例如配合容器使用。
例如,我们首先声明一个类sample,它仅向外界暴露了最小的细节,真正的实现在内部类impl,sample用一个shared_ptr来保存它的指针:
class sample
{
private:
class impl; //不完整的内部类声明
shared_ptr<impl> p; //shared_ptr成员变量
public:
sample(); //构造函数
void print(); //提供给外界的接口
};
//在sample.cpp中定义impl类和其他功能
class sample::impl
{
public:
void print()
{cout << "impl print" << endl;}
};
sample::sample():p(new impl){} //构造函数初始化shared_ptr
void sample::print() //调用pimpl实现print()
{p->print();}
//桥接模式的使用
sample s;
s.print();
桥接模式非常有用,它可以任意改变具体的实现而外界对此一无所知,也减小了文件间的编译依赖,是程序获得了更多的灵活性。而shared_ptr是实现它的最佳工具之一,它解决了指针的共享和引用计数问题。
4.7 应用于工厂模式
工厂模式是一种创建型设计模式,这个模式包装了new操作符的使用,是对象的创建工作集中在工厂类或者工厂函数中,从而更容易适应变化,make_shared()就是工厂模式的一个很好的例子。
但由于c++不能高效的返回一个对象,在程序中编写自己的工厂类或者工厂函数时通常需要再堆上使用new动态分配一个对象,然后返回对象的指针。这种做法很不安全,因为用户很容易忘记对指针调用delete,存在资源泄漏的隐患。
使用shared_ptr可以解决这个问题,只需要修改工厂方法的接口,不再返回一个原始指针,而是返回一个被shared_ptr包装的智能指针,这样可以很好地保护系统资源,而且会更好地控制对接口的使用。
接下来我们使用代码来解释shared_ptr应用于工厂模式的用法,首先实现一个纯抽象的基类,也就是接口类:
class abstract //接口类定义
{
public:
virtual void f() = 0;
virtual void g() = 0;
protected:
virtual ~abstract(){} //注意这里
};
注意abstract的析构函数,被定义为保护的,意味着除了它自己和它的子类,其他任何对象都无权调用delete来删除它。
然后再定义abstract的实现子类:
class impl:public abstract
{
public:
virtual void f()
{cout << "class impl f" << endl;}
virtual void g()
{cout << "class impl g" << endl;}
};
随后的工厂函数返回基类的shared_ptr:
shared_ptr<abstract> create()
{return shared_ptr<abstract>(new impl);}
这样我们就完成了全部工厂模式的实现,现在可以把这些组合起来:
int main()
{
shared_ptr<abstract> 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
这样就可以任意操作原本处于shared_ptr控制之下的原始指针了,但最好永远也不要这样做,因为也会使shared_ptr在析构时删除可能已经不存在的指针,引发未定义行为。
4.8 定制删除器
前面没有特意讨论shared_ptr一种形式的构造函数shared_ptr(Y * p,D d),它涉及shared_ptr的另一种重要概念:删除器。
shared_ptr(Y * p,D d)的第一个参数是要被管理的指针,它的含义与其他构造函数的参数相同。而第二个删除器参数d则告诉shared_ptr在析构时不是使用delete来操作指针p,而要用d来操作,即把delete p换成d§。
在这里删除器d可以是一个函数对象,也可以是一个函数指针,只要它能够像函数那样被调用,使得d§成立即可。对删除器的要求是它必须是可拷贝的,行为必须也像delete那样,不能抛出异常。
为了配合删除器的工作,shared_ptr提供一个自由函数get_deleter(shared_ptr<T> const & p),它能够返回删除器的指针。
有了删除器的概念,我们就可以用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)
{
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不仅仅能够管理内存资源,而是成为一个"万能"的资源管理工具。
4.9 高级议题
4.9.1 shared_ptr<void>
shared_ptr<void>能够存储void*型的指针,而void*型指针可以指向任意类型,因此shared_ptr<void>就像是一个泛型的指针容器,拥有容纳任意类型的能力。
但将指针存储为void*同时也丧失了原来的类型信息,为了在需要的时候正确使用,可以用static_pointer_cast<T>等转型函数重新转为原来的指针。但这涉及到运行时动态类型转换,它会使代码不够安全,建议最好不要这样使用。
4.9.2 删除器的高级用法
基于shared_ptr<void>和定制删除器,shared_ptr可以有更惊人的用法。由于空指针可以是任意指针类型,因此shared_ptr<void>还可以实现退出作用域时调用任意函数。例如:
void any_func(void *p) //一个可执行任意功能的函数
{cout << "some operate" << endl;}
int main()
{
shared_ptr<void> p((void*)0,any_func); //容纳空指针,定制删除器
} //退出作用域时将执行any_func()
shared_ptr<void>存储一个空指针,并指定了删除器是操作void*的一个函数,因此当它析构时会自动调用函数any_func(),从而执行任意我们想做的工作。
4.9.3 其他高级用法
shared_ptr的功能已经远远超出了智能指针的范围,除了以上的用法外它还有很多其他用途,如包装成员函数、延时释放等。
5 shared_array
shared_array类似shared_ptr,它包装了new[]操作符在堆上分配的动态数组,同样使用引用计数机制为动态数组提供了一个代理,可以在程序的生命周期里长期存在,直到没有任何引用后才释放内存。
5.1 类摘要
shared_array的类摘要如下:
template<class T> class shared_array{
public:
explicit shared_array(T * p = 0);
template<class D> shared_array(T * p,D d);
~shared_array();
shared_array(shared_array const & r);
shared_array & operator=(shared_array const & r);
void reset(T * p = 0);
template<class D> void reset(T * p,D d);
T & operator[](std::ptrdiff_t i) const;
T * get() const;
bool unique() const;
long use_count() const;
void swap(shared_array<T> & b);
};
shared_array的接口与功能几乎是与shared_ptr是相同的,主要区别如下:
- 构造函数接受的指针p必须是new[]的结果,而不能是new表达式的结果;
- 提供operator[]操作符重载,可以向普通数组一样用下标访问元素;
- 没有*、->操作符重载,因为
shared_array持有的不是一个普通指针; - 析构函数使用delete[]释放资源,而不是delete。
5.2 用法
shared_array就像是shared_ptr和scoped_array的结合体——即具有shared_ptr的优点,也具有scoped_array的缺点。有关shared_ptr和scoped_array的讨论大都适合它,因此这里不再详细讲解,仅给出一个下例子说明:
#include <boose/smart_ptr.hpp>
using namespace boost;
int main()
{
int *p = new int[100]; //一个动态数组
shared_array<int> sa(p); //shared_array代理动态数组
shared_array<int> sa2 = sa; //共享数组,引用计数增加
sa[0] = 10; //可以使用opearator[]访问元素
assert(sa2[0] == 10);
} //离开作用域,自动删除动态数组
同样的,在使用shared_array重载的operator[]时要小心,shared_array不提供数组索引的范围检查,如果使用了超过动态数组大小的索引或者负数索引将引发可怕的未定义行为。
shared_array能力有限,多数情况下它可以用shared_ptr<std::vector>或者std::vector<shared_ptr>来代替,这两个方案具有更好的安全性和更多的灵活性,而所付出的代价几乎可以忽略不计。
6 weak_ptr
weak_ptr是为配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->。它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
6.1 类摘要
weak_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;
void reset();
void swap(weak_ptr<T> & b);
};
weak_ptr的接口很小,正如它的名字,是一个"弱"指针,但它能够完成一些特殊的工作,足以证明它的存在价值。
6.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。
示例:
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将获得一个空指针
6.3 获得this的shared_ptr
weak_ptr的一个重要用途是获得this指针的shared_ptr,使对象自己能够生产shared_ptr管理自己:对象使用weak_ptr观测this指针,这并不影响引用计数,在需要的时候就调用lock()函数,返回一个符合要求的shared_ptr供外界使用。
这个解决方案被实现为一个惯用法,在头文件<boost/enable_shared_from_this.hpp>定义了一个助手类enbale_shared_from_this<T>,它的声明摘要如下:
template<class T>
class enable_shared_from_this
{
public:
shared_ptr<T> shared_from_this();
}
使用的时候只需要让想被shared_ptr管理的类从它继承即可,成员函数shared_from_this()会返回this的shared_ptr。例如:
#include <boost/enable_shared_from_this.hpp>
#include <boost/make_shared.hpp>
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()
{
shared_ptr<self_shared> sp = make_shared<self_shared>(314);
sp->print();
shared_ptr<self_shared> p = sp->shared_from_this();
p->x = 1000;
p->print();
}
需要注意的是千万不能从一个普通对象(非shared_ptr)使用shared_from_this()获取shared_ptr,例如:
self_shared ss;
shared_ptr<self_shared> p = ss.shared_from_this(); //错误!
这样的语法上正确,编译也无问题,但在运行时会导致shared_ptr析构时企图删除一个栈上分配的对象,发生未定义行为。
6.4 enable_shared_from_this
6.4.1 使用场景
enable_shared_from_this是为了解决**在类的内部获取自己的shared_ptr**这件事而存在的。
每一个对象都能通过this指针来访问自己的地址。this指针也是所有成员函数的隐含参数。然而有些时候,我们需要的不仅是this,而是一个"this的智能指针"。
例如,如下场景:
class A{
public:
A():did_it_(false){}
~A(){
std::cout << "destoried" << std::endl;
}
void OnDo(bool did){
did_it_ = did;
std::cout << "something did" << std::endl;
}
void DoSth_Async(){
std::thread t([this](){
std::this_thread::sleep_for(std::chrono::seconds(5));
//...do somthing
OnDo(ture);
});
t.detach();
}
private:
bool did_it_;
};
代码如上:在异步方法DoSth_Async()中调用了成员方法OnDo(bool)这里存在一个问题:
当OnDo()被调用的时候,这个类是否还在生存中:
int main()
{
{
std::shared_ptr<A> ptr(new A());
ptr->DoSth_Async();
}
std::this_thread::sleep_for(std::chrono::seconds(5));
return 0;
}
智能指着ptr在出作用域后立即被释放。所以当OnDo()被调用的时候,其所在的对象实际已经被释放了。如果确保在OnDo()被调用的时候,该对象仍然在生命周期内呢?一个方便的方法是,在构建线程的时候,将该对象的shared_ptr传入到线程。在该线程的生命周期内,该对象就会一直存在。这是一种利用shared_ptr的保活机制。
此时,enable_shared_from_this就有存在的必要了:
class A:public std::enable_shared_from_this<A>{
public:
A():did_it_(false){}
~A(){
std::cout << "destoried" << std::endl;
}
void OnDo(bool did){
did_it_ = did;
std::cout << "somthing did" << std::endl;
}
void DoSth_Async(){
auto self = shared_from_this();
std::thread t([this,self](){
std::this_thread::sleep_for(std::chrono::seconds(3));
//...do somthing
OnDo(true);
});
t.detach();
}
private:
bool did_it_;
};
enable_shared_from_this是一个模板类。它一般用作基类,它的成员shared_from_this()、weak_from_this()可以使继承此类的类从当前对象获取其本身的shared_ptr(并增加引用计数)或者weak_ptr。
我们直接使用this指针来构建自身的shared_ptr不可以吗?就像下面代码所表现的?
class C{
public:
std::shared_ptr<C> GetSelf(){
return std::shared_ptr<C>(this);
}
void DoSomthing(){
auto ptr = GetSelf();
std::cout << ptr.use_count() << std::endl;
}
};
int main(){
std::shared_ptr<C> ptr_c(new C());
ptr_c->DoSomthing(); //print 1
std::cout << ptr_c.use_count() << std::endl; //print 1
return 0;
}
这种方法在使用的时候可能看不出问题,但是当对象析构的时候,将会出现问题:一个对象将被释放两次,在DoSomthing()方法结束后它将释放一次,在main()函数完成后又将释放一次。究其原因,GetSelf()构造智能指针时,其引用计数并没有自增。
6.4.2 原理
enable_shared_from_this位于<memory>头文件中,其实现非常简单:
// CLASS TEMPLATE enable_shared_from_this
template<class _Ty>
class enable_shared_from_this
{ // provide member functions that create shared_ptr to this
public:
using _Esft_type = enable_shared_from_this;
_NODISCARD shared_ptr<_Ty> shared_from_this()
{ // return shared_ptr
return (shared_ptr<_Ty>(_Wptr));
}
_NODISCARD shared_ptr<const _Ty> shared_from_this() const
{ // return shared_ptr
return (shared_ptr<const _Ty>(_Wptr));
}
_NODISCARD weak_ptr<_Ty> weak_from_this() noexcept
{ // return weak_ptr
return (_Wptr);
}
_NODISCARD weak_ptr<const _Ty> weak_from_this() const noexcept
{ // return weak_ptr
return (_Wptr);
}
protected:
constexpr enable_shared_from_this() noexcept
: _Wptr()
{ // construct
}
enable_shared_from_this(const enable_shared_from_this&) noexcept
: _Wptr()
{ // construct (must value-initialize _Wptr)
}
enable_shared_from_this& operator=(const enable_shared_from_this&) noexcept
{ // assign (must not change _Wptr)
return (*this);
}
~enable_shared_from_this() = default;
private:
template<class _Other,class _Yty>
friend void _Enable_shared_from_this1(const shared_ptr<_Other>& _This, _Yty * _Ptr, true_type);
mutable weak_ptr<_Ty> _Wptr;
};
其中友元_Enable_shared_from_this1()将会被类shared_ptr调用。在这个友元里,会尝试着给私有成员_Wptr赋值。当shared_from_this()被调用时,使用_Wptr构造一个shared_ptr,此时引用计数增1。
6.4.3 注意事项
-
在原生指针的对象里调用了
shared_from_this代码如下:
A *a = new A(); a->DoSth_Async();由于没有使用
shared_ptr,友元_Enable_shared_from_this1()不会被调用,此时_Wptr是empty的。如果强行调用shared_from_this()将会引发异常exception:std::bad_weak_ptr。 -
过早调用
shared_from_this()代码如下:在构造函数中,调用了
shared_from_this()时,还没有给_Wptr赋值。此时会引发exception:std::bad_weak_ptrclass Sample:public std::enable_shared_from_this<Sample>{ public: Sample(){ auto ptr = shared_from_this(); } }; -
关于继承
在一课继承树里重复的继承
std::enable_shared_from_this将引发编译错误。如果需要在子类中使用shared_from_this可以这么写:class Super:public std::enable_shared_from_this<Super>{ public: virtual ~Super(){} //重要!! }; class Sub:public Super{ public: std::shared_ptr<Sub> shared_from_this(){ return std::dynamic_pointer_cast<Sub>(Super::shared_from_this()); } };
最后需要注意两点:
- 使用
std::dynamic_pointer_cast<T>()需要基类中存在虚函数,这是由于这个转换函数使用的输入类型和目标类型中,是否存在相同签名的虚函数作为转换能够成功的标注。最简单也是正确的解决方法是,将基类中的析构函数声明为虚函数。 - 不能在构造函数中使用
shared_from_this()。这是由于std::enable_share_from_this在实现时使用了一个对象的weak_ptr,而这个weak_ptr需要对象的shared_ptr进行初始化。由于此时对象尚未构造完成,所以会抛出std::bad_weak_ptr的异常。关于这点目前没有较为完美的方案,可以尝试写一个init()函数,在对象构造后手动调用。或是手动写一个std::shared_ptr<Derived>(this)使用,但这种解决方案可能造成循环引用。更多方案请查阅StackOverFlow。
7 intrusive_ptr
intrusive_ptr是一个侵入式的引用计数型指针,它可以用于以下两种情形:
- 对内存占用的要求非常严格,要求必须与原始指针一样;
- 现在代码已经有了引用计数机制管理的对象。
Boost库不推荐使用intrusive_ptr,因为shared_ptr已经非常强大且灵活,工作的足够好,可以满足绝大部分(99.99%)的需求。
如果真的有非常特别的需求,而且shared_ptr在性能、控件开销等方面影响了程序的运行(几乎不可能),那么可以参考Boost文档以了解intrusive_ptr的详细用法。
8 pool库概述
如果读者学习过操作系统相关的课程,学习过操作系统的内存管理机制和内存分配算法等知识,那么可能了解"内存池"的概念。简单来说,内存池预先分配了一块大的内存空间,然后就可以在其中使用某种算法实现高效快速的自定制内存分配。
boost.pool库基于简单分隔存储思想实现了一个快速、紧凑的内存池库,不仅能够管理大量的对象,还可以被用作STL的内存分配器。某种程度上讲,它近似于一个小型的垃圾回收机制,在需要大量地分配/释放小对象时很有效率,而且完全不需要考虑delete。
pool库包含四个组成部分:最简单的pool、分配类实例的object_pool、单件内存池singleton_pool和可用于标准库的pool_alloc。
9 pool
pool是最简单也最容易使用的内存池类,可以返回一个简单数据类型(POD)的内存指针。它位于名字空间boost,为了使用pool组件,需要包含头文件<boost/pool/pool.hpp>,即
#include <boost/pool/pool.hpp>
using namespace boost;
POD是c++标准中的技术术语,是"普通旧式数据"(Plain Old Data)的缩写。
9.1 类摘要
pool的类摘要如下:
template <typename UserAllocator= ...>
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();
};
9.2 操作函数
pool的模板类型参数UserAllocator是一个用户定义的内存分配器,它实现了特定的内存分配算法,通常可以直接默认的default_user_allocator_new_delete。
pool的构造函数接受一个size_type类型的整数requested_size,指示每次pool分配内存块的大小(而不是pool内存池的大小),这个值可以用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()。这两个函数一般情况下也不应该由程序员手工调用。
9.3 用法
pool很容易使用,可以像C中的malloc()一样分配内存,然后随意使用。除非由特殊要求,否则不必对分配的内存调用free()释放,pool会很好地管理内存。例如:
#include <boost/pool/pool.hpp>
using namespace boost;
int main()
{
pool<> pl(sizeof(int)); //一个可分配int的内存池
int *p = (int *)pl.malloc(); //必须把void*转换成需要的类型
assert(pl.isfrom(p));
pl.free(p); //释放内存池分配的内存块
for(int i = 0;i < 100;++i) //连续分配大量的内存
{
pl.ordered_malloc(10);
}
} //内存池对象析构,所有分配的内存在这里都被释放
因为pool在分配内存失败的时候不会抛出异常,所以实际编写的代码应该检查malloc()函数返回的指针,以防止空指针错误,不过通常这种情况极少出现:
int *p = (int *)pl.malloc();
if(p != NULL)
...
关于pool<>没有更多的解释,因为它真的很容易使用,只需要注意一点:它只能作为普通数据类型如int、double等的内存池,不能应用于复杂的类和对象,因为它只分配内存,不调用构造函数,这个时候我们需要用object_pool。
10 object_pool
object_pool是用于类实例(对象)的内存池,它的功能与pool类似,但会在析构时对所有分配的内存块调用析构函数,从而正确地释放资源。
object_pool位于名字空间boost,为了使用object_pool组件,需要包含头文件<boost/pool/object_pool.hpp>,即:
#include <boost/pool/object_pool.hpp>
using namespace boose;
10.1 类摘要
object_pool的类摘要如下:
template <typename ElementType>
class object_pool:protected pool
{
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);
};
10.2 操作函数
object_pool是pool的子类,但它使用的是保护继承,因此不能使用pool的接口,但基本操作还是很相似的。
object_pool的模板类型参数ElementType指定了object_pool要分配的元素类型,要求其析构函数不能抛出异常。一旦在模板中指定了类型,object_pool实例就不能再用于分配其他类型的对象。
malloc()和free()函数分别分配和释放一块类型为ElementType*的内存块,同样,可以用is_from()来测试内存块的归属,只有是本地内存池分配的内存才能被free()释放。但它们被调用时并不调用类的构造函数和析构函数,也就是操作的是一块原始内存块,里面的值是未定义的,因此我们应当尽量少使用malloc()和free()。
object_pool的特殊之处是construct()和destroy()函数,这两个函数是object_pool的真正价值所在。
construct()实际上是一组函数,有多个参数的重载形式(目前最多支持三个参数,但可以扩展),它先调用malloc()分配内存,然后再在内存块上使用传入的参数调用类的构造函数,返回的是一个已经初始化的对象指针。destory()则先调用对象的析构函数,然后再用free()释放内存块。
10.3 用法
object_pool的用法也是很简单,我们既可以像pool那样分配原始内存块,也可以使用construct()来直接在内存池中创建对象。当然,后一种使用方法是最方便的,也是本身所推荐的。
下面的代码示范了object_pool的用法:
#include <boost/pool/object_pool.hpp>
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: //对象内存池
demo_class *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;
}
} //所有创建的对象在这里都被正确析构、释放内存
10.4 使用更多的构造函数
默认情况下,在使用object_pool和construct()的时候我们只能最多使用3个参数来创建对象。大多数情况下这都是足够的,但有的时候我们可能会定义3个以上参数的构造函数,此时construct()的默认重载形式就不能用了。
但construct()被设计为是可扩展的,它基于宏预处理m4(通常Unix和Linux系统自带,也有Windows的版本)实现了一个扩展机制,可以自动生成接收任意数量参数的construct()函数。
pool库在目录/boost/pool/detail下提供了一个名为pool_construct.m4和pool_construct_simple.m4的脚本,并同时提供可在Unix/Linux和Windows下运行的同名sh和bat可执行脚本文件。只需要简单地向批处理脚本传递一个整数的参数N,m4就会自动生成能够创建具有N个参数的construct()函数源代码。
例如,在Linux下,执行命令:
./pool_construct_simple.sh 5;./pool_construct.sh 5
将生成两个同名的.inc文件,里面包含了新的construct()函数定义,能够支持最多传递5个参数创建对象。由于m4生成的是c++源代码,因此.inc文件也可以拷贝到其他操作系统的boost库中使用。
如果只是临时的需要增加construct()函数的参数数量,或者工作的系统上m4不可用,我们也可以简单地定义一个辅助模板函数。
下面的代码模仿construct()函数实现了一个可接受4个参数的创建函数:
template<typename P,typename T0,typename T1,typename T2,typename T3>
inline typename P::element_type*
construct(P& p,const T0& a0,const T1& a1,const T2& a2,const T3& a3)
{
typename P::element_type* mem = p.malloc();
assert(mem != 0);
new(mem) P::element_type(a0,a1,a2,a3);
return mem;
}
自由函数construct()接受5个参数,第一个是object_pool对象,其后是创建对象所需的4个参数,要创建的对象类型可以使用object_pool的内部类型定义element_type来获得。函数中首先调用malloc()分配一块内存,然后调用不太常见的"定位new表达式"(placement new expression)创建对象。
假设我们有如下的一个4参数构造函数的类和一个object_pool对象:
struct demo_class
{
demo_class(int,int,int,int)
{cout << "demo_class ctor" << endl;}
~demo_class()
{cout << "demo_class dtor" << endl;}
};
object_pool<demo_class> pl;
那么使用m4和自定义的construct()创建对象的代码就是:
demo_class* d1 = pl.construct(1,2,3,4); //使用m4扩展
demo_class* d2 = construct(pl,1,2,3,4); //使用自定义扩展
11 singleton_pool
singleton_pool与pool的接口完全一致,可以分配简单数据类型(POD)的内存指针,但它是一个单件,并提供线程安全。
由于目前Boost还未提供标准的单件库,singleton_pool在其内部实现了一个较简单、泛型的单件类,保证在main()函数运行之前就创建单件。
singleton_pool位于名字空间boost,为了使用singleton_pool组件,需要包含头文件<boost/pool/singleton_pool.hpp>,即:
#include <boost/pool/singleton_pool.hpp>
using namespace boost;
11.1 类摘要
singleton_pool的类摘要如下:
template<typename Tag,unsigned RequestedSize>
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();
};
11.2 用法
singleton_pool主要有两个模板类型参数(其余的可以使用却省值)。第一个Tag仅仅是用于标记不同的单件,可以是空类,甚至是声明(这个用法还被用于boost.exception)。第二个参数RequestedSize等同于pool构造函数中的整数requested_size,指示pool分配内存块的大小。
singleton_pool的接口与pool完全一致,但成员函数均是静态的,因此不需要声明singleton_pool的实例,直接用域操作符::来调用静态成员函数。因为sigleton_pool是单件,所以它的生命周期与整个程序同样长,除非手动调用release_memory()或purge_memory(),否则singleto_pool不会自动释放所占用的内存。除了这两点,singleton_pool的用法与pool完全相同。
下面的代码示范了singleton_pool的用法:
#include <boost/pool/singleton_pool.hpp>
using namespace boost;
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;
12 pool_alloc
pool_alloc提供了两个可以用于标准容器模板参数的内存分配器,分别是pool_alloc和fast_pool_allocator,它们的行为与之前的内存池类有一点不同——当内存分配失败时,会抛出异常std::bad_alloc。它们位于名字空间boost,需要包含头文件<boost/pool/pool_alloc.hpp>。
除非有特别的需求,我们应该总使用STL实现自带的内存分配器,使用pool_alloc需要经过仔细的测试,以保证它与容器可以共同工作。
下面的代码示范了pool_alloc的用法:
#include <boost/pool/pool_aclloc.hpp>
using namespace boost;
int main()
{
//使用pool_allocator代替标准容器默认的内存分配器
vector<int,pool_allocator<int>> v;
v.push_back(10); //vector将使用新的分配器良好工作
cout << v.size();
}
13 总结
内存管理是c++程序开发中永恒的话题,因为没有垃圾回收机制,小心谨慎地管理内存等系统资源是每一个c++程序员都必须面对的问题。c++98标准提供了auto_ptr,可以自动释放资源,但没有解决所有问题。本章讨论了Boost关于内存管理的两个库:smart_ptr和pool,并对smart_ptr倾注了大量的篇幅。
boost.smart_ptr库提供了数种新型智能指针,弥补了std::auto_ptr的不足,可以有效地消除new和delete的显示使用,减少甚至杜绝代码资源泄漏。
scoped_ptr是smart_ptr库中最容易学习和使用的一个,它的行为类似auto_ptr,但所有权更明确,清晰地表明了这种智能指针只能在声明的作用域中使用,不能转让,任何对它的复制企图都会失败。这个特点对代码的后期维护工作非常有用。
shared_ptr可能是最有用的智能指针,也是这些智能指针中最"智能"的一个,不仅可以管理内存,也可以管理其他系统资源,能够应用于许多场合。它可以自动地计算指针的引用计数,其行为最接近原始指针。几乎可以在任何可以使用原始指针的地方使用shared_ptr,并且不用承担资源泄漏的风险。shared_ptr不仅可以保存指针,通过配置删除器也可以自动释放指针关联的资源。
在基本的用法之外,我们还讨论了shared_ptr的很多其他用法,如实现pimpl惯用法(桥接模式)、应用与工厂模式、持有任意对象的指针等,这些用法进一步展示了它的强大功能。为了方便shared_ptr的使用,smart_ptr库还提供了工厂函数make_shared(),进一步消除了代码中new操作符的使用。
scoped_array和shared_array是scoped_ptr和shared_ptr对动态数组的扩展,它们为动态数组提供了可自动删除的代理,shared_array比scoped_array有更多的用途。但我们更应该使用vector和shared_ptr<vector<>>,除非程序对性能有非常苛刻的要求。
本章还简要讨论了smart_ptr的另两个组件:weak_ptr能够"静态"地观察shared_ptr而不影响引用计数,intrusize_ptr则为实现浸入式智能指针提供了技术方案。
pool库是Boost程序库在内存管理方面提供的另一个有用工具,它实现了高效的内存池,用于管理内存资源。pool库提供了pool、object_pool、singleton_pool和pool_alloc四种形式的内存池,适合于各种情形的应用。可以完全把它们当做一个小型的垃圾回收机制,在内存池中随意地动态创建对象,而完全不用关心它的回收,也不用对原有类做任何形式的修改。
pool库的四个内存池类中前三个都很有用,尤其是object_pool,它可以统一地管理各种对象的创建与销毁,能够很好地应用在各种规模的面向对象软件系统中。至于pool_alloc,它是符合c++标准的一个内存分配器实现,快速且高效,但通常STL自带的内存分配器会更好地与容器配合工作,使用pool_alloc时需要仔细地评估可能带来的影响。
pool库还提供一个底层的实现类simple_segregated_storage,它实现了简单分配存储的管理机制,是pool库其他类的基础。它不适合大多数库用户,但可以作为自行实现内存池类的一个很好的起点。
&spm=1001.2101.3001.5002&articleId=144173805&d=1&t=3&u=c2a18b60fb0d4f518e110ac80368c8c3)
166

被折叠的 条评论
为什么被折叠?



