文章目录
- 1. 智能指针出现的意义
- 1.1 内存泄漏
- 1.2 智能指针初识
- 2. C++标准库中的智能指针
- 2.1 auto_ptr
- 2.2 std::unique_ptr
- 2.3 std::shared_ptr
- 2.4 std::weak_ptr
- 3. 智能指针中的定制删除
前言: 智能指针,它是指针嘛?它是一个类具有指针的功能,我去,那不是还有一个迭代器嘛,迭代器不就是一个类具有指针的功能。注意这俩可不敢混淆。迭代器是自定义对象的指针,可以这么理解,迭代器的出现使得自定义对象,也可以像内置类型一般进行指针操作。那么智能指针的出现,又有什么意义呢?
1. 智能指针出现的意义
1.1 内存泄漏
内存泄漏可以分为两类:
- 堆空间上的内存泄漏:
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用。 - 系统资源中的内存泄漏 :
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
智能指针可以解决内存泄漏的问题,它相对于一种预防手段。因为C++没有回收机制嘛,所以内存泄漏的问题,解决起来十分困难。
比如:你虽然严格按照 new delete ,malloc free 这样写代码。但是 如果程序中途抛异常 有可能就会跳过 你写的 delete 或是 free ,这很难受。造成了内存泄漏。
怎么说呢,内存泄漏是很危险的,尤其是那种长时间运行的程序。一旦出现内存泄漏,会导致程序越来越卡,甚至导致服务器 宕机。所以 程序员再处理内存,指针之类的 都格外小心。有没有一种机制,可以帮助我们 减轻些负担呢?那就是智能指针
。
1.2 智能指针初识
智能指针利用的是,RALL技术:利用对象的生命周期来控制程序资源。
简单来说:构造类对象,会自动调用构造函数;对象 销毁时,会自动调用对象的析构函数。利用类对象这一特性,就不需要我们手动的释放内存空间。
对于这个大家基本上都懂,但是我也用代码演示一下:
#include<iostream>
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 T>
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;
}
};
智能指针原理总结:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为。
我们来简单的使用一下,上面的智能指针:
int* iptr = new int(2);
SmartPtr<int> sptr(iptr);
*sptr = 10;
来看程序的运行:
可以看到,是自动释放new 出来的 空间的。
2. C++标准库中的智能指针
C++11之前是有一个:
- auto_ptr
但由于被喷的惨,所以基本没人用。
C++11 更新后,新给出了三类智能指针:
- std::unique_ptr
- std::shared_ptr
- std::weak_ptr
下面我会一 一 介绍,
2.1 auto_ptr
我们上面不是写过一个简易版本的智能指针,大家可以再看一下。会发现我没有写拷贝构造和赋值重载。其实智能指针,难点就是这俩。我先用上面的简易智能指针去完成一下拷贝,看看会出现什么问题。
SmartPtr<int> sptr(new int(1));
SmartPtr<int>sptr1(sptr);
抛异常了,我们试着,捕获一下:
try
{
SmartPtr<int> sptr(new int(1));
SmartPtr<int>sptr1(sptr);
}
catch (const exception& e)
{
cout << e.what() << endl;
}
这样不能捕获,因为这个异常抛的是我们自定义类型的,所以不好搞。
我直接说原因吧,sptr 赋值给 sptr1 默认的拷贝构造是 浅拷贝,所以导致同一个块资源被释放了 两次。
怎么解决这个问题呢?有多种方式,就这个问题衍生出的多类的智能指针。
auto_ptr是这样解决的:管理权转移的思想,也就是说,这一块资源我交给要拷贝我的人来管理,我自己呢撒手掌柜,不管了。
这是感性的理解,还是代码实现一下:
template<class T>
class auto_ptr
{
private:
T* _ptr;
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
auto_ptr(auto_ptr<T>& tmp)
:_ptr(tmp._ptr)
{
tmp._ptr = nullptr;
}
auto_ptr& operator=(auto_ptr<T> &tmp)
{
if (_ptr)
{
delete _ptr;
}
_ptr = tmp._ptr;
tmp._ptr = nullptr;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
};
关键是这一步:
给段代码,通过调试帮助大家理解:
ly::auto_ptr<int> aptr(new int(1));
ly::auto_ptr<int> aptr1(aptr);
ly::auto_ptr<int> aptr2(new int(2));
aptr2 = aptr1;
但是有一个明显的缺陷,那就是拷贝构造,完成了权限转移,直接把我原来管理的资源置为空。虽然我不再管理了,直接把置空了,那么我很难受。我虽然没有权力去释放这块资源,但是 我连访问都成问题了。这样做是不是有点太绝了。
比如:
ly::auto_ptr<int> aptr(new int(1));
ly::auto_ptr<int> aptr1(aptr);
cout << *aptr << endl;
现在就变成了堆空指针的解引用,必然抛异常。所以auto_ptr 这样的做法有点太 一刀两断了。我被别人拷贝了,把管理权交出去了,没有权利去释放资源,但是不能直接把我置空,导致我 无法访问资源。
综上:auto_ptr 用的少,但是 前人踩坑,后人才能避坑。
2.2 std::unique_ptr
怎么说呢,unique_ptr更加暴力,直接就是不允许发生智能指针的拷贝和赋值。呵呵,很强势,当然很简单,我直接实现一下:
template<class T>
class unique_ptr
{
private:
T* _ptr;
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
unique_ptr(unique_ptr<T>& tmp) = delete;
unique_ptr& operator=(unique_ptr<T>& tmp) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
};
}
就是将 拷贝构造和赋值重载 delete 关键字修饰一下。
这个好理解对吧。不过多赘述了哈。要提一点就是:
库里面有第二个模板参数,这个模板参数是指定删除。放后面讲。
2.3 std::shared_ptr
这才是 真正意义上支持拷贝和赋值额 智能指针。利用的是引用计数的思想,也就是说 类中有用于保存 指向此块资源对象的个数,等指向此资源的对象只剩下一个时,如果要析构才会释放资源,其余情况 都是 指向资源个数 减一。
图解:
简易实现:
我们先来实现一个简易版本的,不考虑线程安全问题,那么想让 所有对象 共用一份 count 计数,有几种方式呢?
我给出两种:
- 将引用计数设置为静态成员变量
- 使用指针,引用计数 是堆上 开辟的,所有对象都可以通过指针,来访问同一块堆上空间
那么 简易点就是 第一种方式嘛:
template<class T>
class 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;
}
};
template<class T>
int shared_ptr<T>::_count = 0;
这就是支持拷贝构造,不考虑线程安全的版本,很简单哈。
其实线程安全问题的解决无非就是 加个锁。 多个线程 对 count进行 ++ - - 操作,这里是线程不安全的,所以 对count 操作的地方,都需要加锁。
template<class T>
class 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<T>& 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<int> sptr(new int(1));
shared_ptr<int> sptr1(sptr);
shared_ptr<int> sptr2(new int(2));
sptr2 = sptr1;
shared_ptr 其实还有一个问题需要格外注意循环引用
。循环引用会导致,本该释放的资源,得不到释放,也就是说 count 加多了,这解释有点牵强,大家一会看图理解:
首先给出一个例子,链表的节点:
节点里面的指针,我们可以用智能指针嘛?试着用一下,因为 智能指针 还是有好处的,它可以预防 内存泄漏 对吧,但是这里会出现 循环引用的问题:
struct Node
{
int val;
ly::shared_ptr<Node> _next;
ly::shared_ptr<Node> _prev;
~Node()
{
cout << "~Node" << endl;
}
};
假如我这样使用节点:
shared_ptr<Node> n1(new Node);
shared_ptr<Node> n2(new Node);
cout<<n1.get_count()<<endl;
cout << n2.get_count() << endl;
看结果,是对的:
假如我让它俩互相指向呢?
shared_ptr<Node> n1(new Node);
shared_ptr<Node> n2(new Node);
cout<<n1.get_count()<<endl;
cout << n2.get_count() << endl;
n1->_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 不会 ++。
2.4 std::weak_ptr
weak_ptr 其实就是 专门用于 解决 shared_ptr 中循环指向的问题的。
废话不多说,直接就是 将节点中的 shared_ptr 换成 weak_ptr 就可以了:
在这呢 先使用 标准库中的 shared_ptr 和 weak_ptr ,之后再模拟实现weak_ptr,
struct Node
{
int val;
std::weak_ptr<Node> _next;
std::weak_ptr<Node> _prev;
~Node()
{
cout << "~Node" << endl;
}
};
int main()
{
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
cout<<n1.use_count()<<endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
看运行结果,很明显完成了资源释放:
而且 发现 count的值 没有变成 2,原因很简单 weak_ptr 智能指针不参与 资源管理。
那么 我们来模拟实现一下,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;
};
对吧,就是不让它参与资源管理就行,唯一需要注意的就是它的拷贝构造,可以是shared_ptr 也可以是 weak_ptr 。
3. 智能指针中的定制删除
像上面所有的智能指针模拟实现都是 new 和delete ;但是 还有别的情况 比如 delete [] , free ,fclose() 。对吧,所以呢 智能指针 提供了 定制删除,默认情况下是 delete 。
看 默认参数D 是 default_delete< T >:
看到了吧,这是啥?仿函数呀,昂,可以 。我们一会 来模拟实现一下 free 版本的
但是 shared_ptr 中 定制删除 不是给的模板参数,而是 在构造函数重载中的一个:
所以定制删除在智能指针中的使用,要自己去标准库中查看,但定制删除一般都是是仿函数。区别就是 模板参数 给的是类型,构造函数中 给的是 对象。
但是 定制删除 如果是在 sharde_ptr中给的不就是个对象嘛,所以也可以给 lambda表达式。
所以 我们先来 给出 两个定制删除器:
- delete []
template<class T>
struct DeleteArray
{
void operator()(const T* ptr)
{
delete[] ptr;
}
};
- fclose()
struct DeleteFile
{
void operator()(FILE* ptr)
{
fclose(ptr);
}
};
使用起来也很简单:
std::unique_ptr<A, DeleteArray<A>> up2(new A[10]);
std::unique_ptr<FILE, DeleteFile> up3(fopen("test.txt", "w"));
std::shared_ptr<A> sp2(new A[10], DeleteArray<A>());
std::shared_ptr<FILE> sp3(fopen("test.txt", "w"), DeleteFile());