概念
智能指针本质是对象,但行为却和指针极其相似。
是存储指向动态分配(堆)对象指针的类,把一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象。
为什么需要智能指针?
比如在异常学习中我们知道,异常很可能会造成资源泄漏,而且由于C++没有垃圾回收机制,就会导致那些申请了的忘记释放的资源的浪费,所以产生智能指针来解决这些问题。
智能指针的使用及原理:
RAII:
RAII是一种利用对象生命周期来控制程序资源的技术。
在对象构造时将资源交给该对象,保证资源在对象生命周期内有效,在对象析构时释放资源。
优点:
- 不需要显示释放资源
- 对象所需资源在其生命周期内始终有效
// RAII具体实现实例
template<class T>
class SmartPtr{
public:
// 调用构造函数,所创建对象获取资源
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
// 析构时就将资源释放
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
void func()
{
int* temp = new int [size];
// 为防止内存泄漏,将temp的这块资源托管给 对象sp
SmartPtr<int> sp(temp);
... // 其他代码段
}
int main()
{
try{
func();
}
catch(const exception& e){
cout << e.what() << endl;
}
}
除了RAII技术,还需要将 *和 ->重载,使得我们的sp对象能具有指针一样的操作。
// 重载方式比较简单
T& operator*() {return *_ptr;}
T& operator->() {return _ptr;}
所以智能指针原理: RAII + (* ->)运算符重载
如果我们要将我们的资源拷贝一份,通过执行下面代码:
#include <iostream>
using namespace std;
template<class T>
class SmartPtr{
public:
// 调用构造函数,所创建对象获取资源
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
T& operator*() { return *_ptr; }
T& operator->() { return _ptr; }
// 析构时就将资源释放
~SmartPtr()
{
if (_ptr)
delete _ptr;
cout << "~SmartPtr Success" << endl;
}
private:
T* _ptr;
};
void func()
{
int* temp = new int [100];
// 为防止内存泄漏,将temp的这块资源托管给 对象sp
SmartPtr<int> sp(temp);
SmartPtr<int> sp2(sp);
}
int main()
{
try{
func();
}
catch (const exception& e){
cout << e.what() << endl;
}
return 0;
}
通过运行结果可以发现现在所实现的智能指针存在浅拷贝的问题。
浅拷贝:也叫位拷贝,就是成员数据间的赋值。但如果对象中包含资源时,就产生两个对象指向同一资源,且都能对资源进行访问、控制。
那要怎么解决?
浅拷贝解决方式通常为:
1. 深拷贝:也叫值拷贝,它不仅将对象的数据拷贝一份,也会将对象的资源拷贝一份。
但这里不能使用深拷贝,因为这里的资源是用户申请的。
2. 写时拷贝:维护一个计数(指针变量),记录使用资源的对象,只有到最后一个对象时,才由该对象完成资源的释放
C++98中的智能指针:
auto_ptr:
头文件 #include
std::auto_ptr 能够方便的管理单个堆内存对象。
原理:将资源的管理权转移的思想,即每创建一个对象,新对象指向资源,旧对象与资源断开。
// 重载拷贝构造
SmartPtr(SmartPtr<T>& copy)
:_ptr(copy._ptr)
{
copy._ptr = nullptr;
}
SmartPtr<T>& operator=(SmartPtr<T>& copy)
{
// 检测是否自己给自己赋值
if (this != ©){
// 释放当前对象资源
if (_ptr)
delete _ptr;
// 资源管理权传递
_ptr = copy._ptr;
copy._ptr = nullptr;
}
}
之前浅拷贝问题就得到解决
但是在代码实现中我们知道,这个过程中断开了了旧对象与资源的联系,那就表示我们不能再通过之前的对象访问资源。
所以auto_ptr是存在很大的漏洞,到C++11 也已经被淘汰了。
C++11下的智能指针:
unique_ptr:
头文件 #include。
原理:禁止拷贝和赋值,对象独占一份资源。
使用:
unique_ptr虽然不能赋值,但是可以通过move()函数转移对象的所有权(有点类似于auto_ptrde caoz );
reset()可以提前释放unique_ptr指针;
代码实现:
template<class T>
class SmartPtr{
public:
// 调用构造函数,所创建对象获取资源
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
T& operator*() { return *_ptr; }
T& operator->() { return _ptr; }
// 析构时就将资源释放
~SmartPtr()
{
if (_ptr)
delete _ptr;
cout << "~SmartPtr Success" << endl;
}
private:
T* _ptr;
// C++11 中防止拷贝的方式:delete
SmartPtr (SmartPtr<T> const &) = delete;
SmartPtr& operator=(SmartPtr<T>& ) = delete;
};
shared_ptr:
原理:通过引用计数的方式来实现多个shared_ptr对象间的资源共享。
使用:
- 智能指针是具有指针行为的类,所以初始化时不能直接将指针赋值给智能指针。可以使用构造、拷贝构造和make_shared函数初始化;
- 在多线程时,就需要对属于临界资源的引用计数进行保护。可以加锁保证线程安全;
- 智能指针的对象存放在堆区,如果有两个线程同时访问,就也会导致线程安全问题;
- 不要用原始指针初始化多个shared_ptr,否则会造成二次释放同一资源;
- 注意避免循环引用。
代码实现:
include <iostream>
#include <thread>
#include <mutex>///
using namespace std;
template<class T>
class SharedPtr{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1))
, _pMutex(new mutex)
{
// 如果是一个空指针对象,引用计数给0
if (_ptr == nullptr)
*_pCount = 0;
}
SharedPtr(SharedPtr<T>& copy)
: _ptr(copy._ptr)
, _pCount(copy._pCount)
, _pMutex(copy._pMutex)
{
if (_ptr)
AddCount();
}
SharedPtr<T>& operator=(const SharedPtr<T>& copy)
{
// 防止自赋值
if (_ptr != copy_ptr){
// 释放_ptr旧资源
Release();
// 共享copy对象资源
_ptr = copy._ptr;
// 计数增加
_pCount = copy._pCount;
_pMutex = copy._pMutex;
if (_ptr)
AddCount();
}
return *this;
}
T& operator*() { return *_ptr; }
T& operator->() { return _ptr; }
// 查看当前计数
int UseCount() { return *_pCount; }
// 获取原始指针
T* Get(){ return _ptr; }
// 如果有新对象,增加引用计数
int AddCount()
{
// 为保证多线程下的线程安全,执行锁操作
_pMutex->lock();
++(*_pCount);
_pMutex->unlock();
return *_pCount;
}
// 如果有对象调用析构,减少引用计数
int SubCount()
{
_pMutex->lock();
--(*_pCount);
_pMutex->unlock();
return *_pCount;
}
~SharedPtr()
{
Release();
}
private:
// 释放资源
void Release()
{
// 如果引用计数减为0,则释放资源
if (_ptr && SubCount() == 0){
delete _ptr;
delete _pCount;
}
}
private:
int* _pCount; // 引用计数
T* _ptr; // 指向管理资源的指针
mutex* _pMutex; // 互斥锁
};
int main()
{
SharedPtr<int> sp1(new int(10));
cout << "Before Add SP2:" << sp1.UseCount() << endl;
SharedPtr<int> sp2(sp1);
cout << "After Add SP2:" << sp1.UseCount() << endl;
return 0;
}
资源共享
引用计数
除了线程安全问题外,还可能产生其他问题:
解决这个死锁问题可以通过:1. atomic int_32t 2. 范围锁(scoped_t lock)
另外就是shared_ptr的循环引用。
测试代码:
class ListNode{
public:
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl; // 1
cout << node2.use_count() << endl; // 1
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl; // 2
cout << node2.use_count() << endl; // 2
return 0;
}
运行结果:
可以发现程序结束但都没有调用析构函数释放资源,为什么呢?
循环引用:
-如果node1要析构,就要node2的_prev先析构;
-而_prev是node2的成员,就要node2析构;
-但node2被node1的_next管理,就要求node1的_next先析构;
-而node1的_next要析构,又要求node1先析构。
-如此循环引用,析构函数一直调不到。
怎么解决循环引用的问题呢?
在C++11中有weak_ptr专门来帮助解决循环引用的问题。
只要使用weak_ptr去声明_prev和_next就可以解决。
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
运行结果:
weak_ptr:
它没有重载的operator* 和 -> ,所以不具有普通指针的功能。
作用:相当于shared_ptr的助理,仅仅为了解决循环引用。
原理:
-weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,但weak_ptr没有共享资源,所以它的构造不会引起指针
-引用计数的增加;
-weak_ptr通过成员函数lock()从被观测的shared_ptr中获得一个可用的shared_ptr对象,从而操作资源;
当被观察的shared_ptr失效后,相应的weak_ptr也相应失效。
C++11和boost中智能指针的关系:
C++11的智能指针大都参考boost中实现的。
std | boost | 功能说明 |
---|---|---|
unique_ptr | scoped_ptr | 独占指针对象,并保证指针所指对象生命周期与其一致 |
shared_ptr | shared_ptr | 可共享指针对象,可以赋值给shared_ptr或weak_ptr。指针所指对象在所有的相关联的shared_ptr生命周期结束时结束,是强引用。 |
weak_ptr | weak_ptr | 它不能决定所指对象的生命周期,引用所指对象时,需要lock()成shared_ptr才能使用。 |