首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++ 内存安全双保险:异常处理 + 智能指针,彻底跟内存泄漏说 “再见”

C++ 内存安全双保险:异常处理 + 智能指针,彻底跟内存泄漏说 “再见”

作者头像
Vect_
发布2025-12-18 18:01:55
发布2025-12-18 18:01:55
1530
举报
个人主页
个人主页

🎬 个人主页Vect个人主页 🎬 GitHubVect的代码仓库 🔥 个人专栏: 《数据结构与算法》《C++学习之旅》《计算机基础

⛺️Per aspera ad astra.



1. 异常

1.1. C传统处理错误的方式

  1. 终止程序,如assert,缺陷:用户体验感差,莫名其妙就终止程序,需要重新载入程序
  2. 返回错误码,缺陷:需要程序员自己去查找对应的错误,太过琐碎
代码语言:javascript
复制
#include <cassert>
void errorC() {
	// 1. 错误码
	int* arr = (int*)malloc(4 * sizeof(int));
	if (arr == NULL) {
		perror("malloc error:");
		return;
	}

	// 2. assert
	int num = 0;
	assert(num != 0);

}

1.2. C++异常概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数直接的或间接的调用者处理这个错误

  • throw:用来抛出异常,传递错误信息
  • catch:用来捕获异常,并进行处理。可以有多个catch进行捕获
  • try:包裹可能抛出异常的代码
代码语言:javascript
复制
void division(double a, double b) {
	if (b == 0)
		throw logic_error("分母为零!\n");
	cout << "结果: " << a / b << endl;
}
int main() {
	try {
		division(10, 0);
	}
	catch(const logic_error& e){
		cout << "错误:" << e.what() << endl;
	}

	return 0;
}

1.3. 异常的抛出和捕获

异常的抛出和匹配原则
1. 异常是通过抛出对象引发的

异常通过 throw 关键字抛出,抛出的对象类型决定了将要激活哪个 catch 块。

代码语言:javascript
复制
void func1() {
	throw "1111111";
}

int main() {
	try {
		func1();
	}
	catch (const char* errmsg) {
		cout << "捕获到的异常:" << errmsg << endl;
	}
}
  • throw抛出了个字符串
  • catch(const char* errmsg)捕获了异常并进行处理,catch()里面是异常的类型
2. 被选中的处理代码是调用链中离抛出异常位置最近的那个

异常会在调用栈中从当前函数向上逐层传播,直到找到与异常类型匹配的 catch 块。如果找到了匹配的 catch 块,异常就被捕获并处理。

代码语言:javascript
复制
void func() {
	throw out_of_range("越界!");
}

void Func() {
	try {
		func();
	}
	catch (const out_of_range& e) {
		cout << "Func 捕获到的异常:" << e.what() << endl;
	}
}

int main() {
	try {
		Func();
	}
	catch(const exception& e){
		cout << "main 捕获到的异常:" << e.what() << endl;
	}

	return 0;
}

func1 抛出了 out_of_range 异常。

异常首先从 func1func2 传播,在 func2 中找到与类型匹配的 catch 块并处理异常。

如果没有在 func2 找到匹配的异常处理,异常会继续传播到 main

3.异常对象的拷贝

当异常被抛出时,会生成一个异常对象的拷贝。这是因为抛出的异常可能是一个临时对象,它需要被拷贝到调用栈中,以便可以在 catch 块中访问。

代码语言:javascript
复制
void func() {
	throw out_of_range("越界!");
}
int main() {
	try {
		func();
	}
	catch (const exception& e) {
		cout << "exception 捕获:" << e.what() << endl;
	}
	return 0;
}

throw 抛出异常时,异常对象(如 out_of_range)会被拷贝到 catch 中,确保异常对象不会丢失。

4. catch(...)捕获所有类型的异常

catch(...) 是一个通用捕获语法,它可以捕获任何类型的异常,但缺点是不知道异常的具体类型。

代码语言:javascript
复制
void func() {
	throw 42;  // 抛出整数类型异常
}

int main() {
	try {
		func();
	}
	catch (...) {  // 捕获所有异常
		std::cout << "Caught some exception" << std::endl;
	}
	return 0;
}
异常栈展开匹配原则

异常会沿着函数调用栈帧向上搜索匹配的catch,直到找到一个合适的为止,如果在栈顶(main函数)都没有匹配,程序终止

