首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【c++】异常与智能指针

【c++】异常与智能指针

作者头像
mosheng
发布2026-01-14 18:53:58
发布2026-01-14 18:53:58
800
举报
文章被收录于专栏:c++c++

hello~ 很高兴见到大家! 这次带来的是C++中关于智能指针这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢? 个 人 主 页: 默|笙

一、异常的关键记忆点

  1. 异常是一种处理错误的方式,它允许程序在遇到无法正常执行的状况时,跳出当前的执行流程,并寻找一个异常处理程序来处理这个错误。也就是将错误代码和正常代码分离开来。
  2. 当程序出现问题时,会通过抛出(throw)一个对象来引发一个异常,后续会用catch捕捉这个异常。
  3. 这个对象的类型和调用链决定了由哪个catch代码来处理这个异常,会选择类型匹配且离抛出位置最近的catch来处理。
  4. 异常抛出时,后面的代码将不会再执行,会直接跳转到catch位置处理异常。 而这可能会存在内存泄漏问题,比如new出一块内存,在之后是要用delete释放的,而一旦new的时候发生了异常跳过了delete到catch,资源就没有得到释放。
  5. 处理异常导致的内存泄漏问题有两个解决办法,一个是不断嵌套try…catch语句,因为抛出异常的语句不同对应需要释放的资源也可能不同,还有一个就是接下来要讲到的智能指针
  6. 这个抛出的对象是一个异常对象的拷贝,因为这个异常对象极有可能是一个局部对象,这个拷贝的对象在catch语句执行完销毁。
  7. 这里也存在类型转换:非常量(const)到常量(权限缩小),数组转换为指向数组元素类型的指针,函数转换为指向函数的指针,以及派生类类型到基类类型的转换。
  8. 一般main语句中会**catch(…)**来捕捉异常,这样可以避免程序终止(如果一个异常直到main函数都没有匹配的catch语句,编译器会调用terminate函数终止程序。
  9. 异常捕获之后可以用throw再次直接抛出。
  10. C++11里:加上noexcept的函数代表不会抛异常。

二、智能指针

1. RAII与智能指针

  1. RAII是Resource Acquisition Is Initialization的缩写,它是一种管理资源的类的设计思想,它利用对象生命周期来管理获取到的动态资源,因为对象出作用域后会自动调用析构函数,自动释放资源,从而避免资源泄漏。这里的资源可以是内存、文件指针、网络连接、互斥锁等。
  2. RAII在获取资源后会把资源交给这个类的对象进行管理,通过这个对象控制对资源的访问,资源会在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源
  3. 智能指针类就是一个满足了RAII设计思路的类,同时重载了operator->/operator*/operatro[]等运算符,实现对资源的访问。
  4. 我的理解是:智能指针类通过传递给它的一个指针来构造一个对象,本质是对这个指针的封装,将这个指针升级为了能够自动调用析构的对象,从而避免内存泄漏。

一个智能指针类的基本结构:

代码语言:javascript
复制
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{
	}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete[] _ptr;
	}

	// 重载运算符,模拟指针的行为,方便访问资源
	T& operator*()
	{
		return *_ptr;
	}

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

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

public:
	T* _ptr;
};

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

C++标准库中的智能指针都在头文件<memory>里,只要包了这个头文件就能够使用接下来的几种智能指针。 其中出了weak_ptr智能指针外,其他的都是应用了RAII的设计思路。

2.1 auto_ptr
  1. auto_ptr是C++98版本里设计出来的智能指针,它会在拷贝时将这个智能指针对象管理的资源转移给拷贝出的智能指针对象。这就会导致一个问题,那就是原来的智能指针对象会置空。这个时候如果我们调用原来的智能指针对象访问资源就会访问报错,因为它不再管理任何资源了。不建议使用这个智能指针。
代码语言:javascript
复制
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> ap1(new Date);

	auto_ptr<Date> ap2(ap1);
	return 0;
}

拷贝之前:

在这里插入图片描述
在这里插入图片描述

拷贝之后,可以看到,ap1被置空了,它的管理的资源都转移到了ap2那里。

