我很喜欢 C++11 的智能指针. 在很多时候, 对很多讨厌自己管理内存的人来说是天赐的礼物. 在我看来, C++11 的智能指针能使得 C++ 新手教学更简单.
其实, 我已经使用 C++ 两年多了, 我无意中发现多种错误使用 C++11 智能指针的案例, 这些错误会使程序效率很低或者直接崩溃. 为了方便查找, 我把它们按照下文进行了归类.
在开始之前, 我们用一个简单的 Aircraft 类来展示一下这些错误.
- class Aircraft
- {
- private:
- string m_model;
- public:
- int m_flyCount;
- weak_ptr<aircraft> myWingMan;
- void Fly()
- {
- cout <<"Aircraft type" << m_model << "is flying !" << endl;
- }
- Aircraft(string model)
- {
- m_model = model;
- cout << "Aircraft type" << model << "is created" << endl;
- }
- Aircraft()
- {
- m_model = "Generic Model";
- cout << "Generic Model Aircraft created." << endl;
- }
- ~Aircraft()
- {
- cout << "Aircraft type" << m_model << "is destroyed" << endl;
- }
- };
- </aircraft>
错误 #1: 当唯一指针够用时却使用了共享指针
我最近在一个继承的代码库项目中工作, 它使用了一个 shared_ptr(译者注: 共享指针)创建和管理所有的对象. 我分析了这些代码, 发现在 90% 的案例中, 被 shared_ptr 管理的资源并非是共享的.
有两个理由可以指出这是错误的:
1, 如果你真的需要使用独有的资源(对象), 使用 shared_ptr 而不是 unique_ptr 会使你的代码容易出现资源泄露和一些 bug.
不易察觉的 bug: 有没有想过这种情况, 如果有其他程序员无意间通过赋值给另一个共享指针而修改了你共享出来的资源 / 对象, 而你却从没有预料到这种事情!
不必要的资源使用: 即使其他的指针不会修改你的对象资源, 但也可能会过长时间地占用你的内存, 甚至已经超出了原始 shared_ptr 的作用范围.
2, 创建 shared_ptr 比创建 unique_ptr 更加资源密集.
shared_ptr 需要维护一个指向动态内存对象的线程安全的引用计数器以及背后的一个控制块, 这使它比 unique_ptr 更加复杂.
建议 - 默认情况下, 你应该使用 unique_ptr. 如果接下来有共享这个对象所有权的需求, 你依然可以把它变成一个 shared_ptr.
错误 #2: 没有保证 shared_ptr 共享的资源 / 对象的线程安全性!
Shared_ptr 可以让你通过多个指针来共享资源, 这些指针自然可以用于多线程. 有些人想当然地认为用一个 shared_ptr 来指向一个对象就一定是线程安全的, 这是错误的. 你仍然有责任使用一些同步原语来保证被 shared_ptr 管理的共享对象是线程安全的.
建议 - 如果你没有打算在多个线程之间来共享资源的话, 那么就请使用 unique_ptr.
错误 #3: 使用 auto_ptr!
auto_ptr 的特性非常危险, 并且现在已经被弃用了. 当该指针被当作参数进行值传递时会被拷贝构造函数转移所有权, 那么当原始 auto 指针被再次引用时就会造成系统致命的崩溃. 看看下面这个例子:
- int main()
- {
- auto_ptr<aircraft> myAutoPtr(new Aircraft("F-15"));
- SetFlightCountWithAutoPtr(myAutoPtr); // Invokes the copy constructor for the auto_ptr
- myAutoPtr->m_flyCount = 10; // <span style="color: #ff0000;">CRASH !!!</span>
- }
- </aircraft>
建议 - unique_ptr 可以实现 auto_ptr 的所有功能. 你应该搜索你的代码库, 然后找到其中所有使用 auto_ptr 的地方, 将其替换成 unique_ptr. 最后别忘了重新测试一下你的代码!
错误 #4: 没有使用 make_shared 来初始化 shared_ptr!
相较于使用裸指针, make_share 有两个独特的优点:
1. 性能: 当你用 new 创建一个对象的同时创建一个 shared_ptr 时, 这时会发生两次动态申请内存: 一次是给使用 new 申请的对象本身的, 而另一次则是由 shared_ptr 的构造函数引发的为资源管理对象分配的.
- shared_ptr<aircraft> pAircraft(new Aircraft("F-16")); // Two Dynamic Memory allocations - SLOW !!!
- </aircraft>
与此相反, 当你使用 make_shared 的时候, C++ 编译器只会一次性分配一个足够大的内存, 用来保存这个资源管理者和这个新建对象.
- shared_ptr<aircraft> pAircraft = make_shared<aircraft>("F-16"); // Single allocation - FAST !
- </aircraft></aircraft>
2, 在看了 MS 编译器的 memory 头文件实现以后, 我发现当内存分配失败时, 这个对象就会被删除掉. 这样的话使用裸指针初始化也不用担心安全问题了.
建议 - 使用 make_shared 而不是裸指针来初始化共享指针.
错误 #5: 在创建一个对象 (裸指针) 时没有立即把它赋给 shared_ptr.
一个对象应该在被创建的时候就立即被赋给 shared_ptr. 裸指针永远不应该被再次使用.
看看下面则个例子:
- int main()
- {
- Aircraft* myAircraft = new Aircraft("F-16");
- shared_ptr<aircraft> pAircraft(myAircraft);
- cout <<pAircraft.use_count() << endl; // ref-count is 1
- shared_ptr<aircraft> pAircraft2(myAircraft);
- cout <<pAircraft2.use_count() << endl; // ref-count is 1
- return 0;
- }
- </aircraft>
这将会造成 ACCESS VIOLATION(译者注: 非法访问)并导致程序崩溃!!!
这样做的问题是当第一个 shared_ptr 超出作用域时, myAircraft 对象就会被销毁, 当第二个 shared_ptr 超出作用域时, 程序就会再次尝试销毁这个已经被销毁了的对象!
建议 - 如果不使用 make_shared 创建 shared_ptr, 至少应该像下面这段代码一样创建使用智能指针管理的对象:
- shared_ptr<aircraft> pAircraft(new Aircraft("F-16"));
- </aircraft>
错误 #6: 删掉被 shared_ptr 使用的裸指针!
你可以使用 shared_ptr.get()这个 API 从一个 shared_ptr 获得一个裸指针的句柄. 然而, 这是非常冒险的, 应该尽量避免这种情况. 看看下面这段代码:
- void StartJob()
- {
- shared_ptr<aircraft> pAircraft(new Aircraft("F-16"));
- Aircraft* myAircraft = pAircraft.get(); // returns the raw pointer
- delete myAircraft; // myAircraft is gone
- }
- </aircraft>
一旦我们从这个共享指针中获取到对应的裸指针(myAircraft), 我们可能会删掉它. 然而, 当这个函数结束后, 共享指针 pAircraft 就会因为超出作用域而去试图删除 myAircraft 这个已经被删除过的对象, 而这样做的结果就是我们非常熟悉的 ACCESS VIOLATION(非法访问)!
建议 - 在你从共享指针中获取对应的裸指针之前请仔细考虑清楚. 你永远不知道别人什么时候会调用 delete 来删除这个裸指针, 到那个时候你的共享指针 (shared_ptr) 就会出现 Access Violate(非法访问)的错误.
错误 #7: 当使用一个 shared_ptr 指向指针数组时没有使用自定义的删除方法!
看看下面这段代码:
- void StartJob()
- {
- shared_ptr<aircraft> ppAircraft(new Aircraft[3]);
- }
- </aircraft>
这个共享指针将仅仅指向 Aircraft[0] -- Aircraft[1]和 Aircraft[2]将会在智能指针超出作用域时未被删除而造成内存泄露. 如果你在使用 Visual Studio 2015, 就会出现堆损坏 (heap corruption) 的错误.
建议 - 保证在使用 shared_ptr 管理一组对象时总是传递给它一个自定义的删除方法. 下面这段代码就修复了这个问题:
- void StartJob()
- {
- shared_ptr<aircraft> ppAircraft(new Aircraft[3], [](Aircraft* p) {
- delete[] p;
- });
- }
- </aircraft>
错误 #8: 在使用共享指针时使用循环引用!
在很多情况下, 当一个类包含了 shared_ptr 引用时, 就有可能陷入循环引用. 试想以下场景: 我们想要创建两个 Aircraft 对象, 一个由 Maverick 驾驶而另一个是由 Iceman 驾驶的 (我忍不住要引用一下《壮志凌云》(TopGun)!!!).Maverick 和 Iceman 的僚机驾驶员(Wingman) 互相指向对方.
所以我们最初的设计会在 Aircraft 类中引入一个指向自己的 shared_ptr.
- class Aircraft
- {
- private:
- string m_model;
- public:
- int m_flyCount;
- shared_ptr<Aircraft> myWingMan;
- ....
然后在 main()函数中, 创建 Aircraft 型对象 Maverick 和 Goose, 然后给每个对象指定他们的 wingman:
- int main()
- {
- shared_ptr<aircraft> pMaverick = make_shared<aircraft>("Maverick: F-14");
- shared_ptr<aircraft> pIceman = make_shared<aircraft>("Iceman: F-14");
- pMaverick->myWingMan = pIceman; // So far so good - no cycles yet
- pIceman->myWingMan = pMaverick; // now we got a cycle - neither maverick nor goose will ever be destroyed
- return 0;
- }
- </aircraft>
当 main()函数返回时, 我们希望的是这两个共享指针都被销毁 -- 但事实是它们两个都不会被删除, 因为它们之间造成了循环引用. 即使这两个智能指针本身被从栈上销毁, 但由于它们指向的对象的引用计数都不为 0 而使得那两个对象永远不会被销毁.
下面是这段程序运行的输出结果:
- Aircraft type Maverick: F-14 is created
- Aircraft type Iceman: F-14 is created
所以应该怎么修复这个 Bug 呢? 我们应该替换 Aircraft 类中的 shared_ptr 为 weak_ptr! 下面是修改后的 main()程序再次运行的输出结果:
- Aircraft type Maverick: F-14 is created
- Aircraft type Iceman: F-14 is created
- Aircraft type Iceman: F-14 is destroyed
- Aircraft type Maverick: F-14 is destroyed
注意到如何销毁两个 Aircraft 对象了吗.
建议 - 在设计类的时候, 当不需要资源的所有权, 而且你不想指定这个对象的生命周期时, 可以考虑使用 weak_ptr 代替 shared_ptr.
错误 #9: 没有删除通过 unique_ptr.release()返回的裸指针!
Release()方法不会销毁 unique_ptr 指向的对象, 但是调用 Release 后 unique_ptr 则从销毁对象的责任中解脱出来. 其他人 (你!) 必须手动删除这个对象.
下面这段代码会出现内存泄露, 因为 Aircraft 对象会一直存活, 即使 main()已经退出.
- int main()
- {
- unique_ptr<aircraft> myAircraft = make_unique<aircraft>("F-22");
- Aircraft* rawPtr = myAircraft.release();
- return 0;
- }
- </aircraft>
建议 - 无论何时, 在对 unique_ptr 使用 Release()方法后, 记得一定要删除对应的裸指针. 如果你是想要删掉 unique_ptr 指向的对象, 可以使用 unique_ptr.reset()方法.
错误 #10: 在调用 weak_ptr.lock()的时候没检查它的有效性!
在使用 weak_ptr 之前, 你需要调用 lock()方法来获取这个 weak_ptr.lock()方法的本质是把这个 weak_ptr 升级为一个 shared_ptr, 这样你就可以像使用 shared_ptr 一样使用它了. 然而, 当 weak_ptr 指向的这个 shared_ptr 对象不再有效的时候, 这个 weak_ptr 就为空了. 使用一个失效的 weak_ptr 进行任何调用都会造成 ACESS VIOLATION(非法访问).
举个例子, 在下面这段代码中, 名为 "myWingMan" 的 weak_ptr 指向的这个 shared_ptr, 在调用 pIceman.reset()时已经被销毁. 如果此时调用这个 weak_ptr 执行任何操作, 都会造成非法访问.
- int main()
- {
- shared_ptr<aircraft> pMaverick = make_shared<aircraft>("F-22");
- shared_ptr<aircraft> pIceman = make_shared<aircraft>("F-14");
- pMaverick->myWingMan = pIceman;
- pIceman->m_flyCount = 17;
- pIceman.reset(); // destroy the object managed by pIceman
- cout <<pMaverick->myWingMan.lock()->m_flyCount <<endl; // <span style="color: #ff0000;">ACCESS VIOLATION</span>
- return 0;
- }
- </aircraft>
这个问题的修复方法很简单, 在使用 myWingMan 这个 weak_ptr 之前进行一下有效性检查就可以了.
- if (!pMaverick->myWingMan.expired())
- {
- cout <<pMaverick->myWingMan.lock()->m_flyCount <<endl;
- }
校正: 我的很多读者指出, 上面这段代码不能在多线程的环境下使用 - 如今 99% 的软件都使用了多线程. weak_ptr 可能会在被检查有效性之后, 获取 lock 返回值之前失效. 非常感谢我的读者们指出这个问题! 我将采用 Manuel Freiholz 给出的解决方案: 在使用 shared_ptr 之前, 调用 lock()函数之后再检查一下 shared_ptr 是否为空.
- shared_ptr<aircraft> wingMan = pMaverick->myWingMan.lock();
- if (wingMan)
- {
- cout <<wingMan->m_flyCount << endl;
- }
建议 - 一定要检查 weak_ptr 是否有效 - 其实就是在使用共享指针之前, 检查 lock()函数的返回值是否为空.
所以, 接下来是什么呢?
如果你想学习更多关于 C++11 智能指针的细节或者 C++11 的更多知识, 我向你推荐下面这些书.
1. C++ Primer (6th Edition)by Stanley Lippman (译者注: C++ Primer(第六版), 作者: Stanley Lippman)
2. Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14 by Scott Meyers (译者注: C++ 模板进阶指南: 42 个改善 C++11 和 C++14 用法的细节, 作者: Scott Meyers)
希望你在探索 C++11 特性的旅途中一切顺利. 另外本人从事在线教育多年, 将自己的资料整合建了一个 QQ 群, 对于有兴趣一起交流学习 c/c++ 的初学者可以加群: 941636044, 里面有大神会给予解答, 也会有许多的资源可以供大家学习分享, 欢迎大家前来一起学习进步!
来源: http://www.jianshu.com/p/1a62a5d66005