【C++】深入解析C++智能指针:从auto_ptr到unique_ptr与shared_ptr

文章目录

  • 前言:
  • [1. 智能指针的使用及原理](#1. 智能指针的使用及原理)
  • [2. C++ 98 标准库中的 auto_ptr:](#2. C++ 98 标准库中的 auto_ptr:)
  • [3. C++ 11 中的智能指针](#3. C++ 11 中的智能指针)
    • 循环引用:
    • [shared_ptr 定制删除器](#shared_ptr 定制删除器)
  • [4. 内存泄漏](#4. 内存泄漏)
  • 总结:

前言:

随着C++语言的发展,智能指针作为现代C++编程中管理动态分配内存的一种重要工具,越来越受到开发者的青睐。智能指针不仅简化了内存管理,还有助于避免内存泄漏等常见问题。本文将深入探讨智能指针的使用及其原理,从C++98标准库中的auto_ptr开始,逐步过渡到C++11中更为强大和灵活的智能指针类型,如unique_ptrshared_ptr。此外,文章还将讨论循环引用问题、内存泄漏的原因及其危害,并提供相应的解决方案。通过本文的学习,读者将能够更好地理解和运用智能指针,编写出更安全、更高效的C++代码。

1. 智能指针的使用及原理

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源 (如内

存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源
。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做

法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
cpp 复制代码
// SmartPtr.h
// 使用RAII思想设计的smartPtr类

template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr) {
			std::cout << "delete: " << _ptr << std::endl;
			delete _ptr;
		}
	}

private:
	T* _ptr;
};
    

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
 		throw invalid_argument("除0错误");
 		
 	return a / b;
}
void Func()
{
	ShardPtr<int> sp1(new int);
    ShardPtr<int> sp2(new int);
	cout << div() << endl;
}

int main()
{
	try {
		Func();
	}
    catch(const exception& e)
    {
        cout<<e.what()<<endl;
   	}
   	
 	return 0;
}
cpp 复制代码
//test.cpp
#include <iostream>
#include "SmartPtr.h"
using namespace std;

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);

	cout << div() << endl; 
}

int main()
{
	try {
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}
  • 需要像指针一样的去使用:
cpp 复制代码
// 像指针一样使用
T& operator*()
{
	return *_ptr;
}

T* operator->()
{
	return _ptr;
}
cpp 复制代码
SmartPtr<int> sp1(new int(1));
SmartPtr<int> sp2(new int(0));
*sp1 += 10;

SmartPtr<pair<string, int>> sp3(new pair<string, int>);
sp3->first = "apple";
sp3->second = 1; // 等价于 sp3.opertor->()->second = 1;

cout << sp3->first << " " << sp3->second << endl;
  • 智能指针的拷贝问题
cpp 复制代码
// 智能指针的拷贝问题
int main()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<int> sp2(sp1);

	return 0;
}

vector / list.... 需要深拷贝,它们都是利用资源存储数据,资源是自己的。拷贝时,每个对象各自一份资源,各管各的,所以深拷贝。

智能指针 / 迭代器... 期望的是浅拷贝

资源不是自己的,代为持有,方便访问修改数据。他们拷贝的时候期望的指向同一资源,所以浅拷贝。而且智能指针还要负责释放资源。

cpp 复制代码
itertor it = begin();

2. C++ 98 标准库中的 auto_ptr:

auto_ptr 管理权转移,被拷贝的对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空
注意 :在使用auto_ptr 过后不能访问对象,否则就出现空指针了。很多公司禁止使用它,因为他很坑!

cpp 复制代码
 // 智能指针的拷贝问题
// 1. auto_ptr 管理权转移,被拷贝的对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空
// 注意:在使用auto_ptr 过后不能访问对象,否则就出现空指针了。很多公司禁止使用它,因为他很坑!
int main()
{
	std::auto_ptr<int> sp1(new int(1));
	std::auto_ptr<int> sp2(sp1);

	*sp2 += 10;

	// 悬空
	*sp1 += 10;

	return 0;
}

auto_ptr 的实现:

