一、为什么使用智能指针
为管理内存等资源,C++采取RAII机制(资源获取即初始化,Resource Acquisition Is Initialization),在使用资源的类的构造函数中申请资源并使用,最终在析构函数中释放资源。使用new在堆上创建对象时,其析构函数不会自动调用,需要使用delete才能释放资源,若因为异常导致程序未能执行delete,则存在内存泄露的问题。C++98标准中的“自动指针std::auto_ptr(C++11中废弃,改用unique_ptr)部分解决了获取资源自动释放的问题。
二、boost库提供的智能指针
boost.smart_ptr库提供六种智能指针,包括socped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr和intrusive_ptr。他们都是轻量级的对象,速度与原始指针相差无几,都是异常安全的,而且对于所指向的类型T也仅有一个很小且合理的要求:类型T的析构函数不能抛出异常。
头文件:#include<boost/smart_ptr.hpp>
scoped_ptr | 保证智能指针只能在本作用域中使用,拥有对象的唯一所有权,不可以复制。如果一个类中有scoped_ptr成员变量,则该类也不可拷贝或赋值。 | <boost/scoped_ptr.hpp> |
scoped_array | 类似scoped_ptr,只不过包装的是new[ ]操作符,不推荐使用 | <boost/scoped_array.hpp> |
shared_ptr | 实现了引用计数,可以拷贝或赋值,当引用计数为0时自动删除动态分配的对象 | <boost/shared_ptr.hpp> |
shared_array | 类似shared_ptr,但包装的是new[ ]分配的动态数组 | <boost/shared_array.hpp> |
weak_ptr | 配合shared_ptr而引入,不具备普通指针的行为,作为shared_ptr拥有对象的非拥有观察者 | <boost/weak_ptr.hpp> |
intrusive_ptr | 引用计数型智能指针,可以包装已有对象得到与shar_ptr类似的智能指针 | <boost/intrusive_ptr.hpp> |
1. socped_ptr
scoped_ptr包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确地删除,scoped_ptr获取对象管理权后就不能再转让。
scoped_ptr的构造函数接受一个类型为T*的指针p,创建出一个scoped_ptr对象,并在内部保存指针参数p。p必须是new表达式动态分配的结果,或者是一个空指针(nullptr)。当scoped_ptr对象的生命期结束时,析构函数~scoped_ptr()会使用delete操作符自动销毁所保存的指针对象来正确回收资源。拷贝构造函数和赋值操作符被声明为私有的,禁止对智能指针的拷贝操作,保证了被它管理的指针不能被转让所有权。
scoped_ptr提供在bool语境(如if的条件表达式)中自动转换成bool值的功能,用来测试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,将在离开作用域时自动析构,从而关闭文件释放资源
scopoed_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管理的指针自动被删除
2. scoped_array
包装了new[ ]操作符在堆上分配的动态数组,为动态数组提供了一个代理,来保证正确释放内存。主要特点:
- 构造函数接受的指针p必须是new[ ]的结果,而不是new表达式的结果;
- 没有*、->操作符重载,因为scoped_array持有的不是一个普通指针;
- 析构函数使用delete[ ]操作重载,而不是delete;
- 提供operator操作符重载,可以想普通数组一样用下标访问元素;
- 没有begin()、end()等类似容器的迭代器操作函数。
#include<boost/smart_ptr.hpp>
using namespace boost;
int main(){
int *arr = 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. shared_ptr
shared_ptr是最像指针的”智能指针“,是boost.smart_ptr库中最有价值、最重要、最有用的组成部分。他包装了new操作符在堆上分配的动态对象,实现了引用计数,可以自由地拷贝和赋值,在任意地方进行共享。也可以安全地放到标准容器中,是在STL容器中储存指针的最标准解法。
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;
- 别名构造函数(aliasing),不增加引用计数的特殊用法。
reset()函数将shared_ptr的引用计数减1,停止对指针的共享,除非引用计数为0,否则不会发生删除操作;带参数的reset()类似相同形式的构造函数,原引用计数减1的同时改为管理另一个指针。
unique()和use_count()专门用来检查引用计数,unique()在指针是唯一所有者时返回true,是可靠的,任何时候都可用;use_count()返回当前指针的引用计数,不提供高效率的操作。
shared_ptr支持比较运算,可以测试两个shared_ptr的相等或不相等,比较基于内部保存的指针,相当于a.get() == b.get();还可以使用operator<比较大小,但不提供除此外的比较操作符。
此外shared_ptr还支持流输出操作符operator<< ,输出内部的指针值,方便调试。
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();
}
工厂函数make_shared()可以接受若干参数,然后把它们传递给类型T的构造函数,创建一个shared_ptr<T>对象并返回。这要比直接创建shared_ptr对象的方式快且高效,因为内部仅分配了一次内存,消除了shared_ptr构造时的开销。allocate_shared()与make_shared()相同,但多一个定制的内存分配器类型参数。
auto sp = make_shared<string>("make_shared"); //创建string的共享指针
auto spv = make_shared<vector<int>>(10,2); //创建vector的共享指针
shared_ptr应用于标准容器、应用于桥接模式、应用于工厂函数、定制删除器.......
4. shared_array
shared_array与shared_ptr基本相同,具有shared_ptr的优点和scoped_array的缺点,主要区别:
- 构造函数接受的指针p是new[ ] 的结果;
- 提供operator[ ] 操作符重载,可以像数组一样用下标访问元素;
- 没有*、->操作符重载;
- 析构函数使用delete [ ] 释放资源,而不是delete。
#include<boost/smart_ptr.hpp>
using namespace boost;
int main{
int *p = new [100]; //一个动态数组
shared_array<int> sa(p); //shared_array代理动态数组
assert(sa.unique()); //唯一持有指针
shared_array<int> sa2 = sa; //共享数组,引用计数增加
assert(sa2.use_count()==2); //引用计数增加
sa[0] = 10; //可以使用operator[]访问元素
assert(sa2[0] == 10);
} //离开作用域,自动删除动态数组
shared_array不提供数组索引的范围检查,如果使用超过动态数组大小的索引或负索引将引发未定义行为。
5. weak_ptr
weak_ptr被设计与shared_ptr协同工作,可以从一个shared_ptr或另一个weak_ptr对象构造,获得资源的观察权,构造和析构不会引起指针引用计数的增加或减少。
use_count()可以观测资源的引用计数,expired()的功能等价于use_count()==0,但更快,表示被观测的资源不复存在。lock() 从被观测的shared_ptr获得一个可用的shared_ptr对象,把弱关系转换为强关系,从而操作资源,当expired()==true时,lock()返回空指针的shared_ptr。
enable_shared_from_this
weak_ptr一个重要用途是获取this指针的shared_ptr,使对象能够自己生产shared_ptr管理自己:对象使用weak_ptr观测this指针,这并不影响引用计数,需要时调用lock()函数,返回一个符合要求的shared_ptr供外界使用。成员函数shared_from_this()会返回this的shared_ptr。使用方法:
class self_shared :
public enable_shared_from_this<self_shared>{
pulic:
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(); //返回this的shared_ptr
p -> x = 1000;
p -> print();
}
enable_shared_from_raw
与enable_shared_from_this类似,但不要求对象必须被一个shared_ptr管理,可以直接从一个原始指针创建出shared_ptr。
#include<boost/smart_ptr/enable_shared_from_row.hpp>
class raw_shared : public boost::enable_shared_from_raw{
public:
raw_shared(){
cout << "raw_shared ctor" << endl;
}
~raw_shared(){
cout << "raw_shared dtor" << endl;
}
};
int main(){
raw_shared x; //一个普通对象
asser(!weak_from_raw(&x).use_count()); //此时无引用,注意要用&取地址
auto px = shared_from_raw(&x); //获取shared_ptr
assert(px.use_count()==2); //引用计数为2!
} //对象自动删除
循环引用
代码中出现“循环引用”时,shared_ptr的引用机制会失效,导致不能正确析构释放资源。使用weak_ptr在可能存在循环引用的地方打破循环,在需要shared_ptr的时候调用weak_ptr的lock()函数。
class node{ //链表节点的类
public:
...
typeder 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均正确析构
6. intrusive_ptr
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()多出一个bool add_ref参数,表示是否增加引用计数,如果add_ref == true,那么它就相当于weak_ptr,只是简单地观察对象。
参考文档:
注:
assert是C/C++提供的准确性验证、测试支持的宏,详见 BOOST程序库完全开发指南第6章:正确性与测试