C++_智能指针详解

发布于:2024-10-09 ⋅ 阅读:(42) ⋅ 点赞:(0)

什么是智能指针?为什么要有智能指针?到目前为止,我们编写的程序所使用的对象都有着严格定义的生命周期。比如说,全局对象在程序启动时分配,在程序结束时销毁;再比如说局部static对象在第一次使用前分配,在程序结束时销毁......除了这些对象,C++还支持动态分配对象。动态分配的对象的生命周期与它们在哪里创建无关,只有当显式地被释放时,这些对象才会被销毁

如果不显式地释放,则很有可能造成内存泄漏!!而动态对象的正确释放是编程中最容易出错的地方,所以C++引入了智能指针的概念,来帮助我们编程人员更好的释放。

了解内存泄漏与其危害

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死!

智能指针的使用及原理

RAII

RAII(Resource Acquisition Is Initialization,资源获得即初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构时释放资源。因此,我们实际上就是把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源(因为析构函数是自动调用的)。
  • 采用这种方式,对象所需的资源在其生命周期内始终保持有效
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)        
		:_ptr(ptr)             // 把需要管理的对象托管在成员变量中
	{}
	~SmartPtr()
	{
        if(_ptr)
		    delete _ptr;       // 当该类析构的时候,自动清理资源
	}
private:
	T* _ptr;
};
double Division()
{
    int a, b;
    cin >> a >> b;
	if (b == 0)                // 当b == 0时抛出异常
		throw "Division by zero condition!";
	return (double)a / (double)b;
}
void Func()
{
    ShardPtr<int> sp1(new int);        // 动态创建对象
    ShardPtr<int> sp2(new int);
    cout << Division() << endl;
}
int main()
{
    try 
        Func();
    catch(const exception& e)
        cout<<e.what()<<endl;
    return 0;
}

智能指针的原理

上面代码中的SmartPtr类可以说是智能指针吗?不,还不是!既然叫做指针,那么必须要具有指针的各种行为。比如说:指针可以解引用,也可以通过->去访问所指空间中的内容,因此,还应在SmartPtr类中添加类似以下功能的代码:

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

总而言之,智能指针的原理应包括以下两点:

  • RAII特性。
  • 重载operator*和opertaor->,具有像指针一样的行为。

 std::auto_ptr

在C++98版本的库中提供了auto_ptr的智能指针。auto_ptr在实现原理上使用了管理权转移的思想。

// Date为日期类
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);        // 拷贝,管理权转移
// ap1的所有权都给了ap2,所以ap1现在什么都没有
// 访问ap1的任何内容都会报错
ap1->_year++;                   // 报错

auto_ptr<Date> ap2(ap1)构造拷贝出ap2,ap2完全夺取了ap1的管理权,进而导致ap1无家可归,进行 ap1->访问时程序就会报错。同样道理,当进行了ap2 = ap1,程序也存在这样的问题,原因依旧在于ap1被彻彻底底的夺走了一切,所以这种编程思想是十分危险的。总之,auto_ptr是一个失败设计,很多公司明确要求禁止使用auto_ptr。
 

 std::unique_ptr

unique_ptr的实现原理很简单,就是简单粗暴的防拷贝。unique_ptr类中的拷贝和赋值都被禁掉了。

template<class T>
class unique_ptr
{
public:
    unique_ptr(T* ptr)
        :_ptr(ptr)
    {}
    ~unique_ptr()
    {
        if (_ptr)
        {
            cout << "delete:" << _ptr << endl;
            delete _ptr;
        }
    }
    // 像指针一样使用
    T& operator*()    {return *_ptr;}
    T* operator->()    {return _ptr;}
    // 被禁掉的拷贝构造和赋值
    unique_ptr(const unique_ptr<T>& sp) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
    T* _ptr;
};

上面的unique_ptr用于处理单个的对象,而new/delete和new[]/delete[]底层实现机制又是不同的,对于new[]出来的多个对象unique_ptr又给出了特化的版本,请点击unique_ptr文档

// Date为日期类
// 处理单个对象
unique_ptr<Date> up1(new Date);
// 处理多个对象
unique_ptr<Date[]> up2(new Date[5]);

std::shared_ptr

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

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

看着上面的理论吧啦吧啦的,我个人感觉不是很好理解,结合图示和模拟实现的代码可能好理解一点,下图是拷贝的过程原理,赋值类似。

// 基础版的shared_ptr模拟实现
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))	// 这里新开一块空间,用作计数
	{}
	// s2(s1)
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		++(*_pcount);
	}
	// sp3 = sp1
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			release();			// 清理sp3之前指向的空间

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
		return *this;
	}
    void release()
	{
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
			_ptr = nullptr;
			_pcount = nullptr;
		}
	}
	~shared_ptr()
	{
		release();
	}

    // 相应的指针行为 
	T* get()           {return _ptr;}
	int use_count()    {return *_pcount;}
	T& operator*()     {return *_ptr;}
	T* operator->()    {return _ptr;}
private:
	T* _ptr = nullptr;
	int* _pcount;
};

注:shared_ptr不需要显式的实现移动构造和移动赋值。

循环引用

尽管shared_ptr是一种比较完美的编程思想,但是再完美也会有一定的瑕疵,而这个瑕疵-就是循环引用。一般情况下,循环引用是不会轻易遇到的,如果遇到了,那你就自认倒霉叭~~~

我们先来认识一下循环引用,请看代码

// 循环引用的场景
struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

	n1->_next = n2;    // n1指向n2,语句1
	n2->_prev = n1;    // n2指向n1,语句2

	return 0;//输出结果为空
}

上述代码中,出现循环引用的主要原因就是语句1与语句2同时存在。这两行代码存在其中的任何一行都不会有问题,怕的就是两行代码同时存在!!!它们同时存在会构成循环引用,循环引用会导致内存泄漏。

为了解决上述情况,C++11引入了weak_ptr,weak_ptr不同于上述智能指针,它不支持管理资源,只是与shared_ptr配合使用。严格来说,weak_ptr不是智能指针,因为它不支持RAII。只要将上述代码中的ListNode类中的shared_ptr改为weak_ptr,循环引用就可以得到很好的解决:

struct ListNode
{
	int _data;
    // 改为weak_ptr
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};