智能指针(C++)

智能指针 (RAII)

  • 前言
    • [1. 引入智能指针](#1. 引入智能指针)
    • [2. 智能指针的设计和使用](#2. 智能指针的设计和使用)
  • 一、库中的智能指针
    • [1. std::auto_ptr(C++98了解即可)](#1. std::auto_ptr(C++98了解即可))
    • [2. std::unique_ptr(C++11)](#2. std::unique_ptr(C++11))
    • [3. std::shared_ptr(C++11)](#3. std::shared_ptr(C++11))
    • [4. 定制删除器](#4. 定制删除器)

前言

1. 引入智能指针

我在之前的博客介绍了异常(见到认识就行),用于处理一些函数内无法处理的问题,但是它会导致执行流乱跳。所以也就可能会导致内存泄漏等内存管理问题。

eg:抛出异常之后,无法delete,导致内存泄漏

cpp 复制代码
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除0错误");
	}
	return a / b;
}

void Func() 
{
    int* p1 = new int;
    int* p2 = new int;
    cout << div() << endl; // 如果这里抛出异常(如除0错误)
    delete p1; // 这些代码可能无法执行
    delete p2; 
}

为了解决上述问题,避免手动管理内存的缺陷,C++引入了 RAII(Resource Acquisition Is Initialization) 技术。

  1. 核心原理: 利用对象的生命周期来控制资源。构造时获取资源,析构时释放资源。实际上把一份资源的责任托管给了一个对象
  2. 优势: 无需显式释放,对象离开作用域(无论是正常结束还是异常退出)都会自动调用析构函数,保证资源不泄漏

2. 智能指针的设计和使用

指针指针的原理:用RAII思想,封装一个简单的SmartPtr类,且要像指针一样使用。

cpp 复制代码
template<class T>
class SmartPtr
{
public:
	// 获取资源
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	// 释放资源
	~SmartPtr()
	{
		delete _ptr;
	}

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

eg: 使用智能指针

cpp 复制代码
void f()
{
	SmartPtr<pair<string, string>> sp1(new pair<string, string>("1111", "22222"));
	SmartPtr<string> sp2(new string("xxxxx"));

	// 正常像指针一样使用
	cout << sp1->first << endl;
	cout << sp1->second << endl;
	cout << *sp2 << endl;

	div();   // 这里异常了也会正常释放资源
}

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

一、库中的智能指针

1. std::auto_ptr(C++98了解即可)

  1. auto_ptr的实现原理:管理权转移的思想。
  2. 问题: 当发生拷贝构造或赋值时,会把被拷贝对象的资源管理权转移给拷贝对象(赋值同理),原指针会变为 nullptr,导致原指针悬空(Dangling Pointer)

实现一个简易版本的auto_ptr:

cpp 复制代码
namespace kpl
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			delete _ptr;
		}

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

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

		// ap2(ap1)   赋值同理
		// 管理权转移
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

	private:
		T* _ptr;
	};
}

使用:

cpp 复制代码
// 用自定义类型测试
class A
{
public:
	A(int a = 0)
		:_a(a)
	{}
	~A()
	{}
	int _a;
};

int main()
{
	kpl::auto_ptr<A> ap1(new A(1));
	kpl::auto_ptr<A> ap2(ap1);

	// 崩溃
	// ap1->_a++;  // 此时的ap1是nullptr 悬空
	ap2->_a++;

	return 0;
}

2. std::unique_ptr(C++11)

  1. std::unique_ptr原理: 简单粗暴的防拷贝。它明确禁止了拷贝构造和赋值操作(通过 = delete)。
  2. 特点: 独占所指对象的内存,同一时刻只能有一个unique_ptr指向该资源,用于不需要共享所有权的场景。

一个简易版本的unique_ptr:

cpp 复制代码
namespace kpl
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			delete _ptr;
		}

		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		// 防拷贝
		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
	private:
		T* _ptr;
	};
}

使用:

cpp 复制代码
int main()
{
	// A是自定义类型
	kpl::unique_ptr<A> up1(new A(1));
	kpl::unique_ptr<A> up2(new A(2));

	// 都被禁止
	//kpl:unique_ptr<A> up3(up1);
	//up1 = up2;
	return 0;
}

3. std::shared_ptr(C++11)

①std::shared_ptr

std::shared_ptr实现原理:关键就是引用计数(Reference Counting),在构造时创建

  • 内部维护一个计数器,记录有多少个 shared_ptr 共享同一个对象。
  • 每次拷贝构造或赋值,计数器 +1。
  • 每次析构,计数器 -1。
  • 当计数器变为 0 时,真正释放内存。

一个简易版本的shared_ptr:

cpp 复制代码
namespace kpl
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			// 这个引用计数要保证多个对象共享,且指向同一份资源的时候共同使用
			// 使用static就不能保证多个对象都有这个引用计数
			// 使用成员变量则不能保证指向同一个资源时共享
			, _pcount(new int(1))
		{}
		~shared_ptr()
		{
			// 引用计数为0时,才释放资源
			if (--(*_pcount) == 0)
			{
				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)
		{
			// 引用计数+1
			++(*_pcount);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			// 要禁掉自己给自己赋值,否则它就会满足下一个if条件把自己的资源释放掉
			// 再执行赋值操作,但是此时_ptr已经被释放
			if (_ptr == sp._ptr)
				return *this;

			// 赋值是两个已经创建的对象,此时左侧对象指向右侧对象的资源
			// 但是右侧的资源则少一个对象指向所以要减引用计数
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);

			return *this;
		}

		// 返回引用计数
		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

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

