异常与智能指针(Exception & Smart_Ptr)

本文详细介绍了C++中异常处理机制,包括异常的抛出与捕获,以及如何通过智能指针(如unique_ptr和shared_ptr)来管理资源,以避免内存泄漏。讨论了RAII原则和智能指针在处理异常后的资源清理问题。
摘要由CSDN通过智能技术生成

文章目录

异常

C语言其实有两种处理错误的办法:

  • 1.一个是终止程序,如assert
  • 2. 程序返回错误码,由程序员处理
    C++兼容C,自然以上处理机制,一旦出错,比如一个小错,就终止是很不合理。比如服务器。返回错误码呢,并不直观,处理起来十分的麻烦,同时不同错误码对应也很麻烦。
    C++处理错误的方式:
    异常:一个函数发现出现自己无法处理的错误时,就抛出异常,让直接调用或者间接调用去处理这个错误。
    * throw:抛出异常就是通过这个关键字实现。
    throw + 对象 (可以是自定义类,也可以时内置类型)
    * catch :捕获throw抛出得对象,但是要求类型匹配。
    catch(const typename e){}
    * try :在try代码块里面函数,其得throw才会别识别,否则就无法识别。其后面跟 不定个数 catch,便可捕捉里面发生得异常。
    try {} <br/>catch(){}
    可以借助下面代码加深理解:
try
{
  // 保护的标识代码
}catch( ExceptionName e1 )
{
  // catch 块
}catch( ExceptionName e2 )
{
  // catch 块
}catch( ExceptionName eN )
{
  // catch 块
}

异常抛出与捕获

  • 异常由抛出对象引发对象类型决定激活哪个catch的代码,且激活的时距离try最近的那个匹配的 catch
  • 抛出异常对象后会生成一个异常对象的拷贝,很有可能时因为抛出的是一个临时对象,生成了一个拷贝对象。
  • catch(…)可以捕获任意类型的异常,但是不知道异常的错误类型。一般就标识未知异常
  • 一般来说,抛出与捕获是需要匹配的,哪怕是一个是const,一个不是都不行,但是抛出派生类,可以用基类接受

运行下面的代码,打断点调试下,对除0函数里面的注释定义的取消,看看效果。

#include <bits/stdc++.h>
using namespace std;

//经过下面的测试,可以发现,异常是一层一层栈帧的寻找,找不到就退出当前栈帧,当前定义的局部变量也会销毁,其后续的代码也不再执行
//实际上到main函数都没找到接受异常的,总体就是异常无法处理,程序就会退出。
class A
{
   public:
   ~A()
   {
       cout<<"~A\n";
   }
};

double Exception_divise_zero(int a,int factor)
{
   if(factor==0)
   {
       // const  string s ="Division by zero condition";
       // char s[100] = "Division by zero condition";
       char* s = "Division by zero condition";
       throw (char*)s;//抛出什么类型,接受什么类型,但是基类和派生类之间,基类接受派生类是可行的
       //如这里是const char*,那接受就必须是const char* ,少了const或者用const string都不行
   }
   else
   {
       return (double)a/factor;
   }
}

void CallDivision()
{
   A b;//但是可以发现,在一层层处理当前栈帧的时候,会销毁这时候定义的局部变量
   Exception_divise_zero(10,0);
   A a;//经过测验可以发现,抛异常之后,之后的代码不会再执行了
}


void Test_Exception(){
   try
   {
       CallDivision();
   }
   catch(const string s)
   {
       std::cerr<<"const"<<s<<'\n';
   }
   catch(string s)
   {
       std::cerr<<s<<'\n';
   }
   catch(char* e)
   {
       std::cout << e<< '\n';
   }
   catch(const char* e)
   {
       std::cerr <<"const "<< e<< '\n';
   }
   catch(...)//可以接受任何异常,下面一般写未知异常,这样的话,就能避免直接杀死程序
   {
       cerr<<"unkown Exception \n";
   }
}   ```
结论:如果你运行过看到了现象,**抛出异常之后,函数是跳转级别的,有点像C语言的goto的感觉,直接回“跳过”好几个函数栈帧,当然实际不是这么回事,上面的代码证明,抛出异常之后呢,会结束当前函数栈帧,但是该做的局部变量释放那些都会处理。**
如果你解除了注释,抛出了字符数组的s,你会发现接受之后发现打印时乱码。
这个原因是因为,就是验证局部变量是否被消除,其实就是因为定义在里面的变量被释放罢了。

**分别注释catch,我们发现,const对普通非const是可以接受的,这意味着,你想抛出非const,就不要把const放在前面。**,当然实践里面,推荐不要搞这些易混淆的操作,害人害己。


下面是父类接受子类:
```cpp

class MyExeception{
   protected:
   int _eid;
   string _errorMsg;
   public:
   MyExeception(int eid,string eMsg)
   :_eid(eid)
   ,_errorMsg(eMsg)
   {

   }
   virtual string what()const
   {
       return "eid::"+to_string(_eid)+"\n"+"eMsg::"+_errorMsg;
   }
};

class net_serve_exceoptin: public MyExeception{
   string Mysign;
   public:
   net_serve_exceoptin(int eid,string eMsg)
   :MyExeception(eid,eMsg)
   ,Mysign("网络错误")
   {
       ;
   }
};


