个人主页:Yupureki-CSDN博客

C++专栏:C++_Yupureki的博客-CSDN博客

目录

前言

1. C++异常处理机制

1.1 异常的基本概念

1.2 异常的抛出和捕获

基本语法

异常处理的重要特性

1.3 栈展开过程

1.4 异常匹配规则

1.5 异常重新抛出

1.6 异常安全问题

1.7 异常规范

1.8 标准库异常体系

2. 智能指针的原理与使用

2.1 传统内存管理的问题

2.2 RAII设计思想

2.3 C++标准库智能指针

智能指针类型概览

基本使用示例

2.4 智能指针的原理

2.5 删除器(Deleter)

2.6 智能指针的实现原理

unique_ptr实现

shared_ptr实现

2.7 shared_ptr的循环引用问题

解决方案:weak_ptr

3. 内存泄漏与防护

3.1 内存泄漏的概念与危害

3.2 内存泄漏检测

Linux检测工具

Windows检测工具

3.3 内存泄漏防护策略

总结

核心技术要点

实践建议


上一篇:从零开始的C++学习生活 16:C++11新特性全解析-CSDN博客

前言

在现代C++开发中,异常处理和内存管理是两个至关重要的主题。异常处理机制允许程序在运行时对错误情况进行优雅的处理,而智能指针则通过RAII(Resource Acquisition Is Initialization)技术从根本上解决内存泄漏问题。

请不要小看本章节哦,写出安全的代码至关重要。如果在企业中不规范地写代码,轻则写出bug,重则丢失数据从而造成事故,是十分严重的。

我将深入探讨C++异常处理机制的原理和使用方法,详细分析各种智能指针的设计思想和实现原理,带你写出安全的代码。

1. C++异常处理机制

1.1 异常的基本概念

异常处理机制允许程序中独立开发的部分在运行时对出现的问题进行通信并做出相应的处理。与C语言主要通过错误码处理错误的方式不同,C++异常通过抛出对象来传递更丰富的错误信息。

异常处理的核心优势

  • 错误检测与处理分离
  • 传递更详细的错误信息
  • 自动的资源清理

1.2 异常的抛出和捕获

基本语法
程序出现问题时,我们通过抛出(throw)⼀个对象来引发⼀个异常。异常的抛出必须需要抓取(catch),该对象的类型以及当前的调用链决定了应该由哪个catch的处理代码来处理该异常。
int main()
{
    int a = 1;
    int b = 0;
    try 
    {
        if (b == 0)
        {
            string s("Divide by zero condition!");
            throw s;
        }
    }
    catch (const string& s)
    {
        cout << s;
    }
}

try和catch是处理异常信息的配合。try包含了可能含有异常错误的代码(0不能做除数),如果有错误那么就会throw抛出一个变量让catch接受,如果没有抛出那么会跳过catch。这里我们选择抛出一个字符串作为错误信息,让catch接受,catch需要设定能够抓取的错误信息,就像函数参数一样。上面的catch的参数就是const string&,即抓取string类型的变量,跟我们抛出的s相同。

值得注意的是,可以有多个catch,因为异常的信息多种多样,我们会自定义多种信息,也就需要多种catch来抓取

并且当throw抛出后,throw后面的代码段不会执行,一定会跳转到catch中,如果没有对应的catch就会报错

总结一下

异常处理的重要特性
  1. 控制权转移:throw执行后,后面的语句不再执行,控制权转移到匹配的catch块
  2. 对象拷贝:抛出异常对象会生成一个拷贝,因为原对象可能是局部对象
  3. 栈展开:沿着调用链查找匹配的catch子句
double Divide(int a, int b)
{
     try
     {
         // 当b == 0时抛出异常
         if (b == 0)
         {
             string s("Divide by zero condition!");//抛出异常后会先在该try-catch组合查看
             throw s;
         }
         else
         {
             return ((double)a / (double)b);
         }
     }
     catch (int errid)//抓取类型是int,不匹配,则跳出该函数继续找
     {
         cout << errid << endl;
     }
     return 0;
     }

