👍作者主页:进击的1++
🤩 专栏链接:【1++的C++进阶】
文章目录
- 一,什么是智能指针
- 二,为什么需要智能指针
- 三,智能指针的发展
一,什么是智能指针
要了解智能指针,我们先要了解RAII.
RAII是一种利用对象生命周期来控制资源的技术。
在对象初始化时,其接管资源,在对象的生命周期内其管理的资源始终保持有效,最后当对象析构时,释放资源。
那么什么是智能指针呢?
智能指针就是利用了RALL的原理,并且通过封装,使得它的对象能够向指针一样使用。因此其重载了*,->。
下面是一个简单的智能指针代码:
template<class T>
class smart_ptr
{
public:
smart_ptr(T* ptr)
:_ptr(ptr)
{}
~smart_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator ->()
{
return &_ptr;
}
private:
T* _ptr;
};
二,为什么需要智能指针
我们来看一段代码:
double divide(int a, int b)
{
if (b == 0)
throw "除0错误";
else
return a / b;
}
void func()
{
int* arr = new int[10];//申请空间
try
{
int a, b;
cin >> a >> b;
divide(a, b);
}
catch (...)
{
//在这里进行校对后,再抛出
delete[]arr;
cout << "delete[]arr" << endl;
throw;
}
delete []arr;//若出现异常,则不会执行到这里,会造成内存泄漏。
cout << "delete[]arr" << endl;
}
int main()
{
try
{
func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "错误" << endl;
}
return 0;
}
上述代码中,我们为了防止内存泄漏,因此进行了校正,再抛出的操作,这样做相对来说比较麻烦,而且我们在写代码时也是容易忘记校正的操作,这时就需要我们的智能指针了。我们在申请资源的函数内部实例化除一个智能指针的对象,将资源交给它管理,当这个函数调用完成退出后,这个对象也会进行析构,其管理的资源也就释放了。是不是相当方便。
上述提到了内存泄漏,接下来我们就专门来谈一谈内存泄漏。
什么是内存泄漏呢?
内存泄漏是指由于我们的失误,未能释放我们已经不适用的内存,而造成资源的浪费。也可以说是,我们由于设计错误,而对该段内存失去了控制权,进而造成了空间的浪费。
内存泄漏分类:
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak - 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
三,智能指针的发展
在我们前面写的智能指针中我们会发现其不能够拷贝构造和赋值。因为若我们用默认生成的拷贝构造或赋值的话,由于两个指针指向同一块空间,那么这块空间在释放时就会被释放两次而导致错误。
因此在C++98中就有了auto_ptr的出现,它在面对拷贝构造和赋值问题的解决办法是:管理权的转移-----也就是:当通过A构造出或将A赋值给B时,A的管理权转让给B,A不再进行管理。
这样的设计在后来经常为人所诟病。因为其在拷贝或赋值后,原来的对象指向的空间会被置空,也就是失去了管理权。
下面是其实现代码:
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{
}
auto_ptr(auto_ptr& s)
{
_ptr = s._ptr;
s._ptr = nullptr;
}
auto_ptr& operator=(auto_ptr s)
{
if (s._ptr != this->_ptr)
{
delete _ptr;
_ptr = s._ptr;
s._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator ->()
{
return &_ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
};
由于auto_ptrd的缺陷,在C++11中出现了unique_ptr。
unique_ptr的的原理非常粗暴----既然拷贝和赋值是个坑,那我直接禁用你。。。
因此在unique_ptr中,其主要改变就是禁用了拷贝和赋值。
禁用拷贝和赋值有下面几种方法:
- 成员函数私有化。
- 只声明不定义
- 用delete关键字修饰。
下面是unique_ptr的实现:
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{
}
unique_ptr(unique_ptr& s) = delete;
unique_ptr<T>& operator=(unique_ptr s) = delete;
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator ->()
{
return &_ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
};
但是,在有些场景下两个对象管理同一块空间的这种需求还是有的,因此C++11中还出现了更加靠谱的shared_ptr。
其原理就是增加了引用计数。对于同一块资源,每赋值或拷贝一次,计数就加1 。每析构一个对象就减一。直到为0也就是只有一个对象在维护着这块空间时,其就释放这块资源。
下面是shared_ptr的实现:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, count_ptr(new int(1))
{
}
shared_ptr(const shared_ptr& s)
:_ptr(s._ptr)
{
(*s.count_ptr)++;
count_ptr = s.count_ptr;
}
shared_ptr<T>& operator=(const shared_ptr& s)
{
if (_ptr != s._ptr)
{
_ptr = s._ptr;
(*s.count_ptr)++;
count_ptr = s.count_ptr;
}
return *this;
}
~shared_ptr()
{
if (*count_ptr > 1)
{
(*count_ptr)--;
}
else
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()const
{
return _ptr;
}
private:
T* _ptr;
int* count_ptr;
};
但是,shared_ptr也是有一些问题的。
- 线程安全问题----我们在后面的文章会进行讲解。
- 循环引用问题和对象的删除问题
接下来我们讲 2 中的两个问题。
循环引用问题:
我们先来看看什么是循环引用问题:
如图,就是我们的循环引用,其到底会产生什么问题呢?
我们慢慢往下看。
当我们node1,node2析构后,其计数都为1,还并没有释放掉。只有_next释放掉,node2才能释放,_prev释放掉,node1才能释放。而只有node1释放掉_next才能释放,node2释放,_prev才能释放。
这就形成了一个死循环。谁的释放不了。
因此引入了weak_ptr。
其就是用来解决循环引用问题的。
其解决循环引用的原理就是:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr类型。node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
这样在析构node1和node2时,其管理的资源也都进行了释放。
下面是weak_ptr的实现代码:
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{
}
weak_ptr(const shared_ptr<T>& s)
:_ptr(s.get())
{
}
weak_ptr<T>& operator=(const shared_ptr<T>& s)
{
_ptr = s.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
我们再来解决删除的问题:
我们在《内存管理》这篇文章中讲过new 和delete要搭配使用,new …[n]要与delete [] …,malloc与free搭配…
并且我们讲了new和malloc的区别,delete与free的区别。
那么new 与new[] ,delete与delete[]有什么区别呢?
下面我们来用一张图讲解清楚:
可见delete与delete[]区别在于调用析构函数的次数和释放空间的指针的位置。因此,当我们用delete去释放一个本该用delete[]释放的对象时,便会发生错误。
我们也不能去修改库里的析构函数。那我们该怎么解决呢?
C++11中给我们提供了在构造智能指针对象时可以传一个删除器的对象来应对不同方式申请的对象。
下面我们用仿函数的形式进行模拟:
代买实现如下:
template<class T,class D>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, count_ptr(new int(1))
{
cout << "构造" << endl;
}
shared_ptr(const shared_ptr& s)
:_ptr(s._ptr)
{
(*s.count_ptr)++;
count_ptr = s.count_ptr;
cout << "拷贝构造" << endl;
}
shared_ptr<T,D>& operator=(const shared_ptr& s)
{
if (_ptr != s._ptr)
{
_ptr = s._ptr;
(*s.count_ptr)++;
count_ptr = s.count_ptr;
cout << "赋值" << endl;
}
return *this;
}
~shared_ptr()
{
if (*count_ptr > 1)
{
(*count_ptr)--;
cout << "count--" << endl;
}
else
{
D()(_ptr);
//cout << "delete" << endl;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()const
{
return _ptr;
}
private:
T* _ptr;
int* count_ptr;
};
删除器
template<class T>
class Free
{
public:
void operator()(T* ptr)
{
cout << "Free" << endl;
free(ptr);
}
};
template<class T>
class Delete
{
public:
void operator()(T* ptr)
{
cout << "Delete" << endl;
delete ptr;
}
};
template<class T>
class Delete_
{
public:
void operator()(T* ptr)
{
cout << "Delete_" << endl;
delete [] ptr;
}
};