资讯专栏INFORMATION COLUMN

终于有一篇小白能看懂的智能指针详解了!

includecmath / 2693人阅读

摘要:在我以为我和缘分尚浅的时候,搬来救兵,智能指针横空出世,打败了内存泄漏,拯救了我们的关系。智能指针引入了智能指针的概念,使用了引用计数的想法,让程序员不再需要关心手动释放内存。它还增加了一个成员函数用于交换两个智能指针的值。

内容较多,建议收藏,以后复习

为什么会出现智能指针?

对于程序员来说,如果我不需要,或者我没那么需要,再或者说有替代品,我都不会再去实现一个新东西。

废话,程序员哪里有勤快的!

既然它被发明出来,一定是解决问题的!

在之前,我愚昧的小白生涯中,见到复杂的指针,时常俩股战战,因为搞不懂什么值传递、地址传递、实参、形参、引用、一级指针、多级指针、数组指针、函数指针等等概念,

但是仗着胆子大,就直接上手,唉,似乎没有那么难?

什么动态申请内存,指针赋值,输出字符串,手到擒来,展示!

char *ch = (char*)malloc(12);memcpy(ch, "Hello World", 12);std::cout << ch << std::endl;

在我的黑框框编程生涯中,它是没有打击我的,总是非常给面子的输出正确答案!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lyu9xOL1-1630808532107)(C:/Users/11073/AppData/Roaming/Typora/typora-user-images/image-20210904170337518.png)]

那时候,我以为我和C++的故事会一直这么美好!

那么问题来了?是谁打破了这一美好的幻境!

内存泄漏打破美好

大学期间,上课走神的我,上完了《C++程序设计》之后,都没有听过这个“内存泄漏”,也正是我对他的不熟悉,打破了我和C++稳固的关系,(程序:“呵,我不是能运行就行?”)

什么是内存泄漏?

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。以上定义来自于百度,并且十分准确。

什么!我之前忘记使用free()竟然会造成这么大的后果!

但是我已经形成习惯,已经很想记住,但总是不经意间会忘记释放内存,毕竟“手动”,一定不是最佳方案。这里有一个思想:RAII 资源获取即初始化技术:对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在 离开作用域时调用)的时候释放空间。

在我以为我和C++缘分尚浅的时候,C++11搬来救兵,“智能指针”横空出世,打败了内存泄漏,拯救了我们的关系。

智能指针

C++11 引入了智能指针的概念,使用了引用计数的想法,让程序 员不再需要关心手动释放内存。也就是说,原本需要我们手动就一一对应的malloc/free或new/delete的工作由智能指针来代替了!

这种自动化,程序员最爱!

这些智能指针就包括 std::shared_ptr/std::unique_ptr/std::weak_ptr, 使用它们需要包含头文件 。

unique_ptr

unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

  1. 拥有它指向的对象
  2. 无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作
  3. 保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

功能

unique_ptr 可以实现如下功能:

  1. 为动态申请的内存提供异常安全
  2. 讲动态申请的内存所有权传递给某函数
  3. 从某个函数返回动态申请内存的所有权
  4. 在容器中保存指针

实验

#include#include#includeusing namespace std;class Test{public:    Test(string s)    {        str = s;        cout << "Test creat/n";    }    ~Test()    {        cout << "Test delete:" << str << endl;    }    string& getStr()    {        return str;    }    void setStr(string s)    {        str = s;    }    void print()    {        cout << str << endl;    }private:    string str;};unique_ptr fun(){    return unique_ptr(new Test("789"));}int main(){    unique_ptr ptest(new Test("123"));    unique_ptr ptest2(new Test("456"));    ptest->print();    ptest2 = std::move(ptest);//不能直接ptest2 = ptest    if (ptest == NULL)        cout << "ptest = NULL/n";    Test* p = ptest2.release();    p->print();    ptest.reset(p);    ptest->print();    ptest2 = fun(); //这里可以用=,因为使用了移动构造函数        ptest2->print();    return 0;}

unique_ptr 和 auto_ptr用法很相似,不过不能使用两个智能指针赋值操作,应该使用std::move; 而且它可以直接用if(ptest == NULL)来判断是否空指针;release、get、reset等用法也和auto_ptr一致,使用函数的返回值赋值时,可以直接使用=, 这里使用c++11 的移动语义特性。另外注意的是当把它当做参数传递给函数时(使用值传递,应用传递时不用这样),传实参时也要使用std::move,比如foo(std::move(ptest))。它还增加了一个成员函数swap用于交换两个智能指针的值。

shared_ptr

std::shared_ptr 是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象,从而消除 显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。

但还不够,因为使用 std::shared_ptr 仍然需要使用 new 来调用,这使得代码出现了某种程度上的 不对称。

std::make_shared 就能够用来消除显式的使用 new,所以 std::make_shared 会分配创建传入参 数中的对象,并返回这个对象类型的 std::shared_ptr 指针。例如:

#include #include void foo(std::shared_ptr i) {	(*i)++;}int main() {	// auto pointer = new int(10); // illegal, no direct assignment	// Constructed a std::shared_ptr	auto pointer = std::make_shared(10);	foo(pointer);	std::cout << *pointer << std::endl; // 11										// The shared_ptr will be destructed before leaving the scope	return 0;}

