目录
异常
异常的定义
异常的抛出和捕获
异常安全问题
异常的规范
智能指针
RAII思想
使用RAII的例子
智能指针
文件资源
在linux中管理锁资源
智能指针发展历程
auto_ptr
unique_ptr
异常
异常的定义
异常是一种处理运行时错误的机制,它允许程序在发生错误时能够以一种可预测和可控的方式响应,而不是使程序直接崩溃。异常是程序运行时抛出的一个信号,表明程序中出现了某种特殊的情况,需要被处理。
异常的抛出和捕获
异常的抛出:抛异常是对程序发生错误的预测,当程序猿自己都不知道自己的代码错误的情况时,异常也就无从抛出了。所以异常抛出的位置是由程序猿主观设定的,使用异常处理来确保程序的健壮性和可维护性。
语法:将可能抛出异常的代码块用try包裹,在try块内部throw 异常对象
异常的捕获:将抛出的异常在距离最近的且类型最匹配的catch块内进行捕获,对程序中出现某种异常情况进行处理。如果当前栈并没有捕获逻辑,则销毁当前栈继续向上寻找可以匹配的catch块来捕获,若到main函数栈依旧没有匹配的,则程序崩溃。
语法:使用类似形参的方式接受异常对象,将处理异常的方式定义在catch块内部。
通常情况,可以将异常类设计为父子关系,具体的异常类都是继承自一个基类exception类,我们可以使用exception类形参来接受抛出的任何的异常对象,调用成员函数what()可以得知某个异常的具体描述,这也是多态的例子。
异常安全问题
异常安全问题是由于异常在抛出时,会停止执行当前try块的代码,寻找最近的最匹配的catch块进行捕获,这就使得某些释放资源的代码可能不会被执行,造成资源的泄露。
举个栗子:
总结:这样的情况下必会造成资源泄露。
try{
资源申请;
异常的抛出;
资源释放;
}
catch(exception& e)
{
捕获逻辑;
}
资源泄露的种类:new delete内存泄露、open close文件描述符泄露、lock unlock死锁...
C++中如何解决异常安全问题:
使用RAII思想,让对象管理资源,栈帧销毁时自动调用析构释放申请的资源。
异常的规范
异常的使用规范是为了让编程者知道哪些函数是否会抛出异常,会抛出什么类型的异常。当然也可以不遵守这个规范,但是写一份易维护的代码是编写者需要慢慢养成的素养。
void func() throw(异常类型A,异常类型B,异常类型C); //表明此函数可能抛出A,B,C三种类型的异常
void pow() throw(异常类型A); //表明此函数只可能抛出A这一种类型的异常
void run() throw(); //表明此函数不会抛出异常
智能指针
RAII思想
RAII(Resource Acquisition Is Initialization资源获取即初始化),是一种将资源的生命周期和对象的生命周期绑定在一起的思想,从而消除资源泄露的问题。
即可以让栈上的对象来管理申请的资源(堆空间、套接字、文件描述符、锁...),在对象出作用域时自动调用析构函数将申请的资源释放。
使用RAII的例子
智能指针
智能指针就使用了这种思想,下面实现一个最简单的智能指针:
template<class T>
class SmartPtr //使用类来封装资源
{
public:
SmartPtr(T* ptr):_ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
cout << "delete->" << _ptr << endl;
}
private:
T* _ptr;//被管理的堆空间指针
};
int main()
{
SmartPtr<int> ptr(new int(2));//ptr对象的生命周期和堆资源的生命周期绑定了
return 0;
}
这样简单的智能指针,在对象析构时会自动调用delete释放申请的堆空间。这样就消除了忘记释放堆空间而导致内存泄露的危害。
文件资源
同样使用RAII思想还可以管理其他需要手动释放的资源,比如打开的文件:
#include<iostream>
#include<fstream>
using namespace std;
class SmartFile
{
public:
SmartFile(const char* path)
{
_filehandle.open(path, ios::out);
}
fstream& gethandle()
{
return _filehandle;//获取句柄,方便对文件进行操作
}
~SmartFile()
{
_filehandle.close();//对象生命周期结束自动关闭文件
cout << "file closed" << endl;
}
private:
fstream _filehandle;//文件句柄
};
int main()
{
SmartFile file("1.txt");
char buf[] = "sas";
file.gethandle().write(buf,strlen(buf));
}
在linux中管理锁资源
#include<pthread.h>
class mutexguard
{
public:
mutexguard(pthread_mutex_t * mutex) :_mutex(mutex)
{
pthread_mutex_lock(_mutex);//加锁
}
~mutexguard()
{
pthread_mutex_unlock(_mutex);//解锁
}
private:
pthread_mutex_t * _mutex;
};
int main()
{
pthread_mutex_t _mutex;
pthread_mutex_init(&_mutex, nullptr);
mutexguard(&mutex);
return 0;
}
上面所实现的最简单的智能指针存在拷贝和赋值的问题,例如:
int main()
{
SmartPtr<int> ptr1(new int(2));
SmartPtr<int> ptr2 = ptr1;//拷贝给ptr2,让ptr2也可以访问同一个指针
return 0;
}
此时程序一旦运行,立马崩溃
原因是此时智能指针这个类没有实现拷贝构造函数,只有默认的拷贝构造:对内置类型按字节浅拷贝。使得ptr1和ptr2内管理的指针指向同一块堆空间。
当出作用域时,两个对象都调用析构函数,使得同一块堆空间被释放两次,程序崩溃。
如何解决这个问题?C++的发展过程出现了多种解决方式如下:
智能指针发展历程
auto_ptr
初代智能指针,当拷贝智能指针时,会将智能指针的所有权转移。
auto_ptr<int> ptr1(new int(2));//ptr1->new int(2)
auto_ptr<int> ptr2=ptr1; //ptr2->new int(2) ptr1->nullptr
缺点:
没有解决需求:如果有人想同时使用ptr1,ptr2管理同一指针是无法做到的,且如果不了解管理权转移的特性还会出现访问空指针的错误。
不支持数组:auto_ptr默认指向的是一个对象,如果指向的是一块由多个对象组成的连续堆空间,释放时智能释放第一个对象。(定制删除器来解决:传入可调用对象来释放空间)
unique_ptr
它是c++11引用的一个智能指针:它拥有其指向对象的独占所有权,即只可以有一个智能指针指向要管理的指针(暴力方法)。通过私有拷贝构造、赋值函数来禁止unique_ptr的拷贝和赋值。
缺点:还是没有解决同时用多个个智能指针管理同一个指针的需求。
shared_ptr
使用引用计数的思想来实现,解决了用多个智能指针管理同一指针的需求。
引用计数:使用计数器来统计指向资源的指针个数,新增指针指向时,计数器++,有指针移除时,计数器--,如果计数器减到0,表明已经没有指针指向这块资源了,即马上将资源释放。
下面实现一个简单的shared_ptr
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr) :_ptr(ptr), _cnt(new int(1))//新的智能指针指向新资源,默认计数器为1
{}
shared_ptr(const shared_ptr& sp)//拷贝构造
{
//指向相同,计数器++
_ptr = sp._ptr;
_cnt = sp._cnt;//两智能指针使用同一计数器
(*_cnt)++;
}
void del()
{
if (--(*_cnt) == 0)//智能指针解除对一个堆空间指向时,要判断计数器是否为0,为0才delete
{
cout << "delete->" << _ptr << endl;
delete _ptr;
delete _cnt;
}
}
shared_ptr& operator=(const shared_ptr& sp)//赋值运算符重载
{
if (sp._ptr != _ptr)//保证指向相同的智能指针不会给自己赋值
{
del();//左边指向资源指针--
_ptr = sp._ptr;
_cnt = sp._cnt;
*(_cnt)++;//右边指向资源指针++
}
return *this;
}
~shared_ptr()
{
del();
}
//重载*,->使智能指针像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;//所管理的指针
int* _cnt;//计数器
};
使用以下代码测试
结果:
shared_ptr设计非常优雅,可用性强,但在实际开发过程中发现了另一问题:循环引用
循环引用的分析和解决:
循环引用是指管理一块堆空间的智能指针存在于另一块堆空间,依赖于另一块堆空间的释放这块堆空间才会释放;而管理另一块堆空间的智能指针又在这一块堆空间上,依赖于这一块堆空间的释放。
一旦形成这种关系,两块堆空间都将无法释放,造成内存泄露!
解决方法:在堆内部创建智能指针shared_ptr时,改为创建weak_ptr,weak_ptr只有指向的作用,不会占用引用计数而导致堆空间无法释放。