前叙
回顾
前面我们了解到RALL的基本用法,可以在方法执行完以后,主动将堆内存对象释放掉,从而简化了内存管理,解决内存泄漏的可能,这次我们学习下RALL,如果做一个完善的智能指针。
智能指针的本质
它的出现其实是为了解决由于动态内存分配而导致的一些内存问题,比如内存泄漏、生命周期管理、悬挂指针或空指针的问题。智能指针通过RALL管理对象的生命周期,提供少量异常类似普通指针的操作接口,在对象构造时候分配内存,在对象作用域之外释放掉内存,帮助我们管理动态内存。相信你跟我一样,看完这句话其实也不太明白,那大概率是对指针的概念不理解,我们来补充点指针的知识吧
到底指针是什么
要是上来就给指针下个定义,估计也没人看得懂,我们直接来看图。
先来看一个变量的内存模型
上面一张图就表明了,一个变量p,它的内存模型就是上面那样,p就是0x11234564地址所对应的存储单元中的数据,这里一个框代表一个字节,由于int是四个字节,所以你看到的才是四个框组成的一个单元。这里需要说明的是c标准中并没规定哪个数据类型占多少字节,这个跟具体机器和编译器有关。你也可以看到,数值其实是按16进制存储的,0x14转10进制后就是20。 你是不是也发现了,其实每个字节都有一个自己的内存地址。对的,这就是跟我接下来讲的指针有关。请往下看。
简单指针
int p = 20; int * a; a = &p; //& 符号可以取得p的内存地址
我们来分析上面这段内存模型,你就明白指针是个什么了
其实指针就是一个内存地址,而指针变量_ a 在内存中保存了指针指向的内存地址,那为什么我们不用_a来做下面的赋值操作呢?这就跟_号的用法有关,你可以简单这么理解,_ 在=号前面的时候代码指针变量,而在=号后面使用的时候,其实是在去指针对应的值。所以接着往下学习,仔细看指针变量a,它的四个字节合起来的值是 0x11234564,正好是变量p的内存地址,而指针a自己的内存地址是:0x11234568,所以你现在是不是明白了一个简单的指针是什么?它跟普通变量唯一的区别就是,它的值存储的是一个内存地址,如果没赋值的话,可能是0x00000000,也可能是其他随机数字。
空指针&野指针
我们再了解什么是空指针和野指针,所谓的空指针是指不指向任何东西的指针,需要注意的是,当我们定义一个指针变量时,如果没有赋值,那么它指针变量存储是一个随机值,如果这个随机值指向了内存中的代码区域,那么就很危险,所以这个时候一定不能写入操作,很有可能对实际的数据产生污染。建议对一个指针变量赋值为NULL。
int * p = NULL;
野指针是地址已经失效的指针,具体说就是当一个指针指向堆内存中的值时,如果这个堆内存被delete后,你还继续操作这个指针,那么就很危险了,所以不建议你这么操作。 好了言归正传,我们来继续研究智能指针。
为什么使用智能指针
在我们简单了解了内存管理和指针后,就很容易理解下面这三点
- 智能指针能够帮助我们处理资源泄露问题
- 它也能够帮我们处理空悬指针(野指针)的问题
- 它还能够帮我们处理比较隐晦的由异常造成的资源泄露
常见的智能指针
智能指针在C11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr。 其实还有auto_ptr,这个在C11中已经被抛弃,我们就不学习过时的技术了。 如果是讲unique_ptr,shared_ptr的用法,其实有大量的文章讲解,完全没必要继续看下去对吧,我们这次接着上篇的代码,来改造一下,实现一个通用的完整的智能指针。先来看下上次的代码
class TestRALL { public: TestRALL() { std::cout << "TestRALL done" << std::endl; }; ~TestRALL() { std::cout << "~TestRALL done" << std::endl; }; void print() { std::cout << 1 << std::endl; } }; TestRALL *createTest() { return new TestRALL(); } class TRDelete { public: explicit TRDelete(TestRALL *tr = nullptr) : tr_(tr) {} ~TRDelete() { delete tr_; } TestRALL *get() const { return tr_; } private: TestRALL *tr_; }; void print() { TRDelete trDelete(createTest()); trDelete.get()->print(); } int main() { print(); return 0; }
我们发现,TRDelete只适用于TestRALL这一个类,在Java中,我可以通过泛型来解决通用性的问题,那么C++中有吗?肯定有,哈哈,那我们来改造下,代码如下
#include <iostream> class TestRALL { public: TestRALL() { std::cout << "TestRALL done" << std::endl; }; ~TestRALL() { std::cout << "~TestRALL done" << std::endl; }; void print() { std::cout << 1 << std::endl; } }; TestRALL *createTest() { return new TestRALL(); } template <typename T> class HeapDel { public: explicit HeapDel(T *tr = nullptr) : tr_(tr) {} ~HeapDel() { delete tr_; } T *get() const { return tr_; } private: T *tr_; }; void print() { HeapDel<TestRALL> heapDel(createTest()); heapDel.get()->print(); } int main() { print(); return 0; }
为了通用性和编码规范,我们重新取名HeapDel,意思是释放堆内存,用template 来定义个模板,它的标准格式如下
template <class identifier> function_declaration; template <typename identifier> function_declaration;
示例
template <typename T> void swap(T& t1, T& t2);
感觉跟Java的泛型很像,有机会我们再深入讨论。我们改造后的代码运行结果如下:
TestRALL done 1 ~TestRALL done
同样达到了预期。但是这样就是完整的智能指针了吗,肯定不是哈。再来优化一下
void print() { HeapDel<TestRALL> heapDel(createTest()); // heapDel.get()->print(); heapDel->print();
如果我想直接用heapDel来调用TestRALL的print方法可以吗?肯定是可以的,要不然每次都get岂不是很累。
template<typename T> class HeapDel { public: explicit HeapDel(T *tr = nullptr) : tr_(tr) {} T *operator->() const { return tr_; } //加入该行代码即可 ~HeapDel() { delete tr_; } T *get() const { return tr_; } private: T *tr_; };
这里用 operator 重载标识,重载 -> 返回 tr_ 指针变量。具体详细用法请自己查阅哈,目前我对他也不是很理解,后续再学习中了解它。上面我们是想了通过->来方位tr_的成员,那*tr_获取指针的值,该怎么做呢?
T &operator*() const { return *tr_; }
注意: // & 取内存地址,* 取指针的值,因为指针的值就是指向的地址,所以赋值给&修饰的变量,其实就是地址与地址的赋值,没什么问题。 再加上面一行,你就可以实现 *heapDel 其实就是 *tr_ 一样的作用。 接下来我们看一个问题,如果我这样操作
void print() { HeapDel<TestRALL> heapDel(createTest()); heapDel->print(); HeapDel<TestRALL> heapDel2(heapDel); heapDel2->print(); }
运行后是这样的
untitled1(3107,0x102f4ae00) malloc: *** error for object 0x7fb06ac05a00: pointer being freed was not allocated untitled1(3107,0x102f4ae00) malloc: *** set a breakpoint in malloc_error_break to debug TestRALL done 1 1 ~TestRALL done ~TestRALL done
~TestRALL析构函数被执行两次,意味着你释放了两次,着肯定是不允许的,程序已经崩溃报错了,那我们如何避免这个问题呢?
template<typename T> class HeapDel { public: HeapDel(const HeapDel &) = delete; HeapDel &operator=(const HeapDel &) = delete; explicit HeapDel(T *tr = nullptr) : tr_(tr) {} T *operator->() const { return tr_; } T &operator*() const { return *tr_; } ~HeapDel() { delete tr_; } T *get() const { return tr_; } private: T *tr_; };
我们把HeapDel的构造函数给禁掉了,这个时候你再次调用 heapDel2(heapDel) 的时候已经不允许了,会提示如下:
Call to deleted constructor of 'HeapDel<TestRALL>'
但这样做真的合理吗?如果我真需要用一个新的智能指针来获取这个所有权,怎么办呢?
template<typename T> class HeapDel { public: HeapDel(HeapDel &other) { //给当前对象的指针赋值 tr_ = other.release(); } HeapDel &operator=(HeapDel &hd) { HeapDel(hd).swap(*this); return *this; } explicit HeapDel(T *tr = nullptr) : tr_(tr) {} T *operator->() const { return tr_; } T &operator*() const { return *tr_; } ~HeapDel() { delete tr_; } /** * 创建新的指针变量返回 * 将老的指针变量赋值空指针 * @return */ T *release() { T *tr = tr_; tr_ = nullptr; return tr; } void swap(HeapDel &hd) { using std::swap; //用std 的swap,来交换tr_ swap(tr_, hd.tr_); } T *get() const { return tr_; } private: T *tr_; };
新增了俩函数都是为了交换 tr_ 的所有权,再运行刚的代码,你会发现正常了
void print() { HeapDel<TestRALL> heapDel(createTest()); heapDel->print(); HeapDel<TestRALL> heapDel2(heapDel); heapDel2->print(); } //运行结果如下: TestRALL done 1 1 ~TestRALL done
到目前为止你就实现了被 C++ 11 抛弃的版本 auto_ptr 它的核心逻辑。为什么会抛弃呢?你也看到了在swap中,其实是用HeapDel(hd).swap(*this); 这相当于构造了一个临时对象,再调用swap,如果再赋值过程中发生了异常,this对象可能会部分破坏,就不是一个完整的状态了。而且它最大的问题在于,如果用了新的HeapDel,那之前的HeapDel就不再拥有tr_。下面来看下unique_ptr 智能指针是如何解决上面问题的
template<typename T> class HeapDel { public: HeapDel(HeapDel &&other) noexcept { //给当前对象的指针赋值 tr_ = other.release(); } HeapDel &operator=(HeapDel hd) { hd.swap(*this); return *this; } explicit HeapDel(T *tr = nullptr) : tr_(tr) {} T *operator->() const { return tr_; } T &operator*() const { return *tr_; } ~HeapDel() { delete tr_; } /** * 创建新的指针变量返回 * 将老的指针变量赋值空指针 * @return */ T *release() { T *tr = tr_; tr_ = nullptr; return tr; } void swap(HeapDel &hd) { using std::swap; //用std 的swap,来交换tr_ swap(tr_, hd.tr_); } T *get() const { return tr_; } private: T *tr_; };
我们将HeapDel重载的构造函数,参数other,改为了&&修饰,原本的实现其实是叫拷贝构造函数,在 C++ 11 标准之前(C++ 98/03 标准中),如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数,也就是HeapDel(HeapDel &other) 的写法。现在用两个&&,则变成了移动构造函数,你是不是又多了一个问号,它是干嘛用的呢? 它的来源其实就是由于拷贝构造函数在做对象初始化过程中,底层是进行了两次深拷贝,如果申请的堆空间较小也无伤大雅,可谁能保证呢?随着业务的增多,肯定会需要申请大的空间,从而影响拷贝的执行效率。 移动构造函数,指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。想要深入了解的,可以自查哦,嘿嘿。 再一个就是将重载的 = 改为了hd.swap(*this)的实现,并且参数hd去掉了&,我们知道这种方式叫值传递,hd的任何修改对实参无影响。下面来看下改造后如何用
void print() { HeapDel<TestRALL> heapDel(createTest()); heapDel->print(); HeapDel<TestRALL> heapDel2; heapDel2 = std::move(heapDel); heapDel2->print(); }
std::move() 函数就是强制调用HeapDel的移动构造函数,如果还有拷贝构造或者其他构造函数。 现在unique_ptr的实现,你已经知道了。还有更复杂的shared_ptr,如果想把它搞明白,估计需要不少的知识,我们后续有机会再讨论,后面继续学习基础知识。
总结
本期,对智能指针两种实现unique_ptr、auto_ptr,有了深刻的理解,也明白了指针到底是什么,收获很多,确实感觉到C++的复杂性,一个构造函数就有这么多的变数。想要学明白就要理解它背后的动机以及设计的规范,很多设计的背后其实在原理上还是有共通的点,这里分享一个简单且高效的学习过程:先知道是什么,且一定要弄明白为什么,然后才是怎么用。这次分享就到这里,感谢跟我一起学习。加油。