1、什么是智能指针
几乎每一个有分量的程序都需要“在相同时间的多处地点处理或使用对象”的能力。为此,我们必须在程序的多个地点指向(refer to)同一对象。虽然C++语言提供引用(reference)和指针(pointer),还是不够,因为我们往往必须确保当“指向对象”的最末一个引用被删除时该对象本身也被删除,毕竟对象被删除时析构函数可以要求某些操作,例如释放内存或归还资源等等。
在C++中,为了确保指针的寿命和其所指向的对象的寿命一致是件棘手的事,特别是当多个指针指向同一对象时。例如,为了让多个集合拥有同一对象,你必须把指向该对象的指针放进那些集合内,而且当其中一个指针被销毁时不该出现问题,也就是不该出现所谓的空悬指针(也叫野指针)或多次删除被指向对象,最后一个指针被销毁时也不该出现资源泄露问题。为了避免上述问题的一个通用做法是使用智能指针,智能指针能够知道它自己是不是指向某物的最后一个指针,并且运用这样的知识,在它的确是该对象的最后一个拥有者而且它被删除时,销毁它所指向的对象。shared_ptr的目标就是,在其所指向的对象不再被使用之后(而非之前),自动释放与对象相关的资源。
2、智能指针的实现(参考)
智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类(智能指针自身保存在栈中),用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。它的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。智能指针类的具体实现参考如下:
#include <iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
SmartPtr(T *p); // 构造函数
~SmartPtr(); // 析构函数
SmartPtr(const SmartPtr<T> &orig); // 复制构造函数
SmartPtr<T>& operator=(const SmartPtr<T> &rhs); // 赋值运算操作符
T& operator*(); // 解引用
T* operator->(); // 取成员操作符
private:
T *ptr;
int *use_count; // 将use_count声明成指针是为了方便对其的递增或递减操作
};
template<class T>
SmartPtr<T>::SmartPtr(T *p) : ptr(p) //构造函数定义
{
try
{
use_count = new int(1);
}
catch (...)
{
delete ptr;
ptr = nullptr;
use_count = nullptr;
cout << "Allocate memory for use_count fails." << endl;
exit(1);
}
cout << "Constructor is called!" << endl;
}
template<class T>
SmartPtr<T>::~SmartPtr() //析构函数定义
{
// 只在最后一个对象引用ptr时才释放内存
if (--(*use_count) == 0)
{
delete ptr;
delete use_count;
ptr = nullptr;
use_count = nullptr;
cout << "Destructor is called!" << endl;
}
}
template<class T>
SmartPtr<T>::SmartPtr(const SmartPtr<T> &orig) //复制构造函数定义
{
ptr = orig.ptr;
use_count = orig.use_count;
++(*use_count);
cout << "Copy constructor is called!" << endl;
}
// 重载等号函数不同于复制构造函数,即等号左边的对象可能已经指向某块内存。
// 这样,我们就得先判断左边对象指向的内存已经被引用的次数。如果次数为1,
// 表明我们可以释放这块内存;反之则不释放,由其他对象来释放。
template<class T>
SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T> &rhs) //赋值运算操作符定义
{
// 《C++ primer》:“这个赋值操作符在减少左操作数的使用计数之前使rhs的使用计数加1,
// 从而防止自身赋值”而导致的提早释放内存
++(*rhs.use_count);
// 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象
if (--(*use_count) == 0)
{
delete ptr;
delete use_count;
cout << "Left side object is deleted!" << endl;
}
ptr = rhs.ptr;
use_count = rhs.use_count;
cout << "Assignment operator overloaded is called!" << endl;
return *this;
}
template<class T>
T& SmartPtr<T>::operator*()
{
return *(this->ptr);
}
template<class T>
T* SmartPtr<T>::operator->()
{
return this->ptr;
}
int main()
{
SmartPtr<int> ptr(new int(10));
std::cout << *ptr << std::endl;
return 0;
}
3、几种常见的智能指针
(1)auto_ptr
auto_ptr是C++98提供的,它不支持复制(拷贝构造函数)和赋值(operator =),此类模板已被弃用,unique_ptr是具有相似功能但具有改进的安全性的新工具。auto_ptr是一个智能指针,用于管理通过新表达式获取的对象,并在auto_ptr本身被销毁时删除该对象。当使用auto_ptr类描述一个对象时,它存储一个指向单个分配对象的指针,该对象可以确保当它超出范围时,它指向的对象必须被自动销毁。 它基于独占所有权模式,即同一类型的两个指针不能同时指向相同的资源。auto_ptr的拷贝构造函数和赋值运算符实际上并没有复制存储的指针,而是将它们传输出去,使第一个auto_ptr对象变为空。 这是实现严格所有权的一种方法,因此只有一个auto_ptr对象可以在任何给定时间拥有该指针,即在需要复制语义的情况下不应使用auto_ptr。
(2)unique_ptr
std :: unique_ptr是在C ++ 11中开发的,用于替代std :: auto_ptr。unique_ptr是具有类似功能的新工具,但具有改进的安全性(无假拷贝分配),添加功能(删除器)和数组支持。 它是一个原始指针的容器。 它明确地防止复制其包含的指针,正如正常赋值那样会发生,即它只允许底层指针的一个所有者。所以,当使用unique_ptr时,在任何一个资源上最多只能有一个unique_ptr,当该unique_ptr被破坏时,该资源将被自动声明。 另外,由于任何资源只能有一个unique_ptr,所以任何创建unique_ptr副本的尝试将导致编译时错误。
unique_ptr<A> ptr1 (new A);
unique_ptr<A> ptr2 = ptr1; // Error: can't copy unique_ptr
但是,unique_ptr可以使用新的语义,即使用std :: move()函数将包含的指针的所有权转移到另一个unique_ptr。
unique_ptr<A> ptr2 = move(ptr1); //此时ptr1为NULL,ptr2指向该对象
(3)shared_ptr
shared_ptr是原始指针的容器。 它是一个引用计数模型,即它与shared_ptr的所有副本合作维护其包含的指针的引用计数。 因此,每当一个新的指针指向资源时,计数器就会递增,当调用对象的析构函数时递减计数器。引用计数:这是一种将资源数量,指针或句柄存入资源(如对象,内存块,磁盘空间或其他资源)的技术。引用计数大于0,直到所有的shared_ptr副本都被删除,所包含的原始指针引用的对象将不会被销毁。因此,当我们要为一个原始指针分配给多个所有者时,我们应该使用shared_ptr。
#include<iostream>
#include<memory>
using namespace std;
class A
{
public:
void show()
{
cout<<"A::show()"<<endl;
}
};
int main()
{
shared_ptr<A> p1 (new A);
cout << p1.get() << endl;
p1->show();
shared_ptr<A> p2 (p1);
p2->show();
cout << p1.get() << endl;
cout << p2.get() << endl;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p1.reset();
cout << p1.get() << endl;
cout << p2.use_count() << endl;
cout << p2.get() << endl;
return 0;
}
输出:
0x1c41c20
A::show()
A::show()
0x1c41c20
0x1c41c20
2
2
0 // NULL
1
0x1c41c20
关于shared_ptr的线程安全级别:
(shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。根据文档(http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm#ThreadSafety), shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:
- 一个 shared_ptr 对象实体可被多个线程同时读取(文档例1);
- 两个 shared_ptr 对象实体可以被两个线程同时写入(例2),“析构”算写操作;
- 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁(例3~5)。
请注意,以上是 shared_ptr 对象本身的线程安全级别,不是它管理的对象的线程安全级别。
(4)weak_ptr
虽然使用shared_ptr可以在对象的引用个数为0时自动销毁对象,但是shared_ptr也存在一些缺点,比如循环引用问题。“循环引用”简单来说就是:两个对象互相使用一个shared_ptr成员变量指向对方的会造成循环引用。导致引用计数失效。下面给段代码来说明循环引用:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:// 为了省去一些步骤这里 数据成员也声明为public
//weak_ptr<B> pb;
shared_ptr<B> pb;
void doSomthing()
{
// if(pb.lock())
// {
//
// }
}
~A()
{
cout << "kill A\n";
}
};
class B
{
public:
//weak_ptr<A> pa;
shared_ptr<A> pa;
~B()
{
cout <<"kill B\n";
}
};
int main(int argc, char** argv)
{
shared_ptr<A> sa(new A());
shared_ptr<B> sb(new B());
if(sa && sb)
{
sa->pb=sb;
sb->pa=sa;
}
cout<<"sa use count:"<<sa.use_count()<<endl;
return 0;
}
输出:sa use count:2
注意此时sa,sb都没有释放,产生了内存泄露问题。即A内部有指向B,B内部有指向A,这样对于A,B必定是在A析构后B才析构,对于B,A必定是在B析构后才析构A,这就是循环引用问题,违反常规,导致内存泄露。解决循环引用问题的方法有很多,最常用的是使用weak_ptr。将weak_ptr创建为shared_ptr的副本。 它提供对一个或多个shared_ptr实例拥有的对象的访问权限,但不参与引用计数。 weak_ptr的存在或破坏对shared_ptr或其他副本没有影响。
对于强引用而言,当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放),share_ptr就是强引用。对于弱引用而言,当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
代码如下:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:// 为了省去一些步骤这里 数据成员也声明为public
weak_ptr<B> pb;
//shared_ptr<B> pb;
void doSomthing()
{
shared_ptr<B> pp = pb.lock();
if(pp)//通过lock()方法来判断它所管理的资源是否被释放
{
cout<<"sb use count:"<<pp.use_count()<<endl;
}
}
~A()
{
cout << "kill A\n";
}
};
class B
{
public:
//weak_ptr<A> pa;
shared_ptr<A> pa;
~B()
{
cout <<"kill B\n";
}
};
int main(int argc, char** argv)
{
shared_ptr<A> sa(new A());
shared_ptr<B> sb(new B());
if(sa && sb)
{
sa->pb=sb;
sb->pa=sa;
}
sa->doSomthing();
cout<<"sb use count:"<<sb.use_count()<<endl;
return 0;
}
注意: weak_ptr除了对所管理对象的基本访问功能(通过get()函数)外,还有两个常用的功能函数:expired()用于检测所管理的对象是否已经释放;lock()用于获取所管理的对象的强引用指针。不能直接通过weak_ptr来访问资源。那么如何通过weak_ptr来间接访问资源呢?答案是:在需要访问资源的时候weak_ptr为你生成一个shared_ptr,shared_ptr能够保证在shared_ptr没有被释放之前,其所管理的资源是不会被释放的。创建shared_ptr的方法就是lock()方法。