使用:

cpp 复制代码
int main()
{
	shared_ptr<A> sp1(new A(1));
	shared_ptr<A> sp2(new A(2));

	// 拷贝构造
	shared_ptr<A> sp3(sp1);
	sp1->_a++;  // 都可以使用
	sp3->_a++;  // 都可以使用

	shared_ptr<A> sp4(sp2);
	shared_ptr<A> sp5(sp4);
	sp1 = sp5;
	sp3 = sp5;
	sp1 = sp1;
	return 0;
}

②线程安全问题

shared_ptr的线程安全问题:两个方面

  1. 引用计数的线程安全问题
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题(这不是库的问题是程序员写代码时需要注意的问题,不是这里要解决的)

解决引用计数的线程安全问题,把new出来的_pcount都使用atomic<int>

cpp 复制代码
namespace kpl
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new atomic<int>(1)) // 改
		{}
		~shared_ptr()
		{
			// 这里就会调用函数操作(原子操作),保证了线程安全,++或--是一次执行完成
			if (--(*_pcount) == 0)   
			{
				delete _ptr;
				delete _pcount;
			}
		}
	private:
		T* _ptr;
		atomic<int>* _pcount;   // 换成原子操作
	};
}

test:线程安全的测试

cpp 复制代码
namespace kpl
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new atomic<int>(1))
		{}
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				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 (_ptr == sp._ptr)
				return *this;
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);

			return *this;
		}


	private:
		T* _ptr;
		atomic<int>* _pcount;   // 换成原子操作
	};
}

int main()
{
	size_t n1 = 10000;
	size_t n2 = 10000;
	mutex mtx;
	kpl::shared_ptr<double> sp(new double(1.1));

	atomic<size_t> x = 0;
	thread t1([&]() {
		for (size_t i = 0; i < n1; i++)
		{
			kpl::shared_ptr<double> copy1(sp); 

			{
				// 外面这个大括号的作用,就是给这个unique_lock用的,这是一个域
				unique_lock<mutex> lock(mtx);
				++(*copy1);
			}
		}
		});

	thread t2([&]() {
		for (size_t i = 0; i < n2; i++)
		{
			kpl::shared_ptr<double> copy1(sp);

			{
				unique_lock<mutex> lock(mtx);
				++(*copy1);
			}
		}
		});

	t1.join();
	t2.join();

	cout << *sp << endl;
}

③weak_ptr

  1. weak_ptr不是RAII智能指针,专门用来解决shared_ptr循环引用问题
  2. 作用:weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理

一个简易版本的weak_ptr:

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

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}

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

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

使用:

cpp 复制代码
struct Node
{
	A _val;
	// Node*类型不满足智能指针的使用
	// Node* _next;

	// 会调用拷贝构造,所以会导致引用计数增加
	// kpl::shared_ptr<Node> _next;

	// 不会增加引用计数
	kpl::weak_ptr<Node> _next;
	kpl::weak_ptr<Node> _prev;

};

int main()
{
	
	kpl::shared_ptr<Node> sp1(new Node);
	kpl::shared_ptr<Node> sp2(new Node);
	
	// 循环引用
	sp1->_next = sp2;
	sp2->_prev = sp1;
	return 0;
}

4. 定制删除器

在C++11中的智能指针有个参数是del,即接下来要介绍的定制删除器。在上面我们自主实现的析构都是用delete进行释放资源,可是如果不是new一个数据而是一堆呢?是不是就要使用delete[],如果是打开一个文件的指针给智能指针管理是不是还得用fclose释放。定制删除器可以有效的释放资源

eg:shared_ptr库中的构造函数就包括定制删除器

cpp 复制代码
template <class U, class D> 
shared_ptr (U* p, D del); // 这里的D就是可调用对象类型,del就是可调用对象(只是其中一个例子)

使用:

cpp 复制代码
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};
int main()
{
	// 用什么方法创建/打开的资源就要使用对应的方式去释放
	shared_ptr<A> sp1(new A[10], DeleteArray<A>());
	shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
	shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {
		fclose(ptr);
		});
	return 0;
}

再在上面实现的shared_ptr中增加一个构造和一个成员变量,用来实现这个定制删除器的使用

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

	// function<void(T*)> _del;  所添加的构造
	template<class D>
	shared_ptr(T* ptr, D del)
		: _ptr(ptr)
		, _pcount(new atomic<int>(1))   // 解决线程安全问题
		, _del(del)  // 添加:定制器
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			//delete _ptr;
			delete _pcount;
			
			// 添加:调整释放方式
			_del(_ptr);
		}
	}

private:
	// 添加的成员变量 借用包装器返回值是void,参数是T*。给一个默认值一个lambda对象
	function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
相关推荐
Darkwanderor2 小时前
高精度计算——基础模板整理
c++·算法·高精度计算
Tanecious.3 小时前
蓝桥杯备赛:Day5-P1036 选数
c++·蓝桥杯
mmz12073 小时前
深度优先搜索DFS(c++)
c++·算法·深度优先
憧憬从前4 小时前
算法学习记录DAY1
c++·学习
A.A呐5 小时前
【C++第二十四章】异常
开发语言·c++
xiaoye-duck5 小时前
《算法题讲解指南:动态规划算法--子序列问题》--29.最长递增子序列的个数,30.最长数对链,31.最长定差子序列
c++·算法·动态规划
森G5 小时前
39、拓展知识---------事件系统
c++·qt
tankeven5 小时前
HJ164 太阳系DISCO
c++·算法
君鼎6 小时前
C++17 新特性全面总结
c++