异常
认识异常
C语言中的处理异常的方式
- 错误码,或者全局的异常变量
- assert或者exit()等其他的方式去终止整个程序,但是这个问题一般比较严重
C++通过异常机制来解决问题 C++提供了一个标准的类去进行处理异常-exception
在编写C++代码时,除了语法错误,连接错误,可以在变成可执行程序之前发现,但是还有一些逻辑错误,可能会导致越界,程序崩溃等等。 异常 Exception 是程序可能检测到的 运行时刻不正常的情况 如被 0 除 数组越界 访问或空闲存储内存耗尽等等 这样的异常存在于程序的正常函数之外 而且要求程序立即处理。
这里面向对象语言C++提供的异常处理机制。 面对可能会出现异常的代码,可以检测出(try),然后被捕捉(catch),然后抛出(throw),这样就不会导致程序的逻辑问题,不会崩溃之类的。
当发生异常时,不能简单地结束任务,而是要退回到任务发生的起点,然后又由用户自定义执行下一步错误,即引发崩溃的就不能再运行这个指令。 在即将引发崩溃的条件下,进行抛出,然后捕捉,阻止程序崩溃。并不是等程序崩溃后再捕捉。
捕捉catch会捕捉throw抛出的特定类型的的变量,然后跳转到catch中,执行用户定义的代码
对于抛出的类型,如果没有进行特定类型的捕捉,则这个异常依旧会造成报错或者崩溃
异常造成的内存泄漏的问题
使用C++的异常机制会导致一个问题:内存泄漏。 很多人会感到疑惑和不解,C++异常机制就是用来解决这些问题的,怎么还会引发这些问题? C++的异常机制的组成是在try 块域中进行异常抛出throw 的检测,然后在catch 进行异常的捕获。但是这样是会打乱程序的执行流。强制程序执行其他步骤。这也是由于人的问题引发的缺陷。 如以下代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
void fuc1()
{
int* arr = (int*)malloc(sizeof(int)*0x7ffffff);
FILE* f = fopen("test.txt", "r");
if (arr == nullptr)
{
throw string("开辟空间失败");
}
if (f == nullptr)
{
throw string("文件打开失败");
}
free(arr);
fclose(f);
}
int main(void)
{
try
{
fuc1();
}
catch (string& err)
{
cout <<err<< endl;
}
return 0;
}
看似没什么问题,在函数中既malloc了空间,也有free;对于文件操作,有open也有close。看起来没有引发内存泄漏的可能。 但是,关键就在于异常抛出上。 当arr 开辟空间与文件指针f 打开文件后,如果malloc开辟的空间太大,导致开辟失败 ,或者当前目录下没有这个文件,导致打开失败 ,那么符合判断条件,然后就会被抛出,随即被检测到,然后被catch 捕获。出现这个出现执行流的构过程。如果出现以上的某一个问题,那么函数中后面的代码就不会执行,直接跳出。比如文件打开失败,而文件指针是nullptr ,这个没什么影响,但是malloc,这个没有出问题,确实在堆中开辟了一定的空间,而异常抛出导致后面代码没法执行,且malloc出来的空间也没有回收,这样就会导致内存泄漏的问题。 如果这样改进以下,就可以避免内存泄漏的问题
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
void fuc1()
{
int* arr = (int*)malloc(sizeof(int)*0x7fffffff);
FILE* f = fopen("test.txt", "r");
if (arr == nullptr)
{
if(f!=nullptr)
fclose(f);
throw string("开辟空间失败");
}
if (f == nullptr)
{
if(arr!=nullptr)
free(arr);
throw string("文件打开失败");
}
free(arr);
fclose(f);
}
int main(void)
{
try
{
fuc1();
}
catch (string& err)
{
cout <<err<< endl;
}
return 0;
}
这个解决方案虽然比较不太方便、高效,但是能用。 不过,这只是针对这些情况,如果想达到C++的简洁、高效的效果,需要定义一个类去操作。 可以选择创建一个基类,存放基本的异常信息:错误识别码,错误信息
class cz_Exception
{
public:
cz_Exception(size_t errid,string errmess)
:_errid(errid)
,_errmess(errmess)
{}
size_t GetID(void)
{
return _errid;
}
string GetMessage(void)
{
return _errmess;
}
protected:
size_t _errid;
string _errmess;
};
在其他地方使用还可以继承这个基类,使用派生类在不同地方使用不同的异常处理
异常的抛出和捕获规则
- 抛异常,可以抛出任意类型,但是对于捕获异常,是由抛出的异常的类型来决定被哪个catch捕获块来捕获。
- 异常被抛出后,会一层层作用域的去与所在try块匹配的catch捕获快进行类型匹配,只要匹配到一次就会结束,最后一个作用域是main函数中的,如果都没有匹配的catch块,那么就会给系统,进而直接报错。
- catch(…){},可以捕获任意类型的异常,所有异常。但是关键问题是我们并不清楚捕获的异常到底是什么,所以,一般选择处理未知异常,防止因为某些疏忽造成未知异常,导致程序崩溃。
- 实际上的抛出和捕获并不是要求完全类型匹配,对于基类也能捕获派生类的异常,这个在项目中非常实用,所以C++利用多态的特性来使用一个类来封装异常。
- 对于throw抛出的异常,实际上是对异常信息的临时拷贝,因为普通的对象出了作用域会被销毁。且这个临时对象的异常信息,被捕获后也会被销毁。
捕获就非常像传参调用 。
auto小知识
auto类型是不能用来作为参数,返回值的类型,因为编译器在编译器时无法推到到底该生成哪一个。 平常使用auto去接受某些对象时,都是能提前确定好了的,编译器在编译时会去自动推导,生成一样的类型的对象。 所以想用catch(auto e){}去替代catch(…){},是不现实的,IDE会直接报错的。
项目中的异常使用
在一个项目中,需要不同的功能模块。这个需要不同的开发者去实现。所以定义一个最基本的基本类,让开发者在不同的项目中去继承这个基类,这样在catch时可以直接使用一个catch(基类){}就能完成所有异常的捕获。比如说这个样子
class cz_Exception
{
public:
cz_Exception(size_t errid,string errmess)
:_errid(errid)
,_errmess(errmess)
{}
virtual size_t GetID(void)
{
return _errid;
}
virtual string GetMessage(void)
{
return _errmess;
}
protected:
size_t _errid;
string _errmess;
};
class cz_Move_Exception :public cz_Exception
{
};
class cz_OpenCV_Exception :public cz_Exception
{
};
class cz_Vision_Exception :public cz_Exception
{
};
异常安全规范
异常的优缺点
优点
- 可以返回错误码以及更具体的错误信息
- 不用层层处理捕获的异常,只需要抛出-捕获即可
- 大项目和框架都会使用异常进行测试处理
- 像一些重载函数和返回值不好处理的函数,选择异常比选择断言要更好处理,可以简化错误和查找错误原因
缺点
- 异常会打断执行流的进度,发生错误时,会造成执行流乱跳,调试和分析时,会造成一些问题
- 对硬件需要一些开销,尤其是小性能的硬件.
- 会引发一些异常安全的问题,比如内存泄漏之类的,不过这可以靠RALL智能指针来解决
- 不同厂商对异常由自己的定义,有些标准混乱的问题。
异常的多次抛出
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
class cz_Exception
{
public:
cz_Exception(size_t errid,string errmess)
:_errid(errid)
,_errmess(errmess)
{}
size_t GetID(void)
{
return _errid;
}
string GetMessage(void)
{
return _errmess;
}
protected:
size_t _errid;
string _errmess;
};
void fuc1()
{
int* arr;
FILE* f;
try
{
arr = (int*)malloc(sizeof(int) * 0x7ffffff);
f = fopen("test.txt", "r");
if (arr == nullptr)
{
throw cz_Exception(1, "malloc开辟失败");
}
if (f == nullptr)
{
throw cz_Exception(2, "文件打开失败");
}
}
catch (cz_Exception& err)
{
if (err.GetID() == 1)
{
if(f != nullptr)
{
cout << "关闭文件" << endl;
fclose(f);
}
throw err;
}
if (err.GetID() == 2)
{
if (arr != nullptr)
{
cout << "回收资源" << endl;
free(arr);
}
throw err;
}
}
free(arr);
fclose(f);
}
int main(void)
{
try
{
fuc1();
}
catch (cz_Exception& err)
{
cout <<err.GetID()<<" " << err.GetMessage() << endl;
}
return 0;
}
但是这样的操作依旧非常累赘,所以简便的解决方案就是智能指针。
智能指针
智能指针其实就是类封装。
使用一个类来维护指针,维护指针开辟的资源。由于类只能在作用域内有效,所以,使用一个类来去维护开辟的资源。 且在抛异常时,异常也算一个临时变量的拷贝,然后异常会不断进入下一个作用域栈帧,在执行流离开一个作用域,系统会清理资源
对于对象而言,离开了作用域,系统会调用析构函数来清理当前作用域栈中的对象。随着异常的抛出离开当前作用域,则就自动调用析构函数来清理资源,所以我们可以用智能指针一个类来维护用指针开辟的资源,然后再析构函数 中来delete指针的资源
一个简单的智能指针
#pragma once
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
std::cout << "delete:" << _ptr << endl;
delete _ptr;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
由于智能指针的需求,需要想像普通指针一样,正常使用,需要能够访问指针的内容,修改指针的内容。
所以,必须需要-> 和* 的重载。
智能指针解决异常中内存泄漏的问题
#include <iostream>
#include <string>
using namespace std;
#include "SmartPtr.h"
class Exct
{
public:
Exct(int id,string str)
:_id(id)
,_str(str)
{}
const int& GetID(void)
{
return _id;
}
const string& what(void)
{
return _str;
}
private:
int _id;
string _str;
};
void fuc(void) throw(Exct)
{
SmartPtr<pair<int,int>> pa(new pair<int,int>);
SmartPtr<int> pb(new int);
pa->first = 10;
pa->second = 10;
*pb = 0;
if (*pb == 0)
throw Exct(1, "除零错误");
cout << "a / b = " << pa->second / *pb << endl;
}
int main()
{
try
{
fuc();
}
catch (Exct& err)
{
cout <<"错误码:"<<err.GetID() << " 错误描述:" << err.what() << endl;
}
}
效果展示 使用智能指针后,代码中并没有显示的专门去处理malloc之类的资源回收,但是依旧不会造成内存泄漏,这就是RAII机制,利用系统对于资源的管理,进行垃圾回收。
RAII
这种技术也叫RAII。可以利用对象的生命周期来控制程序资源(内 存,文件句柄,网络连接,互斥量等等)。 利用类的构造函数类自动获取资源,利用类的析构函数来自动释放、 清理资源,就是我们把创建的资源托管给了系统,系统会来帮我们进 行创建、删除资源的问题 好处:
- 不需要显式的释放资源
- 采用这种方式,对象再生命周期内始终有效,且不会泄漏丢失。
像智能指针、递归互斥锁的锁首位等待,都是使用这种技术
智能指针的拷贝问题
智能指针想要达到和普通指针一样的使用效果,又希望具有RAII的特性,且指针需要解决一个关键的问题,就是指针拷贝的问题。指针的拷贝并不是深拷贝,而是浅拷贝问题。但是仅仅是普通的浅拷贝又会因为RAII特性,而造成多次析构的问题。
auto_ptr
从C++98标准开始,C++标准委员会就由auto_ptr开始解决智能指针拷贝的问题。
auto_ptr选择的是管理权转移 多个智能指针指向一个空间资源时,资源的管理权在最新的一个指针,其他智能指针为空。
unique_ptr
然后又有一个解决方案unique_ptr 解决不了问题就解决问题的源头
直接禁止智能指针拷贝和复制
unique_ptr(const unique_ptr<T>& up) = delete; unique_ptr<T> operator=(const unique_ptr<T>& up) = delete; 但是依旧达不到需求。
shared_ptr
最优秀的解决方案,来自boost库,最后被引入C++11标准 shared_ptr,运用了引用计数的方法完成普通指针的效果
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _ptrCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _ptrCount(sp._ptrCount)
{
*_ptrCount += 1;
}
const shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
if (*_ptrCount == 1)
{
cout << "operator=:删除一个空间" << endl;
delete _ptr;
delete _ptrCount;
}
if (*_ptrCount > 1)
{
*_ptrCount -= 1;
}
_ptr = sp._ptr;
_ptrCount = sp._ptrCount;
*_ptrCount += 1;
}
return *this;
}
~shared_ptr()
{
cout << "~shared_ptr:";
if (*_ptrCount == 1)
{
cout << "删除一个智能指针指向的空间" << endl;
std::cout << "delete:" << _ptr << endl;
delete _ptr;
delete _ptrCount;
_ptr = nullptr;
}
else
{
*_ptrCount -= 1;
cout << "删除一个智能指针" << endl;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
int* _ptrCount;
};
为了满足线程安全的需求,使用了mutex指针来维护引用计数
template <class T>
class shared_ptr
{
void AddRef(void)
{
_ptrMutex->lock();
++(*_ptrCount);
_ptrMutex->unlock();
}
void ReleaseRef(void)
{
_ptrMutex->lock();
bool flag = false;
if (--(*_ptrCount) == 0)
{
delete _ptr;
delete _ptrCount;
flag = true;
}
_ptrMutex->unlock();
if (flag == true)
{
delete _ptrMutex;
}
}
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_ptrCount(new int(1))
,_ptrMutex(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_ptrCount(sp._ptrCount)
,_ptrMutex(sp._ptrMutex)
{
AddRef();
}
const shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
ReleaseRef();
_ptr = sp._ptr;
_ptrCount = sp._ptrCount;
AddRef();
}
return *this;
}
~shared_ptr()
{
cout << "~shared_ptr" << endl;
ReleaseRef();
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
int* _ptrCount;
mutex* _ptrMutex;
};
使用了mutex指针来维护资源的引用计数,使用指针是为了保证一个资源有且只有一个锁。
weak_ptr
解决了智能指针循环引用的问题
什么是循环引用
当需要清理这两个资源时,由于都只剩下另外一块资源的指针,所以引用计数都是1 当清理这个资源1时,需要先清理内部的自定义类型的成员,清理sp1,就需要清理其指向的资源2, 清理资源2,又需要先清理sp2,清理sp2,就需要先析构资源1。
就这样,析构资源1,就需要析构sp1,析构sp1又需要清理资源2,清理资源2,有需要先析构sp2, 清理sp2,又需要先清理资源1. 这就是智能指针的循环引用问题,陷入死循环了 所以,出现循环引用的问题,就是资源的最后管理权变成了对方的,导致互相管理权交换,死循环
关键是,不要增加资源内部的智能指针造成的引用计数的增加,官方提供了weak_ptr来解决 这个可以像指针一样访问资源,但是不会增加引用计数,weak_ptr不会接受原生指针进行构造, 而是可以接受智能指针进行资源操作
weak_ptr对于资源只读不写,不会参与资源的管理,这样就不会该改变资源的引用计数。
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const cz::shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T> operator=(const cz::shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
~weak_ptr()
{
cout << "weak_ptr" << endl;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
删除器
|