1. 检查抛异常的位置是否在try内,如果在 try 块内抛出,程序会继续搜索匹配的 catch 块。 2. 没有匹配的 catch 块时,退出当前函数栈,继续向上传递到调用该函数的上层函数。直到找到一个合适的 catch **3. 如果异常传播到 main 还是没有匹配的 catch,程序会终止。 ** **4. 当异常被某个 catch 捕获并处理后,程序会继续执行 catch 块之后的代码。 **

来看这段代码:

代码语言:javascript
复制
// 栈展开
void func1() {
    throw out_of_range("Out of range error in func1");  // 异常在 func1 中抛出
}

void func2() {
    try {
        func1();  // 异常从 func1 传递到 func2
    }
    catch (const out_of_range& e) {
        cout << "Caught in func2: " << e.what() << endl;  // func2 捕获异常
        throw;  // 重新抛出异常
    }
}

int main() {
    try {
        func2();  // func2 捕获异常并重新抛出
    }
    catch (const exception& e) {
        cout << "Caught in main: " << e.what() << endl;  // main 捕获异常并处理
    }
    return 0;
}

梳理一下思路:

代码语言:javascript
复制
1. main() 调用 func2()
|
2. func2() 调用 func1()
|
3. func1() 中抛出 out_of_range 异常
|
4. 异常从 func1() 向 func2() 传播,func2() 捕获该异常
|
5. 如果 func2() 中没有捕获异常,异常会继续传递给 main() 函数
|
6. 如果 main() 没有捕获异常,程序终止

栈展开是这样的:

代码语言:javascript
复制
┌──────────────┐
│    main()    │  <-- 异常会传递到 main(),如果没有捕获程序终止
└──────────────┘
       ↑
┌──────────────┐
│   func2()    │  <-- 如果 func2() 捕获异常,栈展开停止
└──────────────┘
       ↑
┌──────────────┐
│   func1()    │  <-- 异常从 func1() 抛出并传递
└──────────────┘

总结:

  • 异常会从抛出的位置开始沿着调用链向上传播,直到找到匹配的 catch 块。
  • 如果没有匹配的 catch,程序会终止。
  • catch(...) 可以捕获所有类型的异常,但它无法获取异常的详细信息。
  • 异常传播过程叫做 栈展开,从当前函数栈一直传递到调用栈的顶部(main)。

1.4. 异常的重新抛出

异常的重新抛出是指在 catch 块内捕获到异常后,不完全处理该异常,而是将其再次抛出,交给外层的调用者进行进一步处理。

为什么需要重新抛出异常?

在一些情况下,你可能希望在捕获到异常后,做一些日志记录、清理工作或者部分恢复工作,但不希望完全处理异常。重新抛出异常后,外层调用者(通常是 main 函数或更外层的 catch)可以继续处理这个异常。

如何重新抛出异常?

catch 块中,我们可以通过 throw 关键字重新抛出异常。需要注意的是,重新抛出的异常类型和原始异常类型一致。

代码语言:javascript
复制
// 异常重新抛出
double division(double a, double b) {
	if (0 == b) {
		throw "分母为零!";
	}
	return a / b;
}

void Func() {
	// 如果发生分母为零抛出异常 但是arr还未释放
	// 所以这里捕获了异常但不处理
	// 把异常交给外界处理 捕获之后重新抛出去
	int* arr = new int[10];
	try {
		cout << division(10.1,0) << endl;
	}
	catch (...) {
		cout << "delete[]" << arr << endl;
		delete[] arr;
		throw;
	}

	cout << "delete[]" << arr << endl;
	delete[] arr;
}

int main() {

	try {
		Func();
	}
	catch (const char* errmsg) {
		cout << errmsg << endl;
	}
	return 0;
}

1.5. 异常安全

  • 构造函数完成对象的构造和初始化,不要在构造中抛异常,否则可能会导致对象不完整或没有完全初始化
  • 析构函数主要完成资源清理,最好不要在析构中抛异常,否则可能导致资源泄露
  • C++中会经常出现资源泄露的问题,而C++常用RAII解决问题,我们在后文中详解

1.6. 异常规范

异常规范于声明函数可能抛出的异常类型。它的作用是让函数的使用者了解该函数会抛出哪些异常类型,以及是否抛出异常。