void Func()
{
    int len, time;
    cin >> len >> time;
    try
    {
        cout << Divide(len, time) << endl;
    }
    catch (const char* errmsg)//仍然不匹配,继续跳出
    {
        cout << errmsg << endl;
    }
    cout <<__FUNCTION__<<":" << __LINE__ << "⾏执⾏" << endl;
}
int main()
{
    while (1)
    {
        try
        {
            Func();
        }
        catch (const string& errmsg)//找到了,抓取异常
        {
            cout << errmsg << endl;
        }    
    }
    return 0;
}

1.3 栈展开过程

当异常被抛出时,程序会暂停当前函数的执行,开始寻找匹配的catch子句:

  1. 首先检查throw是否在try块内部
  2. 如果在,查找匹配的catch语句
  3. 如果当前函数中没有匹配的catch,则退出当前函数,在外层调用函数中继续查找
  4. 如果到达main函数仍没有匹配,程序调用terminate终止

1.4 异常匹配规则

异常匹配遵循特定规则:

  • 类型完全匹配
  • 允许从非常量向常量的转换(权限缩小)
  • 允许数组/函数向指针的转换
  • 允许从派生类向基类的转换(最实用)

1.5 异常重新抛出

在某些情况下,我们需要对异常进行分类处理,部分异常在当前函数处理,其他异常重新抛出给外层

例如在微信等社交平台下,我们给他人发送信息时如果在转圈,那么其实就出现了异常。如果出现了异常肯定不能立马报错,而是要多试几次,这也是为什么转圈了一会才会提示错误

// 模拟消息发送,网络不稳定时重试
void _SendMsg(const string& s)
{
    if (rand() % 2 == 0)
    {
        throw HttpException("网络不稳定,发送失败", 102, "put");
    }
    else if (rand() % 7 == 0)
    {
        throw HttpException("你已经不是对方的好友,发送失败", 103, "put");
    }
    else
    {
        cout << "发送成功" << endl;
    }
}

void SendMsg(const string& s)
{
    // 发送消息失败时重试3次
    for (size_t i = 0; i < 4; i++)
    {
        try
        {
            _SendMsg(s);
            break;  // 发送成功,退出循环
        }
        catch (const Exception& e)
        {
            // 102号错误:网络不稳定,重试
            if (e.getid() == 102)
            {
                // 重试三次后仍然失败,重新抛出异常
                if (i == 3)
                    throw;
                cout << "开始第" << i + 1 << "次重试" << endl;
            }
            else
            {
                // 其他错误直接重新抛出
                throw;
            }
        }
    }
}

int main()
{
    srand(time(0));
    string str;
    while (cin >> str)
    {
        try
        {
            SendMsg(str);
        }
        catch (const Exception& e)
        {
            cout << e.what() << endl << endl;
        }
        catch (...)
        {
            cout << "Unkown Exception" << endl;
        }
    }
    return 0;
}

1.6 异常安全问题

异常抛出可能导致资源泄漏

因为throw抛出后,后面的代码不再执行,如果后面的代码有析构资源的代码,不执行就会造成资源泄露

double Divide(int a, int b)
{
    if (b == 0)
    {
        throw "Division by zero condition!";
    }
    return (double)a / (double)b;
}

void Func()
{
    // 异常可能导致array内存泄漏
    int* array = new int[10];
    
    try 
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        // 捕获异常时释放内存
        cout << "delete []" << array << endl;
        delete[] array;
        throw;  // 重新抛出异常
    }
    
    // 正常执行时的释放
    cout << "delete []" << array << endl;
    delete[] array;
}

