当前位置:首页 > 技术学院 > 技术前线
[导读]很多C++开发者天天用标准库的unique_ptr、shared_ptr,却始终没有亲手实现过一个属于自己的智能指针,对它的底层运行逻辑始终停留在模糊的概念层面。智能指针的核心设计逻辑并不复杂,它本质上就是一个封装了裸指针的模板类,依托C++的RAII机制,利用对象生命周期结束时自动调用析构函数的特性,自动完成动态内存的释放,从根源上避免手动管理内存带来的泄漏、重复释放等问题。从零开始一步步实现一个功能完整的智能指针,你就能彻底吃透它的底层原理,避开日常使用中的各类隐蔽陷阱。

很多C++开发者天天用标准库的unique_ptr、shared_ptr,却始终没有亲手实现过一个属于自己的智能指针,对它的底层运行逻辑始终停留在模糊的概念层面。智能指针的核心设计逻辑并不复杂,它本质上就是一个封装了裸指针的模板类,依托C++的RAII机制,利用对象生命周期结束时自动调用析构函数的特性,自动完成动态内存的释放,从根源上避免手动管理内存带来的泄漏、重复释放等问题。从零开始一步步实现一个功能完整的智能指针,你就能彻底吃透它的底层原理,避开日常使用中的各类隐蔽陷阱。

一、核心设计基石:RAII机制与基础独占智能指针的雏形

实现智能指针的第一步,首先要牢牢抓住RAII这个最核心的设计思想。RAII的核心逻辑非常简单:把动态申请的堆内存资源,完全委托给一个栈上分配的对象来管理。对象在构造函数里拿到裸指针的所有权,在它的生命周期结束、自动调用析构函数的时候,自动释放所持有的堆内存。这样一来,哪怕程序中间抛出异常,栈上的对象也会被自动析构,对应的堆内存一定会被正常释放,彻底避免手动delete时遇到异常就内存泄漏的问题。

我们首先从最基础的独占式智能指针开始实现,它的设计目标是保证同一块堆内存,同一时间只会被一个智能指针对象拥有,绝对不会出现多个对象同时管理同一块内存的情况,从根源上避免重复释放的问题。首先我们把这个智能指针定义成模板类,这样它就可以适配任意类型的堆对象,不用为每一种数据类型单独写一套实现。

类的内部只需要保存一个私有成员变量,就是我们要管理的裸指针。在构造函数里,我们把外部传入的裸指针赋值给这个内部成员,同时把构造函数标记为explicit,避免编译器做隐式类型转换,防止意外把一个裸指针偷偷转换成智能指针带来的隐患。接下来实现最核心的析构函数,在析构函数里直接调用delete释放内部保存的裸指针,这样只要智能指针对象离开作用域,析构函数就会被自动调用,对应的堆内存就会被自动释放,完全不需要开发者手动写delete语句。

到这里我们已经实现了智能指针最基础的自动释放能力,但是现在这个智能指针用起来还不像普通的裸指针,普通指针可以用星号解引用、用箭头直接访问成员,我们的智能指针还不支持这些操作。接下来我们重载星号解引用运算符和箭头运算符,解引用运算符直接返回内部裸指针指向的对象的引用,箭头运算符直接返回内部保存的裸指针,这样我们的智能指针就可以像原生裸指针一样,直接用熟悉的语法访问指向的内容,使用体验和原生指针几乎没有任何区别。

二、完善独占语义:禁用拷贝与实现移动语义,彻底规避重复释放

现在这个基础版本的智能指针,还存在一个致命的缺陷:如果我们用一个智能指针对象去拷贝构造另一个新的智能指针,两个对象的内部裸指针会指向同一块堆内存。当两个对象先后离开作用域的时候,它们的析构函数都会尝试释放同一块内存,就会出现重复释放的严重错误,直接导致程序崩溃。

为了解决这个问题,我们必须给独占式智能指针加上独占语义,直接把拷贝构造函数和拷贝赋值运算符用delete关键字禁用掉,从语法层面禁止任何拷贝操作。这样开发者就不能把一个独占智能指针赋值给另一个智能指针,从根源上避免两个对象同时管理同一块内存的问题。

但是完全禁止拷贝之后,我们就没办法把智能指针的所有权从一个对象转移给另一个对象,使用起来非常不灵活。这个时候我们就可以引入移动语义,实现移动构造函数和移动赋值运算符。在移动构造函数里,我们把传入的右值对象的内部裸指针直接接管过来赋值给自己的成员变量,然后把原对象的内部裸指针置为空,这样所有权就完整地从原对象转移到了新对象身上,原对象不再管理这块内存,自然不会在析构的时候重复释放。

移动赋值运算符的逻辑和移动构造类似,首先要判断不能把自己移动给自己,避免出现逻辑错误。然后先释放自己当前持有的旧内存,接管传入的右值对象的裸指针,再把原对象的指针置空,完成所有权的转移。到这里我们的独占智能指针就完全实现了标准库unique_ptr的核心功能,支持所有权转移,完全禁止拷贝,不会出现重复释放和内存泄漏的问题。

接下来我们再给它补充几个实用的辅助接口:get方法直接返回内部保存的裸指针,方便一些需要传入裸指针的第三方库调用;release方法把内部裸指针返回,同时把智能指针内部的指针置空,主动放弃内存的所有权,后续需要开发者手动释放这块内存;reset方法接收一个新的裸指针,先释放当前持有的旧内存,再把内部指针替换成新传入的指针,支持动态更换智能指针管理的内存。

三、扩展引用计数智能指针:实现多对象共享同一块内存的能力

独占式智能指针虽然安全,但是很多业务场景下,我们需要多个智能指针对象同时共享同一块堆内存,当最后一个持有这块内存的智能指针销毁的时候,才真正释放这块内存。这个时候我们就需要实现基于引用计数的共享智能指针,也就是标准库shared_ptr的简化版本。

共享智能指针的核心是在堆上额外维护一个引用计数变量,这个计数记录当前有多少个智能指针对象在共享同一块内存。当我们新建第一个共享智能指针的时候,除了分配要管理的对象内存,还要在堆上分配一个引用计数变量,把它的初始值设置为1。当我们用这个智能指针拷贝构造一个新对象的时候,直接把内部的裸指针和引用计数的指针赋值给新对象,然后把引用计数加1,代表现在多了一个新的持有者。

当任意一个共享智能指针对象销毁的时候,我们先把引用计数减1,然后判断减完之后的计数值是不是变成了0。如果变成了0,说明已经没有任何智能指针持有这块内存了,我们就先释放管理的对象内存,再释放堆上的引用计数变量;如果计数值还大于0,说明还有其他对象在共享这块内存,直接什么都不做就可以。

拷贝赋值的逻辑也遵循同样的规则:先把当前对象持有的旧内存的引用计数减1,判断如果减到0就释放旧内存,然后接管右侧对象的裸指针和引用计数,把新引用计数加1,完成赋值操作。这样我们就实现了多对象共享同一块内存的能力,多个智能指针可以随意拷贝,只有最后一个持有者销毁的时候,内存才会被真正释放,完全不会出现重复释放的问题。

到这里我们已经从零实现了两个最核心的智能指针:独占式和共享式,它们完全依托C++的RAII机制,不需要任何额外的性能开销,就可以从根源上解决手动管理内存的各类常见问题。亲手实现一遍之后,你就会彻底理解标准库智能指针的所有底层设计细节,日常使用的时候完全不会踩中各类隐蔽的使用陷阱。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