throw()noexcept 语法:
  • throw():表示该函数不抛出任何异常。
  • noexcept:表示函数保证不抛出异常。
代码语言:javascript
复制
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

1.7. 异常的优缺点

优点:
  1. 错误分离:异常机制将错误处理从正常业务逻辑中分离出来,使代码更简洁、更易读。
  2. 自动清理:结合 RAII(资源获取即初始化)原理,异常会自动管理资源的释放,避免内存泄漏和其他资源泄漏。
  3. 详细错误信息:通过抛出异常对象,可以传递大量的错误信息,包括错误类型、上下文、堆栈跟踪等,有助于调试和定位问题。
  4. 灵活的错误处理:异常可以沿着调用栈向上传递,允许高层函数根据需求处理不同类型的错误。
缺点:
  1. 性能开销:虽然现代硬件已经使得异常处理的性能开销变得相对较小,但在某些情况下,异常处理仍然会影响程序的性能(如栈展开和对象拷贝)。
  2. 程序控制流混乱:异常会导致程序控制流的跳转,可能使得代码的执行路径变得不容易追踪,增加调试的难度。
  3. 使用不当可能导致复杂性:如果使用异常处理不当,可能导致过多的 try/catch 语句,或者异常未被正确捕获,增加代码复杂性。

2. 智能指针

2.1. 为什么需要智能指针?

先来看一段代码:

代码语言:javascript
复制
#include <iostream>
using namespace std;

double div() {
	double a = 0, b = 0;
	cin >> a >> b;
	if (0 == b) throw "分母为零 !\0";
	return a / b;
} 

void funcCatch() {
	double* pa = new double;
	double* pb = new double;

	cout << div() << endl;

	delete pa;
	delete pb;
}

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

	return 0;
}

分析一下这段代码的缺陷:

只对分母为零的情况做了抛异常

  • 如果分母不为零,funcCatch正常运行,papb正常释放
  • 如果分母为零,接收到div抛出的异常,程序终止,此时papb还未释放,造成内存泄漏

所以,我们以前手动管理指针的释放过于复杂,稍有不慎忘记释放哪个指针都会造成内存泄漏,这是很严重的问题,

  • 内存泄漏:由于程序设计不当或操作失误,未能及时释放不再使用的内存空间。它并不意味着内存物理上的消失,而是程序在分配内存后,失去了对这段内存的控制,导致内存空间无法被有效回收,从而造成资源浪费
  • 内存泄露的危害:长期存在内存泄漏的程序,随着时间的推移,会导致可用内存逐渐减少,从而影响系统性能,表现为程序响应变慢,甚至最终导致崩溃或卡死的情况

这是内存泄漏的情况:

代码语言:javascript
复制
void funcCatch() {
	double* pa = new double;
	double* pb = new double;

	cout << div() << endl;

	delete pa;
	delete pb;
}

// 内存泄漏情况
void memoryLeak() {
	// 1. 指针未释放
	int a = 10;
	int* ptr1 = &a;

	// 2. 异常造成的资源未释放
	int* arr = new int[10];
	funcCatch();

	// 先捕获到异常 造成程序终止 arr未被释放
	delete[] arr;
}

而C++11引入RAII(Resource Acquisition Is Initialization)机制,就能有效避免这种问题。

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

2.2.1. RAII

RAII(Resource Acquisition Is Initialization)利用对象声明周期控制程序资源,在对象构造时获取资源,在对象析构时释放资源,这样的好处是:

  • 无需显式释放资源
  • 对象所需的资源在生命周期内始终保持有效

实际上我们就是定义一个类来控制资源:

代码语言:javascript
复制
template <class T>
class smartPtr {
private:
	T* _ptr;
public:
	smartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{
		cout << "smartPtr构造: " << _ptr << endl;
	}

	~smartPtr() {
		if (_ptr) {
			cout << "smartPtr析构: " << _ptr << endl;
			delete _ptr;
		}
	}
};
2.2.2. 智能指针的原理

上述的smartPtr还不能称之为智能指针,还缺少指针的行为。

指针可以解引用,可以通过->访问空间内容,所以,还需重载*->

代码语言:javascript
复制
#include <iostream>
using namespace std;