在这里插入图片描述
在这里插入图片描述
2.2 unique_ptr
在这里插入图片描述
在这里插入图片描述
  1. unique_ptr是C++11版本里设计出来的智能指针,被译为唯一指针它只支持移动,不支持拷贝。如果是将右值的资源交给智能指针管理,就非常推荐使用它。
代码语言:javascript
复制
unique_ptr<Date> up1(new Date);//用指向资源的指针进行构造
unique_ptr<Date> up2(up1);//不支持拷贝
unique_ptr<Date> up3 = make_unique<Date>();//支持make_unique,移动构造
unique_ptr<Date> up4(Date());//易错点,这里编译器会识别成函数声明,这样调用不了移动构造
up1 = up2;//不支持赋值
  1. 要注意的是up4会被编译器识别为函数声明,而不会调用移动构造。如果要使用移动构造管理临时对象的资源,最好使用make_unique(c++14引入)函数模板,可以直接进行构造。
2.3 shared_ptr
  1. shared_ptr是C++11版本里设计出来的智能指针,被译为共享指针,它既支持移动,也支持拷贝。
  2. 它拷贝出来的智能指针对象会与它一起管理这个资源。它的底层是用引用计数来实现的。具体会在接下来的原理部分讲解。
在这里插入图片描述
在这里插入图片描述
  1. shared_ptr除了支持使用指向资源的指针进行构造,还支持使用make_shared直接构造,make_shard和make_pair有些像,可以用初始化资源对象的值直接进行构造。
代码语言:javascript
复制
shared_ptr<Date> sp1(new Date);//用指向资源的指针进行构造
shared_ptr<Date> sp2 = sp1;//拷贝构造
shared_ptr<Date> sp3 = make_shared<Date>(Date());//利用make_shared函数模板默认构造sp3
在这里插入图片描述
在这里插入图片描述
  1. sp1和sp2都指向了同一个资源,它们共同管理。
  2. unique_ptr和shared_ptr的构造函数都得使用explicit修饰,防止一般对象发生隐式转换转换成智能指针对象,这也是创建智能指针对象必须显式初始化的原因。
  3. unique_ptr和shared_ptr都支持了operator bool的类型转换,如果智能指针对象没有管理资源就返回false,否则返回true。这意味着我们可以将智能指针对象交给if语句判断是否为空
2.4 weak_ptr
  1. weak_ptr被译为弱指针,它不是借用RAII设计思路设计出来的指针,我们不能用它来直接管理资源,它被设计出来是为了解决shared_ptr循环引用导致内存泄漏的问题。
2.5 删除器
  1. 智能指针析构的时候默认都是使用delete进行释放,这意味着如果资源不是通过new出来的,那么智能指针对象在析构释放资源时就会出现问题,程序崩溃。
  2. 智能指针支持在构造时给一个删除器,删除器的本质就是一个可调用对象,这个可调用对象中实现的是你想要的释放资源的方式。在构造智能指针对象时,如果给了定制的删除器,析构时就会去调用这个删除器进行对应的删除。
  3. 由于new[]经常使用,为了方便,所以unique_ptr和shared_ptr都特化了一份[]版本,使用时unique_ptr<Date[]> up1(new Date[5])这样就能够在析构时调用delete[]进行析构。
  4. 给unique_ptr删除器是要给删除器的类型,删除器的类型是模板参数的一部分,而给shared_ptr删除器是给一个对象,它通过构造函数的参数传入
代码语言:javascript
复制
shared_ptr<Date> sp1(new Date[10], DeleteArray<Date>());
shared_ptr<Date> sp2(new Date[10], [](Date* ptr) {delete[] ptr; });
unique_ptr<Date, DeleteArray<Date>> up1(new Date[5]);
  1. 像是shared_ptr我们就可以传递一个可调用对象,比如lambda表达式,仿函数对象等,但对于unique_ptr,我们必须传递类型,使用lambda表达式就会非常麻烦,需要用decltype得到lambda表达式类型。

3. 智能指针的原理

3.1 auto_ptr和unique_ptr

  1. auto_ptr的实现它的拷贝构造是将原来的对象的资源转移给拷贝对象,然后将原来对象里的指针置空(nullptr)。unique_ptr它的移动构造跟auto_ptr拷贝构造的实现十分相像,同时它不支持拷贝构造和赋值重载函数,拷贝构造和赋值重载只声明不定义且在后面加上=delete,表明被禁止使用。