cpp 复制代码
namespace hd
{
	template<class T>
	class auto_ptr {
	public:
		// RAII
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		// ap2(ap1)
		auto_ptr(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
	
		~auto_ptr()
		{
			if (_ptr) {
				std::cout << "delete: " << _ptr << std::endl;
				delete _ptr;                                                             
			}
		}
	
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
	
		T* operator->()
		{
			return _ptr;
		}
	
	private:
		T* _ptr;
	};
}

3. C++ 11 中的智能指针

boost 智能指针
scoped_ptr / scoped_array
shared_ptr / shared_array

C++ 11
unique_ptrscoped_ptr类似的
shared_ptrshared_ptr类似的

unique_ptr

禁止拷贝,简单粗暴,适合于不需要拷贝的场景

赋值也禁掉了:

unique_ptr:实现

cpp 复制代码
namespace hd
{
	template<class T>
	class unique_ptr {
	public:
		// RAII
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		// ap2(ap1)
		unique_ptr(const unique_ptr<T>& ap) = delete;  // 禁掉拷贝构造
		// 赋值也要禁掉,赋值会生成默认成员函数,浅拷贝,也会出现问题
		unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;


		~unique_ptr()
		{
			if (_ptr) {
				std::cout << "delete: " << _ptr << std::endl;
				delete _ptr;
			}
		}

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr;
	};
}

如果必须要拷贝用shared_ptr:
shared_ptr 允许自由拷贝,使用引用计数解决多次释放的问题

引用计数: 记录有几个对象参与管理这个资源

shared_ptr 实现:

使用静态成员变量实现。

cpp 复制代码
namespace hd
{
	template<class T>
	class shared_ptr {
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{
			_count = 1;
		}

		// sp(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			++_count;
		}

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

		int use_count()
		{
			return _count;
		}

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr;
		
		static int _count;
	};

	template<class T>
	int shared_ptr<T>::_count = 0;
}

中释放了一个资源!

如果使用静态成员属于这个类,属于这个类的所有对象
需求:每个资源配一个引用计数,而不是全部都是一个引用计数!

所以,一个资源配一个引用计数无论多少个对象管理这个资源,只有这一个计数对象!

怎么找到这个引用呢?每个对象存一个指向计数的指针!

cpp 复制代码
namespace hd
{
	template<class T>
	class shared_ptr {
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pcount(new int(1))
		{}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			// 拷贝时++计数
			++(*_pcount);
		}


		void release()
		{
			// 说明最后一个管理对象析构了,可以释放资源了
			if (--(*_pcount) == 0)
			{
				std::cout << "delete:" << _ptr << std::endl;
				delete _ptr;
				delete _pcount;
			}
		}


		// 赋值 sp1 = sp3;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) // 避免自己给自己赋值
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;

				// 拷贝时++计数
				++(*_pcount);
			}

			return *this;
		}

		~shared_ptr()
		{
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr;
		int* _pcount;
	};

}

shared_ptr 的缺陷:

cpp 复制代码
// shared_ptr 的缺陷
struct ListNode
{
	int _val;
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;

	ListNode(int val = 0)
		:_val(val)
		,_next(nullptr)
		,_prev(nullptr)
	{}

};

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(10));
	std::shared_ptr<ListNode> n2(new ListNode(20));

	n1->_next = n2;
	n2->_prev = n1;

	//delete n1;
	//delete n2;

	return 0;
}

循环引用:

  1. 左边的节点,是由右边的节点_prev管着的,_prev析构,引用计数减到 0, 左边的节点就是释放
  2. 右边节点中_prev 什么时候析构呢?右边的节点被delete时,_prev 析构。
  3. 右边节点什么时候delete呢?右边的节点被左边的节点的_next管着的,_next析构,右边的节点就释放了。
  4. _next 什么时候析构呢?_next 是左边节点的成员,左边节点 delete, _next 就析构了
  5. 左边节点什么时候释放呢?回调 1 点 又循环上去了

右边节点释放 -> _prev析构 -> 左边节点的释放 -> _next析构 -> 右边节点释放

所以这是 shared_ptr 特定场景下的缺陷, 只要有两个shared_ptr 互相管理就会出现这样的情况,所以即使用了智能指针,同样可能导致内存的泄漏。

cpp 复制代码
struct ListNode
{
	int _val;
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;

	ListNode(int val = 0)
		:_val(val)
	{}

};

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(10));
	std::shared_ptr<ListNode> n2(new ListNode(20));

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	n1->_next = n2;
	n2->_prev = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	//delete n1;
	//delete n2;

	return 0;
}

