内存泄漏可以分为两类:
智能指针可以解决内存泄漏的问题,它相对于一种预防手段。因为C++没有回收机制嘛,所以内存泄漏的问题,解决起来十分困难。
比如:你虽然严格按照 new delete ,malloc free 这样写代码。但是 如果程序中途抛异常 有可能就会跳过 你写的 delete 或是 free ,这很难受。造成了内存泄漏。
怎么说呢,内存泄漏是很危险的,尤其是那种长时间运行的程序。一旦出现内存泄漏,会导致程序越来越卡,甚至导致服务器 宕机。所以 程序员再处理内存,指针之类的 都格外小心。有没有一种机制,可以帮助我们 减轻些负担呢?那就是智能指针。
智能指针利用的是,RALL技术:利用对象的生命周期来控制程序资源。
简单来说:构造类对象,会自动调用构造函数;对象 销毁时,会自动调用对象的析构函数。利用类对象这一特性,就不需要我们手动的释放内存空间。
对于这个大家基本上都懂,但是我也用代码演示一下:
#include
using namespace std;class A
{
private:int _a;
public:A(int a = 0):_a(a){cout << "构造:A()" << endl;}~A(){cout << "析构:~A()" << endl;}
};int main()
{A a;return 0;
}
构造一个A类对象a,我们来看程序运行结果:

嗯,那么我们来实现一个简易版本的智能指针:
template
class SmartPtr
{
private:T* _ptr;
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr){delete _ptr;cout << "delete _ptr" << endl;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
};
智能指针原理总结:
我们来简单的使用一下,上面的智能指针:
int* iptr = new int(2);SmartPtr sptr(iptr);*sptr = 10;
来看程序的运行:

可以看到,是自动释放new 出来的 空间的。
C++11之前是有一个:
但由于被喷的惨,所以基本没人用。
C++11 更新后,新给出了三类智能指针:
下面我会一 一 介绍,
我们上面不是写过一个简易版本的智能指针,大家可以再看一下。会发现我没有写拷贝构造和赋值重载。其实智能指针,难点就是这俩。我先用上面的简易智能指针去完成一下拷贝,看看会出现什么问题。
SmartPtr sptr(new int(1));SmartPtrsptr1(sptr);

抛异常了,我们试着,捕获一下:
try {SmartPtr sptr(new int(1));SmartPtrsptr1(sptr);}catch (const exception& e){cout << e.what() << endl;}
这样不能捕获,因为这个异常抛的是我们自定义类型的,所以不好搞。
我直接说原因吧,sptr 赋值给 sptr1 默认的拷贝构造是 浅拷贝,所以导致同一个块资源被释放了 两次。
怎么解决这个问题呢?有多种方式,就这个问题衍生出的多类的智能指针。
auto_ptr是这样解决的:管理权转移的思想,也就是说,这一块资源我交给要拷贝我的人来管理,我自己呢撒手掌柜,不管了。
这是感性的理解,还是代码实现一下:
templateclass auto_ptr{private:T* _ptr;public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}~auto_ptr(){if (_ptr){delete _ptr;}}auto_ptr(auto_ptr& tmp):_ptr(tmp._ptr){tmp._ptr = nullptr;}auto_ptr& operator=(auto_ptr &tmp){if (_ptr){delete _ptr;}_ptr = tmp._ptr;tmp._ptr = nullptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}};
关键是这一步:

给段代码,通过调试帮助大家理解:
ly::auto_ptr aptr(new int(1));ly::auto_ptr aptr1(aptr);ly::auto_ptr aptr2(new int(2));aptr2 = aptr1;



但是有一个明显的缺陷,那就是拷贝构造,完成了权限转移,直接把我原来管理的资源置为空。虽然我不再管理了,直接把置空了,那么我很难受。我虽然没有权力去释放这块资源,但是 我连访问都成问题了。这样做是不是有点太绝了。
比如:
ly::auto_ptr aptr(new int(1));ly::auto_ptr aptr1(aptr);cout << *aptr << endl;
现在就变成了堆空指针的解引用,必然抛异常。所以auto_ptr 这样的做法有点太 一刀两断了。我被别人拷贝了,把管理权交出去了,没有权利去释放资源,但是不能直接把我置空,导致我 无法访问资源。
综上:auto_ptr 用的少,但是 前人踩坑,后人才能避坑。
怎么说呢,unique_ptr更加暴力,直接就是不允许发生智能指针的拷贝和赋值。呵呵,很强势,当然很简单,我直接实现一下:
templateclass unique_ptr{private:T* _ptr;public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}~unique_ptr(){if (_ptr){delete _ptr;}}unique_ptr(unique_ptr& tmp) = delete;unique_ptr& operator=(unique_ptr& tmp) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}};
}
就是将 拷贝构造和赋值重载 delete 关键字修饰一下。
这个好理解对吧。不过多赘述了哈。要提一点就是:

库里面有第二个模板参数,这个模板参数是指定删除。放后面讲。
这才是 真正意义上支持拷贝和赋值额 智能指针。利用的是引用计数的思想,也就是说 类中有用于保存 指向此块资源对象的个数,等指向此资源的对象只剩下一个时,如果要析构才会释放资源,其余情况 都是 指向资源个数 减一。
图解:



