一、异常(Exception)

在 C 语言里:
❌ 出错靠返回值
❌ 一层一层 if 判断

在 C++ 里:
✅ 错误是一等公民
✅ 出问题就“跳出正常流程”


1.1 throw 和 catch(异常是什么)

1.1.1 异常解决的到底是什么问题?

先看一个 你一定写过的 C 风格代码

int func(int x)
{
    if (x < 0)
        return -1;
    if (x == 0)
        return -2;
    return 100 / x;
}

调用者:

int ret = func(x);
if (ret == -1) { /* 处理错误 */ }
else if (ret == -2) { /* 处理错误 */ }

问题非常多:

  • 错误码和正常值混在一起
  • 容易漏判断
  • 多层调用时代码爆炸

1.1.2 异常的核心思想

🔴 一句话理解异常:

错误不走“返回路径”,而是直接跳出调用链


1.1.3 throw:抛出异常

throw 10;

这行代码的含义是:

❌ 当前函数不再继续执行
🚀 直接向“外层调用者”抛一个对象


1.1.4 catch:捕获异常

try {
    // 可能出错的代码
}
catch (int e) {
    // 处理异常
}

📌 throw + catch 必须配合 try 使用


1.2 一个完整的异常示例(新手必看)

#include <iostream>
using namespace std;

int div(int a, int b)
{
    if (b == 0)
        throw "divide by zero";
    return a / b;
}

int main()
{
    try {
        cout << div(10, 0) << endl;
    }
    catch (const char* msg) {
        cout << "error: " << msg << endl;
    }

    cout << "程序还能继续运行" << endl;
}

执行流程

