C++从入门到起飞之——智能指针!

发布于:2025-08-01 ⋅ 阅读:(24) ⋅ 点赞:(0)

1. 智能指针的使用场景

下面程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后面的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来麻烦。智能指针放到这样的场景里面就让问题简单多了。

#include <iostream>

using namespace std;

double Divide(int a, int b)
{
	// 当b == 0时抛出异常 
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}
void Func()
{
	// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。 
	// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。 
	// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案 
	// 是智能指针,否则代码太戳了 
	int* array1 = new int[10];
	int* array2 = new int[10]; // 抛异常呢 
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array1 << endl;
		cout << "delete []" << array2 << endl;

		delete[] array1;
		delete[] array2;
		throw; // 异常重新抛出,捕获到什么抛出什么 
	}
	// ...
	cout << "delete []" << array1 << endl;
	delete[] array1;
	cout << "delete []" << array2 << endl;
	delete[] array2;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

2. RAII和智能指针的设计思路

RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类⼀
样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。

template<class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr) :_ptr(ptr)
	{}

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

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

	T& operator[](size_t i)
	{
		return _ptr[i];
	}

	~smart_ptr()
	{
		delete[] _ptr;
	}
private:
	T* _ptr;
};

void Func()
{
    这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了 
	smart_ptr<int> p1 = new int[10];
	smart_ptr<pair<string,int>> p2 = new pair<string, int>[10];

	int len, time;
	cin >> len >> time;
	cout << Divide(len, time) << endl;
}

3. C++标准库智能指针的使用

C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使用了,
智能指针有好几种,除了weak_ptr他们都符合RAII和像指针⼀样访问的行为,原理上而言主要是解
决智能指针拷贝时的思路不同。

> auto_ptr

auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给
拷贝对象
,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计
出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用
这个智能指针的。

#include <iostream>
#include <memory>

using namespace std;

struct Date
{
	int _year;
	int _month;
	int _day;
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	~Date()
	{
		cout << "~Date()" << endl;
	}
};

int main()
{
	auto_ptr<Date> p1(new Date);

	auto_ptr<Date> p2 = p1;

	return 0;
}

被拷贝对象p1直接就为空了,p1是一个左值,如果再访问程序就会报错 !!

> unique_ptr

unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他

	unique_ptr<Date> p1(new Date);
	//无法拷贝
	//unique_ptr<Date> p2 = p1;
	//可以move移动构造转移资源
	unique_ptr<Date> p2(move(p1));

虽然move移动转移资源后,p1是空的,但是p1本就是左值,使用者显然是知道move可能会带来的后果的。所以,move左值的时候一定要谨慎。

下面简单封装一下unique_ptr:

#include <iostream>
#include <memory>

using namespace std;

namespace my_unique_ptr
{
	template<class T>
	class unique_ptr
	{
	public:
		//删除拷贝构造和拷贝赋值
		unique_ptr(const unique_ptr<T>& uptr) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& uptr) = delete;

		//构造函数
		explicit unique_ptr(T* ptr) :_ptr(ptr)
		{}

		//移动构造
		unique_ptr(unique_ptr<T>&& uptr) :_ptr(uptr._ptr)
		{
			uptr._ptr = nullptr;
		}
		//移动赋值
		unique_ptr<T>& operator=(const unique_ptr<T>&& uptr)
		{
			//释放当前资源
			if (_ptr) delete _ptr;

			_ptr = uptr._ptr;
			uptr._ptr = nullptr;
			return *this;
		}

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

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

		T& operator[](size_t i)
		{
			return _ptr[i];
		}

		const T& operator[](size_t i) const
		{
			return _ptr[i];
		}

		~unique_ptr()
		{
			if(_ptr)
			{
				delete[] _ptr;
				cout << "~unique_ptr()" << endl;
			}
		}

	private:
		T* _ptr;
	};
}

