C++11---智能指针

Posted Moua

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11---智能指针相关的知识,希望对你有一定的参考价值。

在使用C++编程时,要求使用malloc/new申请出来的空间必须使用free/delete进行释放,如果程序员没有对使用malloc/new申请的空间在使用free/delete进行释放,则可能会造成内存泄露问题。但是,在C++中有些情况下,即使成员进行了释放也可能存在一些安全隐患,例如下面的程序:

void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int)*n);
    _MergeSort(a, 0, n - 1, tmp);
    // 这里假设处理了一些其他逻辑
    vector<int> v(1000000000, 10);
    // ...
    free(tmp);
}
int main()
{
    int a[5] = { 4, 5, 2, 3, 1 };
    MergeSort(a, 5);
    return 0;
}
  • 在MergeSort中使用malloc申请了空间,虽然说在最后使用free进行了释放。
  • 但是,如果在malloc和free之间抛出了异常,该异常由main函数中进行捕获,那么free也是无法被执行的。

这时,就需要使用智能指针来解决了。

1、RAII

RAII是一种利用对象的生命周期对资源进行管理的简单方式。

在构造对象时获取资源,接着控制对资源的访问,使之在对象声明周期内都有效,最后在对象析构的时候释放资源。

2、智能指针原理

智能指针就是在RAII的方式上,对指针类加上*重载和->重载,使之具有指针的行为。

template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
        : _ptr(ptr)
    {}
    ~SmartPtr()
    {
        if(_ptr)
            delete _ptr;
    }
    T& operator*() {return *_ptr;}
    T* operator->() {return _ptr;}
private:
    T* _ptr;
};
struct Date
{
    int _year;
    int _month;
    int _day;
};
int main()
{
    SmartPtr<int> sp1(new int);
    *sp1 = 10
    cout<<*sp1<<endl;
    SmartPtr<int> sparray(new Date);
    // 需要注意的是这里应该是sparray.operator->()->_year = 2018;
    // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
    sparray->_year = 2018;
    sparray->_month = 1;
    sparray->_day = 1;
}

简单来说,智能指针就是使用RAII的特性+operator*和operator->使其具备有指针一样的行为。 

3、智能指针的演进过程

在只能指针中存在一个问题就是,当使用智能指针拷贝对象时,会发生一些意想不到的事情。

  • 在拷贝只能指针对象时,只能是使用值拷贝(针对不同的对象指针有不同的拷贝方式,因此没办法做到深拷贝)。
  • 值拷贝存在一个问题,多个对象指向同一块内存。
  • 其中一个释放,其他的也就不能访问了。

其实C++中对智能指针的演进过程其实也就是对这个问题的解决过程。

1)auto_ptr

C++98的库中提供的智能指针就是auto_ptr:

// C++库中的智能指针都定义在memory这个头文件中
#include <memory>
class Date
{
public:
    Date() { cout << "Date()" << endl;}
    ~Date(){ cout << "~Date()" << endl;}
    int _year;
    int _month;
    int _day;
};
int main()
{
    auto_ptr<Date> ap(new Date);
    auto_ptr<Date> copy(ap);
    // auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
    // C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
    ap->_year = 2018;
    return 0;
}

在C++98中auto_ptr解决这个问题的方法是,拷贝构造采用类似移动构造的方式,将原来对象的空间移动到新的对象上。但是,这也存一个问题,前边的对象就会被悬空,如果使用者不注意可能会出现一些意向不到的错误。 

// 模拟实现一份简答的AutoPtr,了解原理
template<class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr = NULL)
        : _ptr(ptr)
    {}
    ~AutoPtr()
    {
        if(_ptr)
            delete _ptr;
    }
    // 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
    // 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
    AutoPtr(AutoPtr<T>& ap)
        : _ptr(ap._ptr)
    {
        ap._ptr = NULL;
    }
    AutoPtr<T>& operator=(AutoPtr<T>& ap)
    {
        // 检测是否为自己给自己赋值
        if(this != &ap)
        {
            // 释放当前对象中资源
            if(_ptr)
                delete _ptr;
            // 转移ap中资源到当前对象中
            _ptr = ap._ptr;
            ap._ptr = NULL;
        }
        return *this;
    }
    T& operator*() {return *_ptr;}
    T* operator->() { return _ptr;}
private:
    T* _ptr;
};
int main()
{
    AutoPtr<Date> ap(new Date);
    // 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
    // 通过ap对象访问资源时就会出现问题。
    AutoPtr<Date> copy(ap);
    ap->_year = 2018;
    return 0;
}

2)unique_ptr

