现代 C++ 的安全

现代 C++ 的安全
Everett Rain引言
随着 C++11 标准的提出,智能指针作为工具引入了这个以手动管理内存而闻名的编程语言,用以自动管理动态内存,避免 C++ 开发中司空见惯的内存泄漏和指针错误问题。在此之前,C++ 的指针均沿用 C 风格的传统指针,依赖开发者手动管理,安全性差。
当然,为了自动管理动态内存,智能指针自然引入了额外的机制、更加复杂的写法,以及独立于传统 C 指针之外的、对于先前的大批 C++ 程序员而言的额外的学习成本。这无疑会带来争论:在 C 指针如此成熟的当下,我们是否真正需要智能指针?
答案是肯定的:我们不仅需要智能指针,而且应该尽可能多地在大型项目中使用智能指针,避免使用传统指针
为什么需要使用智能指针
简单而言,使用传统的 C 风格指针容易出现以下两方面的问题:
-
由于忘记释放内存,造成内存泄露以致程序崩溃
-
指针的管理权问题:即当多个函数同时调用同一个指针,无法清晰地确定该指针的生命周期在何处结束
问题 1:内存泄漏
问题 1 是非常传统的 C 指针问题,也是讨论最多的。虽然解决这个问题的方法说起来非常简单:在写代码时更加专心、更加细致,但即使是再有经验的程序员(这里并没有使用“好程序员”,因为我们不能简单要求一个程序员成为一名“好程序员”,而自己都难以给出所谓“好”的标准),都无法确保在面对数十万乃至数百万行代码的大型项目时,依然可以完全正确地管理好每一个指针。
同样,C 指针的问题也会出现在团队工作交接时,哪怕你再了解你创建的每一个指针及其生命周期,你也无法确保下一个拿到你的代码的团队成员可以清楚这一点。可能你的继任者的一个疏忽,你辛辛苦苦创建的代码的性能优势就由于一个或数个小小的指针错误而荡然无存。
问题 2:指针管理权
传统的 C 风格指针还存在一个问题:当一个指针被传入多个不同的函数,“哪个函数负责指针的生命周期管理”这一问题将越来越难以处理,即造成指针管理权混淆问题。如果错误地赋予了某个函数指针管理权,既可能提前回收指针内存,造成其他函数无法获取到并崩溃;也可能持续保留一个完全不再使用的指针,造成内存泄漏。
举一个最简单的例子:一个指针被传入两个不同的函数,如何确定哪个函数最后结束其工作?只有确定了这个问题的答案,才能确定指针何时应该被销毁。但不出意外的话,这两个函数无法得知对方的执行情况,也就无法只通过这两个函数确定指针何时销毁。在这种情况下,只能再引入第三个函数,负责确认前两者的完成情况并管理指针的生命周期。这种解决方案无疑提高了程序复杂度,是非常不应该出现的方案。
上面的两个问题看上去很难在 C 风格指针下找到合适的解决方案,但如果使用 C++ 提供的智能指针,程序就会自动处理所有问题,程序员只需要定义指针为智能指针即可。
是否应继续使用传统指针
这个问题的答案也很明显:需要,但更多应该根据项目规模而定。
在引言中给出的说法是“我们不仅需要智能指针,而且应该尽可能多地在大型项目中使用智能指针,避免使用传统指针”。但这里的重要前提是“大型项目”,也是传统指针造成问题和低安全性代码的重灾区。面对成百上千的指针,使用智能指针无疑是最好的解决方案。
但对于 C++ 的学习者而言,智能指针的必要性就不再那么明显了。因为大部分个人开发者不会涉及到如此巨大、需要管理数不胜数的指针的情况;其次,使用智能指针意味着需要引入 C++ 标准库(当然也可以选择自行实现一个简单的智能指针),这也会增大项目的体积,对一个较小项目而言显然没有必要。
如果我只是需要开发一个百余行的小型沙箱项目,我绝对不希望为了智能指针引入一个巨大的 STL 标准库。
其次,对于 C++ 的使用而言,将 C 风格指针插入代码中的确是使用成本最低、无需思考的。比起智能指针超长的声明,只需一个简单的 * 号即可引入一个指针,这不会打断程序员(更多是初学者)的思路,他们不需要费心思考虑这里需要使用什么类型的智能指针,只要简单地引入一个指针即可,这也无疑可以提高初学者和小项目开发者的工作效率。