int main()
{
    try
    {
        Func();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
    catch (...)
    {
        cout << "Unkown Exception" << endl;
    }
    return 0;
}

重要提醒

  • 析构函数中抛出异常要特别小心,可能导致资源泄漏
  • 使用RAII技术(如智能指针)可以更好地解决异常安全问题

1.7 异常规范

C++提供了异常规范机制来声明函数是否会抛出异常:

.例如noexcept明确表示该函数不会抛出异常

// noexcept表示不会抛出异常
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;

double Divide(int a, int b) noexcept
{
    if (b == 0)
    {
        throw "Division by zero condition!";  // 违反noexcept声明
    }
    return (double)a / (double)b;
}

int main()
{
    // noexcept运算符检测表达式是否会抛出异常
    int i = 0;
    cout << noexcept(Divide(1,2)) << endl;  // false
    cout << noexcept(Divide(1,0)) << endl;  // false  
    cout << noexcept(++i) << endl;          // true
    
    return 0;
}

注意:编译器不会在编译时检查noexcept,但违反noexcept声明会导致程序终止。

1.8 标准库异常体系

C++标准库定义了一套异常继承体系,基类是std::exception

#include <exception>
#include <iostream>
using namespace std;

int main()
{
    try
    {
        // 可能抛出异常的操作
        throw runtime_error("运行时错误");
    }
    catch (const exception& e)  // 捕获所有标准异常
    {
        cout << "标准异常: " << e.what() << endl;
    }
    catch (...)
    {
        cout << "未知异常" << endl;
    }
    return 0;
}

2. 智能指针的原理与使用

2.1 传统内存管理的问题

传统手动内存管理在异常场景下容易导致内存泄漏:

double Divide(int a, int b)
{
    if (b == 0)
    {
        throw "Divide by zero condition!";
    }
    return (double)a / (double)b;
}

void Func()
{
    // 异常可能导致array1和array2内存泄漏
    int* array1 = new int[10];
    int* array2 = new int[10];  // 如果这里抛出异常,array1会泄漏
    
    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        // 需要手动释放所有资源
        cout << "delete []" << array1 << endl;
        delete[] array1;
        cout << "delete []" << array2 << endl;
        delete[] array2;
        throw;
    }
    
    // 正常路径也需要释放
    cout << "delete []" << array1 << endl;
    delete[] array1;
    cout << "delete []" << array2 << endl;
    delete[] array2;
}

可以看见throw抛出会违背我们所预想的代码执行逻辑,会导致没有执行释放内存的操作,造成内存泄漏。

2.2 RAII设计思想

RAII(Resource Acquisition Is Initialization)利用对象生命周期管理资源

RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

说人话就是给一个类来保管,因为程序结束后会自动调用类的析构函数

template<class T>
class SmartPtr
{
public:
    // RAII:构造函数获取资源
    SmartPtr(T* ptr) : _ptr(ptr) {}
    
    // RAII:析构函数释放资源
    ~SmartPtr()
    {
        cout << "delete[] " << _ptr << endl;
        delete[] _ptr;
    }
    
    // 重载运算符,模拟指针行为
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
    T& operator[](size_t i) { return _ptr[i]; }
    
private:
    T* _ptr;
};

// 使用RAII智能指针
void Func()
{
    SmartPtr<int> sp1 = new int[10];
    SmartPtr<int> sp2 = new int[10];
    
    for (size_t i = 0; i < 10; i++)
    {
        sp1[i] = sp2[i] = i;
    }
    
    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;
    // 无论是否发生异常,资源都会自动释放
}

2.3 C++标准库智能指针

智能指针类型概览
类型 C++版本 特点 使用场景
auto_ptr C++98 拷贝时转移所有权(已废弃) 不推荐使用
unique_ptr C++11 独占所有权,不支持拷贝 不需要共享所有权的场景
shared_ptr C++11 共享所有权,引用计数 需要共享所有权的场景
weak_ptr C++11 不增加引用计数 解决循环引用问题

智能指针是封装的类,具体功能与指针基本一致

基本使用示例
#include <memory>
#include <iostream>
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(已废弃,仅作了解)
    auto_ptr<Date> ap1(new Date);
    auto_ptr<Date> ap2(ap1);  // ap1变为空指针
    // ap1->_year++;  // 错误:ap1已悬空
    
    // unique_ptr:独占所有权
    unique_ptr<Date> up1(new Date);
    // unique_ptr<Date> up2(up1);  // 错误:不支持拷贝
    unique_ptr<Date> up3(move(up1));  // 支持移动
    
    // shared_ptr:共享所有权
    shared_ptr<Date> sp1(new Date);
    shared_ptr<Date> sp2(sp1);  // 支持拷贝
    shared_ptr<Date> sp3(sp2);
    
    cout << "引用计数: " << sp1.use_count() << endl;  // 输出3
    
    sp1->_year++;
    cout << sp1->_year << endl;
    cout << sp2->_year << endl;  // 所有shared_ptr共享同一对象
    
    return 0;
}