int main()
{
	my_unique_ptr::unique_ptr<int> p1(new int[10]);
	my_unique_ptr::unique_ptr<int> p2(new int[10]);

	for (int i = 0; i < 10; i++)
	{
		p1[i] = i;
		p2[i] = i;
	}

	my_unique_ptr::unique_ptr<int> p3(move(p1));
	my_unique_ptr::unique_ptr<int> p4 = move(p2);

	for (int i = 0; i < 10; i++)
	{
		cout << p3[i] << " ";
	}
	cout << endl;

	return 0;
}

> shared_ptr

shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝, 也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的

我们先来简单使用一下:

int main()
{
	shared_ptr<int> sp1(new int(10));
	shared_ptr<int> sp2(new int[10]);
	//支持拷贝
	shared_ptr<int> sp3 = sp1;

	return 0;
}

我们知道智能指针如果支持拷贝,那一定是浅拷贝。因为智能指针的本质是方便我们访问和管理我们new的资源。如果是深拷贝,那就违背智能指针的初衷了!!不过,浅拷贝又会引发两个问题:多个指针管理同一片资源,资源的释放时机,资源的释放次数所以,我们要确保这份资源只析构一次,并且只有在没有指针管理这份资源的时候才释放!!

因此,在share_ptr的底层中使用了引用计数的方式解决这些问题!!原理就是,用一个计数器管理一篇资源,计数器的数目代表有多少个指针管理这片资源。那我们该怎么实现呢??在share_ptr中,我们new一个count,用这个count来维护。这里不能使用static的原因就是静态变量是属于整个类实例化出来的对象的,因此只有一个计数器是无法管理多份资源的!!

下面简单封装了一下share_ptr:

template<class T>
class share_ptr
{
public:
	//构造
	share_ptr(T* ptr)
		:_ptr(ptr),
		_pcnt(new int(1))
	{
	}
	//析构
	~share_ptr()
	{
		if (--(*_pcnt) == 0)
		{
			delete _ptr;
			delete _pcnt;
		}
	}

	//拷贝构造
	share_ptr(const share_ptr<T>& sptr)
		:_ptr(sptr._ptr)
		, _pcnt(sptr._pcnt)
	{
		++(*_pcnt);
	}

	//拷贝赋值
	share_ptr<T>& operator=(const share_ptr<T>& sptr)
	{
		// 如果指向同一份资源就不要赋值了
		if (_ptr != sptr._ptr)
		{
			//当前引用计数--
			if (--(*_pcnt) == 0)
			{
				delete _ptr;
				delete _pcnt;
			}
			//更改资源
			_ptr = sptr._ptr;
			_pcnt = sptr._pcnt;
			(*_pcnt)++;
		}
		return *this;
	}

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

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

	T& operator[](size_t i)
	{
		return _ptr[i];
	}

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

> 删除器

无论是unique_ptr还是share_ptr在默认析构时都是使用delete释放资源的,所以如果智能指针管理的资源不是new出来的,程序在释放资源时就会崩溃。

//这样写程序就会崩溃
shared_ptr<Date> sp1(new Date[10]);
unique_ptr<Date> sp2(new Date[10]);

为了解决这个问题,shared_ptr支持在构造的时候给定一个删除器,这个删除器本质就是一个可调用对象,比如:仿函数,函数指针,lambda表达式……而我们的智能指针在大多数情况下用的都是new所以就特化了一个delete[]的版本,我们只需要如下传递模版参数即可,很方便

shared_ptr<Date[]> sp1(new Date[10]);
unique_ptr<Date[]> sp2(new Date[10]);

而传删除器是一种通用的方法【这里直接传lambda表达式是真的香】。

	shared_ptr<Date[]> sp1(new Date[10], [](Date* ptr) {
		delete[] ptr;
		});

	shared_ptr<FILE> sp3(fopen("code1.cpp", "r"),[](FILE* ptr) {
		fclose(ptr);
		});

对于特化的new来说,unique_ptr和shared_ptr使用删除器都一样,但是如果是其他情况就不一样了。因为unique_ptr和shared_ptr对删除器的设计有所不同,shared_ptr是在类内部定义的构造函数模版,我们直接传对象编译器就会自动推导类型。但是,unique_ptr却是在类模版多定义了一个模版参数来支持删除器。所以,在使用unique_ptr传删除器的时候,使用仿函数类型会比较方便,因为仿函数类型可以直接定义对象。但是lambda表达式和函数指针是不能直接定义对象的,所以还是要在后面传递一个实例化的对象

//仿函数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
//函数指针
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
//lambda
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);