int Test()
{
   throw net_serve_exceoptin(1,"net error");
   return 0;
}

// int main()
// {
//     while(1)
//     {
//         try
//         {
//             Test();
//         }
//         catch(MyExeception& e)
//         {
//             std::cerr << e.what() << '\n';
//         }
//         catch (...)
//         {
//             cout << "Unkown Exception" << endl;
//         }
//     }
//     return 0;
// }

下面来看异常的缺陷:

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

void Exception_Newdelete()
{
    int *arr = new int[10];
    //Division(10,0);//这个Division的函数,抛出一场之后,由于当前函数没有,接受的,后面代码不执行,显然会导致内存错误
    //----------------------------------------------------
    //解决方法一,重新抛出,但是解决的问题十分受限,本质并没有解决完.倘若有调用多个函数,各自又都开辟后续的空间在该处异常之后释放,显然会导致大量的问题
    try{
        Division(10,0);
    }
    catch(const char* e)
    {
        delete[]arr;
        arr = nullptr;
        //重新再次抛出
        throw "Division error";
    }
    catch(...)//捕获什么异常,抛出什么异常
    {
        delete[]arr;
        arr = nullptr;
        throw;
    }
    //因此另外的解决方法就是智能指针
    delete[] arr;   
}

void Exception_get()
{
    try{
        Exception_Newdelete();
    }
    catch(const char* e)
    {
        cerr<<e<<endl;
    }
}

运行上面的代码,很容易就发现,指针申请的空间由于抛出异常,没有释放,这导致了内存泄漏。这就是异常带来的问题。解决方案就是智能指针。

智能指针

首先了解下RAII思想。
RAII(Resource Acquisition Is Initialization):利用对象的生命周期来控制程序的资源
而智能指针,就是基于这个特点去设计的。
简单来说:设计一个类,使其实例化对象的时候,获取资源;析构的时候,释放资源
我们知道,C++支持重载运算符,所以只要重载*和->就可以解决像指针的使用。
其采用的是转移控制权的思想,指针要赋值拷贝,原指针呢,就会被置为空。 这其实不好,万一使用者不注意,就会产生解引用空指针等问题造成一系列问题。
在这里插入图片描述
2.unque_ptr:这是C++后来的另外一种设计,禁止赋值和拷贝,这样就不会有指针悬空的情况,但是对某些情况就无法处理。
下面是简化的模拟实现:

template<class T>
    class unique_ptr_1{
        public:
        unique_ptr_1(T* p)
        :ptr(p)
        {

        }
        unique_ptr_1(const unique_ptr_1& a) = delete;
        unique_ptr_1 operator=(const unique_ptr_1& a) = delete;
        T& operator*()
        {
            if(ptr)
            {
                return *ptr;
            }
            else
            {
                throw "Ref nullptr";
            }
        }
        T* operator->()
        {
            return ptr;
        }
        ~unique_ptr_1()
        {
            delete[] ptr;
        }
        private:
        T* ptr;
    };

3.share_ptr:这种指针就是用一个计数的办法,存储有多少个指向对应空间的指针,用这个来解决不能赋值的问题。

template <class T>
    class sharePtr{
        typedef sharePtr<T> Self;
        T* m_ptr;
        int* count;
        function<void(T*)> _del= [](T*ptr)mutable->void{delete ptr;};//解决申请数组空间释放的问题
        public:
        T* get()
        {
            return m_ptr;
        }
        int Usecount()
        {
            return *count;
        }
        sharePtr(T* p)
        :m_ptr(p)
        ,count(new int(1))
        {
            
        }
        template<class Del>
        sharePtr(T* p,Del del )
        :m_ptr(p)
        ,count(new int(1))
        ,_del(del)
        {
            
        }


        //可以直接写这个含缺省参数取顶替上面两个,这样就不需要在声明那里给默认值
        // template<class Del>
        // sharePtr(T* p,Del del = [](T*ptr)mutable->void{delete ptr;})
        // :m_ptr(p)
        // ,count(new int(1))
        // ,_del(del)
        // {
            
        // }
        sharePtr()
        :m_ptr(nullptr)
        ,count(new int(1))
        {
            
        }
        Self operator=(const Self& ptr)
        {
            if(this ==&ptr)
            {
                return *this;
            }
            release();
            m_ptr = ptr.m_ptr;
            count = ptr.count;
            (*count)++;
            return *this;
        }
        sharePtr(const Self& ptr)
        :m_ptr(ptr.m_ptr)
        ,count(ptr.count)
        {
            if(count && *count>0)
            {
                ++*count;
            }
        }
        T* operator->()
        {
            return m_ptr;
        }
        T& operator*()
        {
            return *m_ptr;
        }
        ~sharePtr()
        {
            release();
        }
        void release()
        {
            if(--*count ==0)
            {
                _del(m_ptr);
                delete count;
                count = nullptr;
                m_ptr = nullptr;
            }
        }
    };

有这个指针,基本解决了90%的场景,但是呢有一个特殊的场景没法解决。循环引用

在这里插入图片描述
目前没有方便的解决办法,C++里面提供的方案就是对意识到是循环引用的情况,就使用另外一种指针。
weakPtr。让节点里面是weakPtr,让节点申请的使用sharePtr就可以解决问题了。当然,需要实现sharePtr到WeakPtr的构造。
C++11就是如此实现的。
在这里插入图片描述

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值