基于RAII的智能指针原理和模拟实现智能指针
基于RAII的智能指针原理和模拟实现智能指针
- 为什么需要智能指针
- 智能指针的使用及原理
- 库里的智能指针
- c++98的std::auto_ptr
- c++11的std::unique_ptr
- c++11的std::shared_ptr
- 引用计数的原理
- 循环引用的缺陷
- c++11的std::weak_ptr
- c++11和boost中智能指针的关系
为什么需要智能指针
下面我们先分析一下下面这段程序有没有什么内存方面的问题。
int div() {int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func() {// 这里存在安全隐患int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}int main() {try {Func();}catch (exception& e) {cout << e.what() << endl;}return 0;
}
在函数Func
中:
- 如果
p1
这里new
抛异常,由于p1
和p2
都未被成功分配,因此没有内存泄漏。 - 如果
p2
这里new
抛异常,由于p1
已经分配但未被释放,会导致p1
指向的内存泄漏。 - 如果
div
调用这里又会抛异常,由于p1
和p2
已经成功分配,这会导致p1
和p2
指向的内存泄漏。
关于内存泄漏,详见c++内存管理和new、delete的使用-CSDN博客。
这也是异常无法很好处理的资源管理问题。所以就有人想,若这个指针和类一样,自己申请资源后,即使用户忘记释放申请的资源,自身的析构函数也会自主释放资源。于是就诞生了智能指针这个概念。
Java实现有垃圾回收机制。大致原理:
Java虚拟机会将所有使用的资源记录下来,之后设计某种周期性检查资源是否有在使用的程序,检查到没有使用的资源就进行释放。
这也是Java学会怎么用很容易,但熟练掌握很难的原因。Java会用只需会用接口就行,就像C++会用各种函数和STL的工具就行,但熟练掌握需要了解垃圾回收(Garbage Collection, GC)等各种复杂的机制。
C++ 是追求效率的语言,因此不可能有这种垃圾回收机制。这种机制会随时占用一部分资源来运行这个回收机制。这也是为什么很多游戏的底层用C++实现,而不是Java。
智能指针的使用及原理
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
-
不需要显式地释放资源。
-
采用这种方式,对象所需的资源在其生命期内始终保持有效
所以理想的智能指针的原理:
- 具有 RAII 特性。
- 重载
operator*
和opertaor->
,具有像指针一样的行为。
简单的说,让指针和普通对象一样,初始化时可以申请内存,析构时自己释放内存。根据智能指针的原理,可以设计出简易的智能指针SmartPtr
类。
#include<iostream>
#include<string>
using namespace std;// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr): _ptr(ptr) {}~SmartPtr() {if (_ptr) {cout << "~SmartPtr:delete " << endl;delete _ptr;}}//给SmartPtr对象重载*和->让它具有指针的功能T& operator*() {return *_ptr;}T* operator->() {return _ptr;}private:T* _ptr;
};
int div() {int a, b;//cin >> a >> b;a = 3; b = 0;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func() {SmartPtr<int> sp1(new int(3));SmartPtr<string> sp2(new string("alpha"));SmartPtr<pair<int, int>> sp3(new pair<int, int>(3, 4));cout << *sp1 << endl;cout << *sp2 << endl;cout << "[" << sp3->first << ':' << sp3->second << "]\n";cout << div() << endl;//当发生异常后,智能指针也能自动清理空间
}int main() {try {Func();}catch (const exception& e) {cout << e.what() << endl;}return 0;
}
这个智能指针在申请单个对象时没什么问题,但若是申请数组,则析构函数会造成内存泄漏。根据c++内存管理和new、delete的使用-CSDN博客的结论,使用delete
删除对象数组,会导致空间的一部分用于存储数组元素个数的数据没能释放。
库里的智能指针
c++98的std::auto_ptr
C++98版本的库中就提供了auto_ptr
的智能指针。详见auto_ptr - C++ Reference。
auto_ptr
的实现原理:管理权转移的思想,下面简单模拟实现了一份auto_ptr
来了解它的原理。
template<class T>
class auto_ptr
{
public:auto_ptr(T* ptr):_ptr(ptr) {}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr) {// 管理权转移sp._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap) {// 检测是否为自己给自己赋值if (this != &ap) {// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = NULL;}return *this;}~auto_ptr() {if (_ptr) {cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*() {return *_ptr;}T* operator->() {return _ptr;}
private:T* _ptr;
};
但它是一个失败的设计,很多公司明确要求不能使用auto_ptr
。因为这个指针之间的拷贝构造是浅拷贝,在使用不当的情况会导致同一片空间被析构2次,即使明确提示了交换管理权,但总有人不按规定使用。例如这个使用会导致越界访问:
int main() {auto_ptr<int> sp1(new int);auto_ptr<int> sp2(sp1); // 管理权转移// sp1悬空*sp2 = 10;cout << *sp2 << endl;cout << *sp1 << endl;//sp1实际被架空return 0;
}
指针比较特殊,不能使用深拷贝,因为使用指针的目的是管理资源,这种拷贝行为的初衷是2个指针管理同一片空间。
c++11的std::unique_ptr
c++98的auto_ptr
对指针之间的拷贝设计的管理权限转移并不适用,有人就尝试禁止类的拷贝构造和赋值重载的使用。
且若只声明不实现,其他人可能会在类外自己实现。也可以将拷贝构造和赋值重载设置为私有,这是c++98的做法。
到了c++11,库里才更新智能指针实现。但问题是c++11出来之前,boost
搞出了更好用的scoped_ptr
、shared_ptr
和weak_ptr
,于是c++11就将boost
库中智能指针精华部分吸收了过来。
unique_ptr - C++ Reference
unique_ptr
的实现原理:简单粗暴的防拷贝,下面简单模拟实现了一份UniquePtr
来了解它的原理。
template<class T>
class unique_ptr {
public:unique_ptr(T* ptr):_ptr(ptr) {}~unique_ptr() {if (_ptr) {cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*() {return *_ptr;}T* operator->() {return _ptr;}//防止拷贝unique_ptr(const unique_ptr<T>&sp) = delete;//防止赋值unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
private:T* _ptr;
};
c++11的std::shared_ptr
但总有人忍不住乱用指针之间的拷贝和赋值,于是就有了shared_ptr
。
shared_ptr - C++ Reference
shared_ptr
的原理:是通过引用计数的方式来实现多个shared_ptr
对象之间共享资源。关于引用计数,在c++STL-string的使用-CSDN博客中有提到类似思想的引用计数和写时拷贝。例如:
老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。
-
shared_ptr
在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。 -
在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
-
如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
-
如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
引用计数的原理
这里分析引用计数的理论实现:
2个或多个智能指针都指向同一片空间,智能指针类设置整型成员变量来记录有多少个指针指向空间,这样做最大的问题是,一个对象销毁,其他对象并不能及时做出反应。
设置静态整型的成员变量也不行,因为静态成员变量只能代表1个空间的信息。
用
map
或unordered_map
设置<T*,int>
型键值对,平时自己玩还可以,但若是放在其他地方应用,最大的隐患还是多线程的情况,这个map
就会变成木桶,所有智能指针都要访问这个map
,在不同线程可能会导致线程安全问题,操作系统对线程安全问题的处理方式是加锁,但加锁会导致访问效率下降。所以最好的方式是每个空间附带一个整型变量,用来统计多少个指针指向本空间。这样每个智能指针多带1个整型指针指向空间附带的整型变量,即可达到所有智能指针都能监视空间数量的变化。
对每个空间都附带一个整型变量,用来统计多少个指针指向本空间。这样每个智能指针多带1个整型指针指向空间附带的整型变量,即可达到所有智能指针都能监视空间数量的变化。
既如此,完全可以将指向空间的指针和统计用的整型设置成同一个类:
class share_ptr{
private:T* ptr;int* pcount;
};
引入引用计数的理念后,对这个智能指针的功能进行规定:
- 拷贝构造:指针进行浅拷贝,计数变量增加。
- 赋值重载:若成员指针指向的地址不同则允许赋值并增加计数变量。若允许成员指针指向的地址相同,则会导致计数变量混乱。
- 析构函数:优先减去计数变量。若计数变量为0,则正式清理内存。
引用计数智能指针最开始的设计:
namespace mystd {//引用计数template<class T>class shared_ptr {public:shared_ptr(T* ptr=nullptr): ptr(ptr), pcount(new int(1)) {}~shared_ptr() {Release();}private:void Release() {if (--(*pcount) == 0) {delete ptr;delete pcount;}}public:shared_ptr(const shared_ptr<T>& sp):ptr(sp.ptr), pcount(sp.pcount) {++(*pcount);}shared_ptr<T>& operator=(const shared_ptr<T>& sp) {if (ptr != sp.ptr) {Release();ptr = sp.ptr;pcount = sp.pcount;++(*pcount);}return *this;}// 像指针一样T& operator*() {return *ptr;}T* operator->() {return ptr;}int use_count() const {return *pcount;}T* get() const {return ptr;}private:T* ptr;int* pcount;};
}
智能指针会配置定制删除器,将delete
和delete[]
封装成仿函数后作为模板参数或初始化参数上传给智能指针,用以应对申请的空间是对象数组的情况。仿函数可以换成lambda函数。
还有一种场景,就是智能指针上传的是用fopen
打开的文件,这时定制删除器也要上传关闭文件的fclose
函数的封装。
若是拥有定制删除器的版本,则增加特定构造函数和修改内置的Release
方法即可。删除器可以通过包装器function
作为类的成员,也可以使用带缺省值的模板参数。
这里给模拟的智能指针引入包装器。
namespace mystd {//引用计数template<class T>class shared_ptr {public:shared_ptr(T* ptr = nullptr): ptr(ptr), pcount(new int(1)) {}~shared_ptr() {Release();}template<class D>shared_ptr(T* ptr,D Del): ptr(ptr), pcount(new int(1)), Del(Del){}private:void Release() {if (--(*pcount) == 0) {Del(ptr);delete pcount;}}public:shared_ptr(const shared_ptr<T>& sp):ptr(sp.ptr), pcount(sp.pcount) {++(*pcount);}shared_ptr<T>& operator=(const shared_ptr<T>& sp) {if (ptr != sp.ptr) {Release();ptr = sp.ptr;pcount = sp.pcount;++(*pcount);}return *this;}// 像指针一样T& operator*() {return *ptr;}T* operator->() {return ptr;}int use_count() const {return *pcount;}T* get() const {return ptr;}private:T* ptr;int* pcount;//默认情况下删除器直接删除对象function<void(T*)> Del = [](T* ptr) {delete ptr; };};
}
当然这个设计和库中的设计有很大的差距,仅做参考。
循环引用的缺陷
若使用环境是双向循环链表的构建:
struct ListNode {int data;shared_ptr<ListNode> prev;shared_ptr<ListNode> next;ListNode(int data=0, shared_ptr<ListNode> prev=nullptr, shared_ptr<ListNode> next = nullptr): data(data), prev(prev), next(next){}~ListNode() {cout << "~ListNode()" << endl;}
};
void testListNode() {mystd::shared_ptr<ListNode> node1(new ListNode);mystd::shared_ptr<ListNode> node2(new ListNode);cout << node1.use_count() << endl;cout << node2.use_count() << endl;node1->next = node2;node2->prev = node1;cout << node1.use_count() << endl;cout << node2.use_count() << endl;return;
}
使用shared_ptr
表示双向循环链表的个结点,对2个这样的结点进行链接操作时会发生如下变化:
-
node1
和node2
这两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete
。 -
node1
的next
指向node2
,node2
的prev
指向node1
,引用计数变成2。
之后程序接受,node2
先调用析构,node1
后调用,这中间发生的事:
-
node1
和node2
析构,引用计数减到1,但是node1
的next
还指向node2
,node2
的prev
还指向node1
。 -
之后函数结束,
node1
和node2
调用过1次析构函数,名义上已经不存在,但实际上它们在堆区申请的空间依旧存在。 -
node1
和node2
作为函数栈帧里的成员被销毁,但因为不会第2次调用析构函数,导致它们各自申请的空间依旧存在,而且这2个空间的智能指针依旧指向已经被销毁的node1
和node2
。
这就是智能指针对象之间的互相引用导致引用计数无法降为0,间接导致空间无法被回收造成内存泄漏的循环引用问题。
c++11的std::weak_ptr
循环引用这个缺陷源于shared_ptr
的设计缺陷,但直至目前依旧没有更好的方案可以替代shared_ptr
,于是过去的人们新增一个weak_ptr
来解决这一块问题。
关于weak_ptr
见weak_ptr - C++ Reference。weak_ptr
不是传统的智能指针,不支持 RAII,它支持shared_ptr
的对象的赋值,但不参与引用计数。
即使不参与shared_ptr
的引用计数,它也可以观察shared_ptr
的引用计数变量,通过库中的函数use_count
实现。
这里简单模拟一个weak_ptr
,库里的std::weak_ptr
比这个要考虑更多问题,设计比这个完善。
namespace mystd{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;};
}
之前在2个智能指针互相指向对方,或双向链表构建时造成的引用计数问题,解决的方法是将双向链表的next
和prev
更换成weak_ptr
类型。
struct ListNode {int data;mystd::weak_ptr<ListNode> prev;//这里的指针指向shared_ptr不应调整引用计数mystd::weak_ptr<ListNode> next;ListNode(int data=0, shared_ptr<ListNode> prev=nullptr, shared_ptr<ListNode> next = nullptr): data(data), prev(prev), next(next){}~ListNode() {cout << "~ListNode()" << endl;}
};
完整的测试程序如下:
mystd.h
#pragma once
#include<string>
#include<iostream>
#include<vector>
#include<cstdlib>
#include<typeinfo>
#include<cstring>
#include<cassert>
#include<algorithm>
using std::vector;
using std::string;
using std::cout;
using std::endl;
using std::reverse;
using std::forward;namespace mystd {//引用计数template<class T>class shared_ptr {public:shared_ptr(T* ptr=nullptr): ptr(ptr), pcount(new int(1)) {}~shared_ptr() {Release();}private:void Release() {if (--(*pcount) == 0) {delete ptr;delete pcount;}}public:shared_ptr(const shared_ptr<T>& sp):ptr(sp.ptr), pcount(sp.pcount) {++(*pcount);}shared_ptr<T>& operator=(const shared_ptr<T>& sp) {if (ptr != sp.ptr) {Release();ptr = sp.ptr;pcount = sp.pcount;++(*pcount);}return *this;}// 像指针一样T& operator*() {return *ptr;}T* operator->() {return ptr;}int use_count() const {return *pcount;}T* get() const {return ptr;}private:T* ptr;int* pcount;};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;};struct ListNode {int data;mystd::weak_ptr<ListNode> prev;mystd::weak_ptr<ListNode> next;ListNode(int data=0, shared_ptr<ListNode> prev=nullptr, shared_ptr<ListNode> next = nullptr): data(data), prev(prev), next(next){}~ListNode() {cout << "~ListNode()" << endl;}};void testListNode() {mystd::shared_ptr<ListNode> node1(new ListNode);mystd::shared_ptr<ListNode> node2(new ListNode);cout << node1.use_count() << endl;cout << node2.use_count() << endl;cout << endl;node1->next = node2;node2->prev = node1;cout << node1.use_count() << endl;cout << node2.use_count() << endl;return;}
}
main
函数只负责调用测试用的函数。
#include<iostream>
#include"mystd.h"
using namespace std;int main() {mystd::testListNode();return 0;
}
输出:
1
11
1
~ListNode()
~ListNode()
这里的测试可以将mystd::
更换成std::
,即使用库里的,结果不会发生变化。
之后智能指针的使用还包括操作系统中线程的问题,以后有机会会单独提及。
c++11和boost中智能指针的关系
因为c++每次将某个功能引入新标准都会进行讨论,确认不会出问题或即使出问题了也能接受,才允许这个功能加入新标准。为减少这种模式对c++标准的更新速度造成的影响,于是就有了 boost 。
boost 是c++的第三方库,算是准标准库,负责c++更前沿的研发。而标准库的要求是所有编译器都要支持,因此标准库只有在确保万无一失的情况才会更新标准。c++的标准会借鉴 boost 的研发成果,将boost好用的功能加入新标准。
其中关于智能指针的历史:
-
c++ 98 中产生了第一个智能指针
auto_ptr
。个人认为,即使当初的作者因为历史原因和社会背景,设计出这种几乎无人使用的工具,也不能完全否定作者对c++的完善做出的贡献。
-
c++
boost
给出了更实用的scoped_ptr
和shared_ptr
和weak_ptr
。 -
c++ TR1,引入了
shared_ptr
等。不过注意的是TR1并不是标准版。 -
C++ 11,引入了
unique_ptr
和shared_ptr
和weak_ptr
。需要注意的是unique_ptr
对应boost
。的scoped_ptr
。并且这些智能指针的实现原理是参考boost
中的实现的。