weak_ptr 可以通过不增加引用计数的方式,避免这个问题。(存在单独自己的 引用计数)
weak_ptr 不支持RAII, 不参与资源管理,不支持指针初始化,但是还是能起到指向你的作用
weak_ptr 的实现:

cpp 复制代码
namespace hd
{
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
		}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{  
			_ptr = sp.get(); // 用 get方法调原生指针
		}

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr;
	};
}

shared_ptr 定制删除器

cpp 复制代码
template<class T>
struct DeleteArry
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

// 定制删除器
int main()
{
	std::shared_ptr<ListNode> p1(new ListNode(10));
	std::shared_ptr<ListNode[]> p2(new ListNode[10]); // 可以用数组的

	std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArry<ListNode>()); // 用仿函数的对象去释放!
	std::shared_ptr<FILE> p3(fopen("test.cpp", "r"), [](FILE* ptr) {fclose(ptr);  }); // 用lamada表达式也是可以的

	return 0;
}

定制删除器实现:

cpp 复制代码
namespace hd
{
	template<class T>
	class shared_ptr
	{
	public:

		// function<void(T*)> _del = [](T* ptr) {delete ptr; };
		template<class D>
		shared_ptr(T* ptr, D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			, _del(del)
		{}

		// RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			// 拷贝时++计数
			++(*_pcount);
		}

		// sp1 = sp4
		// sp4 = sp4;
		// sp1 = sp2;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;

				// 拷贝时++计数
				++(*_pcount);
			}

			return *this;
		}

		void release()
		{
			// 说明最后一个管理对象析构了,可以释放资源了
			if (--(*_pcount) == 0)
			{
				std::cout << "delete:" << _ptr << std::endl;
				//delete _ptr;
				_del(_ptr);

				delete _pcount;
			}
		}

		~shared_ptr()
		{
			// 析构时,--计数,计数减到0,
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;

		std::function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};

}

4. 内存泄漏

什么是内存泄漏 :内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内

存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对

该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害 :长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现

内存泄漏会导致响应越来越慢,最终卡死。

cpp 复制代码
void MemoryLeaks()
{
   // 1.内存申请了忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  
  // 2.异常安全问题
  int* p3 = new int[10];
  
  Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  
  delete[] p3;
}

总结:

本文详细介绍了智能指针的概念、使用和原理,从C++98的auto_ptr到C++11的unique_ptrshared_ptr,展示了智能指针在现代C++编程中的应用和发展。我们了解到RAII(资源获取即初始化)的设计模式,它通过将资源管理封装在对象的生命周期中,简化了资源的获取和释放过程。文章还讨论了智能指针的拷贝问题,特别是auto_ptr的缺陷和shared_ptr的循环引用问题,以及如何使用weak_ptr和定制删除器来解决这些问题。

此外,文章还探讨了内存泄漏的概念、原因和危害,以及如何在实际编程中避免这些问题。通过具体的例子和代码,我们学习了如何使用智能指针来管理资源,确保资源在使用完毕后能够被正确释放,从而避免内存泄漏和其他潜在的资源管理问题。

总的来说,智能指针是C++中一个强大的特性,它不仅提高了代码的安全性和效率,还使得资源管理变得更加简单和直观。通过本文的学习,读者应该能够更加自信地在C++项目中使用智能指针,编写出更加健壮和可靠的软件。

相关推荐
星星法术嗲人2 分钟前
【Java】—— 集合框架:Collections工具类的使用
java·开发语言
Ljubim.te8 分钟前
软件设计师——数据结构
数据结构·笔记
Eric.Lee202115 分钟前
数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall
python·深度学习·算法·目标检测·计算机视觉·数据集·螃蟹检测
黑不溜秋的16 分钟前
C++ 语言特性29 - 协程介绍
开发语言·c++
一丝晨光21 分钟前
C++、Ruby和JavaScript
java·开发语言·javascript·c++·python·c·ruby
天上掉下来个程小白23 分钟前
Stream流的中间方法
java·开发语言·windows
林辞忧24 分钟前
算法修炼之路之滑动窗口
算法
xujinwei_gingko34 分钟前
JAVA基础面试题汇总(持续更新)
java·开发语言
￴ㅤ￴￴ㅤ9527超级帅35 分钟前
LeetCode hot100---二叉树专题(C++语言)
c++·算法·leetcode
liuyang-neu36 分钟前
力扣 简单 110.平衡二叉树
java·算法·leetcode·深度优先