2.4 智能指针的原理

auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用
因为auto_ptr的拷贝说是拷贝,其实就是掠夺他人的资源,使得别的指针成为野指针,就容易引起野指针的访问。
unique_ptr的思路是不支持拷贝,而是移动构造和移动赋值。虽然unique_ptr也可以说是掠夺他人的资源,但是明确的说只有传递右值,即move(左值)才会进行转移,说白了不会造成意外的移动资源,而是你自愿的。你明确自愿转移资源如果出错了就怪不了别人了awa

shared_ptr的思路是共享资源,即几个指针共同指向同一块空间。但是指向同一块资源时析构就会出问题,假设程序结束,几个指针一起析构,第一个指针已经把指向的资源释放了,下一个继续释放就会出错。所以我们引入一个计数,即指向同一块空间指针的个数,只有减为0时才会释放资源

2.5 删除器(Deleter)

我们封装智能指针使用的是模板,模板能够让我们接受不同类型的参数

但是也有一个问题,就是析构函数是锁死的,我们不知道实例化出的模板具体是什么类型,如果是内置类型还好说,无脑delete即可。但是如果是自定义函数我们不知道该怎么释放对应的资源

因此智能指针支持自定义删除器来管理不同类型的资源。这个删除器是我们根据我们所传入的自定义类型而专门设计用来释放资源的仿函数

#include <memory>
#include <iostream>
#include <cstdio>
using namespace std;

// 函数指针删除器
template<class T>
void DeleteArrayFunc(T* ptr)
{
    delete[] ptr;
}

// 仿函数删除器
template<class T>
class DeleteArray
{
public:
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};

// 文件关闭删除器
class Fclose
{
public:
    void operator()(FILE* ptr)
    {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

int main()
{
    // 方法1:使用特化版本(针对new[])
    unique_ptr<Date[]> up1(new Date[5]);
    shared_ptr<Date[]> sp1(new Date[5]);
    
    // 方法2:自定义删除器
    // unique_ptr的删除器在模板参数中指定
    unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
    
    // shared_ptr的删除器在构造函数中指定
    shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
    
    // 函数指针删除器
    unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
    shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
    
    // lambda表达式删除器
    auto delArray = [](Date* ptr) { delete[] ptr; };
    unique_ptr<Date, decltype(delArray)> up4(new Date[5], delArray);
    shared_ptr<Date> sp4(new Date[5], delArray);
    
    // 文件资源管理
    shared_ptr<FILE> sp5(fopen("test.txt", "r"), Fclose());
    shared_ptr<FILE> sp6(fopen("test.txt", "r"), [](FILE* ptr) {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    });
    
    return 0;
}

2.6 智能指针的实现原理

unique_ptr实现
namespace bit
{
    template<class T>
    class unique_ptr
    {
    public:
        explicit unique_ptr(T* ptr) : _ptr(ptr) {}
        
        ~unique_ptr()
        {
            if (_ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
            }
        }
        
        // 像指针一样使用
        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }
        
        // 禁用拷贝
        unique_ptr(const unique_ptr<T>&) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
        
        // 支持移动语义
        unique_ptr(unique_ptr<T>&& sp) : _ptr(sp._ptr)
        {
            sp._ptr = nullptr;
        }
        
        unique_ptr<T>& operator=(unique_ptr<T>&& sp)
        {
            if (this != &sp)
            {
                delete _ptr;
                _ptr = sp._ptr;
                sp._ptr = nullptr;
            }
            return *this;
        }
        
    private:
        T* _ptr;
    };
}

shared_ptr实现
namespace bit
{
    template<class T>
    class shared_ptr
    {
    public:
        explicit 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 (_ptr != sp._ptr)
            {
                release();
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                ++(*_pcount);
                _del = sp._del;
            }
            return *this;
        }
        
        ~shared_ptr()
        {
            release();
        }
        
        T* get() const { return _ptr; }
        int use_count() const { return *_pcount; }
        
        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }
        
    private:
        void release()
        {
            if (--(*_pcount) == 0)
            {
                // 最后一个管理对象,释放资源
                _del(_ptr);
                delete _pcount;
                _ptr = nullptr;
                _pcount = nullptr;
            }
        }
        
