C++异常机制和智能指针机制的杂谈
Posted Booksort
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++异常机制和智能指针机制的杂谈相关的知识,希望对你有一定的参考价值。
目录
异常
认识异常
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)//判断一定要加,万一有多个变量G了怎么办
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://使用protected来封装成员,保证继承类能够得到
size_t _errid;
string _errmess;
//其他信息,时间,栈序列,等等
;
//可以在派生类里面添加一些其他的protected信息,比如说:来自哪个功能..
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://使用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:
//RAII特性
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)//如果出现除零错误,就会导致指针a,b无法delete,造成内存泄漏,选择多次 捕获抛出 或者 智能指针
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 << "该赋值会造成内存泄漏" << endl;
//assert(false);
//删除空间,防止内存泄漏
cout << "operator=:删除一个空间" << endl;
delete _ptr;
delete _ptrCount;
//解决bug-指向某个空间的智能指针指向另外一个空间
if (*_ptrCount > 1)//对于*_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;
//在构造时创建一个变量,并赋值1
//在拷贝时,指针指向拷贝过来的变量,并加1
//在析构的时候,--,当值只有1时,就清理资源
;
为了满足线程安全的需求,使用了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._ptrMutexC++智能指针详解:智能指针的引入