C手把手带你实现一个智能指针
为什么需要智能指针?
这个问题承接上一篇文章《C++ 堆,栈,RAII》,在RAII的指引下,我们需要实现一个类来管理资源,将资源和类对象的生命周期进行绑定,这样我们就可以不用手动释放资源了。
那为什么把类实现成 like pointer ?
上篇文章中说道,因为C++存在对象切片,而使用指针就避免了这个问题。
我们现在来实现一个智能指针:
首先,我们应该满足资源管理的需求: 构造函数获得资源,析构函数释放资源。 class Type { }; class smart_ptr { public: smart_ptr(Type* ptr = NULL) : m_ptr(ptr) {} ~smart_ptr() { delete m_ptr; } private: Type* m_ptr; }; smart_ptr(new Type);
好了,我们这样就实现了一个可以管理 Type 类指针的 smart_ptr 。
显然,我们这个代码不够通用,只能管理 Type这个类型,我们把 smart_ptr 改成模板,把类型传进去,这样就通用了。 template class smart_ptr { public: smart_ptr(T* ptr = NULL) : m_ptr(ptr) {} ~smart_ptr() { delete m_ptr; } private: T* m_ptr; };
我们只需要把 smart_ptr(new Type); 改成 smart_ptr(new Type); ,就对Type进行了管理。
这样我们就实现了资源管理的功能,下面我们还需要实现 like pointer 功能。
指针有什么操作? T& operator*() const; T* operator->() const; operator bool() const;
我们需要实现指针的解引用, -> 运算, bool 运算, bool 运算这里涉及到一个隐式转换的问题,有机会再说。 注意这里函数括号后面加上 const ,表示当前成员函数不改变对象成员,这是个好习惯,不清楚的话可以看下我之前的文章。 template class smart_ptr { public: smart_ptr(T* ptr = NULL) : m_ptr(ptr) {} ~smart_ptr() { delete m_ptr; } T& operator*() const { return *m_ptr; } T* operator->() const { return m_ptr; } operator bool() const { return m_ptr; } private: T* m_ptr; }; // 测试 class Type { public: int a = 1; }; smart_ptr sptr(new Type); cout << sptr->a << endl; // 1 cout << (*sptr).a << endl; // 1 if (sptr) { // true cout << "true" << endl; }
这样,我们就初步实现了指针的这几个行为。
一般类内包含一个指针,这种设计叫委托,类的功能基本上都是由类内这个指针来完成的,可以理解为委托它实现的。
类内包含指针的话,就少不了三个函数,拷贝构造,拷贝赋值,析构函数。
有指针存在,就涉及到浅拷贝还是深拷贝的问题,如果不写拷贝构造,用到了拷贝函数时,编译器会提供一个默认的拷贝构造,而这个默认的拷贝构造做的事情是把你的成员变量逐个拷贝下来。
显然,类内的指针就被copy了一份,新的对象中就有一个新拷贝的指针了,然而这个指针和被拷贝的对象里的指针一样,都是指向原指针的地址。 如果原对象指针所指的空间被释放了,这个拷贝对象指针再去操作这块空间,结果可想而知。
所以,有指针的话则需要我们手动去定义一个拷贝构造,在拷贝构造内申请空间,把原指针的内存空间copy过来,新对象里的指针指向这块空间。
拷贝赋值的操作是这样的: b = a; 把 a的值赋值给b,首先我得把b内指针占用的空间给释放掉,再把a内指针指向的空间拷贝一份,再将b内指针指过去,这样就完成了拷贝赋值,这里有个问题值得注意的是,如果 b 和 a指向的是同一片区域,相当于把自己赋值给自己,根据上面的操作,先把内存给释放了,在下一步拷贝时则会引发异常,所以,我们在释放空间之前,得先判断一下咱是不是自己啊!
析构函数就很好理解了,有指针指向堆空间,最后肯定要在析构函数内释放内存啦。
所以,类内包含指针的话这三个函数缺一不可,其他的函数则根据需要进行补充。
现在回归我们的话题,智能指针需要实现拷贝构造,拷贝赋值吗?
最简单的方式就是禁用拷贝构造,拷贝赋值。 如果我们不写拷贝构造,拷贝赋值,编译器就会提供一个默认的浅拷贝,我们一用,就会两次析构同一块内存引发异常。 就像下面这样: class Type { public: int a = 1; }; template class smart_ptr { public: smart_ptr(T* ptr = NULL) : m_ptr(ptr) {} ~smart_ptr() { delete m_ptr; } T& operator*() const { return *m_ptr; } T* operator->() const { return m_ptr; } operator bool() const { return m_ptr; } private: T* m_ptr; }; int main() { smart_ptr sptr(new Type); smart_ptr sptr2(sptr); return 0; }
所以,我们需要显式把这两个函数给删掉,C++11 提供的 =delete 就可以用上了 class Type { public: int a = 1; }; template class smart_ptr { public: smart_ptr(T* ptr = NULL) : m_ptr(ptr) {} ~smart_ptr() { delete m_ptr; } // 加上下面这两行 smart_ptr(const smart_ptr&) = delete; smart_ptr& operator=(const smart_ptr) = delete; T& operator*() const { return *m_ptr; } T* operator->() const { return m_ptr; } operator bool() const { return m_ptr; } private: T* m_ptr; }; int main() { smart_ptr sptr(new Type); smart_ptr sptr2(sptr); // 这行就直接编译不过了 return 0; }
这样就杜绝了这两个函数被调用。
第二种方案,我们需要去实现拷贝构造和拷贝赋值,问题是要怎么实现这两个函数,我们要不要在智能指针拷贝时把对象拷贝一份?
不行,通常大家都不会这么用,因为使用智能指针的目的就是要减少对象的拷贝,所以我们要实现拷贝就有两种方式了。 在拷贝里面实现搬移的操作,把原指针的对象赋值给新指针,原指针就不能再用了,对应 std::unique_ptr 原指针和新指针都指向那一个对象,在智能指针里添加一个引用计数,引用计数为0后删除该对象,对应 std::shared_ptr
下面我们实现一个搬移的操作。 #include using namespace std; class Type { public: int a = 1; }; template class smart_ptr { public: smart_ptr(T* ptr = NULL) : m_ptr(ptr) {} ~smart_ptr() { delete m_ptr; } T& operator*() const { return *m_ptr; } T* operator->() const { return m_ptr; } operator bool() const { return m_ptr; } // 搬移构造 smart_ptr(smart_ptr&& rhs) noexcept { m_ptr = rhs.m_ptr; rhs.m_ptr = NULL; } // 搬移赋值 smart_ptr& operator=(smart_ptr&& rhs) noexcept { m_ptr = rhs.m_ptr; rhs.m_ptr = NULL; return *this; } private: T* m_ptr; }; int main() { smart_ptr sptr(new Type); smart_ptr sptr2(std::move(sptr)); // 调用搬移构造 smart_ptr sptr3; sptr3 = std::move(sptr2); // 调用搬移赋值 return 0; }
根据C++的规则,我们提供了搬移构造而没有提供拷贝构造,那拷贝构造就自动被禁用。
搬移构造需要的参数是一个右值,我们借用标准库的 std::move() 函数让它帮我们把 sptr 转型为一个右值,我们在构造函数内把 rhs的指针给拿过来,再把rhs指向空,这时rhs以后再也不能被用了,因为它的资源已经被我偷过来了。 类似的,搬移赋值也是一样的道理。
如果你发现我在 move function 后加了 noexcept 关键字,这对于智能指针能被正确使用是十分必要的,我会在以后的文章中说明。
这样我们就基本实现了一个 unique_ptr 的功能。
unique_ptr 算是一种较为安全的智能指针了。 但是,一个对象只能被单个 unique_ptr 所 拥有 ,这显然不能满足所有使用场合的需求。 一种常见的情况是,多个智能指针同时拥有一个对象;当它们全部都失效时,这个对象也同时会被删除。这也就是 shared_ptr 了。 unique_ptr 和 shared_ptr 的主要 区别如下图所示:
多个不同的 shared_ptr 不仅可以共享一个对象,在共享同一对象时也需要同时共享同一个 计数 。 当最后一个指向对象(和共享计数)的 shared_ptr 析构时,它需要删除对象和共享计数。 我们下面就来实现一下。
我们需要实现一个 share_count 类,用来处理引用计数增加减少的操作,这里没有考虑计数器的线程安全性。 class share_count { public: share_count() : _count(1) {} void add_count() { ++_count; } long reduce_count() { return --_count; } long get_count() const { return _count; } private: long _count; };
下面我们实现一下构造函数,拷贝构造,拷贝赋值,析构函数,具体实现如下。 class Type { public: int a = 1; }; class share_count { public: share_count() : _count(1) {} void add_count() { ++_count; } long reduce_count() { return --_count; } long get_count() const { return _count; } private: long _count; }; template class smart_ptr { public: smart_ptr(T* ptr = NULL) : m_ptr(ptr) { if (ptr) { m_share_count = new share_count; } } ~smart_ptr() { if (m_ptr && !m_share_count->reduce_count()) { delete m_ptr; delete m_share_count; cout << "~smart_ptr" << endl; } } T& operator*() const { return *m_ptr; } T* operator->() const { return m_ptr; } operator bool() const { return m_ptr; } smart_ptr(const smart_ptr& rhs) noexcept { m_ptr = rhs.m_ptr; m_share_count = rhs.m_share_count; m_share_count->add_count(); } smart_ptr& operator=(const smart_ptr& rhs) noexcept { m_ptr = rhs.m_ptr; m_share_count = rhs.m_share_count; m_share_count->add_count(); return *this; } long use_count() const { if (m_ptr) { return m_share_count->get_count(); } return 0; } private: T* m_ptr; share_count* m_share_count; }; int main() { smart_ptr sptr(new Type); cout << "sptr"s share_count:" << sptr.use_count() << endl; smart_ptr sptr2(sptr); cout << "sptr"s share_count:" << sptr2.use_count() << endl; smart_ptr sptr3; sptr3 = sptr2; cout << "sptr"s share_count:" << sptr3.use_count() << endl; return 0; } sptr"s share_count: 1 sptr"s share_count: 2 sptr"s share_count: 3 ~smart_ptr
我们看到最后被正确的执行析构了,这样我们就基本实现了一个 shared_ptr 的功能了。
智能指针浅显的写法就模拟到这里了,可能在具体的场景下还缺少一些功能,根据需求我们可以再增加嘛!
好了,智能指针先说到这里了,如果文章有错误的地方还请给我指出来,大家一起进步嘛。
如果觉得对你有帮助的话请@程序员杨小哥 点个赞,谢谢!
惊喜还是惊吓?全球首批活体机器人能生崽了不少科幻片中都出现过机器人繁衍生息的剧情,而如今这样脑洞大开的情景似乎已成为了现实。近日,美国科学家借助超级计算机,用青蛙细胞打造出的活体机器人,首次完成了自我繁殖,还能四世同堂。
任正非兑现承诺,华为海思开始招聘中科院出身的联想,近日的处境不太好,自从科创板上市一日游后,便成为了网友关注的焦点,尤其是司马南不断揭露联想真面目后,彻底引爆了舆论界,时事评论家胡锡进也于近日改变自己的立场,明确
腾讯成功注册狗头表情包商标,柴犬本尊刚过16岁生日,曾经被主人弃养从去年开始,微信QQ微博等各大平台相继推出狗头表情包,成为笑哭表情包的强力竞争对手。企查查App显示,近期,腾讯申请的狗头商标正式完成注册,商标国际分类涉身器材家具等。早在今年2月
怎么联系微信客服电话微信终于有人工客服了,打这个号码!平时大家用微信可能会遇到各种各样的问题,比如账号被禁转错账等等问题,这时候很多人都想第一时间找到微信的人工客服。网上随便一搜,就有很多人找不过在微
联想比华为早成立3年有中科院背景,为何现在被华为甩得那么远?很简单华为老总任正非是真正爱国的企业家,目光长远联想教父柳传志是资本家,只看眼前利益!任正非想的是为国为民,柳传志想的是自己及少数关系人!这个很简单,一个为自己的钱包,一个是为大家
有没有买过二手单反相机的说下体会?我从来没有买过,因为我有一定程度的洁癖,不喜欢别人用过的东西。不过,我们摄影圈子里,还是有不少人买二手摄影器材,包括单反。有个人就是这样,买了别人升级淘汰下来的尼康d810,以及尼
会不会有人利用京东七天无理由退货无限用新手机的?这样的恶意退货行为是存在的。依稀记得,有个帖子,曾有网友晒出自己在某电商平台的退货记录,从价值几百元到数万元的电子产品。该网友利用电商平台的免费退货规则,免费体验了各式各样的数码产
聚焦精准医疗建设智慧品牌来源人民网人民日报推进品牌建设,是一项打好基础利在长远的战略性工程,也是高质量发展的重要抓手。为解决医疗研发和治疗领域的效率问题,2014年医渡科技开启创业征程,始终坚守安全价值普
想说爱小米手机不容易我不是华为粉也不是米粉,只是作为一个普通的消费者,首先爱国是必须的!米国之前没制裁华为的时候,以前还一直用苹果,从苹果4到苹果6S都用,一直都是新机就买,算是半个果粉。制裁之后就用
爆料!iPhone13出现红绿双色屏Bug小米12全球首发骁龙8Gen1抢首发!小米12全球首发骁龙8Gen1小米CEO雷军于12月2日再次强调小米12骁龙8代全球首发,并发表长文强调首发是实力,也是承诺。近日,高通如期举行骁龙技术峰会,会上正式发布了
基站机房里拔掉一根光纤会有什么后果?那就看运气了,如果你正好拔掉一根是PTN传输光纤很有可能本站传输所下挂基站全部断站,如果是环路的话不会断站,但后台告警马上就出来,没有自动恢复就会有工单发到铁塔代维,代维会快速去现