auto_ptr中的方式,显然存在很大的缺陷,因此C++11中又提供了unique_ptr

unique_ptr的解决方式简单粗暴,既然在智能指针中拷贝构造会出现问题,unique_ptr干脆直接防拷贝。

// 模拟实现一份简答的UniquePtr,了解原理
template<class T>
class UniquePtr
{
public:
    UniquePtr(T * ptr = nullptr)
        : _ptr(ptr)
    {}
    ~UniquePtr()
    {
        if(_ptr)
            delete _ptr;
    }
    T& operator*() {return *_ptr;}
    T* operator->() {return _ptr;}
private:
    // C++98防拷贝的方式:只声明不实现+声明成私有
    UniquePtr(UniquePtr<T> const &);
    UniquePtr & operator=(UniquePtr<T> const &);

    // C++11防拷贝的方式:delete
    UniquePtr(UniquePtr<T> const &) = delete;
    UniquePtr & operator=(UniquePtr<T> const &) = delete;
private:
    T * _ptr;
};

3)shared_ptr

虽然说前边的两种方式都一定程度上解决了拷贝的问题,但是还是不够完美,因此C++11中又提供了shared_ptr。

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  • 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
template<class T>
class sharedPtr
{
private:
	T* _ptr; //
	int* pcount;//引用计数
	mutex* pmutex; //互斥锁
public:
	sharedPtr(T* ptr = nullptr)
		:_ptr(ptr),
		pcount(new int(1))
		, pmutex(new mutex)
	{}

	//拷贝构造和赋值运算符重载
	sharedPtr(const sharedPtr<T>& sp)
		:_ptr(sp._ptr)
		, pcount(sp.pcount)
		, pmutex(sp.pmutex)
	{
		pmutex->lock();
		(*pcount)++;
		pmutex->unlock();
	}

	sharedPtr<T>& operator=(const sharedPtr<T>& sp)
	{
		//不能自己给自己赋值
		if (_ptr != sp._ptr)
		{
			//释放管理的旧资源
			Release();

			//拷贝新资源
			_ptr = sp._ptr;
			pcount = sp.pcount;
			pmutex = sp.pmutex;

			pmutex->lock();
			(*pcount)++;
			pmutex->unlock();
		}

		return *this;
	}

	//指针一样的行为
	T& operator*()
	{
		return *_ptr;
	}

	sharedPtr<T>* operator->()
	{
		return _ptr;
	}

	~sharedPtr()
	{
		Release();
	}
private:
	void Release()
	{
		//锁要最后释放,因此必须有个标志
		bool flag = false;
		//释放资源
		pmutex->lock();
		if (--(*pcount) == 0)
		{
			delete _ptr;
			delete pcount;

			flag = true;
		}
		pmutex->unlock();

		if (flag == true)
			delete pmutex;
	}
};

1)shred_ptr线程安全问题

shared_ptr中_ptr和pcount都是new出来的,多个对象同时操作就有可能出现线程安全问题,但是对pcount的操作都是加锁的,因此pcount不会产生线程安全问题。但是,_ptr每办法加锁,因此多个线程对_ptr操作可能会产生想爱你城安全问题。

void SharePtrFunc(SharedPtr<Date>& sp, size_t n)
{
cout << sp.Get() << endl;
for (size_t i = 0; i < n; ++i)
{
    // 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
    SharedPtr<Date> copy(sp);
    // 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
    copy->_year++;
    copy->_month++;
    copy->_day++;
}
}
int main()
{
    SharedPtr<Date> p(new Date);
    cout << p.Get() << endl;
    const size_t n = 100;
    thread t1(SharePtrFunc, p, n);
    thread t2(SharePtrFunc, p, n);
    t1.join();
    t2.join();
    cout << p->_year << endl;
    cout << p->_month << endl;
    cout << p->_day << endl;
    return 0;
}

2)循环引用

  • node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete
  • node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  • node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  • 也就是说_next析构了,node2就释放了。
  • 也就是说_prev析构了,node1就释放了。
  • 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

那么该如何解决呢?

在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
 

struct ListNode
{
    int _data;
    weak_ptr<ListNode> _prev;
    weak_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;
    cout << node2.use_count() << endl;
    node1->_next = node2;
    node2->_prev = node1;
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    return 0;
}

 注意:RAII除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题。例如unipue_lock和lock_guard

以上是关于C++11---智能指针的主要内容,如果未能解决你的问题,请参考以下文章

[C++11]共享智能指针shared_ptr指定删除器

智能指针11

智能指针11

智能指针11

详解C++11智能指针

在条件或循环中分配智能指针