本文由 伯乐在线 - 欣仔 翻译,wrm 校稿。未经许可,禁止转载!
英文出处:Deb Haldar。欢迎加入翻译组。
我很喜欢新的C++11的智能指针。在很多时候,对很多讨厌自己管理内存的人来说是天赐的礼物。在我看来,C++11的智能指针能使得C++新手教学更简单。
其实,我已经使用C++11两年多了,我无意中发现多种错误使用C++11智能指针的案例,这些错误会使程序效率很低或者直接崩溃。为了方便查找,我把它们按照下文进行了归类。
在开始之前,我们用一个简单的Aircraft类来展示一下这些错误。
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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指针被再次引用时就会造成系统致命的崩溃。看看下面这个例子:
C++
1 2 3 4 5 6 7 | 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的构造函数引发的为资源管理对象分配的。
C++
1 2 | shared_ptr<aircraft> pAircraft(new Aircraft("F-16")); // Two Dynamic Memory allocations - SLOW !!! </aircraft> |
与此相反,当你使用make_shared的时候,C++编译器只会一次性分配一个足够大的内存,用来保存这个资源管理者和这个新建对象。
C++
1 2 | shared_ptr<aircraft> pAircraft = make_shared<aircraft>("F-16"); // Single allocation - FAST ! </aircraft></aircraft> |
2、在看了MS编译器的memory头文件实现以后,我发现当内存分配失败时,这个对象就会被删除掉。这样的话使用裸指针初始化也不用担心安全问题了。
建议- 使用make_shared而不是裸指针来初始化共享指针。
错误#5:在创建一个对象(裸指针)时没有立即把它赋给shared_ptr。
一个对象应该在被创建的时候就立即被赋给shared_ptr。裸指针永远不应该被再次使用。
看看下面则个例子:
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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,至少应该像下面这段代码一样创建使用智能指针管理的对象:
C++
1 2 | shared_ptr<aircraft> pAircraft(new Aircraft("F-16")); </aircraft> |
错误#6:删掉被shared_ptr使用的裸指针!
你可以使用shared_ptr.get()这个api从一个shared_ptr获得一个裸指针的句柄。然而,这是非常冒险的,应该尽量避免这种情况。看看下面这段代码:
C++
1 2 3 4 5 6 7 | 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指向指针数组时没有使用自定义的删除方法!
看看下面这段代码:
C++
1 2 3 4 5 | void StartJob() { shared_ptr<aircraft> ppAircraft(new Aircraft[3]); } </aircraft> |
这个共享指针将仅仅指向Aircraft[0] —— Aircraft[1]和Aircraft[2]将会在智能指针超出作用域时未被删除而造成内存泄露。如果你在使用Visual Studio 2015,就会出现堆损坏(heap corruption)的错误。
建议 – 保证在使用shared_ptr管理一组对象时总是传递给它一个自定义的删除方法。下面这段代码就修复了这个问题:
C++
1 2 3 4 5 | 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。
C++
1 2 3 4 5 6 7 8 | class Aircraft { private: string m_model; public: int m_flyCount; shared_ptr<Aircraft> myWingMan; …. |
然后在main()函数中,创建Aircraft型对象Maverick和Goose,然后给每个对象指定他们的wingman:
C++
1 2 3 4 5 6 7 8 9 10 11 | 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()已经退出。
C++
1 2 3 4 5 6 7 | 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执行任何操作,都会造成非法访问。
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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之前进行一下有效性检查就可以了。
C++
1 2 3 4 | if (!pMaverick->myWingMan.expired()) { cout << pMaverick->myWingMan.lock()->m_flyCount << endl; } |
校正:我的很多读者指出,上面这段代码不能在多线程的环境下使用 – 如今99%的软件都使用了多线程。weak_ptr可能会在被检查有效性之后、获取lock返回值之前失效。非常感谢我的读者们指出这个问题!我将采用Manuel Freiholz给出的解决方案:在使用shared_ptr之前,调用lock()函数之后再检查一下shared_ptr是否为空。
C++
1 2 3 4 5 | shared_ptr<aircraft> wingMan = pMaverick->myWingMan.lock(); if (wingMan) { cout << wingMan->m_flyCount << endl; } |
建议 – 一定要检查weak_ptr是否有效 — 其实就是在使用共享指针之前,检查lock()函数的返回值是否为空。
所以,接下来是什么呢?
如果你想学习更多关于C++11智能指针的细节或者C++11的更多知识,我向你推荐下面这些书。
1. C++ Primer (5th 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)