下面我们就自己实现一下shared_ptr的删除器:


	template<class T>
	class shared_ptr
	{
	public:
		//构造(没有传删除器就走这个构造)
		shared_ptr(T* ptr)
			:_ptr(ptr),
			_pcnt(new int(1))
		{}

		template<class D>
		shared_ptr(T* ptr, D del)
			: _ptr(ptr),
			_pcnt(new int(1)),
			_del(del)
		{}

		//析构
		~shared_ptr()
		{
			if (--(*_pcnt) == 0)
			{
				_del(_ptr);
				delete _pcnt;
			}
		}

		//拷贝构造
		shared_ptr(const shared_ptr<T>& sptr)
			:_ptr(sptr._ptr)
			, _pcnt(sptr._pcnt)
		{
			++(*_pcnt);
		}

		//拷贝赋值
		shared_ptr<T>& operator=(const shared_ptr<T>& sptr)
		{
			// 如果指向同一份资源就不要赋值了
			if (_ptr != sptr._ptr)
			{
				//当前引用计数--
				if (--(*_pcnt) == 0)
				{
					delete _ptr;
					delete _pcnt;
				}
				//更改资源
				_ptr = sptr._ptr;
				_pcnt = sptr._pcnt;
				(*_pcnt)++;
			}
			return *this;
		}

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

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

		T& operator[](size_t i)
		{
			return _ptr[i];
		}

	private:
		T* _ptr;
		int* _pcnt;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};
}

> 使用的小细节

• shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。

• shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否空。

• shared_ptr 和 unique_ptr 都得构造函数都使用explicit【在编程语言中,“explicit”作为关键字,表示需要显式转换的数据类型,需手动调用转换函数,而非自动转换修饰,防止普通指针隐式类型转换成智能指针对象

4. shared_ptr和weak_ptr

4.1 shared_ptr循环引用问题

shared_ptr在大多数资源管理的场景下都可以很好的解决,但是在循环引用的场景下却会发生资源无法释放,内存泄漏的问题!

在如下场景就会造成循环引用,导致内存泄漏!!

struct ListNode
{
	int _data;
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	// 循环引⽤ -- 内存泄露 
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);
	n1->_next = n2;
	n2->_prev = n1;
	return 0;
}

在上图的分析中,next管理着prev指向的资源,而prev也管理着next指向的资源。但是,它们互相指向,也就是说,它们之中一定要有一个释放的话,其条件都是对方要先释放。可是,它们之间相互依赖,彼此制衡,形成一个环状的资源释放链,最终导致它们之间谁都无法释放!!

4.2 weak_ptr

为了解决上述问题,C++11中就引入了weak_ptr。weak_ptr既不支持RAII【资源请求立即初始化】,也不支持访问资源。所以,我们看文档时发现weak_ptr不支持构造时绑定资源,只支持绑定到share_ptr,但是绑定到share_ptr时并不增加计数器的数目。因此,weak_ptr可以解决上述问题

struct ListNode
{
	int _data;
	//std::shared_ptr<listnode> _next;
	//std::shared_ptr<listnode> _prev;
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;

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

weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的
shared_ptr已经释放了资源
,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的
资源是否过期
,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用
lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如
果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

int main()
{
	std::shared_ptr<string> sp1(new string("111111"));
	std::shared_ptr<string> sp2(sp1);
	std::weak_ptr<string> wp = sp1;

	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;

	// sp1和sp2都指向了其他资源,则weak_ptr就过期了 
	sp1 = make_shared<string>("222222");
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;
	sp2 = make_shared<string>("333333");
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;

	wp = sp1;
	auto sp3 = wp.lock();
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;
	*sp3 += "###";

	cout << *sp1 << endl;
	return 0;
}

网站公告

今日签到

点亮在社区的每一天
去签到