简易实现:
我们先来实现一个简易版本的,不考虑线程安全问题,那么想让 所有对象 共用一份 count 计数,有几种方式呢?
我给出两种:
那么 简易点就是 第一种方式嘛:
templateclass shared_ptr{public:static int _count;T* _ptr;public:shared_ptr(T* ptr =nullptr):_ptr(ptr){_count = 1;}~shared_ptr(){if (--_count == 0 && _ptr){delete _ptr;}}shared_ptr(shared_ptr& tmp){// 防止自己赋值给自己if (_ptr != tmp._ptr){_ptr = tmp._ptr;_count = tmp._count;_count++;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}};templateint shared_ptr::_count = 0;
这就是支持拷贝构造,不考虑线程安全的版本,很简单哈。
其实线程安全问题的解决无非就是 加个锁。 多个线程 对 count进行 ++ - - 操作,这里是线程不安全的,所以 对count 操作的地方,都需要加锁。
templateclass shared_ptr{private:T* _ptr;int* _count;mutex* _mutex;public:shared_ptr(T* ptr=nullptr):_ptr(ptr),_count(new int(1)),_mutex(new mutex) {}~shared_ptr(){release();}shared_ptr(shared_ptr& tmp){if (_ptr != tmp._ptr){_ptr = tmp._ptr;_count = tmp._count;_mutex = tmp._mutex;tmp.addcount();}}shared_ptr& operator= (shared_ptr& tmp){if (_ptr != tmp._ptr){release();_ptr = tmp._ptr;_count = tmp._count;_mutex = tmp._mutex;tmp.addcount();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}int get_count(){return *_count;}void release(){_mutex->lock();bool flag = false;if (--(*_count) == 0&&_ptr){delete _ptr;delete _count;flag = true;}_mutex->unlock();if (flag == true){delete _mutex;}}void addcount(){_mutex->lock();(*_count)++;_mutex->unlock();}};
测试代码:
shared_ptr sptr(new int(1));shared_ptr sptr1(sptr);shared_ptr sptr2(new int(2));sptr2 = sptr1;
shared_ptr 其实还有一个问题需要格外注意循环引用。循环引用会导致,本该释放的资源,得不到释放,也就是说 count 加多了,这解释有点牵强,大家一会看图理解:
首先给出一个例子,链表的节点:

节点里面的指针,我们可以用智能指针嘛?试着用一下,因为 智能指针 还是有好处的,它可以预防 内存泄漏 对吧,但是这里会出现 循环引用的问题:
struct Node
{int val;ly::shared_ptr _next;ly::shared_ptr _prev;~Node(){cout << "~Node" << endl;}
};
假如我这样使用节点:
shared_ptr n1(new Node);shared_ptr n2(new Node);cout<
看结果,是对的:

假如我让它俩互相指向呢?
shared_ptr n1(new Node);shared_ptr n2(new Node);cout<_next = n2;n2->_prev = n1;cout << n1.get_count() << endl;cout << n2.get_count() << endl;

发现尽然没有析构,没释放资源。程序都退出了,这就是内存泄漏。
造成这个的原因,我们来分析一下:
刚开始 没问题:

但是由于n1->_next = n2; n2->_prev = n1; 所以 count ++了:

如果进行析构,那么 就是 count – ,它减完之后,变为1 ,所以 不会进行 资源释放。
其实问题已经分析出来了,count 如果不 ++ 那么 资源还能够释放,因为互相指向,所以它俩的count 都 ++了。那么有没有解决办法呢?那就是 std::weak_ptr ,它呢,就是 不参与 资源管理,虽然指向了某块资源,但是 count 不会 ++。
weak_ptr 其实就是 专门用于 解决 shared_ptr 中循环指向的问题的。
废话不多说,直接就是 将节点中的 shared_ptr 换成 weak_ptr 就可以了:
在这呢 先使用 标准库中的 shared_ptr 和 weak_ptr ,之后再模拟实现weak_ptr,
struct Node
{int val;std::weak_ptr _next;std::weak_ptr _prev;~Node(){cout << "~Node" << endl;}
};int main()
{std::shared_ptr n1(new Node);std::shared_ptr n2(new Node);cout<_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}
看运行结果,很明显完成了资源释放:

而且 发现 count的值 没有变成 2,原因很简单 weak_ptr 智能指针不参与 资源管理。
那么 我们来模拟实现一下,weak_ptr ,很简单:
templateclass weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr& sp):_ptr(sp.get()){}weak_ptr& operator=(const shared_ptr& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
对吧,就是不让它参与资源管理就行,唯一需要注意的就是它的拷贝构造,可以是shared_ptr 也可以是 weak_ptr 。

像上面所有的智能指针模拟实现都是 new 和delete ;但是 还有别的情况 比如 delete [] , free ,fclose() 。对吧,所以呢 智能指针 提供了 定制删除,默认情况下是 delete 。

看 默认参数D 是 default_delete< T >:

看到了吧,这是啥?仿函数呀,昂,可以 。我们一会 来模拟实现一下 free 版本的
但是 shared_ptr 中 定制删除 不是给的模板参数,而是 在构造函数重载中的一个:

所以定制删除在智能指针中的使用,要自己去标准库中查看,但定制删除一般都是是仿函数。区别就是 模板参数 给的是类型,构造函数中 给的是 对象。
但是 定制删除 如果是在 sharde_ptr中给的不就是个对象嘛,所以也可以给 lambda表达式。
所以 我们先来 给出 两个定制删除器:
template
struct DeleteArray
{void operator()(const T* ptr){delete[] ptr;}
};
struct DeleteFile
{void operator()(FILE* ptr){fclose(ptr);}
};
使用起来也很简单:
std::unique_ptr> up2(new A[10]);
std::unique_ptr up3(fopen("test.txt", "w"));
std::shared_ptr sp2(new A[10], DeleteArray());
std::shared_ptr sp3(fopen("test.txt", "w"), DeleteFile());