一、智能指针的引入
int Func(int num1, int num2)
{
if (num2 == 0)
{
throw string("除0错误");
}
return num1 / num2;
}
void TestFunc(int num1, int num2)
{
try
{
Func(num1, num2);
}
catch (const string& s)
{
cout << s << endl;
}
}
int main()
{
int* p = new int();
TestFunc(10, 0);
delete p;
return 0;
}
在上面的代码中,调用函数TestFunc中有捕获异常,则程序直接跳到捕获异常的地方 delete p,不能正确完成delete指针,可能造成内存泄漏,引发异常安全的问题,那如何解决呢?
(1)异常的重新抛出
(2)采用智能指针:调用的时候自动进行资源申请,调用结束后自动释放
设计思想
(1)RAII
资源获得即初始化,定义一个模板类,在构造的时候申请资源,在析构的时候释放资源
(2)像指针一样使用
重载operator*,operator->
二、auto_ptr
1. 发展过程
(1)儿童时期的auto_ptr
构造函数的时候申请资源,析构函数的时候释放资源,并且重载operator* operator->,但是当进行拷贝构造和赋值操作的时候进行的是浅拷贝,也就是多个智能指针管理的是同一块空间,在调用结束释放空间的时候也就会对这块空间进行多次释放
(2)少年时期的auto_ptr
在进行拷贝构造和赋值操作的时候,将原来这块空间的管理权从原指针 ap1 转移到拷贝构造的新指针 ap2 上,在进行浅拷贝之后,将原指针ap1赋为空指针nullptr,但是进行拷贝构造完成后 ap1 = NULL ,当我们再想访问 ap1 的时候就会解引用空指针导致程序崩溃
(3)成年后的auto_ptr
在类的成员变量中加一个 bool 类型的 m_owner,用来判断该对象是否具有对这块空间的管理权,所以在进行拷贝构造和赋值运算符重载的时候,在浅拷贝之后,将原指针ap1的 m_owner 赋给现在的指针ap2 的 m_owner, 之后对原指针ap1的 m_owner赋为false,这样对这块空间的管理权就从ap1手中转移到了ap2手中,虽然 auto_ptr 思考了很多解决方案,但是它本身还是存在很大缺陷的,所以建议不要使用auto_ptr
template <class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = nullptr)
: m_ptr(ptr)
, m_owner(false)
{
if (m_ptr)
m_owner = true;
}
AutoPtr(const AutoPtr<T>& ap)
: m_ptr(ap.m_ptr)
, m_owner(ap.m_owner)
{
ap.m_owner = false;
}
T& operator*()
{
return *m_ptr;
}
T* operator->()
{
return m_ptr;
}
AutoPtr<T>& operator=(const AutoPtr<T>& ap)
{
if (this != &ap)
{
if (m_ptr && m_owner)
{
delete m_ptr;
}
m_ptr = ap.m_ptr;
m_owner = ap.m_owner;
ap.m_owner = false;
}
return *this;
}
~AutoPtr()
{
if (m_ptr && m_owner)
{
delete m_ptr;
m_ptr = nullptr;
m_owner = false;
}
}
private:
T* m_ptr;
mutable bool m_owner;
};
三、unique_ptr
unique_ptr 也要有RAII和像指针一样的特点,它采用防拷贝的方式
template <class T>
class UniquePtr
{
public:
UniquePtr(T* ptr = nullptr)
: m_ptr(ptr)
{}
T& operator*()
{
return *m_ptr;
}
T* operator->()
{
return m_ptr;
}
~UniquePtr()
{
if (m_ptr)
{
delete m_ptr;
m_ptr = nullptr;
}
}
private:
// C++98
// UniquePtr(const UniquePtr<T>& up);
// UniquePtr<T>& operator=(const UniquePtr<T>&);
// C++11
UniquePtr(const UniquePtr<T>& up) = delete;
UniquePtr<T>& operator=(const UniquePtr<T>&) = delete;
private:
T* m_ptr;
};
四、shared_ptr
在拷贝构造和赋值操作的时候,进行的是浅拷贝,在析构的时候会对同一块空间析构多次,对于这个问题,可以在拷贝构造和赋值运算符重载的时候,原指针 ap1 和 ap2 共享一个引用计数,对该引用计数进行++,在析构的时候,查看引用计数是否为1,若为1,表示只有当前指针使用这块空间,可以对该指针进行释放清理,若大于1,表示还有其它指针在使用这块空间,只需要将引用计数进行 - -,不需要释放这块空间
template<class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
: m_ptr(ptr)
, m_count(nullptr)
{
if (m_ptr)
{
m_count = new int(1);
}
}
T& operator*()
{
return *m_ptr;
}
T* operator->()
{
return m_ptr;
}
SharedPtr(const SharedPtr<T>& sp)
: m_ptr(sp.m_ptr)
, m_count(sp.m_count)
{
if (m_count)
++(*m_count);
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
if (this != &sp)
{
Release();
m_ptr = sp.m_ptr;
m_count = sp.m_count;
if (m_count)
++(*m_count);
}
}
~SharedPtr()
{
Release();
}
private:
void Release()
{
if (m_ptr && --(*m_couunt))
{
delete m_ptr;
m_ptr = nullptr;
delete m_count;
m_count = nullptr;
}
}
private:
T* m_ptr;
int* m_count;
};
五、weak_ptr
但是shared_ptr会出现一个问题,就是循环引用,就会导致引用计数一直无法减到0,就会导致空间没有释放。例如如下代码
template<class T>
struct ListNode
{
T _data;
SharedPtr<ListNode<T>> _next;
SharedPtr<ListNode<T>> _prev;
ListNode(T data)
:_data(data)
, _next(NULL)
, _prev(NULL)
{}
};
void TestSharedPtr()
{
SharedPtr<ListNode<int>> sp1(new ListNode<int> (10));
SharedPtr<ListNode<int>> sp2(new ListNode<int>(20));
sp1->_next = sp2;
sp2->_prev = sp1;
}
shared_ptr采用weak_ptr解决循环引用的问题,将会出现循环引用的shared_ptr通过weak_ptr的构造函数保存下来,并且weak_ptr里面不增加引用计数,但是它不是严格意义上的智能指针,它是辅助shared_ptr的使用,为了解决shared_ptr循环引用的问题
template<class T>
class WeakPtr
{
public:
WeakPtr(SharedPtr<T> sp)
:m_ptr(sp.ptr)
{}
WeakPtr(const WeakPtr<T>& sp)
:ptr(sp.m_ptr)
{}
WeakPtr<T>& operator=(const WeakPtr<T>& wp)
{
if (this != &wp)
{
m_ptr = wp.m_ptr;
}
return *this;
}
protected:
T* m_ptr;
};