深入解析C++智能指针:从auto_ptr到weak_ptr

发布时间:2023年12月23日

我们经常会遇到指针忘记释放的问题,有时也不可避免,例如捕捉异常时会改变执行流,本来在程序结束前写好了释放,最终没有执行,造成内存泄漏。

有一种解决方法,使用RAII(resource acquisition is initialisition)技术,即使用局部对象控制资源,这就是智能指针。

RAII的原理

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期管理资源的编程技术,特别用于处理资源分配和释放。在C++中,RAII的工作原理基于以下两个关键概念:

  1. 资源与对象的绑定:在RAII中,资源(如动态分配的内存、文件句柄、锁等)被封装在一个对象中。当对象被创建时,资源被分配;当对象的生命周期结束时,其析构函数负责释放资源。

  2. 自动资源管理:由于资源是通过对象管理的,资源的分配和释放是自动进行的。对象的析构函数在对象离开其作用域时自动被调用,无论是由于正常的程序流程还是因为异常。

智能指针(如std::unique_ptrstd::shared_ptr)是RAII原则的经典应用。它们封装了对动态分配内存的指针,确保在智能指针对象销毁时自动释放内存。这样,智能指针帮助避免了内存泄漏——即忘记释放分配的内存,这在使用原始指针时是一个常见问题。

通过使用智能指针,开发者不需要显式地调用delete,大大减少了内存泄漏的风险。这使得代码更安全、更易于维护,并且提高了异常安全性。

C++98——auto_ptr(了解不推荐使用)

auto_ptr在现代C++中被废弃,主要是因为它在设计上存在一些关键的缺陷,尤其是与所有权和对象复制相关的问题:

  1. 所有权转移auto_ptr的一个主要问题是它的复制语义。当一个auto_ptr对象被另一个auto_ptr复制时,所有权(即对内存资源的控制权)会从一个对象转移到另一个对象。这意味着原始的auto_ptr对象将变为空(null),丧失对资源的控制权。

  2. 悬空指针风险:由于所有权的转移,使用auto_ptr容易导致悬空指针问题。一旦auto_ptr对象被复制,原始指针就会失效,但是代码可能仍然尝试使用它,这可能导致不可预测的行为和程序崩溃。

  3. 与STL容器的不兼容auto_ptr不能安全地用在标准模板库(STL)容器中,因为这些容器的元素经常需要被复制和赋值,而auto_ptr的复制语义会导致问题。

例如:

int main()
{
    auto_ptr<int> pi1(new int(1));
    auto_ptr<int> pi2(pi1);
    *pi2 = 10;//程序会崩溃
    return 0;
}

?模拟实现

	template<class T>
	class auto_ptr
	{
	private:
		T* _ptr;
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{

		}

		~auto_ptr()
		{
			std::cout << "delete:" << ' ' << _ptr << std::endl;
			delete _ptr;
		}

		T operator* ()
		{
			return *_ptr;
		}

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

		auto_ptr(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator= (auto_ptr<T>& ap)
		{
			this->_ptr = ap._ptr;
			ap._ptr = nullptr;
			return *this;
		}
	};

C++11——unique_ptr

std::unique_ptr 在 C++ 中是一种智能指针,用于管理动态分配的内存。它保证同一时间只有一个 unique_ptr 拥有对某个对象的所有权,从而确保当 unique_ptr 被销毁时,它指向的对象也会被自动销毁

void testup()
{
	unique_ptr<int> uq1(new int(1));
    unique_ptr<int> uq2(new int(2));

	//unique_ptr<int> uq3(uq1);//报错
}

?模拟实现:

	template<class T>
	class unique_ptr
	{
	private:
		T* _ptr;

	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{

		}

		~unique_ptr()
		{
			delete _ptr;
		}

		T operator*()
		{
			return *_ptr;
		}

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

		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T> operator= (const unique_ptr<T>& up) = delete;
	};

C++11——shared_ptr

std::shared_ptr 是一种智能指针,用于实现多个指针对象之间的资源共享。以下是关于如何使用 shared_ptr 进行资源共享以及其优势和限制的探讨:

资源共享

  • 共享所有权shared_ptr 允许多个指针实例共同拥有一个对象的所有权。当最后一个拥有该对象的 shared_ptr 被销毁或重置时,对象被删除。
  • 引用计数shared_ptr 使用引用计数机制来跟踪有多少个 shared_ptr 实例指向同一个资源。每当新的 shared_ptr 指向该资源或某个 shared_ptr 被销毁时,计数相应增加或减少。

优势

  • 自动内存管理:通过自动管理资源的生命周期,减少了内存泄露的风险。
  • 异常安全:在异常发生时,通过自动释放资源提供了更好的异常安全保障。
  • 适用于共享资源:对于需要由多个所有者共同管理的资源非常有用,例如在复杂数据结构(如图或树)中共享节点。

限制

  • 性能成本:引用计数的维护增加了额外的性能开销,尤其是在多线程环境下。
  • 循环引用问题:如果 shared_ptr 间形成循环引用,会导致内存泄漏。这种情况可以通过使用 weak_ptr 来解决。
  • 资源同步:在多线程应用中,需要额外的机制来确保资源的线程安全性。
void testsp()
{
	mwk::shared_ptr<int> sp1(new int(1));
	mwk::shared_ptr<int> sp2(new int(2));

	mwk::shared_ptr<int> sp3(sp1);
	sp3 = sp3;
	sp1 = sp3;
	mwk::shared_ptr<int> sp4(new int(4));
	sp4 = sp2;
}

?模拟实现:

	template<class T>
	class shared_ptr
	{
	private:
		T* _ptr;
		int* _pcount;

	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{
			std::cout << "new:" << *_ptr << ' ' << std::endl;
		}

		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				std::cout << "delete:" << *_ptr << ' ' << _ptr << std::endl;
				delete _ptr;
				delete _pcount;
			}
		}

		T operator* ()
		{
			return *_ptr;
		}

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

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (this->_ptr != sp._ptr)
			{
				this->~shared_ptr();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}

	};