template <class T>
class smartPtr {
private:
	T* _ptr;
public:
	smartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{
		cout << "smartPtr构造: " << _ptr << endl;
	}

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

	~smartPtr() {
		if (_ptr) {
			cout << "smartPtr析构: " << _ptr << endl;
			delete _ptr;
		}
	}
};

struct Date {
	int year;
	int month;
	int day;

	Date() = default;
};

int main() {
	
	smartPtr<int> sp1(new int);
	*sp1 = 10;

	smartPtr<Date> spDate(new Date);
	// 语法糖: spDate->operator()->
	spDate->year = 2010;
	spDate->month = 1;
	spDate->day = 1;


	return 0;
}

智能指针的原理:

  1. RAII:资源获取及初始化
  2. 重载了operator*operator->,有和指针一样的行为
2.2.3.auto_ptr

auto_ptr是C++98提出的失败的设计,核心是管理权限的转移,原本的资源直接释放

代码语言:javascript
复制
// auto_ptr 管理权限转移 禁止使用!!!!
#include <iostream>
using namespace std;

namespace autoPtr {
	template <class T>
	class auto_ptr {
	private:
		T* _ptr;
	public:
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{ 
			cout << "auto_ptr构造 : " << _ptr << endl;
		}

		// *this <- other other管理权给*this
		auto_ptr(const auto_ptr<T>& other) 
			:_ptr(other._ptr)
		{
			other._ptr = nullptr;
		}

		auto_ptr& operator=(auto_ptr<T>& other) {
			if (this != &other) {
				// 先释放自己的资源
				if (_ptr) {
					delete _ptr;
				}
				// 拿来别人的资源
				_ptr = other._ptr;
				other._ptr = nullptr;
			}
			return *this;
		}

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

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

	struct Date {
		int year;
		int month;
		int day;

		Date() = default;
	};
}

int main() {

	autoPtr::auto_ptr<autoPtr::Date> spDate(new autoPtr::Date);
	spDate->year = 2010;
	spDate->month = 1;
	spDate->day = 1;

	autoPtr::auto_ptr<autoPtr::Date> cpDate(new autoPtr::Date);

	cpDate = spDate;

	return 0;
}
在这里插入图片描述
在这里插入图片描述

🙅‍实践中一定不能使用🙅‍

2.2.4. unique_ptr

C++11使用 更靠谱的unique_ptr简单粗暴禁止拷贝

代码语言:javascript
复制
/**** uniquePtr.h ****/
#pragma once
namespace uniquePtr {
	template <class T>
	class unique_ptr {
	private:
		T* _ptr;
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{
			cout << "unique_ptr构造: " << _ptr << endl;
		}

		~unique_ptr() {
			if (_ptr) {
				cout << "unique_ptr析构: " << _ptr << endl;
			}
		}
		// 简单粗暴 禁止拷贝
		unique_ptr(const unique_ptr<T>& other) = delete;
		unique_ptr& operator=(const unique_ptr<T>& other) = delete;

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

	struct Date {
		int year;
		int month;
		int day;

		Date() = default;
	};
}

/**** test.cpp ****/
#include <iostream>
using namespace std;
#include "uniquePtr.h"

int main() {
	uniquePtr::unique_ptr<uniquePtr::Date> upDate(new uniquePtr::Date);

	upDate->day = 0;
	upDate->month = 0;
	upDate->year = 0;

	//  error C2280: “uniquePtr::unique_ptr<uniquePtr::Date>::unique_ptr(const uniquePtr::unique_ptr<uniquePtr::Date> &)”: 
	// 尝试引用已删除的函数
	// uniquePtr::unique_ptr<uniquePtr::Date> cpDate(upDate);

	return 0;
}
2.2.5. shared_ptr

shared_ptr的原理:通过引用计数的方式来实现多个shared_ptr对象之间的资源共享,例如图书馆借书的例子:

1. 图书馆中的书 在这个例子中,图书馆中的每本书就像一个对象,而 书的编号是指向该书的指针。每本书的编号可以被 多个借书的人(多个 shared_ptr)持有。 2. 借书的人 每个借书的人会得到一个 书的编号,并且他们可以 共享这本书。例如,两个学生借了同一本书,他们都有指向这本书的编号(就像两个 shared_ptr 指向同一个对象)。 3. 借书的规则:引用计数 每当有人借了这本书,图书馆都会增加一个计数,这个计数就代表 当前借书的人数(就像 shared_ptr 中的引用计数)。如果有 2 个人借了同一本书,计数会是 2;如果有 3 个人借了,同样会是 3。 4. 归还书:减少引用计数 当借书的人归还书时,图书馆就会减少该书的借阅计数。每次有人归还书,计数就减少一次。 5. 最后一位归还书时,销毁书最后一个借书的人归还书时(即引用计数变为 0),图书馆会 销毁这本书,表示这本书不再被需要了。也就是说,最后一个 shared_ptr 被销毁时,资源才会被释放。 如何类比到 shared_ptr