        T* _ptr;
        int* _pcount;
        function<void(T*)> _del = [](T* ptr) { delete ptr; };
    };
}

2.7 shared_ptr的循环引用问题

shared_ptr并不万能,还有一点问题

如下图所述场景,n1和n2析构后,管理两个节点的引用计数减到1

这是经典的双向链表。

右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放
_next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释
放了。
_prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏。
把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的
引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题

解决方案:weak_ptr

weak_ptr不支持RAII,不能单独管理资源。在这里weak_ptr是辅助shared_ptr来处理循环引用的问题,具体原理是不增加计数

struct ListNode
{
    int _data;
    weak_ptr<ListNode> _next;  // 使用weak_ptr打破循环引用
    weak_ptr<ListNode> _prev;
    
    ~ListNode()
    {
        cout << "~ListNode()" << endl;
    }
};

int main()
{
    shared_ptr<ListNode> n1(new ListNode);
    shared_ptr<ListNode> n2(new ListNode);
    
    n1->_next = n2;  // n2引用计数仍为1
    n2->_prev = n1;  // n1引用计数仍为1
    
    cout << "n1引用计数: " << n1.use_count() << endl;  // 1
    cout << "n2引用计数: " << n2.use_count() << endl;  // 1
    
    return 0;
    // 正常释放,无内存泄漏
}

3. 内存泄漏与防护

3.1 内存泄漏的概念与危害

内存泄漏:指程序未能释放已经不再使用的内存,通常是由于疏忽或错误导致的。

危害

  • 短期运行的程序影响不大
  • 长期运行的程序(操作系统、后台服务等)会逐渐耗尽内存
  • 导致系统响应变慢,最终卡死
int main()
{
    // 申请1G内存未释放
    char* ptr = new char[1024 * 1024 * 1024];
    cout << "分配内存: " << (void*)ptr << endl;
    
    // 程序结束后操作系统会回收内存
    // 但对于长期运行的程序,这种泄漏是致命的
    return 0;
}

3.2 内存泄漏检测

Linux检测工具
  • Valgrind
  • AddressSanitizer
Windows检测工具
  • Visual Leak Detector (VLD)
  • CRT调试堆

3.3 内存泄漏防护策略

  1. 事前预防

    • 良好的编码规范
    • 使用智能指针管理资源
    • 采用RAII思想
  2. 事后检测

    • 定期使用检测工具
    • 项目上线前全面检测
  3. 最佳实践

// 不好的做法
void bad_function()
{
    int* ptr = new int[100];
    // ... 可能抛出异常
    delete[] ptr;  // 异常时不会执行
}

// 好的做法
void good_function()
{
    unique_ptr<int[]> ptr(new int[100]);
    // ... 即使抛出异常,资源也会自动释放
}

总结

通过本文的详细讲解,我们对C++异常处理和智能指针有了全面的认识:

核心技术要点

  1. 异常处理

    • 提供了比错误码更丰富的错误信息传递机制
    • 通过继承体系可以构建层次化的异常处理
    • 异常安全是编写健壮代码的关键
  2. 智能指针

    • RAII思想是C++资源管理的核心理念
    • unique_ptr用于独占所有权场景
    • shared_ptr用于共享所有权场景
    • weak_ptr解决循环引用问题
  3. 内存安全

    • 智能指针从根本上解决内存泄漏问题
    • 结合异常处理可以构建安全的资源管理
    • 定期检测是保证长期运行稳定的重要手段

实践建议

  1. 优先使用标准库:尽量使用std::unique_ptr和std::shared_ptr,避免手动内存管理

  2. 异常安全设计

    // 使用RAII保证异常安全
    void safe_function()
    {
        auto resource1 = make_unique<Resource>();
        auto resource2 = make_shared<AnotherResource>();
        
        // 即使这里抛出异常,资源也会正确释放
        may_throw_exception();
    }
    

  3. 循环引用预防:在可能形成循环引用的场景中使用weak_ptr

  4. 团队规范:制定统一的异常处理和资源管理规范,提高代码质量

Logo

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

更多推荐