前言
本文的内容将专门对付内存管理,培养起有借有还的好习惯,方可消除资源管理的问题。
正文
所谓的资源就是,一旦用了它,将来必须还给系统。如果不是这样,糟糕的事情就会发生。
C++ 程序内常见的资源:
无论哪一种资源,重要的是,当你不再使用它时,必须将它还给系统,有借有还是个好习惯。
细节 01 :以对象管理资源
把资源放在析构函数,交给析构函数释放资源
假设某个 class 含有个工厂函数,该函数获取了对象的指针:
- A* createA(); // 返回指针,指向的是动态分配对象。
- // 调用者有责任删除它。
如上述注释所言,createA 的调用端使用了函数返回的对象后,有责任删除它。现在考虑有个f函数履行了这个责任:
- void f()
- {
- A *pa = createA(); // 调用工厂函数
- ... // 其他代码
- delete pa; // 释放资源
- }
这看起来稳妥,但存在若干情况f函数可能无法执行到delete pa语句,也就会造成资源泄漏,例如如下情况:
当然可以通过谨慎地编写程序可以防止这一类错误,但你必须想想,代码可能会在时间渐渐过去后被修改,如果是一个新手没有注意这一类情况,那必然又会再次有内存泄漏的可能性。
为确保 A 返回的资源都是被回收,我们需要将资源放进对象内,当对象离开作用域时,该对象的析构函数会自动释放资源。
「智能指针」是个好帮手,交给它去管理指针对象。
对于是由动态分配(new)于堆内存的对象,指针对象离开了作用域并不会自动调用析构函数(需手动delete),为了让指针对象能像普通对象一样,离开作用域自动调用析构函数回收资源,我们需要借助「智能指针」的特性。
常用的「智能指针」有如下三个:
std::auto_ptr
下面示范如何使用 std::auto_ptr 以避免 f 函数潜在的资源泄漏可能性:
- void f()
- {
- std::auto_ptr<A> pa (createA()); // 调用工厂函数
- ... // 一如既往的使用pa
- } // 离开作用域后,经由 auto_ptr 的析构函数自动删除pa;
- std::auto_ptr<A> pa1(createA()); // pa1 指向 createA 返回物
- std::auto_ptr<A> pa2(pa1); // 现在 pa2 指向对象,pa1将被设置为 null
- pa1 = pa2; // 现在 pa1 指向对象,pa2 将被设置为 null
- std::unique_ptr<A> pa1(createA()); // pa1 指向 createA 返回物
- std::unique_ptr<A> pa2(pa1); // 编译出错!
- pa1 = pa2; // 编译出错!
- void f()
- {
- std::shared_ptr<A> pa1(createA()); // pa1 指向 createA 返回物
- std::shared_ptr<A> pa2(pa1); // 引用计数+1,pa2和pa1指向同一个内存
- pa1 = pa2; // 引用计数+1,pa2和pa1指向同一个内存
- }
- std::shared_ptr<A> pA(createA());
假设你希望以某个函数处理 A 对象,像这样: 你想这么调用它:
- std::shared_ptr<A> pA(createA());
- getInfo(pA); // 错误!!
会编译错误,因为 getInfo 需要的是 A 指针对象,而不是类型为std::shared_ptr 的对象。
这时候就需要用 std::shared_ptr 智能指针提供的 get 成员函数访问原始的资源:
- std::shared_ptr<A> pA(createA());
- getInfo(pA.get()); // 很好,将 pA 内的原始指针传递给 getInfo
智能指针「隐式」转换的方式,是通过指针取值操作符。 智能指针都重载了指针取值操作符(operator->和operator*),它们允许隐式转换至底部原始指针:
- class A
- {
- public:
- bool isExist() const;
- ...
- };
- A* createA(); // 工厂函数,创建指针对象
- std::shared_ptr<A> pA(createA()); // 令 shared_ptr 管理对象资源
- bool exist = pA->isExist(); // 经由 operator-> 访问资源
- bool exist2 = (*pA).isExist(); // 经由 operator* 访问资源
多数设计良好的 classes 一样,它隐藏了程序员不需要看到的部分,但是有程序员需要的所有东西。 所以对于自身设计 RAII classes 我们也要提供一个「取得其所管理的资源」的办法。 小结 - 请记住 细节 04:成对使用 new 和 delete以下动作有什么错? 每件事情看起来都井然有序。使用了 new,也搭配了对应的 delete。但还是有某样东西完全错误。strArray 所含的 100 个 string 对象中的 99 个不太可能被适当删除,因为它们的析构函数很可能没有被调用。 当使用 new ,有两件事发生: 当使用 delete,也会有两件事情: delete 的最大问题在于:即将被删除的内存之内究竟有多少对象?这个答案决定了需要执行多少个析构函数。 对象数组所用的内存通常还包括「数组大小」的记录,以便 delete 知道需要调用多少次析构函数。单一对象的内存则没有这笔记录。你可以把两者不同的内存布局想象如下,其中 n 是数组大小: 当你对着一个指针使用 delete,唯一能够让 delete 知道内存中是否存在一个「数组大小记录」的办法就是:由你告诉它。如果你使用 delete 时加上中括号[],delete 便认定指针指向一个数组,否则它便认定指针指向一个单一对象: 游戏规则很简单: 如果你在 new 表达式中使用[],必须在相应的 delete 表达式也使用[]。 如果你在 new 表达式中不使用[],一定不要在相应的 delete 表达式使用[]。 小结 - 请记住 细节 05:以独立语句将 newed 对象置入智能指针 假设我们有个以下示范的函数:
- int getNum();
- void fun(std::shared_ptr<A> pA, int num);
- fun(std::shared_ptr<A>(new A), getNum());
令人想不到吧,上述调用却可能泄露资源。接下来我们来一步一步的分析为什么存在内存泄漏的可能性。
在进入 fun 函数之前,肯定会先执行各个实参。上述第二个实参只是单纯的对getNum 函数的调用,但第一个实参 std::shared_ptr(new A) 由两部分组成:
- std::shared_ptr<A> pA(new A); // 先构造智能指针对象
- fun(pA, getNum()); // 这个调用动作绝不至于造成泄漏。
以上的方式,就能避免原本由于次序导致内存泄漏发生。 小结 - 请记住
像很多事情一样,在COVID-19疫情期间,并购活动从部分虚拟转变为完全虚拟活动。...
【51CTO.com快译】传统上,衡量数据中心的电力容量通常是由其最大IT负载或输入电...
现在,很多开源库都支持构建应用程序。我应该向你推荐一些库,它们可以帮助启动...
【51CTO.com原创稿件】面试官:Redis 有哪几种数据类型?存储原理是什么?具体适应...
数据中心是什么? 数据中心是IDC(Internet Data Center) 的名词释义,机房是对IDC...
随着冠状病毒在今年早些时候席卷了华盛顿州,Providence很快便不得不使数千名员...
长期以来,人们都知道信息安全领域一直是人员不足,资金不足,疫情之前的确是这...
埃森哲公司的全国学徒制项目正在开辟全新的、多样化的人才渠道,以招募那些上进...
本文转载自微信公众号「三太子敖丙」,作者三太子敖丙。转载本文请联系三太子敖...
国外服务器网站会被百度收录吗 ?这是大部分想要租用国外服务器部署网站的站长们...