  1. 每本书 代表管理的 资源(比如内存、文件、数据库连接等)。
  2. 借书的人 就是 shared_ptr 实例,每个实例都拥有对资源的共享所有权。
  3. 书的编号 就是 shared_ptr 中的 指针
  4. 借书人数 就是 引用计数,每当一个 shared_ptr 被创建或者拷贝时,引用计数就增加;当一个 shared_ptr 被销毁时,引用计数就减少。
  5. 最后一个借书的人归还书时,资源被释放,代表 shared_ptr 的资源释放机制

来简单实现一下:

代码语言:javascript
复制
#pragma once
#include<functional>

namespace sharedPtr {
    template <class T>
    class shared_ptr {
    private:
        T* _ptr;                  // 指向资源的指针
        int* _refConut;           // 引用计数指针,记录有多少个 shared_ptr 管理同一个资源
        // 自定义删除器,可以删除任意类型的对象(默认使用 delete)
        std::function<void(T* ptr)> _del = [](T* ptr) { delete ptr; };

    public:
        shared_ptr(const T* ptr = nullptr)
            : _ptr(ptr), _refConut(new int(1))  // 初始化引用计数为 1,表示资源有一个管理者
        {
            std::cout << "shared_ptr构造: " << _ptr << " 数量: " << _refConut << std::endl;
        }

        // 构造函数:传入自定义删除器
        template <class D>
        shared_ptr(const T* ptr = nullptr, D del)
            : _ptr(ptr), _del(del), _refConut(new int(1))  // 自定义删除器
        {
            std::cout << "shared_ptr(const T* ptr = nullptr, D del)" << std::endl;
        }

        // 拷贝构造函数:增加引用计数
        shared_ptr(const shared_ptr<T>& other)
            : _ptr(other._ptr), _refConut(other._refConut)
        {
            ++(*_refConut);  // 引用计数加 1
        }

        // 释放资源:当引用计数减少为 0 时,释放资源
        void release() {
            if (--(*_refConut) == 0) {  
                _del(_ptr);  
                delete _refConut;  
                _ptr = nullptr;  
                _refConut = nullptr;  
            }
        }

        // 赋值运算符重载:释放旧资源,增加引用计数
        shared_ptr<T>& operator=(const shared_ptr<T>& other) {
            if (this != &other) {  // 防止自赋值
                release();  
                _ptr = other._ptr;  
                _refConut = other._refConut;  
                ++(*_refConut);  
            }
            return *this;
        }

        // 析构函数:调用 release 释放资源
        ~shared_ptr() { release(); }

        // 解引用运算符:返回指向的对象
        T* operator->() { return _ptr; }
        T& operator*() { return *_ptr; }

        // 获取原始指针
        T* get() { return _ptr; }

        // 获取当前引用计数
        int useCount() { return *_refConut; }
    };
}
shared_ptr设计逻辑

资源管理

  • shared_ptr 通过引用计数来管理动态分配的资源_ptr。当一个 shared_ptr 被创建时,资源被管理;当最后一个 shared_ptr 被销毁时,资源会被释放。
  • release 函数用于减少引用计数,如果引用计数变为 0,则销毁资源。

引用计数