```text
main
└── try
└── div
└── throw
⬆ 直接跳回 main

🔴 红色重点

throw 之后
👉 当前函数后面的代码 全部不执行


1.3 栈展开(调用过程,必理解)

1.3.1 什么是“栈展开”?

看这个代码:

void f3()
{
    throw 1;
}

void f2()
{
    f3();
}

void f1()
{
    f2();
}

int main()
{
    try {
        f1();
    }
    catch (int e) {
        cout << "catch " << e << endl;
    }
}

1.3.2 发生了什么?(重点)

f3() 执行 throw

f3  ← 被销毁
f2  ← 被销毁
f1  ← 被销毁
main ← catch

在这里插入图片描述

🔴 这整个过程叫:栈展开


1.3.3 栈展开期间发生的事(非常重要)

✅ 所有已经构造完成的对象
👉 都会自动调用析构函数

📌 这就是为什么 C++ 异常必须配合 RAII(后面会讲)


1.4 throw 抛出的到底是什么?

1.4.1 throw 抛的是“对象”

throw 10;              // int 对象
throw "error";         // const char*
throw string("err");   // string 对象

📌 catch 时 类型必须匹配


1.4.2 catch 的匹配规则(重点)

try {
    throw string("error");
}
catch (const string& s) {
    cout << s << endl;
}

✔ 推荐:用 const 引用接收


1.4.3 catch 的“就近原则”

try {
    throw 10;
}
catch (double d) { }
catch (int i) { }   // 这里才会进

🔴 先匹配的先尝试


1.5 重复抛异常(rethrow)

1.5.1 为什么需要“再抛一次”?

有时候:

  • 当前函数只想记录日志
  • 不想真正“吃掉”异常

1.5.2 正确写法(必须记住)

try {
    // ...
}
catch (...) {
    cout << "记录日志" << endl;
    throw;   // 原样抛出
}

🔴 注意区别:

写法 含义
throw; 原异常
throw e; 新对象(会拷贝)

1.6 catch(…) —— 兜底异常

try {
    // 任意异常
}
catch (...) {
    cout << "未知异常" << endl;
}

📌 用途

  • 防止程序直接崩溃
  • main 中非常常见

1.7 异常与继承(非常实用)

class MyException {};
class FileException : public MyException {};
class NetException  : public MyException {};
try {
    throw FileException();
}
catch (const MyException& e) {
    cout << "统一处理异常" << endl;
}

🔴 红色重点

基类 catch
统一处理多个模块异常


1.8 noexcept(异常规范,了解即可)

1.8.1 noexcept 是什么?

void func() noexcept;

含义是:

👉 这个函数 保证不抛异常


1.8.2 为什么要有 noexcept?

  • 提高编译器优化空间
  • STL 容器在移动时会检查它
    在这里插入图片描述

📌 你后面学 vector / move 时会再次遇到它


1.8.3 什么时候要noexcept?

在这里插入图片描述

1.9 异常和错误码的区别总结

yic

1.10 本章图形化总结(脑补图)

正常流程:
func → return → 上层

异常流程:
func → throw → 跳出多层函数 → catch

在这里插入图片描述

栈展开

在这里插入图片描述

异常与继承

在这里插入图片描述


1.11 第一章一句话总结(复习用)

🔴 红色重点总结

  • 异常 = 跳出正常控制流
  • throw 后当前函数立即终止
  • 栈展开会调用析构函数
  • catch 用 const 引用
  • RAII 是异常安全的核心

二、智能指针(Smart Pointer)

如果说:

  • 异常解决的是「出错怎么办」
  • 移动语义解决的是「性能问题」

那么:

🔴 智能指针解决的是 C++ 最致命的问题:内存管理


2.1 RAII 是什么?(必须彻底理解)

2.1.1 先回到一个“经典噩梦”

void func()
{
    int* p = new int(10);

    if (/* 出错 */)
        return;   // ❌ 内存泄漏

    delete p;
}

问题不是你不想 delete
问题是你“来不及” delete


2.1.2 RAII 的核心思想(红色重点)

🔴 RAII(Resource Acquisition Is Initialization)

资源的获取 = 对象的构造
资源的释放 = 对象的析构

📌 一句话翻译成人话:

“把资源交给对象管理,别交给人记”


2.1.3 RAII 的最小示例

class Guard {
public:
    Guard()  { cout << "获取资源\n"; }
    ~Guard() { cout << "释放资源\n"; }
};
void test()
{
    Guard g;
    throw 1;
}

💡 即使抛异常:

构造 Guard
throw
栈展开
调用 ~Guard

🔴 析构一定会执行


2.2 为什么“普通指针”在异常面前不安全?

2.2.1 再看一次栈展开

void f()
{
    int* p = new int(10);
    throw 1;
    delete p;   // 永远执行不到
}

📌 栈展开只会:

  • 调用 对象析构
  • ❌ 不会自动 delete 裸指针

🔴 结论:

异常 + new = 内存泄漏温床


2.3 C++ 标准库给出的答案:智能指针

2.3.1 什么是智能指针?

🔴 本质一句话:

“带析构函数的指针类”

它们都是:

  • 类模板
  • 内部封装了裸指针
  • 析构时自动释放资源

2.4 auto_ptr(了解即可,历史产物)

auto_ptr<int> p1(new int(10));
auto_ptr<int> p2 = p1;

❌ 问题在哪里?

p1 ----X
p2 ---> 10
  • 拷贝后 原指针被置空
  • 访问 p1 直接炸

🔴 已被 C++11 废弃


auto_ptr的简单模拟

	template<class T>
	class auto_ptr
	{
	public:
		//构造
		explicit auto_ptr(T* ptr = nullptr)//不允许隐式类型转换(防止普通指针隐式类型转换成智能指针对象)
			:_ptr(ptr)
		{}

		//拷贝构造
		auto_ptr(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			//转移管理权
			ap._ptr = nullptr;
		}

		//移动构造
		auto_ptr(auto_ptr<T>&& ap)noexcept
			:_ptr(ap._ptr)
		{
			//转移
			ap._ptr = nullptr;
		}

		//赋值
		auto_ptr<T>& operator=(auto_ptr<T>& ap)noexcept
		{
			//防止自己给自己赋值
			if (this != &ap)
			{
				//资源不为空,则释放资源,用于存储ap
				if (_ptr)
				{
					delete _ptr;
				}
				_ptr = ap._ptr;
				ap._ptr = nullptr;//转移管理权
			}
			return *this;
		}

		//移动赋值
		auto_ptr<T>& operator=(auto_ptr<T>&& ap)noexcept
		{
			if (this != &ap)
			{
				if (_ptr)
				{
					delete _ptr;
				}
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		//解引用
		T& operator*()noexcept
		{
			return *_ptr;
		}

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

		//析构函数
		~auto_ptr()
		{
			delete _ptr;
			_ptr = nullptr;
		}


	private:
		T* _ptr;
	};

2.5 unique_ptr —— 独占所有权(非常重要)

2.5.1 unique_ptr 的设计思想

🔴 一句话:

一个资源,只能有一个主人


2.5.2 基本使用

#include <memory>

unique_ptr<int> p(new int(10));

在这里插入图片描述

  • ❌ 不能拷贝
  • ✅ 只能移动

2.5.3 为什么不能拷贝?

unique_ptr<int> p1(new int(10));
unique_ptr<int> p2 = p1;   // ❌ 编译错误

📌 因为:

delete 拷贝构造 + delete 拷贝赋值


2.5.4 unique_ptr 的移动

unique_ptr<int> p1(new int(10));
unique_ptr<int> p2 = std::move(p1);

脑补图(重点):

在这里插入图片描述

🔴 完美体现:

右值引用 + 移动构造 + RAII


2.5.5 unique_ptr 的核心成员

p.get();      // 获取裸指针(不推荐长期用)
p.reset();    // 放弃当前资源
p.release();  // 释放所有权(危险)

2.5.6 unique_ptr的简单模拟

	//不能进行拷贝构造,只能是转移资源权,具有唯一性
	template<class T, class Del = std::default_delete<T>>
	class unique_ptr
	{
	public:
		//构造函数(不支持隐式类型转换)
		explicit unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{
		}

		//不支持拷贝(唯一性)
		unique_ptr(unique_ptr<T>& up) = delete;

		//也不支持赋值构造(唯一性)
		unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;

		//移动构造
		unique_ptr(unique_ptr<T>&& up)noexcept
			:_ptr(up._ptr)
			, _del(up._del)
		{
			up._ptr = nullptr;
		}

		//移动赋值
		unique_ptr<T>& operator=(unique_ptr<T>&& up)noexcept
		{
			if (&up != this)
			{
				//资源不为空,消除资源,为赋值准备
				if (_ptr)
				{
					_del(_ptr);
				}
				_ptr = up._ptr;
				_del = up._del;
				up._ptr = nullptr;//置空
			}
			return *this;
		}

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

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

		//析构
		~unique_ptr()
		{
			_del(_ptr);
			_ptr = nullptr;
		}
	private:
		T* _ptr;
		Del _del;
	};

2.6 shared_ptr —— 共享所有权

2.6.1 为什么需要 shared_ptr?

有些场景:

  • 多个对象要“共同使用一个资源”
  • 生命周期不好明确

2.6.2 核心原理:引用计数

shared_ptr<int> p1(new int(10));
shared_ptr<int> p2 = p1;
引用计数 = 2

当:

  • 一个 shared_ptr 析构
  • 计数 -1
  • 直到 0 才 delete 资源

2.6.3 常用接口

p.use_count();   // 引用计数
p.get();         // 裸指针
p.reset();       // 放弃资源

2.6.4 make_shared(强烈推荐)

auto p = make_shared<int>(10);

🔴 优点:

  • 少一次内存分配
  • 更安全
  • 更高效

2.6.5 shared_ptr的简单模拟

	template<class T>
	class shared_ptr
	{
	public:
		//构造(不支持隐式类型转换)
		explicit shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{
		}

		template<class Del>
		explicit shared_ptr(T* ptr, Del del)//删除器版本初始化构造
			:_ptr(ptr)
			, _pcount(new int(1))
			, _del(del)
		{
		}

		//拷贝构造
		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			//增加引用计数
			++(*_pcount);
		}

		//移动构造
		shared_ptr(shared_ptr<T>&& sp)noexcept
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _del(sp._del)
		{
			sp._ptr = nullptr;
			sp._pcount = nullptr;
		}

		//释放资源
		void release()noexcept
		{
			_del(_ptr);
			delete _pcount;
			_ptr = nullptr;
			_pcount = nullptr;
		}

		//拷贝赋值
		shared_ptr<T>& operator=(shared_ptr<T>& sp)noexcept
		{
			//判断资源是否相同
			if (_ptr != sp._ptr)
			{
				//避免释放空指针
				if (_ptr)
				{
					//判断sp指向的资源是否可以释放
					if (--(*_pcount) == 0)
					{
						release();
					}
				}
				//赋值
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_del = sp._del;
				//别忘了引用计数+1
				++(*_pcount);
			}
			return *this;
		}

		//移动赋值
		shared_ptr<T>& operator=(shared_ptr<T>&& sp)noexcept
		{
			if (_ptr != sp._ptr)
			{
				if (_ptr)
				{
					//判断删除后是否可以释放资源
					if (--(*_pcount) == 0)
					{
						release();
					}
				}
				//赋值
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_del = sp._del;
				sp._ptr = nullptr;
				sp._pcount = nullptr;
			}
			return *this;
		}

		//交换
		void swap(shared_ptr<T>& sp)noexcept
		{
			swap(_ptr, sp._ptr);
			swap(_pcount, sp._pcount);
			swap(_del, sp._del);
		}

		//reset
		void reset()noexcept
		{
			release();
		}

		//get获取资源地址
		T* get()const noexcept
		{
			return _ptr;
		}

		//*解引用
		T& operator*()noexcept
		{
			return *_ptr;
		}

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

		//统计引用计数
		long int use_count()const noexcept
		{
			return (*_pcount);
		}

		//判断资源是否为空
		explicit operator bool() const noexcept
		{
			return _ptr != nullptr;
		}

		//判断是否一个人管理资源
		bool unique() const noexcept
		{
			return (*_pcount) == 1;
		}

		~shared_ptr()
		{
			//判断引用计数是否为0,为0则彻底释放资源
			if (_ptr)
			{
				if (--(*_pcount) == 0)
				{
					release();
				}
			}
		}
	private:
		T* _ptr;
		int* _pcount;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };//默认用lambda表达式(用function来包装)
	};

2.7 shared_ptr 的致命问题:循环引用

2.7.1 经典双向链表示例

struct Node {
    shared_ptr<Node> next;
    shared_ptr<Node> prev;
};
node1 <----> node2

在这里插入图片描述

📌 结果:

  • 引用计数永远不为 0
  • 析构永远不发生
  • ❌ 内存泄漏

2.8 weak_ptr —— 解决循环引用的关键

2.8.1 weak_ptr 的本质

🔴 一句话:

“不增加引用计数的观察者”


2.8.2 正确的双向链表写法

struct Node {
    shared_ptr<Node> next;
    weak_ptr<Node>  prev;
};

📌 prev

  • 不拥有资源
  • 只观察
  • 不影响生命周期

2.8.3 为什么不能直接用 weak_ptr 访问资源?

weak_ptr<int> wp;
*wp;   // ❌ 不允许

🔴 因为:

资源可能已经被释放


2.8.4 lock 的正确用法(你笔记里的重点)

if (auto sp = wp.lock()) {
    // 资源还存在
}
  • 若资源已释放 → 返回空 shared_ptr
  • 安全!

2.8.5 weak_ptr的简单模拟

#include"Share_ptr.h"

namespace bear
{
	template<class T>
	class weak_ptr
	{
	public:
		//构造
		weak_ptr()
		{
		}

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

		//拷贝
		weak_ptr(const weak_ptr<T>& wp)noexcept
		{
			_ptr = wp._ptr;
		}

		//不用写析构,只需要靠shared_ptr里面析构即可,weak_ptr的出现就是为了解决shared_ptr中的循环引用而产生内存泄漏问题

		//赋值
		weak_ptr<T>& operator=(const bear::shared_ptr<T> sp)noexcept
		{
			_ptr = sp.get();
			return *this;
		}
	private:
		T* _ptr = nullptr;
	};
}

2.8.6 shared_ptr(weak_ptr)和unique_ptr的区别

在这里插入图片描述

2.9 第二章图形化总结

在这里插入图片描述


2.10 第二章一句话总结(终极复习)

🔴 红色重点总结

  • 异常让 RAII 成为刚需
  • 智能指针 = RAII 的标准实现
  • unique_ptr:独占 + 移动
  • shared_ptr:共享 + 引用计数
  • weak_ptr:打破循环引用
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