C++11——weak_ptr

std::weak_ptr 在实际编程中主要用于解决 std::shared_ptr 可能产生的循环引用问题,并且用于观察共享对象而不拥有其所有权。以下是一些典型的 weak_ptr 使用场景:

  1. 打破循环引用:当两个对象互相通过 shared_ptr 持有对方时,会产生循环引用,导致内存泄漏。使用 weak_ptr 替换其中一个 shared_ptr 可以打破这种循环。

  2. 缓存实现weak_ptr 可用于实现对象的缓存机制。缓存中的对象可以通过 shared_ptr 管理,而外部访问时使用 weak_ptr,这样即使缓存的对象被删除,也不会影响整体程序逻辑。

  3. 观察者模式:在观察者模式中,weak_ptr 可用于安全地观察被观察对象,而不会延长其生命周期。

  4. 资源的可用性检查:由于 weak_ptr 不影响其指向的对象的生命周期,它可以用来检查资源是否仍然存在。可以通过将 weak_ptr 提升为 shared_ptr 来安全地访问资源,前提是资源仍然存在。

通过这些使用场景,weak_ptr 在避免资源泄漏和提供灵活的资源访问策略方面发挥着重要作用。

循环引用的例子:

class list
{
public:
	std::shared_ptr<list> _next;
	std::shared_ptr<list> _prev;

	~list()
	{
		std::cout << "delete" << std::endl;
	}
};
void testwp()
{
	std::shared_ptr<list> ls1(new list);
	std::shared_ptr<list> ls2(new list);

	ls1->_next = ls2;
	ls2->_prev = ls1;
}

ls1->next = ls2会ls2的计数变为2,ls2->prev = ls1会让ls1的计数变为2;

当析构ls1和ls2的时候,ls1和ls2的计数都变为1,并没有delete;只有当ls1->next析构时,ls2才会析构,当ls2->prev析构时,ls1才会析构;但是next和prev都是成员,只有ls1和ls2析构的时候才会析构。构成一个循环,无法delete。

解决方法就是使用weak_ptr

weak_ptr 通常与 shared_ptr 配合使用。它通过 shared_ptr 来创建,但不增加引用计数。这个例子心中,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

class list
{
public:
	std::weak_ptr<list> _next;
	std::weak_ptr<list> _prev;

	~list()
	{
		std::cout << "delete" << std::endl;
	}
};
void testwp()
{
	std::shared_ptr<list> ls1(new list);
	std::shared_ptr<list> ls2(new list);
	std::cout << ls1.use_count() << std::endl;//1
	std::cout << ls2.use_count() << std::endl;//1

	ls1->_next = ls2;
	ls2->_prev = ls1;

	std::cout << ls1.use_count() << std::endl;//1
	std::cout << ls2.use_count() << std::endl;//1
}

文章来源:https://blog.csdn.net/weixin_74269833/article/details/135134275
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。