  • 每个 shared_ptr 都有一个指向 引用计数_refConut)的指针,用来跟踪当前有多少个 shared_ptr 对象指向同一资源。初始时引用计数为 1,表示只有一个 shared_ptr 管理该资源。
  • 拷贝构造函数赋值运算符 会增加引用计数,表示多个 shared_ptr 对同一资源进行管理。
  • 当一个 shared_ptr 被销毁时,引用计数会减少。如果引用计数变为 0,表示没有其他 shared_ptr 管理该资源,这时资源会被释放。

假设有两个 shared_ptrAB,它们共享同一个资源。资源的引用计数从 1 开始,在每个 shared_ptr 创建时增加。

代码语言:javascript
复制
 +----------------------+
 |   shared_ptr A       |                    +-----------------------+
 |----------------------|                    |  shared_ptr B         |
 | _ptr  -> [Resource]  |					 | _ptr  -> [Resource]   |
 | _refCount -> 2       |		  		     | _refCount -> 2        |
 +----------------------+                    +-----------------------+
        |                                            |
        v                                            v
    +---------+                                +---------+ 
    | Resource| 							   | Resource| 
    +---------+								/  +---------+
       |								  /
       v								/ 
    (delete called when refCount == 0)

删除器 _del

  • _del 是一个 函数对象,默认使用 delete 来释放资源。使用 std::function 来定义删除器,这样可以轻松支持自定义的资源销毁方式,比如 free 或者其他复杂的销毁逻辑。
  • 自定义删除器:通过传入不同的删除器,我们可以管理 newmalloc 分配的资源,或者做一些额外的清理操作

拷贝与赋值

  • 拷贝构造函数:当一个 shared_ptr 被拷贝时,引用计数加 1,表示资源被多个 shared_ptr 对象共享。
在这里插入图片描述
在这里插入图片描述

  • 赋值运算符:在赋值时,首先会释放旧资源,然后复制新资源并增加引用计数。这样确保了 shared_ptr 之间的资源管理一致性。
在这里插入图片描述
在这里插入图片描述
2.2.6. 循环引用问题

循环引用发生在两个对象通过 shared_ptr 相互引用,导致它们的引用计数永远不为 0,从而无法释放资源,最终发生内存泄漏。

假设有个双向链表类,定义两个成员变量ListNode* _prev; ListNode* _next;,现在有两个节点:n1n2

代码语言:javascript
复制
template <class T>
struct ListNode {
    ListNode<T>* _prev = nullptr;
    ListNode<T>* _next = nullptr;

    ListNode() = default;
};

void test() {
    // 创建两个 ListNode<int> 节点,分别由 shared_ptr 管理
    shared_ptr<ListNode<int>> n1(new ListNode<int>);
    shared_ptr<ListNode<int>> n2(new ListNode<int>);

    // 形成双向链表关系
    //n1->_next = n2;   // n1 指向 n2
    //n2->_prev = n1;   // n2 指向 n1

    // 循环引用:n1 和 n2 互相持有对方的 shared_ptr
    // 当 test() 函数结束时,n1 和 n2 的引用计数永远不会为 0,资源不会被释放
}
在这里插入图片描述
在这里插入图片描述

n1n2相互作用,二者无法释放资源,导致内存泄漏

2.2.7. 解决循环引用问题

weak_ptr 是一种不增加引用计数的智能指针,不支持RAII,它用于观察 shared_ptr 管理的资源,在shared_ptr赋值和拷贝的时候,不增加引用计数

代码语言:javascript
复制
 template <class T>
    class weak_ptr {
    private:
        T* _ptr = nullptr;
    public:
        weak_ptr() = default;
        weak_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr)  // 将 shared_ptr 的资源指针 _ptr 复制到 weak_ptr 的 _ptr 成员中
        {
            // 这里的构造函数将一个 shared_ptr 转换为 weak_ptr。
            // weak_ptr 不增加引用计数,它仅仅观察 shared_ptr 管理的资源。
            // 这样设计使得 weak_ptr 可以在不干扰资源生命周期的情况下,观察资源是否存在。
        }

        weak_ptr<T>& operator=(const shared_ptr<T>& other) {
            _ptr = other.get();  // 获取 shared_ptr 的资源指针并赋给 weak_ptr 的 _ptr 成员

            return *this;  // 返回当前的 weak_ptr 对象,以支持链式赋值操作
        }

        ~weak_ptr() {}
    };
代码语言:javascript
复制
template <class T>
struct ListNode {
    ListNode<T>* _prev = nullptr;
    ListNode<T>* _next = nullptr;

    ListNode() = default;
};

