C++ 内存安全双保险:异常处理 + 智能指针,彻底跟内存泄漏说 “再见”
本文介绍了C++异常处理机制,主要包括以下内容: C传统错误处理方式的局限性:终止程序或返回错误码存在用户体验差和繁琐的问题。 C++异常概念:通过throw抛出异常,catch捕获异常,try包裹可能出错的代码。 异常匹配原则: 抛出对象类型决定匹配的catch块 异常沿调用链向上传播,由最近的匹配catch处理 异常对象会被拷贝到catch块中 catch(...)可捕获所有类型异常 异常栈展

🎬 GitHub:Vect的代码仓库
1. 异常
1.1. C传统处理错误的方式
- 终止程序,如
assert,缺陷:用户体验感差,莫名其妙就终止程序,需要重新载入程序- 返回错误码,缺陷:需要程序员自己去查找对应的错误,太过琐碎
#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:包裹可能抛出异常的代码
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 块。
void func1() {
throw "1111111";
}
int main() {
try {
func1();
}
catch (const char* errmsg) {
cout << "捕获到的异常:" << errmsg << endl;
}
}
throw抛出了个字符串catch(const char* errmsg)捕获了异常并进行处理,catch的()里面是异常的类型
2. 被选中的处理代码是调用链中离抛出异常位置最近的那个
异常会在调用栈中从当前函数向上逐层传播,直到找到与异常类型匹配的 catch 块。如果找到了匹配的 catch 块,异常就被捕获并处理。
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 异常。
异常首先从 func1 向 func2 传播,在 func2 中找到与类型匹配的 catch 块并处理异常。
如果没有在 func2 找到匹配的异常处理,异常会继续传播到 main。
3.异常对象的拷贝
当异常被抛出时,会生成一个异常对象的拷贝。这是因为抛出的异常可能是一个临时对象,它需要被拷贝到调用栈中,以便可以在 catch 块中访问。
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(...) 是一个通用捕获语法,它可以捕获任何类型的异常,但缺点是不知道异常的具体类型。
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块之后的代码。 **
来看这段代码:
// 栈展开
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;
}
梳理一下思路:
1. main() 调用 func2()
|
2. func2() 调用 func1()
|
3. func1() 中抛出 out_of_range 异常
|
4. 异常从 func1() 向 func2() 传播,func2() 捕获该异常
|
5. 如果 func2() 中没有捕获异常,异常会继续传递给 main() 函数
|
6. 如果 main() 没有捕获异常,程序终止
栈展开是这样的:
┌──────────────┐
│ main() │ <-- 异常会传递到 main(),如果没有捕获程序终止
└──────────────┘
↑
┌──────────────┐
│ func2() │ <-- 如果 func2() 捕获异常,栈展开停止
└──────────────┘
↑
┌──────────────┐
│ func1() │ <-- 异常从 func1() 抛出并传递
└──────────────┘
总结:
- 异常会从抛出的位置开始沿着调用链向上传播,直到找到匹配的
catch块。 - 如果没有匹配的
catch,程序会终止。 catch(...)可以捕获所有类型的异常,但它无法获取异常的详细信息。- 异常传播过程叫做 栈展开,从当前函数栈一直传递到调用栈的顶部(
main)。
1.4. 异常的重新抛出
异常的重新抛出是指在 catch 块内捕获到异常后,不完全处理该异常,而是将其再次抛出,交给外层的调用者进行进一步处理。
为什么需要重新抛出异常?
在一些情况下,你可能希望在捕获到异常后,做一些日志记录、清理工作或者部分恢复工作,但不希望完全处理异常。重新抛出异常后,外层调用者(通常是 main 函数或更外层的 catch)可以继续处理这个异常。
如何重新抛出异常?
在 catch 块中,我们可以通过 throw 关键字重新抛出异常。需要注意的是,重新抛出的异常类型和原始异常类型一致。
// 异常重新抛出
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:表示函数保证不抛出异常。
// 这里表示这个函数会抛出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. 异常的优缺点
优点:
- 错误分离:异常机制将错误处理从正常业务逻辑中分离出来,使代码更简洁、更易读。
- 自动清理:结合 RAII(资源获取即初始化)原理,异常会自动管理资源的释放,避免内存泄漏和其他资源泄漏。
- 详细错误信息:通过抛出异常对象,可以传递大量的错误信息,包括错误类型、上下文、堆栈跟踪等,有助于调试和定位问题。
- 灵活的错误处理:异常可以沿着调用栈向上传递,允许高层函数根据需求处理不同类型的错误。
缺点:
- 性能开销:虽然现代硬件已经使得异常处理的性能开销变得相对较小,但在某些情况下,异常处理仍然会影响程序的性能(如栈展开和对象拷贝)。
- 程序控制流混乱:异常会导致程序控制流的跳转,可能使得代码的执行路径变得不容易追踪,增加调试的难度。
- 使用不当可能导致复杂性:如果使用异常处理不当,可能导致过多的
try/catch语句,或者异常未被正确捕获,增加代码复杂性。
2. 智能指针
2.1. 为什么需要智能指针?
先来看一段代码:
#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正常运行,pa和pb正常释放- 如果分母为零,接收到
div抛出的异常,程序终止,此时pa和pb还未释放,造成内存泄漏
所以,我们以前手动管理指针的释放过于复杂,稍有不慎忘记释放哪个指针都会造成内存泄漏,这是很严重的问题,
- 内存泄漏:由于程序设计不当或操作失误,未能及时释放不再使用的内存空间。它并不意味着内存物理上的消失,而是程序在分配内存后,失去了对这段内存的控制,导致内存空间无法被有效回收,从而造成资源浪费
- 内存泄露的危害:长期存在内存泄漏的程序,随着时间的推移,会导致可用内存逐渐减少,从而影响系统性能,表现为程序响应变慢,甚至最终导致崩溃或卡死的情况
这是内存泄漏的情况:
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)利用对象声明周期控制程序资源,在对象构造时获取资源,在对象析构时释放资源,这样的好处是:
- 无需显式释放资源
- 对象所需的资源在生命周期内始终保持有效
实际上我们就是定义一个类来控制资源:
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还不能称之为智能指针,还缺少指针的行为。
指针可以解引用,可以通过->访问空间内容,所以,还需重载*和->
#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;
}
智能指针的原理:
- RAII:资源获取及初始化
- 重载了
operator*和operator->,有和指针一样的行为
2.2.3.auto_ptr
auto_ptr是C++98提出的失败的设计,核心是管理权限的转移,原本的资源直接释放
// 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:简单粗暴禁止拷贝
/**** 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?
- 每本书 代表管理的 资源(比如内存、文件、数据库连接等)。
- 借书的人 就是
shared_ptr实例,每个实例都拥有对资源的共享所有权。- 书的编号 就是
shared_ptr中的 指针。- 借书人数 就是 引用计数,每当一个
shared_ptr被创建或者拷贝时,引用计数就增加;当一个shared_ptr被销毁时,引用计数就减少。- 最后一个借书的人归还书时,资源被释放,代表
shared_ptr的资源释放机制。
来简单实现一下:
#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_ptr,A和B,它们共享同一个资源。资源的引用计数从 1 开始,在每个shared_ptr创建时增加。+----------------------+ | 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或者其他复杂的销毁逻辑。- 自定义删除器:通过传入不同的删除器,我们可以管理
new或malloc分配的资源,或者做一些额外的清理操作拷贝与赋值:
- 拷贝构造函数:当一个
shared_ptr被拷贝时,引用计数加 1,表示资源被多个shared_ptr对象共享。
- 赋值运算符:在赋值时,首先会释放旧资源,然后复制新资源并增加引用计数。这样确保了
shared_ptr之间的资源管理一致性。
2.2.6. 循环引用问题
循环引用发生在两个对象通过 shared_ptr 相互引用,导致它们的引用计数永远不为 0,从而无法释放资源,最终发生内存泄漏。
假设有个双向链表类,定义两个成员变量ListNode* _prev; ListNode* _next;,现在有两个节点:n1、n2
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,资源不会被释放
}

n1和n2相互作用,二者无法释放资源,导致内存泄漏
2.2.7. 解决循环引用问题
weak_ptr 是一种不增加引用计数的智能指针,不支持RAII,它用于观察 shared_ptr 管理的资源,在shared_ptr赋值和拷贝的时候,不增加引用计数
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() {}
};
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抛出的异常可以通过func2和main逐层捕获。
- 异常抛出:通过
- 异常的拷贝与重新抛出:
- 异常对象会在被抛出时被拷贝到调用栈中,因此
catch块可以访问它。 - 重新抛出:捕获异常后,可以通过
throw;重新抛出异常,交给更外层的catch块继续处理。
- 异常对象会在被抛出时被拷贝到调用栈中,因此
- 异常规范:
- C++11 引入了
throw()和noexcept语法,分别表示一个函数不抛出任何异常和保证不抛出异常。 - 异常的使用带来了 性能开销 和 控制流复杂性,但它也提供了 错误分离、自动清理 和 详细的错误信息。
- C++11 引入了
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_ptr,weak_ptr不增加引用计数,打破循环引用,避免内存泄漏。
- 循环引用:当两个
3. 栈展开和异常传播
- 异常从抛出点沿着调用栈向上传播,直到找到匹配的
catch块。如果没有匹配的catch,程序终止。 - 栈展开:当异常被抛出时,程序会从当前函数逐层退出,直到找到一个匹配的异常处理器。
- 异常捕获后可以通过
throw重新抛出,交给上层继续处理。
更多推荐




所有评论(0)