以下四个智能指针是我们常用的智能指针
1.auto_ptr 管理权转移,带有缺陷的设计。尽量不要使用它
2.unique_ptr 防拷贝,高效简洁。不需要拷贝/赋值,尽量使用它
3.shared_ptr 共享(引用计数)管理、支持赋值/拷贝。缺陷:循环使用(针对循环使用有weak_ptr).
4.weak_ptr辅助shared_ptr解决循环引用,不增加引用计数。
class Test{
public:
Test(int a = 0)
: _a(a)
{}
~Test(){
cout << "Calling destructor" << endl;
}
public:
int _a;
};
int main() {
auto_ptr ptr(new Test(5));
cout << ptr->_a << endl;
return 0;
}
auto_ptr完成了智能指针的基本功能,在申请资源的时候,调用构造函数进行初始化,达到资源申请即初始化的目的,同时当main函数结束时,ptr出了这个作用域也会自动调用析构函数来释放资源。
但是同时,auto_ptr因为它本身的特点(这个指针通过在拷贝构造和赋值运算符重载时采用管理权转移实现),会出现很多问题。
void Fun(auto_ptr ptr1) {
cout << ptr1->_a << endl;
}
int main() {
auto_ptr ptr(new Test(5));
cout << ptr->_a << endl;
Fun(ptr);
cout << ptr->_a << endl;
return 0;
}
上面的程序会崩溃。本来ptr拥有一块自己的内存,当Fun函数调用后,ptr关联的内存的所有权转移给了Fun函数的参数ptr1,ptr1是ptr的一份拷贝(因为这里采用的是值传递),ptr1拥有的是原来ptr拥有的内存块。当Fun函数执行完以后,ptr1拥有的内存块也就因为智能指针的特性随之释放了。而ptr这个对象在函数传参时它的所有权就没有了,它变成了一个空对象,它内部的指针也变成了一个空指针,这个时候再尝试去调用ptr->_a,就是是解引用空指针,程序崩溃。
然而上述在拷贝之后使用原来指针的情况是经常发生的!因此auto_ptr非常不被推荐使用
下面是关于auto_ptr的模拟实现
template<class T>
class Auto_ptr{
public:
//构造函数
Auto_ptr(T* ptr)
:_ptr(ptr)
{}
//拷贝构造函数
Auto_ptr(Auto_ptr& a)
:_ptr(a._ptr){
//因为要转移指针权限,原来的指针就没有权限了
//如果在这里不像函数体内部这样将a中的_ptr置为NULL,那么a中的_ptr就是指向了一块已经释放了的空间
//再调用a的析构,此时就会出现重复delete的问题
//可以尝试着把下面一行注释掉,这样程序就会崩溃了(如果进行了拷贝构造)
a._ptr = NULL;
}
//赋值运算符重载
Auto_ptr& operator=(Auto_ptr& a){
_ptr = a._ptr;
a._ptr = NULL;
return *this;
}
//析构函数
~Auto_ptr(){
std::cout << "In ~Auto_ptr()" << std::endl;
if(_ptr){
//这里一定要对_ptr判空,如果不为空才delete
delete _ptr;
_ptr = NULL;
}
}
//把这个_ptr设为public方便测试
T* _ptr;
};
因此,在这里总结一下auto_ptr的缺点,缺点太多啦,不推荐使用。
关于第三个缺点,可以看下面的代码
//注意下面的代码是在VS2017环境下测试的,可能因为编译器不同,下面的代码就直接编译不过
int main() {
vector< auto_ptr<int> > v(5);
int i = 0;
for (; i<5; i++)
{
v[i] = auto_ptr<int>(new int(i));
}
vector< auto_ptr<int> > v1(v);
return 0;
}
原来的v中的auto_ptr都变为空了,但这并不是我们的本意,我们不想改变原有容器。
创建
unique_ptr<int> uptr( new int );
unique_ptr相比于auto_ptr,它还可以创建数组对象的特殊方法,当指针离开作用域时,调用delete []来代替delete。当创建unique_ptr时,这一组对象被视作模板参数的部分。这样,程序员就不需要再提供一个指定的析构方法,如下:
unique_ptr<int[ ]> uptr( new int[5] );
可以通过下面的转移语义来转移所有权
unique_ptr<int> ptr(new int(5));
unique_ptr<int> ptr1 = move(ptr);//转移了以后ptr就是一个空对象,内部指针是空指针
接口
unique_ptr提供的接口和传统指针差不多,但是不支持指针运算。
unique_ptr提供一个release()的方法,释放所有权。release和reset的区别在于,release仅仅释放所有权但不释放资源,reset也释放资源。
创建
shared_ptr<int> ptr( new int );
使用make_shared宏来加速创建的过程。
shared_ptr<int> ptr = make_shared<int>(100);
两种创建方式的区别:默认的构造函数会申请两次内存,而make_shared只会申请一次内存。因为shared_ptr内部有一个引用计数以及存放数据的内存,等于说有两部分内存,默认构造函数为数据内存和引用计数每个分别申请一次内存,而make_shared将数据和引用计数的内存申请放到一起。
关于make_shared的缺点
make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题
析构
如果用户采用一个不一样的析构策略时,可以自由指定构造这个shared_ptr的策略
shared_ptr sptr1( new Test[5],
[ ](Test* p) { delete[ ] p; } );
上面的代码表示创建了一个数组对象,并采用了指定的delete []来析构
接口
下面是关于shared_ptr的模拟实现
template<class T>
class Shared_ptr{
public:
Shared_ptr(T* ptr)
:_ptr(ptr),
_ref(new int(1))
{}
Shared_ptr(const Shared_ptr& a)
:_ptr(a._ptr),
_ref(a._ref)
{
std::cout << "拷贝前计数:" << *_ref << std::endl;
//让引用计数++
(*_ref)++;
std::cout << "拷贝后计数:" << *_ref << std::endl;
}
Shared_ptr& operator=(const Shared_ptr& a){
if(_ptr != a._ptr){
int *tmp = _ref;
std::cout << "赋值前原有对象计数:" << *_ref << std::endl;
if(--(*_ref) == 0){
//因为当前引用对象要去引用a对象的引用计数了
//如果当前引用对象只剩一个了,即这个即将要去引用a对象的对象
//那么当前引用对象的计数就为0,此时需要释放该对象
delete _ptr;
delete _ref;
}
std::cout << "赋值后原有对象计数:" << *tmp << std::endl;
std::cout << "赋值前被赋值对象计数:" << *a._ref << std::endl;
_ptr = a._ptr;
_ref = a._ref;
(*_ref)++;
std::cout << "赋值前被赋值对象计数:" << *_ref << std::endl;
}
}
~Shared_ptr(){
if(--(*_ref) == 0){
delete _ptr;
delete _ref;
_ptr = NULL;
_ref = NULL;
}
}
int* _ref;
T* _ptr;
};
但是shared_ptr也是会产生一些问题
int main() {
shared_ptr<int> sptr1(new int); //引用计数为1
shared_ptr<int> sptr2 = sptr1; //引用计数为2
shared_ptr<int> sptr3;
sptr3 = sptr1; //引用计数为3
//析构的时候依次析构sptr3,sptr2,sptr1,引用计数依次减到0,并释放资源
return 0;
}
上面的代码没有问题,下面的代码就有问题了
int main() {
int* p = new int;
shared_ptr<int> sptr1(p);
shared_ptr<int> sptr2(p);
return 0;
}
因为sptr2并不是拷贝sptr1或者通过sptr1赋值过来的,因此两个shared_ptr的引用计数其实都是1,在析构的时候,sptr2先析构,并释放了资源,但是sptr1也是由这个资源初始化的,并且它的引用计数并不是0,所以析构sptr1的时候,就会出现重复释放的问题,释放已经释放了的内存
避免上述问题的方法就是尽量不要从一个裸指针中创建shared_ptr。
当下面的代码运行时,会出现无限循环的问题
struct Node //链表节点的定义
{
int _data;
shared_ptr _prev;
shared_ptr _next;
Node(int data)
:_data(data),
_prev(NULL),
_next(NULL)
{}
};
void Test() {
shared_ptr p1(new Node(1));
cout << p1.use_count() << endl;
shared_ptr p2(new Node(2));
cout << p2.use_count() << endl;
//让p2是p1的next
p1->_next = p2;
cout << p2.use_count() << endl;
//让p1是p2的prev
p2->_prev = p1;
cout << p1.use_count() << endl;
}
int main() {
Test();
return 0;
}
在p1,p2离开作用域时它们的引用计数还是1,也就是说,因为它们的引用计数是1,所以它们的资源其实都没有释放!!!
因此针对上述可能出现的循环引用的问题,引入了weak_ptr
创建
可以以shared_ptr作为参数构造weak_ptr。从shared_ptr创建一个weak_ptr增加了共享指针的弱引用计数(weak_ref,这个引用计数之前介绍过,是专门为了weak_ptr设计的)。但是当shared_ptr离开作用域时,这个计数(弱引用计数)不作为是否释放资源的依据。换句话说,就是除非强引用计数变为0,才会释放掉指针指向的资源
int main() {
shared_ptr<int> sptr(new int(5)); //强引用计数+1
weak_ptr<int> wptr(sptr); //强引用计数不变,弱引用计数+1
weak_ptr<int> wptr1 = wptr; //强引用计数不变,弱引用计数+1
return 0;
}
将一个weak_ptr赋给另一个weak_ptr会增加弱引用计数
所以,当shared_ptr离开作用域时,其内的资源释放了,这时候指向该shared_ptr的weak_ptr发生了什么?weak_ptr过期了(expired)
int main() {
shared_ptr<int> sptr(new int(5)); //强引用计数+1
weak_ptr<int> wptr(sptr); //强引用计数不变,弱引用计数+1
weak_ptr<int> wptr1 = wptr; //强引用计数不变,弱引用计数+1
sptr.reset(); //强引用计数变为0,弱引用计数不变
return 0;
}
如何判断weak_ptr是否指向有效资源,有两种方法:
从weak_ptr调用lock()可以得到shared_ptr
int main() {
shared_ptr<int> sptr(new int(5));
weak_ptr<int> wptr(sptr);
weak_ptr<int> wptr1 = wptr;
shared_ptr<int> sptr2 = wptr.lock();
return 0;
}
从weak_ptr中获取shared_ptr增加强引用计数(上面的图中因为调用了lock强引用从1变成了2)。
所以对于上述的测试代码,只要将结构体中的shared_ptr改成weak_ptr就可以了
struct Node //链表节点的定义
{
int _data;
weak_ptr _prev;
weak_ptr _next;
Node(int data)
:_data(data)
{}
};
void Test() {
shared_ptr p1(new Node(1));
shared_ptr p2(new Node(2));
//让p2是p1的next
p1->_next = weak_ptr(p2);
//让p1是p2的prev
p2->_prev = weak_ptr(p1);
}
int main() {
Test();
return 0;
}
因为p1和p2的强引用计数都是0了,所以它们的资源就被释放,很好地解决了循环引用问题。