平常我们在使用指针的时候,如果使用完没有释放会造成内存泄漏,为了解决这个问题,就出现了智能指针。当然,你说自己注意点保证每次就会触发析构函数删除指针不会导致内存泄漏也可以,这也是基于RAII的思想,而智能指针同样也是从此演变而来。
我看了一篇写的非常好的文章,从底层开始讲解,于是我决定参照这篇文章并加上一些自己的想法,把智能指针的源码也摆上来一同比较。
文章链接:C++智能指针_小倪同学 -_-的博客-CSDN博客_c++ 智能指针
内存泄漏的危害
基类指针指向派生类,而析构函数并非虚函数的话,基类的析构函数不会执行,也就是基类的空间没有被释放,导致了内存泄漏。内存泄漏是指针占用的空间没有被释放,并不是说内存出现了问题,只是内存被垃圾占用了。
而在需要长期执行的程序中,未释放的指针占用的空间会越来越大,最后会导致内存不够用直至程序崩溃。
五大内存存储区域
(1)栈区(stack)
由系统自动分配,如int a=10;
生命周期结束时系统也会自动回收
(2)堆区(heap)
需要自己申请并释放,C++中是使用new,如int *a=new int(10);
注意:a这个指针是放在栈区的,它指向的区域是堆区。而且堆区分配的内存一定要注意需要自己释放!!!如若没有自己释放只会在程序结束的时候才会释放,如程序需要长期运行很容易导致崩溃。
(3)全局区(静态区)
全局区和静态区是放在同一个区域内的
(4)文字常量区
常量字符串的放置区域
(5)程序代码区
存放函数体的二进制代码,不用管
#include <iostream>
using namespace std;
int a = 10; //全局区
char *p1 = nullptr; //全局区
int main(){
int b; //栈区
char s[] = "abc"; //栈区
char *p2; //栈区
char *p3 = "123456"; //123456\0在文字常量区,p3在栈上。
static int c = 0; //全局(静态)区
p1 = new char(10); //p1在栈上,但是指向的内存是在堆区
int *d = new int(2); //同上,d指针保存在栈上,但是指向的内存在堆区
return 0;
}
RAII
RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。
参考博客中写到了符合RAII的指针,并重载了*解引用和->访问内部属性,也就是说智能指针有是符合RAII的指针。
智能指针
智能指针目前存在:auto_ptr(别用,时代的眼泪,稍后会介绍),weak_ptr,shared_ptr,unique_ptr
(1)auto_ptr(基本弃用)
auto_ptr是在c98出现的,弃用的原因是会出现一些奇怪的问题,给个例子就明白了:
#include <iostream>
#include <memory>
using namespace std;
class Date
{
public:
Date() { cout << "Date()" << endl; }
~Date() { cout << "~Date()" << endl; }
int _year;
int _month;
int _day;
};
int main() {
auto_ptr<Date> ap(new Date);
auto_ptr<Date> aa(ap);
// auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
ap->_year = 2018;
system("pause");
return 0;
}
拷贝完成以后,被拷贝的对象为empty了。这跟我们的原意不太符合,设计者可能是想避免野指针的出现(两指针指向同一块区域,一指针释放了,另一指针就成了野指针,指向的区域数据是乱码),所以不允许两个指针指向同一块区域。
为什么会造成这种现象我们可以看看源码就明白了
在构造函数中,新指针实现了拷贝,但是马上被拷贝的指针就被释放了。所以auto_ptr现实使用中是非常少的。
(2)unique_ptr(唯一指针)
与auto_ptr差不多,都是资源独占的指针,只不过unique_ptr是直接禁止拷贝,而非将被拷贝者删除,看看模拟:
template<class T>
class unique_ptr
{
public:
unique_ptr(T * ptr = nullptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
cout << _ptr << endl;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 防拷贝
unique_ptr(unique_ptr<T> const &) = delete;
unique_ptr & operator=(unique_ptr<T> const &) = delete;
private:
T * _ptr;
};
=delete的用法就是禁止调用此函数,直接把拷贝的手段给禁用了,使用会报错。看看源码也是一样把这两个拷贝手段禁用了。
当然也有存在例外,就是拷贝一个即将被销毁的指针如return unique_ptr<int>(new int(1))和局部对象的拷贝。
(3)shared_ptr(支持拷贝)
shared_ptr是一个可以共享资源的指针,原理就是在创建的时候会使用一个连接数来记录当前有多少指针指向这块区域,释放的时候连接数-1,当连接数为0的才会释放这块内存中的数据。来看模拟:
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
{}
~SharedPtr()
{
Release();
}
SharedPtr(const SharedPtr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
{
AddRefCount();
}
// 赋值
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
// 释放管理的旧资源
Release();
// 共享管理新对象的资源,并增加引用计数
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
AddRefCount();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int UseCount()
{
return *_pRefCount;
}
T* Get()
{
return _ptr;
}
void AddRefCount()
{
// 加锁或者使用加1的原子操作
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
private:
void Release()
{
bool deleteflag = false;
// 引用计数减1,如果减到0,则释放资源
_pMutex->lock();
if (--(*_pRefCount) == 0)
{
delete _ptr;
delete _pRefCount;
deleteflag = true;
}
_pMutex->unlock();
if (deleteflag == true)
delete _pMutex;
}
private:
int* _pRefCount; // 引用计数
T* _ptr; // 指向管理资源的指针
mutex* _pMutex; // 互斥锁
};
源码写到了一个计数器类,而这个计数器又包含了weak_ptr的计数,多个模块糅杂在一起,导致阅读起来有着一定的难度,我们就看看模拟的实现就好了。
shared_ptr有一个缺陷就是循环依赖的时候计数器会正常增加变成2,但是析构却只会触发一次计数器只会到1,并不会释放内存数据,同样造成了内存泄漏。如:
class Node {
public:
Node() { ; }
int value;
shared_ptr<Node> next;
~Node() {
cout << "aaa" << endl;
}
};
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
cout << sp1.use_count() << " " << sp2.use_count() << endl;
sp1->next = sp2;//屏蔽掉之后会正常运行析构函数
sp2->next = sp1;//
cout << sp1.use_count() << " " << sp2.use_count() << endl;
//运行结果为:
1 1
2 2
//将那两行屏蔽掉之后运行结果为:
1 1
1 1
aaa //析构函数
aaa
可以看见,将两个指针互相依赖,指向对方的时候,计数器会出现问题,最后析构函数并不会触发。因为指针互相依赖时,在程序结束的时候,各自的计数器会减1,没有达到0就不会触发各自的析构函数。
(4)weak_ptr(弱引用指针)
weak_ptr是为了辅助shared_ptr而存在的,它只提供了访问手段,而没有掌控内存生命周期权,所以称为弱引用指针。一般的实际使用场景中是一个shared_ptr指针配合多个weak_ptr指针一起使用。看看模拟:
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(SharedPtr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
{}
weak_ptr(weak_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
{}
weak_ptr<T>& operator=(SharedPtr<T>& sp)
{
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
return *this;
}
weak_ptr<T>& operator=(weak_ptr<T>& sp)
{
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
return *this;
}
private:
T* _ptr;
int* _pRefCount;
};
使用weak_ptr解决问题:
class Node {
public:
Node() { ; }
int value;
weak_ptr<Node> next; //只需要修改这一条
~Node() {
cout << "aaa" << endl;
}
};
仅仅只需要将Node内的shared_ptr改成weak_ptr即可解决。因为weak_ptr计数器的增加并不会影响到shared_ptr计数器的增加,所以强引用指针的计数器并不会错乱。而weak_ptr的构造和析构也能正常执行,最后shared_ptr和weak_ptr的计数器同时为0的时候才会对内存数据进行释放。