void test() {
    // 使用 weak_ptr 观察 n1 和 n2,而不增加引用计数
    weak_ptr<ListNode<int>> weak_n1(n1);  // weak_ptr 观察 n1
    weak_ptr<ListNode<int>> weak_n2(n2);  // weak_ptr 观察 n2

    // 现在,n1 和 n2 可以正确销毁,引用计数变为 0 时资源被释放
}

3. 总结

1. 异常处理

  • C 传统的错误处理方式
    • 终止程序(assert:简单直接,但用户体验差,程序会突然终止。
    • 返回错误码:增加了错误处理的复杂度,程序员需要手动检查和处理每个错误码。
  • C++ 异常机制
    • 异常抛出:通过 throw 抛出异常,throw 后跟随错误信息,传递给调用者。
    • 异常捕获:使用 try-catch 块来捕获并处理异常。通过 catch 可以捕获特定类型的异常,并提供相应的处理机制。
    • 异常传播:异常会从抛出点沿调用栈传播,直到找到匹配的 catch 块。如果没有匹配,程序终止。
    • 多层次的异常匹配:异常可以在不同层次的 catch 块中被捕获。例如,函数 func1 抛出的异常可以通过 func2main 逐层捕获。
  • 异常的拷贝与重新抛出
    • 异常对象会在被抛出时被拷贝到调用栈中,因此 catch 块可以访问它。
    • 重新抛出:捕获异常后,可以通过 throw; 重新抛出异常,交给更外层的 catch 块继续处理。
  • 异常规范
    • C++11 引入了 throw()noexcept 语法,分别表示一个函数不抛出任何异常和保证不抛出异常。
    • 异常的使用带来了 性能开销控制流复杂性,但它也提供了 错误分离自动清理详细的错误信息

2. 智能指针

  • 为什么需要智能指针
    • 手动管理动态内存容易出现 内存泄漏,例如当抛出异常时没有释放内存。
    • 智能指针自动管理资源的生命周期,通过 RAII(Resource Acquisition Is Initialization) 机制,在对象销毁时自动释放资源。
  • 智能指针的种类
    • unique_ptr:独占所有权,不能拷贝或赋值,避免了资源共享时的冲突。
    • shared_ptr:共享所有权,通过 引用计数 机制实现多个智能指针共享同一资源,直到所有 shared_ptr 被销毁时,资源才会释放。
    • weak_ptr:观察 shared_ptr 管理的资源,不增加引用计数,防止循环引用。
  • shared_ptr 的工作原理
    • 引用计数:每个 shared_ptr 持有一个资源的引用计数,指示有多少个 shared_ptr 管理这个资源。当引用计数为 0 时,资源会被自动销毁。
    • 拷贝构造与赋值:拷贝 shared_ptr 时,引用计数增加;赋值时,会先释放旧资源,再复制新资源。
  • 循环引用问题
    • 循环引用:当两个 shared_ptr 互相引用时,它们的引用计数永远不为 0,从而导致内存泄漏。
    • 解决方案:使用 weak_ptr 代替 shared_ptrweak_ptr 不增加引用计数,打破循环引用,避免内存泄漏。

3. 栈展开和异常传播

  • 异常从抛出点沿着调用栈向上传播,直到找到匹配的 catch 块。如果没有匹配的 catch,程序终止。
  • 栈展开:当异常被抛出时,程序会从当前函数逐层退出,直到找到一个匹配的异常处理器。
  • 异常捕获后可以通过 throw 重新抛出,交给上层继续处理。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 异常
    • 1.1. C传统处理错误的方式
    • 1.2. C++异常概念
    • 1.3. 异常的抛出和捕获
      • 异常的抛出和匹配原则
      • 异常栈展开匹配原则
    • 1.4. 异常的重新抛出
    • 1.5. 异常安全
    • 1.6. 异常规范
    • 1.7. 异常的优缺点
  • 2. 智能指针
    • 2.1. 为什么需要智能指针?
    • 2.2. 智能指针的使用及原理
      • 2.2.1. RAII
      • 2.2.2. 智能指针的原理
      • 2.2.3.auto_ptr
      • 2.2.4. unique_ptr
      • 2.2.5. shared_ptr
      • 2.2.6. 循环引用问题
      • 2.2.7. 解决循环引用问题
  • 3. 总结
    • 1. 异常处理
    • 2. 智能指针
    • 3. 栈展开和异常传播
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档