3.2 shared_ptr的实现

  1. 实现shared_ptr就是要解决多个智能指针对象共同管理一个资源的问题,不能够因为一个shared_ptr指针销毁就直接清理资源,万一还有别的shared_ptr智能指针对象在对这个资源进行管理呢?我们的期望是:一个资源能够被多个智能指针进行管理,比如对数据进行修改,但是只有最后一个管理这个资源的智能指针对象销毁后这个资源才会释放。
  2. 所以统计一个资源被多少智能指针对象所管理就会非常重要,我们需要一个用来计数的变量,多一个管理对象它就++,少一个就–,几个指向相同资源的智能指针对象共用同一份计数变量,我们将这个变量设置为指针或者引用类型,这样修改它的数量就能够影响到其他指向这个资源的其他指向这个资源的智能指针对象。它不能通过设置静态成员变量来实现,因为那样所有的智能指针对象都会共用这个变量,无论它们指向的资源是否相同,而我们的期望是:一个资源对应一个用来计数的变量。
在这里插入图片描述
在这里插入图片描述
  1. 只有在引用计数减为0后才释放资源,否则只是将引用计数- -。
代码语言:javascript
复制
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{ }

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

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

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

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

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

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

	T& operator[](int i)
	{
		return *(_ptr + i);
	}

	T* get()const
	{
		return _ptr;
	}

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

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

4. shared_ptr和weak_ptr

4.1 shared_ptr循环引用问题
  1. shared_ptr在大多数情况下管理资源都不会出现问题,支持RAII,也支持拷贝,但是在循环引用的场景下会导致资源没有释放内存泄漏。如下图所示场景:
在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
std::shared_ptr\<ListNode> n1(new ListNode); 
std::shared_ptr\<ListNode> n2(new ListNode);
n1->next = n2;
n2->prev = n1;
  1. 所示场景,ListNode结构体里prev和next指针都是shared_ptr类型,n1和n2析构以后管理节点的两个引用计数都会减到1,但并不会释放节点(没有减到0),因为左边的节点由右边的prev管着,而右边节点由next管着。prev析构之后左边节点才能够释放,而prev是右边节点的成员,右边节点释放,prev就析构了。而右边节点由next管着,next是左边节点的成员,左边节点释放,next就析构了。它们两个互相牵制着彼此,谁也无法奈何谁,谁都无法释放,这样就构成了循环引用,导致内存泄漏。
  2. 将ListNode结构体中的prev和next改称weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,next和prev不参与资源释放管理逻辑,这样就成功打破了循环引用,解决了这里的问题。
4.2 weak_ptr
  1. weak_ptr不支持RAII,也不支持访问资源,weak_ptr构造不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,它不会增加shared_ptr的引用计数。
  2. weak_ptr也没有重载operator*和operator->等,因为它不参与资源管理,那么如果它绑定的shared_ptr,那么它去访问资源会非常危险。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
2.5 三个智能指针auto_ptr、unique_ptr、shared_ptr的区别
  1. atuo_ptr是转移管理权限,unique_ptr也是转移管理权限,但由于它转移的是右值的管理权限,所以不会对原来的智能指针对象产生影响。
  2. 移动的场景使用unique_ptr就好,拷贝的场景使用shared_ptr。
  3. auto_ptr和unique_ptr是独占所有权,不会像shared_ptr一样出现循环引用的问题。
  4. unique_ptr和shared_ptr传递删除器语法的差异:前者传递类型(模板参数),后者传递一个可调用对象。

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~ 让我们共同努力, 一起走下去!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、异常的关键记忆点
  • 二、智能指针
    • 1. RAII与智能指针
    • 2. C++标准库智能指针的使用
      • 2.1 auto_ptr
      • 2.2 unique_ptr
      • 2.3 shared_ptr
      • 2.4 weak_ptr
      • 2.5 删除器
    • 3. 智能指针的原理
    • 3.1 auto_ptr和unique_ptr
    • 3.2 shared_ptr的实现
    • 4. shared_ptr和weak_ptr
      • 4.1 shared_ptr循环引用问题
      • 4.2 weak_ptr
      • 2.5 三个智能指针auto_ptr、unique_ptr、shared_ptr的区别
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档