• 没有内存泄漏的C++代码,如何用RAII编写,你知道吗


    在有垃圾回收(GC)的编程语言里面,比如Java, Python, Node, Go,不需要程序员随时注意内存是否泄漏了,因为它们自带垃圾回收(负责帮你收拾“残羹剩饭”)(有GC的语言也是存在内存泄漏的)。而C++则需要程序员细心认真的处理内存,避免内存泄漏。没有内存泄漏意味着任何申请的内存都要被释放。


     

    但是实际操作层面,如何实现呢?C++的解决方案是RAII和构建在其上面的引用计数。没有RAII,一切都是空中楼阁。Java 7学了五分RAII,引入了ARM(Automatic Resource Management,也就是try-with-resource);Rust是完完全全学到了;Go有defer,神似RAII。

    例子

    为什么RAII是基石?

    程序员每次申请的内存(通过new, malloc,等等)都需要释放。不释放就会造成内存泄漏,而内存泄漏就会出现没有内存可用的情况,触发系统的OOM,程序从而被KILL掉。

    在远古时代,没有内存泄漏靠的是程序员编写代码的认真细心,自己在申请内存,记得在必要的时候delete掉,比如

    // 代码1

    void f() {

      int*p = new int{3};

      int error = doSomething(p);

      if (error)

          return;

      finalize(p);

      delete p;

    }

    代码1一开始申请了内存,存储int,在最后函数结束的时候,释放内存通过delete p。


     

    看起来这么做,似乎很容易嘛。自己使用记得删除不就行了嘛~可实际上,这么做问题很大!比如上面的代码就有可能内存泄漏。因为假设doSomething 返回了错误,函数就会提前结束,而delete p就不会执行导致内存泄漏了!有人可能会说,你代码写得渣怪谁!那好吧,我们把错误处理加上,代码如下

    // 代码2

    void f() {

      int*p = new int{3};

      int error = doSomething(p);

      if (error) {

          delete p; //释放内存,当出现错误的时候

          return;

        }

      finalize(p);

      delete p;

    }

    是不是添加了上面的错误处理,就不会有内存泄漏了?不!上面的代码可能还存在内存泄漏。如果doSomething抛出异常,那么两个delete p都不会被执行,内存泄漏!

    有人可能会说,事儿怎么这么多,直接加上try catch,在catch的时候释放内存不就OK了嘛?实际上并不OK,因为加上try catch犯的是更严重的错误。而且就算加了,代码还是可能会内存泄漏。因为哪天有程序员(可能是自己)增加新的代码的时候,就可能就忘记delete了。


     

    所以这种靠程序员细致认真的方法,是不靠谱的!

    (Java之父给的答案是,老子不用C++了)

    分析

    因为依赖程序员的记得,有下面的问题:

    代码结束分支太多,遗漏其中一个就会造成泄漏,比如代码1就遗漏的错误分支,异常分支。

    代码会经常修改维护,中间某一次修改可能会增加新的分支而遗忘。特别是在函数又长又臭的时候。(那不写得又长又臭不就可以了嘛?假设你写的代码很美,但是你还是要维护别人的又臭又长的代码)


     

    如果多次释放相同的内存(delete 同一个指针多次)你将会面对更严重的问题。内存泄漏直接的最坏结果是没有内存可用,程序被杀掉。但是现在你面对的是undefined,C++里面最难搞的之一,意味着你的程序员可能莫名奇妙crash,而且随机。想想你熬多少个夜晚,而且还不一定能找到bug。(后续会讲解如何系统地分析crash,减少熬的夜晚)。

    程序员水平深浅不一样,C++里面坑很多,有时候程序员时刻提醒自己,但是他不知道要不要释放啊,什么时候要释放啊。比如调用一个函数 int *p = getArray()。getArrray不是他写的,那么他怎么知道要释放。编译器又不会告诉他。(后续会讲解如何要编译器告诉我们有内存泄漏)

    这四个问题,想想就不容易解决。C++对此提供的解决方案是RAII,以及构建在RAII上面的reference count(引用计数)。


     

    RAII 全称就 Resource acquisition is initialization . 意为资源获取要通过构造函数初始化,然后析构函数负责释放资源。大部分时候又被用于做Scope Guard,Scope Guard同lambda服用,效果更佳。

    RAII,C++之父当年取得比较"随意“的名字。表达的意思就是我们获取资源的时候要通过构造函数初始化。构造函数就不细说(要说起来内容也很多),它就是C++对象的初始化函数。


     

    为什么推荐RAII来管理资源(内存是资源的一种)?首先我们要明白堆对象和栈对象的区别。堆对象就是我们通过new, malloc 动态获取的内存,栈对象就是在存在栈上面的。这里关心的区别是栈对象在失效的时候会自动调用析构函数。为什么呢?因为编译器是知道栈对象什么时候会失效。比如代码1的error,编译器知道它的作用域,只要离开了它的作用域,栈对象自然要析构。


     

    这样子,我们就解决了问题1,因为栈对象会自动通用析构函数,那么我们根本不用关心函数具体从哪个分支结束。我们要的是”我不管你怎么退出,你都要释放内存“。所以我们可以通过创建一个wrapper,代码里面使用wrapper,而不是直接的内存。比如将代码1改成如下

    // 代码3

    class MyInt {

    public:

      int* p;

      MyInt(int i) {

      p = new int{i};

      }

      ~MyInt() {

        delete p;

      }

    };

    void f() {

      MyInt my(3);

      int error = doSomething(my.p);

      if (error) {

          return;

        } 

      finalize(p);

    }

    代码3只需要在MyInt的析构函数delete p,而函数f里面完全不用care哪里需要delete p。而这么写,不管函数如何退出,我们都会保证p指向的内存被释放,因为my是栈对象,编译器会帮我们在各个分支退出的时候插入调用析构函数。(而这么做没有overhead,不会产生性能影响。实际上C++标准库提供了unique_ptr供我们使用,不用自己编写MyInt)


     

    问题1解决了,而问题2和3也同时解决了。因为当保证函数退出的时候内存被释放且只有一次,问题2和3也就解决了。

    对于问题4,构建在RAII上面的引用计数就是用来解决这个的。这个问题需要更长的篇幅来阐述,那么就且见下文分解吧。

    对于这里的代码,我们也可以使用基于RAII的Scope Guard来帮助我们释放内存,代码如下

    // 代码4

    class ScopeGuard {

      std::function<void()> func;

    public:

      ScopeGuard(std::function<void> ifunc) {

        func = ifunc;

      }

      ~ScopeGuard() {

        func();

      }

    };

    void f() {

      int* p = new int{3}

      ScopeGuard s([&p]() { if (p) delete p});

      int error = doSomething(my.p);

      if (error) {

          return;

        } 

      finalize(p);

    }

    在这里ScopeGuard就帮我们保证函数结束时,不论怎么退出,都会delete当前的指针。


     

    总结

    RAII替我们实现了”某个操作在任何分支结束的时候,会被执行,且被执行一次“。而利用这个保证,我们可以不再那么麻烦和痛苦的叮嘱自己一定要记得释放内存,一定不要释放两次及以上。而任何需要这个效果的都可以而且也推荐使用RAII来实现,不要依赖程序员的自觉和认真!

    各位朋友如果有兴趣或是想与我们一起学习计算机技术(软件开发),“点击链接”加入C/C++企鹅圈,还有一些你可能不知道的你感兴趣的事。无论你是大牛还是小白,是想转行还是想入行,都可以来了解一起进步一起学习!

  • 相关阅读:
    Selenium + WebDriver 各浏览器驱动下载地址
    selenium之 文件上传所有方法整理总结【转】
    FakeUserAgentError('Maximum amount of retries reached') 彻底解决办法
    git关联远程仓库
    通过chrome console 快速获取网页连接
    【转】Selenium
    【转】fiddler抓包HTTPS请求
    【转】Wireshark和Fiddler分析Android中的TLS协议包数据(附带案例样本)
    php 通过 create user 和grant 命令无法创建数据库用户和授权的解决办法
    差等生也是需要交卷的
  • 原文地址:https://www.cnblogs.com/mu-ge/p/13827382.html
Copyright © 2020-2023  润新知