大奖18dj18vip-大奖18dj18娱乐官网

【腾讯云】618云聚惠,百款云产品限量抢购,1核2G云服务器首年95元 http://cloud.tencent.com/act/cps/redirect?redirect=1059&cps_key=9f7b3aa0b9ee11c8648ef8bf9d4f15a9&from=console

大奖18dj18vip

查看: 9|回复: 0
打印 上一主题 下一主题

[资讯] 借来的资源,如何还的潇洒?

[复制链接]
  • TA的每日心情
    擦汗
    昨天 14:55
  • 签到天数: 635 天

    [LV.9]以坛为家II

    硕士生

    1万

    主题

    1万

    帖子

    3万

    积分

    Rank: 8Rank: 8

    UID
    15343
    威望
    -561
    贡献
    8107
    在线时间
    321 小时
    注册时间
    2015-10-12
    跳转到指定楼层
    楼主

    前言

    本文的内容将专门对付内存管理,培养起有借有还的好习惯,方可消除资源管理的问题。

    正文

    所谓的资源就是,一旦用了它,将来必须还给系统。如果不是这样,糟糕的事情就会发生。

    C++ 程序内常见的资源:

    • 动态分配内存
    • 文件描述符
    • 互斥锁
    • 图形页面中的字型和笔刷
    • 数据库连接
    • 网络 sockets

    无论哪一种资源,重要的是,当你不再使用它时,必须将它还给系统,有借有还是个好习惯。


    细节 01 :以对象管理资源

    把资源放在析构函数,交给析构函数释放资源

    假设某个 class 含有个工厂函数,该函数获取了对象的指针:

    • A* createA();    // 返回指针,指向的是动态分配对象。
    •                  // 调用者有责任删除它。

    如上述注释所言,createA 的调用端使用了函数返回的对象后,有责任删除它。现在考虑有个f函数履行了这个责任:

    • void f()
    • {
    •     A *pa = createA();  // 调用工厂函数
    •     ...                 // 其他代码
    •     delete pa;          // 释放资源
    • }

    这看起来稳妥,但存在若干情况f函数可能无法执行到delete pa语句,也就会造成资源泄漏,例如如下情况:

    • 或许因为「…」区域内的一个过早的 return 语句;
    • 或许因为「…」区域内的一个循环语句过早的continue 或 goto 语句退出;
    • 或许因为「…」区域内的语句抛出异常,无法执行到 delete。

    当然可以通过谨慎地编写程序可以防止这一类错误,但你必须想想,代码可能会在时间渐渐过去后被修改,如果是一个新手没有注意这一类情况,那必然又会再次有内存泄漏的可能性。

    为确保 A 返回的资源都是被回收,我们需要将资源放进对象内,当对象离开作用域时,该对象的析构函数会自动释放资源。

    「智能指针」是个好帮手,交给它去管理指针对象。

    对于是由动态分配(new)于堆内存的对象,指针对象离开了作用域并不会自动调用析构函数(需手动delete),为了让指针对象能像普通对象一样,离开作用域自动调用析构函数回收资源,我们需要借助「智能指针」的特性。

    常用的「智能指针」有如下三个:

    • std::auto_ptr( C++ 98 提供、C++ 11 建议摒弃不用 )
    • std::unique_ptr( C++ 11 提供 )
    • std::shared_ptr( C++ 11 提供 )

    std::auto_ptr

    下面示范如何使用 std::auto_ptr 以避免 f 函数潜在的资源泄漏可能性:

    • void f()
    • {
    •     std::auto_ptr[url=]pa (createA()); // 调用工厂函数
    •     ...  // 一如既往的使用pa
    • }        // 离开作用域后,经由 auto_ptr 的析构函数自动删除pa;

    这个简单的例子示范「以对象管理资源」的两个关键想法:

    • 获得资源后立刻放进管理对象内。以上代码中 createA 返回的资源被当做其管理者 auto_ptr 的初值,也就立刻被放进了管理对象中。
    • 管理对象运用析构函数确保资源释放。不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用,于是资源被释放。

    为什么在 C++11 建议弃用 auto_ptr 吗?当然是 auto_ptr 存在缺陷,所以后续不被建议使用。

    auto_ptr 有一个不寻常的特质:若通过「复制构造函数或赋值操作符函数」 copy 它们,它们会变成 null ,而复制所得的指针将获取资源的唯一拥有权!

    见如下例子说明:



    • std::auto_ptr[/url][url=]pa1(createA()); // pa1 指向 createA 返回物
    • std::auto_ptr[/url][url=]pa2(pa1); // 现在 pa2 指向对象,pa1将被设置为 null
    • pa1 = pa2; // 现在 pa1 指向对象,pa2 将被设置为 null

    这一诡异的复制行为,如果再次使用指向为 null 的指针,那必然会导致程序奔溃。

    意味着 auto_ptr 并非管理动态分配资源的神兵利器。

    std::unique_ptr

    unique_ptr 也采用所有权模型,但是在使用时,是直接禁止通过复制构造函数或赋值操作符函数 copy 指针对象,如下例子在编译时,会出错:



    • std::unique_ptr[/url][url=]pa1(createA()); // pa1 指向 createA 返回物
    • std::unique_ptr[/url][url=]pa2(pa1); // 编译出错!
    • pa1 = pa2; // 编译出错!

    std::shared_ptr

    shared_ptr 在使用复制构造函数或赋值操作符函数后,引用计数会累加并且两个指针对象指向的都是同一个块内存,这就与 unique_ptr、auto_ptr 不同之处。


    • void f()
    • {
    •     std::shared_ptr[/url][url=]pa1(createA()); // pa1 指向 createA 返回物
    •     std::shared_ptr[/url][url=]pa2(pa1); // 引用计数+1,pa2和pa1指向同一个内存
    •     pa1 = pa2; // 引用计数+1,pa2和pa1指向同一个内存
    • }

    当一个对象离开作用域,shared_ptr 会把引用计数值 -1 ,直到引用计数值为 0 时,才会进行删除对象。

    由于 shared_ptr 释放空间时会事先要判断引用计数值的大小,因此不会出现多次删除一个对象的错误。

    小结 - 请记住

    • 为防止资源泄漏,请使用 RAII(Resource Acquisition Is Initaliaztion - 资源取得时机便是初始化时机) 对象,它们在构造函数中获取资源,并在析构函数中是释放资源
    • 两个建议使用的 RAII classes 分别是 std::unique_ptr 和 std::shared_ptr。前者不允许 copy 动作,后者允许 copy 动作。但是不建议用 std::auto_ptr,若选 auto_ptr,复制动作会使它(被复制物)指向 null 。

    细节 02:在资源管理类中小心 copying 行为

    假设,我们使用 C 语音的 API 函数处理类型为 Mutex 的互斥对象,共有 lock 和 unlock 两函数可用:

    • void locak(Mutex *pm);  // 锁定 pm 所指的互斥器
    • void unlock(Mutex* pm); // 将互斥器解除锁定

    为确保绝不会忘记一个被锁住的 Mutex 解锁,我们可能会希望创立一个 class 来管理锁资源。这样的 class 要遵守 RAII 守则,也就是「资源在构造期间获得,在析构释放期间释放」:

    • class Lock
    • {
    • public:
    •     explicit Lock(Mutex *pm) // 构造函数
    •         : pMutex(pm)
    •     {
    •         lock(pMutex);
    •     }
    •     ~Lock()  // 析构函数
    •     {
    •         unlock(pMutex);
    •     }
    • private:
    •     Mutex* pMutex;
    • };

    这样定义的 Lock,用法符合 RAII 方式:

    • Mutex m;      //定义你需要的互斥锁
    • ...  
    • {                 // 建立一个局部区块作用域
    •     Lock m1(&m);  // 锁定互斥器
    •     ...
    • }                 // 在离开区块作用域,自动解除互斥器锁定

    这很好,但如果 Lock 对象被复制,会发生什么事情?

    • Lock m1(&m);  // 锁定m
    • Lock m2(&m1); // 将 m1 复制到 m2身上,这会发生什么?

    这是我们需要思考和面对的:「当一个 RAII 对象被复制,会发生什么事情?」大多数时候你会选择以下两种可能:

    • 禁止复制。如果 RAII 不允许被复制,那我们需要将 class 的复制构造函数和赋值操作符函数声明在 private。
    • 使用引用计数法。有时候我们希望保有资源,直到它的最后一个对象被消耗。这种情况下复制 RAII 对象时,应该将资源的「被引用数」递增。std::shared_ptr 便是如此。

    如果前述的 Lock 打算使用使用引用计数法,它可以使用 std::shared_ptr 来管理 pMutex 指针,然后很不幸 std::shared_ptr 的默认行为是「当引用次数为 0 时删除其所指物」那不是我们想要的行为,因为要对Mutex释放动作是解锁而非删除。

    幸运的是 std::shared_ptr 允许指定自定义的删除方式,那是一个函数或函数对象。如下:

    • class Lock
    • {
    • public:
    •     explicit Lock(Mutex *pm)   
    •         : pMutex(pm, unlock)  // 以某个 Mutex 初始化 shared_ptr,
    •                               // 并以 unlock 函数为删除器。
    •     {
    •         lock(pMutex.get());  // get 获取指针地址
    •     }
    • private:
    •     std::shared_ptr pMutex; // 使用 shared_ptr
    • };

    请注意,本例的 Lock class 不再声明析构函数。因为编译器会自动创立默认的析构函数,来自动调用其 non-static 成员变量(本例为 pMutex )的析构函数。

    而 pMutex 的析构函数会在互斥器的引用次数为 0 时,自动调用 std::shared_ptr 的删除器(本例为 unlock )。

    小结 - 请记住

    • 复制 RAII 对象必须一并复制它的所管理的资源(深拷贝),所以资源的 copying 行为决定 RAII 对象的 copying 行为。
    • 普通而常见的 RAII class copying 行为是:禁止 copying、执行引用计数法。

    细节 03 :在资源类中提供对原始资源的访问

    智能指针「显式」转换,也就是通过 get 成员函数的方式转换为原始指针对象。

    上面提到的「智能指针」分别是:std::auto_ptr、std::unique_ptr、std::shared_ptr。它们都有访问原始资源的办法,都提供了一个 get 成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件)。

    举个例子,使用智能指针如 std::shared_ptr 保存 createA() 返回的指针对象 :


    • std::shared_ptr[/url][url=]pA(createA());

    假设你希望以某个函数处理 A 对象,像这样:

    • int getInfo(const A* pA);

    你想这么调用它:


    • std::shared_ptr[/url][url=]pA(createA());
    • getInfo(pA);       // 错误!!

    会编译错误,因为 getInfo 需要的是 A 指针对象,而不是类型为std::shared_ptr 的对象。


    这时候就需要用 std::shared_ptr 智能指针提供的 get 成员函数访问原始的资源:


    • std::shared_ptr[/url][url=]pA(createA());
    • getInfo(pA.get());   // 很好,将 pA 内的原始指针传递给 getInfo

    智能指针「隐式」转换的方式,是通过指针取值操作符。

    智能指针都重载了指针取值操作符(operator->和operator*),它们允许隐式转换至底部原始指针:


    • class A
    • {
    • public:
    •     bool isExist() const;
    •     ...
    • };
    • A* createA();  // 工厂函数,创建指针对象
    • std::shared_ptr[/url][url=]pA(createA()); // 令 shared_ptr 管理对象资源
    • bool exist = pA->isExist();    // 经由 operator-> 访问资源
    • bool exist2 = (*pA).isExist(); // 经由 operator* 访问资源

    多数设计良好的 classes 一样,它隐藏了程序员不需要看到的部分,但是有程序员需要的所有东西。

    所以对于自身设计 RAII classes 我们也要提供一个「取得其所管理的资源」的办法。

    小结 - 请记住

    • APIs 往往要求访问原始资源,所以每一个 RAII class 应该提供一个「取得其所管理的资源」的办法。
    • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换比较方便。

    细节 04:成对使用 new 和 delete以下动作有什么错?

    • std::string* strArray = new std::string[100];
    • ...
    • delete strArray;

    每件事情看起来都井然有序。使用了 new,也搭配了对应的 delete。但还是有某样东西完全错误。strArray 所含的 100 个 string 对象中的 99 个不太可能被适当删除,因为它们的析构函数很可能没有被调用。

    当使用 new ,有两件事发生:

    • 内存被分配出来(通过名为 operator new 的函数)
    • 针对此内存会有一个或多个构造函数被调用

    当使用 delete,也会有两件事情:

    • 针对此内存会有一个或多个析构函数被调用
    • 然后内存才被释放(通过名为 operator delete 的函数)

    delete 的最大问题在于:即将被删除的内存之内究竟有多少对象?这个答案决定了需要执行多少个析构函数。

    对象数组所用的内存通常还包括「数组大小」的记录,以便 delete 知道需要调用多少次析构函数。单一对象的内存则没有这笔记录。你可以把两者不同的内存布局想象如下,其中 n 是数组大小:

    当你对着一个指针使用 delete,唯一能够让 delete 知道内存中是否存在一个「数组大小记录」的办法就是:由你告诉它。如果你使用 delete 时加上中括号[],delete 便认定指针指向一个数组,否则它便认定指针指向一个单一对象:

    • std::string* strArray = new std::string[100];
    • std::string* strPtr = new std::strin;
    • ...  
    • delete [] strArray;  // 删除一个对象
    • delete strPtr;       // 删除一个由对象组成的数组

    游戏规则很简单:

    如果你在 new 表达式中使用[],必须在相应的 delete 表达式也使用[]。

    如果你在 new 表达式中不使用[],一定不要在相应的 delete 表达式使用[]。

    小结 - 请记住

    • 如果你在 new 表达式中使用[],必须在相应的 delete 表达式也使用[]。如果你在 new 表达式中不使用[],一定不要在相应的 delete 表达式使用[]。

    细节 05:以独立语句将 newed 对象置入智能指针

    假设我们有个以下示范的函数:


    • int getNum();
    • void fun(std::shared_ptr[/url][url=]pA, int num);

    现在考虑调用 fun:

    • fun(new A(), getNum());

    它不能通过编译,因为 std::shared_ptr 构造函数需要一个原始指针,而且该构造函数是个 explicit 构造函数,无法进行隐式转换。如果写成这样就可以编译通过:


    • fun(std::shared_ptr[/url][url=](new A), getNum());

    令人想不到吧,上述调用却可能泄露资源。接下来我们来一步一步的分析为什么存在内存泄漏的可能性。


    在进入 fun 函数之前,肯定会先执行各个实参。上述第二个实参只是单纯的对getNum 函数的调用,但第一个实参 std::shared_ptr(new A) 由两部分组成:


    • 执行 new A 表达式
    • 调用 std::shared_ptr 构造函数

    于是在调用 fun 函数之前,先必须做以下三件事:

    • 调用 getNum 函数
    • 执行 new A 表达式
    • 调用 std::shared_ptr 构造函数

    那么他们的执行次序是一定如上述那样的吗?可以确定的是 new A 一定比std::shared_ptr 构造函数先被执行。但对 getNum 调用可以排在第一或第二或第三执行。

    如果编译器选择以第二顺位执行它:

    • 执行 new A 表达式
    • 调用 getNum 函数
    • 调用 std::shared_ptr 构造函数

    万一在调用 getNum 函数发生了异常,会发生什么事情?在此情况下 new A 返回的指针将不会置入 std::shared_ptr 智能指针里,就存在内存泄漏的现象。

    避免这类问题的办法很简单:使用分离语句。

    分别写出:

    • 创建 A
    • 将它置入一个智能指针内
    • 然后再把智能指针传递给 fun 函数。

    • std::shared_ptr[/url][url=]pA(new A); // 先构造智能指针对象
    • fun(pA, getNum()); // 这个调用动作绝不至于造成泄漏。

    以上的方式,就能避免原本由于次序导致内存泄漏发生。

    小结 - 请记住

    • 以独立语句将 newed (已 new 过) 对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
    [/url]
    大奖18dj18vip社区温馨提示:
    大奖18dj18vip(www.dastanona.com)十分重视网络版权及其他知识产权的保护,针对网络侵权采取如下版权政策:
    1、大奖18dj18vip有理由相信网友侵犯任何人的版权或作品,(图文,文字,下载,视频,非法传播),大奖18dj18vip有权不事先通知即删除涉嫌侵权的作品和内容
    2、大奖18dj18vip将采取必要的网络技术手段,确认为侵权作品或内容的用户有权进行警告、屏蔽、删除的行为,尽可能的防止侵权行为的发生
    3、大奖18dj18vip影视资源均收集自互联网,没有提供影片资源存储,也未参与录制上传,若大奖18dj18vip收录的资源涉及您的版权或知识产权或其他利益,我们会立即删除
    4、大奖18dj18vip,删帖,投诉,举报,侵权,若大奖18dj18vip侵犯您的权益,附上身份及权利证明,请直接发送邮件到 kefu-sosoba@qq.com 我们将在一个工作日内删除
    soso大奖18dj18vip社区是聚合百度搜索,搜狗搜索,360搜索,新闻,教育,站长,广告,娱乐,影视,微信,网盘,营销,手机,汽车,游戏,论坛综合为一体的大型门户社区www.dastanona.com
    【腾讯云】腾讯云服务器安全可靠高性能,多种配置供您选择
    Powered by www.dastanona.com Copyright © 2013-2020 大奖18dj18vip社区 小黑屋|手机版|地图|关于我们|腾讯云代金券|帮助中心|公共DNS|大奖18dj18vip
    广告服务/项目合作: kefu-sosoba@qq.com  侵权举报邮箱: kefu-sosoba@qq.com  大奖18dj18vip建站时间:创建于2013年07月23日
    免责声明:大奖18dj18vip所有的内容均来自互联网以及第三方作者自由发布,版权归原作者版权所有,大奖18dj18vip不承担任何的法律责任,若有侵权请来信告知,我们立即删除!

    GMT+8, 2020-5-26 16:47 , Processed in 1.151207 second(s), 9 queries , MemCache On.

    快速回复 返回顶部 返回列表