异常
首先我们其实在堆区new空间的时候就见过异常,当我们想要开辟的空间太大而无法申请到就会抛异常:
概念
异常是我们处理错误的一种方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常 catch 关键字用于捕获异常,可以有多个catch进行捕获。
- try:try 块中放置可能抛出异常的代码,它后面通常跟着一个或多个 catch 块。
使用
double Division(int a, int b)//除法计算
{
// 当b == 0时抛出异常
if (b == 0)
throw "不能除0";
else
return ((double)a / (double)b);
}
void Func()
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)//抛出什么类型就捕获什么类型的数据
{
cout << errmsg << endl;
}
catch (...)//捕获抛出的未知异常
{
cout << "unkown exception" << endl;
}
return 0;
}
我们要知道当我们在throw抛出异常的时候,此时就会立马调用最近的catch进行,捕获异常, 如果没有调用到catch的话,或者catch的类型不匹配的话就会出现:
而且我们抛异常之后会被立马捕获异常,而这之间的代码并不会执行,直接就开始执行catch后续代码 。 但是也别忘了在此我们调用函数的过程中是创建了栈帧的,所以我们catch捕获异常后会销毁之前的栈帧空间。
实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获。这里就引入C++标准库异常体系了:
C++标准库异常体系
这个exception类就是所有标准C++异常的父类,所以当我们C++程序内部抛异常时都可以用父类对象接受捕获异常。
int main()
{
try
{
new int[7000000000];
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
return 0;
}
异常规范
//C++11
thread() noexcept;//表示函数体内一定不会抛异常,如果抛了也不会进行捕获
thread (thread&& x) noexcept;
//C++98
void fun() throw();//表示函数体内不会抛异常,如果抛了也会捕获
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void fun() throw (std::bad_alloc);
我们是否注意到异常的一点缺陷,当抛异常后就会立马执行到捕获异常的位置,这之间的一段代码都不会执行,并且函数栈帧都将销毁。但是如果这之间的代码是释放堆区空间的话,那么就会造成内存泄漏。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;//调用div()函数
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
所以我们就有了智能指针的引出:
智能指针使用与实现
我们知道catch接受异常时可能会导致从堆区动态开辟的空间未释放从而造成内存泄漏,所以我们就采用了智能指针去构造的对象,然后在栈帧销毁的时候就会自动调用析构函数了,而不需要我们进行手动调用。
这里我们发现会调用div函数,然后如果抛异常的话就会造成delete函数未执行,从而导致内存泄漏的情况,因此我们就通过智能指针的方式进行改造(以shared_ptr为例):
template<class T>
class Smart_ptr
{
public:
Smart_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~Smart_ptr()
{
delete _ptr;
cout << "delete[] " << _ptr << endl;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
Smart_ptr<int> p1 = new int;
Smart_ptr<int> p2 = new int;
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
通过以上的方式,我们将需要创建的对象去构造智能指针,此时当战争销毁的时候就会自动调用类的析构函数,从而释放空间。而这种方式的原理就是就是RAII:
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做
法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
unique_ptr
#pragma once
#include <iostream>
#include <memory>
#include <functional>
namespace cr_unique
{
template <class T>
struct defaultdel
{
void operator()(T *ptr)
{
delete ptr;
ptr = nullptr;
}
};
template <class T, class Del = defaultdel<T>>
class unique_ptr
{
private:
T *ptr;
Del del;
public:
unique_ptr(T *p = nullptr)
: ptr(p) { std::cout << "unique_ptr(T *p)" << std::endl; }
// 禁掉拷贝构造和赋值
unique_ptr(unique_ptr &p) = delete;
unique_ptr &operator=(unique_ptr &p) = delete;
unique_ptr(unique_ptr &&p)
{
std::cout << "std::unique_ptr(unique_ptr &&p)" << std::endl;
ptr = p.ptr; // 内置类型
p.ptr = nullptr;
}
// unique_ptr(std::unique_ptr<T> &&p)
// {
// std::cout<<"unique_ptr(std::unique_ptr<T> &&p)"<<std::endl;
// ptr=p.get();
// p.release();
// }
unique_ptr &operator=(unique_ptr &&p)
{
std::cout << "unique_ptr &operator=(unique_ptr &&p)" << std::endl;
ptr = p.ptr;
p.ptr = nullptr;
return *this;
}
T *operator->()
{
return ptr;
}
T &operator*()
{
return *ptr;
}
~unique_ptr()
{
if (ptr)
{
del(ptr);
ptr = nullptr;
}
}
};
}
shared_ptr
#pragma once
#include <iostream>
#include <memory>
#include <functional>
namespace cr_shared
{
template <class T>
struct defaultdel
{
void operator()(T *ptr)
{
delete ptr;
ptr = nullptr;
}
};
template <class T, class Del = defaultdel<T>>
class shared_ptr
{
private:
T *ptr;
int *pcount; // 共用一个
Del del;
public:
shared_ptr(T *p = nullptr)
: ptr(p)
{
std::cout << "shared_ptr(T *p)" << std::endl;
pcount = new int(1);
}
shared_ptr(shared_ptr &p)
: ptr(p.ptr)
{
std::cout << "shared_ptr(shared_ptr &p)" << std::endl;
pcount = p.pcount;
(*pcount)++;
}
shared_ptr(shared_ptr &&p)
{
std::cout << "std::shared_ptr(shared_ptr &&p)" << std::endl;
ptr = p.ptr; // 内置类型
pcount = p.pcount;
p.ptr = nullptr;
p.pcount = nullptr;
}
void release()
{
if (--(*pcount) == 0)
{
std::cout<<"void release()"<<endl;
del(ptr);
delete pcount;
ptr = nullptr;
pcount = nullptr;
}
}
shared_ptr &operator=(shared_ptr &p)
{
if (ptr != p.ptr) // 防止自己给自己赋值
{
std::cout << "shared_ptr &operator=(shared_ptr &p)" << std::endl;
release();
ptr = p.ptr;
pcount = p.pcount;
(*pcount)++;
}
return *this;
}
shared_ptr &operator=(shared_ptr &&p)
{
if (ptr != p.ptr)
{
std::cout << "shared_ptr &operator=(shared_ptr &&p)" << std::endl;
release();
ptr = p.ptr;
pcount = p.pcount;
p.ptr = nullptr;
p.pcount = nullptr;
}
return *this;
}
T *operator->()
{
return ptr;
}
T &operator*()
{
return *ptr;
}
~shared_ptr()
{
release();
}
};
}
shared_ptr相对于unique_ptr是更为全面的,但是shared_ptr最大的缺陷就是循环引用:
循环引用(weak_ptr)
循环引用是指在编程中,两个或多个对象相互引用(即互相持有对方的shared_ptr对象),形成一个循环的引用链,导致他们的引用计数始终无法归零,从而内存无法被释放造成内存泄漏。如下就是典型的循环应用实例
struct ListNode
{
ListNode()
{
cout << "ListNode()" << endl;
}
int val;
cr_shared::shared_ptr<ListNode> point;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
void test()
{
cr_shared::shared_ptr<ListNode> n1(new ListNode);
cr_shared::shared_ptr<ListNode> n2(new ListNode);
n1->point = n2;
n2->point = n1;
}
int main()
{
test();
return 0;
}
在该种情形时可以分析出:n1->point=n2,这段代码使得n2的引用记数pcount的值为2,而n2->pre=n1,使得n1的引用记数也为2,而且当该栈帧空间销毁时会先析构n2这块空间,再析构n1这块空间:
当析构n2时会先进行引用计数--,并不会根据定制删除器进行delete释放ListNode这块空间。因为引用计数的数量是2。而后续析构n1时也是一样进行引用计数--,然后就没有后续处理了,因为point对象是ListNode内部成员,也就是说只有当ListNode这块空间被delete释放时才会真正的进行释放内部自定义的成员。
其实就是n1和n2都在等待对方的空间资源释放了以后才可以释放自己所占有的资源,因为n1和n2内部都有着对方的引用(循环引用)。而最终这总情况就相当于双方都少调用了一次析构函数,从未无法使得引用计数置0。
而std::weak_ptr就是一种专门用于解决shared_ptr循环引用无法释放资源的问题。其本质就是当多个对象指向同一空间时不会增加引用计数(其实就是相当于是原生指针的作用,只不过进行封装,使其可以根据shared_ptr进行构造赋值并且不支持RAII不用进行析构释放资源)
// 简化版的weak_ptr
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
此时的_next和_pre 的指针类型是weak_ptr的类型就可以很好的解决shared_ptr的循环引用问题: