前言:在C++11里面提出了一个新的语法 try catch用来捕捉异常,这样子能不使用return和exit的前提下退出程序就得到错误信息,但是随之而来的就是一个新的问题,try catch退出程序之后可能带来了无法释放的内存泄露问题,原因是try catch是跳跃式捕捉的。
目录
一,try catch带来的内存泄漏问题
二,智能指针
1,auto_ptr
1)简单使用
2)源码模拟
3)auto_ptr的缺陷
2,unique_ptr
1)引言
2)源码模拟
4,weak_ptr
一,try catch带来的内存泄漏问题
大家先看一段代码
class A {
public:
A() {
d = new int(666);
}
~A() {
cout << "delete d" << endl;
delete d;
}
private:
int* d;
};
void test02() {
int* a = new int(10);
int* b = new int(0);
A d;
if (*b == 0)
throw "除零错误";
int c = *a / *b;
cout << "delete a"<<endl;
delete a;
cout << "delete b" << endl;
delete b;
}
int main() {
try {
test02();
}
catch(const char* s){
cout << s << endl;
}
return 0;
}
大家有没有想到这段简单的代码有致命的错误,没错就是内存泄漏,但是里面有类开辟的靠近,也有函数自己手动开辟的空间,到底哪些空间没有被释放呢?
答案是类会被调用析构函数释放空间,而函数自己开辟的空间无法被释放,因为代码全部被跳过不执行了,这样子在我们编写大型程序时,如果不加以约束和处理,内存泄漏将会是巨大的问题,那么我们有没有解决办法呢?
二,智能指针
答案是有解决办法,我们发现虽然函数的代码段被跳过了,但是创建的类的析构函数还是会被调用,那么我们如果利用一个类来帮助我们自动管理这些开辟的空间不就可以避免内存泄漏了吗?有了思路现在我们开始本文正片——智能指针。
1,auto_ptr
1)简单使用
这个auto_ptr是C++98提出来的,但是它有一个比较致命缺陷,C++11官方也提出了解决办法,我们这里先不讲他的缺陷,大家先看它的用法及原理,后面我会引导大家理解它的缺陷。
//简单使用auto_ptr
auto_ptr<int> a ( new int(10));
cout << *a;
除此之外auto_ptr还支持,一些运算符重载
大家想要仔细研究可以打开
auto_ptr - C++ Referencehttps://legacy.cplusplus.com/reference/memory/auto_ptr/?kw=auto_ptr
2)源码模拟
如果让我们写一个auto_ptr,我们该如何下手呢?首先auto_ptr本质就是一个类容器,我们利用模板,就能实现识别指针应该是什么类型,然后我们在里面重新定义一个指针,指向传来开辟的空间不就行了吗?
从这个思路出发,我们先写出类的基本框架
namespace bit {
template<class P>
class auto_ptr {
public:
auto_ptr(P* p) {
this->p = p;
}
~auto_ptr() {
delete p;
p = nullptr;
}
private:
P* p;
};
};
上面的代码不就实现了一个类自动管理指针开辟的空间了吗?
至于里面的一些函数功能,相信学到智能指针这块的我们早已经轻车熟路了,如果实在不懂可以参考我往期博客STL源码刨析。
namespace bit {
//auto_ptr没有解决复制拷贝的问题
//当拷贝的时候,被拷贝的auto_ptr的管理权就丧失了,其管理的资源置为了空,
//并且auto_ptr无法管理数组
template<class P>
class auto_ptr {
public:
auto_ptr(P* p) {
this->p = p;
}
auto_ptr(bit::auto_ptr<P>& a) {
p = a.p;
a.get() = nullptr;
}
P* get() {
return p;
}
P* operator->() {
return p;
}
P& operator*() {
return *p;
}
auto_ptr& operator=(auto_ptr a) {
p = a.get();
a.get() = nullptr;
return this;
}
~auto_ptr() {
delete p;
p = nullptr;
}
private:
P* p;
};
};
3)auto_ptr的缺陷
auto_ptr有什么缺陷呢?答案是=重载和拷贝构造,大家先看我运行一段代码及运行结果
这是为什么呢?因为auto_ptr不支持多个智能指针指向同一块空间,因此当我们访问被赋值或者被拷贝构造的原指针就会出现报错,因为赋值或者拷贝构造完成后原指针指向空间会被变成nullptr,这就带来了一个问题,如果我们在接下来的代码里面一旦不小心访问到了原指针就会导致程序报错崩溃。
很多人就想说了,我直接多个指针指向同一块空间不就行了?但是又因此衍生出来了一个问题,那就是析构函数的时候,空间只能释放一次,但是这么多auto_ptr指向这块空间该由谁来析构呢?大家不用担心,我会在shared_ptr讲解决方案的。
2,unique_ptr
unique_ptr作为auto_ptr的一个优化版本,它的解决办法堪称简单粗暴,既然你的拷贝构造和赋值有问题,那我直接把它们定义成私有(相当于delete,外部无法调用,自然相当于被禁止了)不准你们使用不就行了吗?使用方法和auto_ptr差不多,我们就直接看源码模拟吧
namespace bite
{
template<class T>
class unique_ptr
{
// RAII
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
delete _ptr;
}
// 具有指针类似行为
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 防止被拷贝--禁止调用拷贝构造&赋值运算符重载
#if 0
// C++98:只声明不定义 & private
private:
unique_ptr(const unique_ptr<T>& up);
unique_ptr<T>& operator=(const unique_ptr<T>& up);
#endif
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up)=delete;
protected:
T* _ptr;
};
}
3,shared_ptr
1)引言
unique_ptr终归有点简单粗暴了,我们还是有一些多个指针指向同一块空间的使用场景,那我们该如何处理这一块空间呢?
这就不得不涉及到一个巧妙的解决方案了,所有指向同一块空间的智能指针里面都存储一个指针,这个指针里面存储指向这块空间的人数,当人数减为0的时候析构函数才真正的释放资源,否则就减减,这样子就能保证空间一定是最后一个使用的人释放,不会出现同一块空间被多次释放的问题了。
那么话不多说,我们直接开始源码模拟吧。
2)源码模拟
首先我们讲解赋值拷贝
我们只需要将count++即可
template<class T>
class shared_ptr {
public:
shared_ptr(shared_ptr<T>& s) {
data = s.data;
count = s.count;
(*count)++;
}
private:
int* count;
T* data;
};
=号重载有一个小坑,那就是原本的shared_ptr已经指向一块空间了,我们不能想赋值构造那样子无脑的赋值,我们需要先把原本指向的空间进行count--,如果count--之后等于0,那么我们就必须将空间释放再赋值。
shared_ptr& operator=(const shared_ptr<T>& s) {
if (data != s.data) {
if (-- * count == 0) {
delete data;
}
(*s.count)++;
data = s.get();
count = s.count;
}
return *this;
}
其他的也就一个析构函数再谈一下吧,析构函数需要检查count,判断释放需要释放资源。
~shared_ptr() {
if (--*count == 0) {
delete data;
delete count;
cout << "delete" << endl;
}
}
完整源码
template<class T>
class shared_ptr {
public:
shared_ptr(shared_ptr<T>& s) {
data = s.data;
count = s.count;
(*count)++;
}
shared_ptr(T* s=nullptr) {
data = s;
count = new int{ 1 };
}
T& operator*() {
return *data;
}
T* operator->() {
return data;
}
shared_ptr& operator=(const shared_ptr<T>& s) {
if (data != s.data) {
if (-- * count == 0) {
delete data;
cout << "delete" << endl;
}
(*s.count)++;
data = s.get();
count = s.count;
}
return *this;
}
T* get() {
return data;
}
~shared_ptr() {
if (--*count == 0) {
delete data;
delete count;
cout << "delete" << endl;
}
}
private:
int* count;
T* data;
};
3)shared_ptr的缺陷
大家看上面的shared_ptr是不是很好用,代码应该也没有错误,答案是否,大家先看一段shared_ptr经典内存泄漏代码。
struct list {
shared_ptr<list> prv;
shared_ptr<list> next;
};
shared_ptr<list> head(new list), l1(new list);
head->next = l1;
l1->prv = head;
为什么说这段代码会导致内存泄漏呢?
首先我们知道类的析构函数调用顺序,首先是调用本身的析构函数,然后调用类里面的成员类的析构函数,这样子导致了一个问题,当head调用析构函数的时候,它的count为2,减减之后为1,无法正常析构,为什么无法正常析构呢?因为head和l1d空间都是new出来的,是无法主动调用类里面的子类的析构函数将count变为0释放空间,导致最后两个count都是1,空间任然没有被正常释放,那有什么解决办法吗?且看下文讲解
4,weak_ptr
上面说了shared_ptr由于循坏引用导致死循坏,这个时候weak_ptr就应运而生了,weak_ptr也是一种智能指针,而且和shared_ptr能够相互配合使用(限用于循坏引用等情况),它作为shared_ptr的附庸存在,不能单独使用,可能会导致空间资源被多次释放。
这是怎么实现解决循坏引用的问题呢?答案是很简单,虽然shared_ptr能和weak_ptr配合使用,但是weak_ptr和shared_ptr指向同一块空间,weak_ptr并不会引起count的大小,这样子就完美解决了循坏引用的问题。
struct list {
weak_ptr<list> prv;
weak_ptr<list> next;
};
shared_ptr<list> head(new list), l1(new list);
head->next = l1;
l1->prv = head;
源码模拟并不困难,在shared_pt的构造函数添加一个weak_ptr的构造函数,在weak_ptr的构造函数里面加上一个shared_ptr的构造函数就行了。
shared_ptr::shared_ptr - C++ Referencehttps://legacy.cplusplus.com/reference/memory/shared_ptr/shared_ptr/
weak_ptr::weak_ptr - C++ Referencehttps://legacy.cplusplus.com/reference/memory/weak_ptr/weak_ptr/
这里便不再进行源码模拟,留给大家练手吧。如果大家有所收获希望点赞加收藏。