std::shared_ptr 可以通过 get() 方法来获取原始指针,通过 reset() 来减少一个引用计数,并 通过 use_count() 来查看一个对象的引用计数。例如:

auto pointer = std::make_shared(10);auto pointer2 = pointer; // 引用计数 +1auto pointer3 = pointer; // 引用计数 +1int *p = pointer.get(); // 这样不会增加引用计数std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3pointer2.reset();std::cout << "reset pointer2:" << std::endl;std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0, pointer2 已 resetstd::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2pointer3.reset();std::cout << "reset pointer3:" << std::endl;std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0, pointer3 已 reset

成员函数

use_count 返回引用计数的个数

unique 返回是否是独占所有权( use_count 为 1)

swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.

缺陷

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

#include   using namespace std;  #include     class B;    class A  {  public:      shared_ptr ptrA_B;  public:      A()      {          cout << "调用class A的默认构造函数" << endl;      }      ~A()      {          cout << "调用class A的析构函数" << endl;      }  };    class B  {  public:      shared_ptr ptrB_A;  public:      B()      {          cout << "调用class B的默认构造函数" << endl;      }      ~B()      {          cout << "调用class B的析构函数" << endl;      }  };    int main()  {      shared_ptr ptrB = make_shared();      shared_ptr ptrA = make_shared();      ptrA->ptrA_B = ptrB;      ptrB->ptrB_A = ptrA;  }  

运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引 用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样 就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了 内存泄露,如图:

weak_ptr

weak_ptr详解:https://blog.csdn.net/weixin_45590473/article/details/113057545

解决shared_ptr的问题的办法就是使用弱引用指针 std::weak_ptr,std::weak_ptr 是一种弱引用(相比较 而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加,由此我们引入了弱指针。

当换用弱引用时候,最终的 释放流程如图:

在上图中,最后一步只剩下 B,而 B 并没有任何智能指针引用它,因此这块内存资源也会被释放。 std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它的唯一作用就是用于 检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false,否则返回 true。

总结

智能指针这种技术并不新奇,在很多语言中都是一种常见的技术,现代 C++ 将这项技术引进,在 一定程度上消除了 new/delete 的滥用,是一种更加成熟的编程范式。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/119399.html

相关文章

  • Laravel 5.4 入门系列 13. 终篇: 小白能看懂的 Laravel 核心概念讲解

    摘要:但是服务通常由服务提供者来管理的。小结通过上述的例子,基本上可以理解服务容器和服务提供者的使用。懂得了服务容器和服务提供者,理解门面也就不难了。 自动依赖注入 什么是依赖注入,用大白话将通过类型提示的方式向函数传递参数。 实例 1 首先,定义一个类: /routes/web.php class Bar {} 假如我们在其他地方要使用到 Bar 提供的功能(服务),怎么办,直接传入参数即...

    BenCHou 评论0 收藏0
  • 入门级解读:小白能看懂的TensorFlow介绍

    摘要:成本函数成本对于线性回归,成本函数是表示每个预测值与其预期结果之间的聚合差异的某些函数对于逻辑回归,是计算每次预测的正确或错误的某些函数。成本函数的变换涉及到预测结果和实际结果之间数值距离的任何函数都不能作为成本函数。 矩阵和多特征线性回归快速回顾之前文章的前提是:给定特征——任何房屋面积(sqm),我们需要预测结果,也就是对应房价($)。为了做到这一点,我们:我们找到一条「最拟合」所有数据...

    felix0913 评论0 收藏0
  • 【C++从0到1】新手都能看懂的C++入门(上篇),建议收藏

    摘要:上面这三种均不造成重载,现在来说明原因。结论对于引用返回,返回的对象必须是栈帧销毁后还存在的。全局,静态,未销毁的函数栈帧当中的都是可以的指针与引用如图两者底层实现差不多,引用是用指针模拟的。不建议声明和定义分离,分离会导致链接错误。 ...

    xcold 评论0 收藏0
  • 30岁零基础自学编程,先学哪种语言最好?

    摘要:大学,光学工程研究生毕业,和程序猿完全不搭边。那怎么办,试着学一学呗,学习才是程序猿的天性。所以我在想程序猿是不是都需要新知识刺激一下,才能保持兴奋的头脑。有句话说的很对程序猿就像好奇的猫,追着毛球的线头玩,最后一个毛球在脑袋里搅浆糊。 说说我自己的经历。211大学,光学工程研究生毕业,和程序猿完全不搭边。 毕业后进了成都某国字头研究所,在行业里摸爬滚打了四年,2018年机缘巧合在家养...

    xietao3 评论0 收藏0
  • 程序员为什么值得写博客

    摘要:写的人越来越想,阅读的人越来越多的这个信息冗余的年代,会写就代表会思考转载保留程序员为什么值得写博客为什么要写博文写一篇博文意味着要花一定的时间,有时候可能是一个小时,有时候可能会更多,于是人们开始去。 Hire Great Writers 仿佛这是写给自己看的,不过这在其中也有着相当有趣的意义 。虽然自己算是一个能写的人,或许这算是一种不算才华的才华,写博文的意义通常不会在于去描述...

    WrBug 评论0 收藏0

发表评论

0条评论

includecmath

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<