前言
本文的内容将专门对付内存管理,培养起有借有还的好习惯,方可消除资源管理的问题。
正文
所谓的资源就是,一旦用了它,将来必须还给系统。如果不是这样,糟糕的事情就会发生。
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<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* 访问资源
- 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()); // 这个调用动作绝不至于造成泄漏。
以上的方式,就能避免原本由于次序导致内存泄漏发生。 小结 - 请记住