我首先要说的是,使用智能指针,您将永远不必为此担心。
以下代码有什么问题?
Foo * p = new Foo;
// (use p)
delete p;
p = NULL;
这是由另一个问题的答案和评论引起的。尼尔·巴特沃思(Neil Butterworth)的一则评论产生了一些反对意见:
在C ++中,删除后将指针设置为NULL并不是普遍的好习惯。有时候,这是一件好事,有时却是毫无意义的,并且会掩盖错误。
在很多情况下它无济于事。但是以我的经验,这不会伤害。有人启发我。
我首先要说的是,使用智能指针,您将永远不必为此担心。
以下代码有什么问题?
Foo * p = new Foo;
// (use p)
delete p;
p = NULL;
这是由另一个问题的答案和评论引起的。尼尔·巴特沃思(Neil Butterworth)的一则评论产生了一些反对意见:
在C ++中,删除后将指针设置为NULL并不是普遍的好习惯。有时候,这是一件好事,有时却是毫无意义的,并且会掩盖错误。
在很多情况下它无济于事。但是以我的经验,这不会伤害。有人启发我。
Answers:
将指针设置为0(在标准C ++中为“ null”,C中的NULL定义有些不同)可以避免两次删除时崩溃。
考虑以下:
Foo* foo = 0; // Sets the pointer to 0 (C++ NULL)
delete foo; // Won't do anything
鉴于:
Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior
换句话说,如果您不将已删除的指针设置为0,则在进行两次删除时会遇到麻烦。反对在删除后将指针设置为0的论点是,这样做只会掩盖双删除错误,并使其无法处理。
显然,最好不要有双重删除错误,但是根据所有权语义和对象生命周期,这在实践中可能很难实现。与UB相比,我更喜欢隐藏的双重删除错误。
最后,在有关管理对象分配的旁注中,建议您根据需要查看std::unique_ptr
严格/单一所有权,std::shared_ptr
共享所有权或其他智能指针实现。
NULL
可以掩盖双重删除错误。(有些人可能认为此掩码实际上是一种解决方案,它是一种解决方案,但不是很好的解决方案,因为它不能解决问题的根源。)但不要将其设置为NULL可以掩盖更远的事实(FAR!)删除数据后访问数据的常见问题。
unique_ptr
,它auto_ptr
使用移动语义完成了尝试做的事情。
在删除指向指针的指针后将指针设置为NULL肯定不会受到伤害,但是对于一个更基本的问题,它通常会产生一些创可贴:为什么首先要使用指针?我可以看到两个典型的原因:
std::vector
就可以了,它解决了意外将指针留在释放的内存周围的问题。没有指针。new
可能与delete
被调用的指针不同。同时,可能有多个对象同时使用了该对象。在这种情况下,最好使用共享指针或类似的指针。我的经验法则是,如果在用户代码中留下指针,那说明做错了。指针不应该首先指向垃圾。为什么没有对象负责确保其有效性?当指向的对象出现作用域时,为什么作用域没有结束?
new
。
我有一个更好的最佳实践:尽可能结束变量的作用域!
{
Foo* pFoo = new Foo;
// use pFoo
delete pFoo;
}
在删除它所指向的对象之后,我总是将指针设置为NULL
(现在nullptr
)。
它可以帮助捕获对释放的内存的许多引用(假设在空指针的deref上出现平台错误)。
例如,如果您周围有指针的副本,它将不会捕获对释放内存的所有引用。但是有些总比没有好。
它将屏蔽两次删除,但是我发现它们远不及访问已释放的内存。
在许多情况下,编译器会对其进行优化。因此,没有必要的论点并不能说服我。
如果您已经在使用RAII,那么delete
您的代码中就没有多少s了,因此,额外分配导致混乱的论点并不能说服我。
在调试时,查看空值而不是陈旧的指针通常很方便。
如果这仍然困扰您,请改用智能指针或引用。
当资源被释放时,我还将其他类型的资源句柄设置为no-resource值(通常仅在为封装资源而编写的RAII包装器的析构函数中)。
我开发了一个大型(900万条语句)商业产品(主要是C语言)。一方面,当释放内存时,我们使用宏魔术来使指针无效。这立即暴露出许多潜伏的错误,这些错误已得到及时修复。据我所记得,我们从未遇到过双重释放错误。
更新: Microsoft认为这是安全的良好做法,并在其SDL策略中推荐该做法。显然,如果使用/ SDL选项进行编译,则MSVC ++ 11将自动处理已删除的指针(在许多情况下)。
首先,关于此主题和与之密切相关的主题存在很多现有问题,例如,为什么不删除将指针设置为NULL?。
在您的代码中,问题出在哪儿(使用p)。例如,如果某个地方有这样的代码:
Foo * p2 = p;
然后将p设置为NULL几乎无法完成,因为您仍然需要担心指针p2。
这并不是说将指针设置为NULL总是没有意义的。例如,如果p是指向资源的成员变量,而该资源的生命周期与包含p的类并不完全相同,则将p设置为NULL可能是指示资源存在或不存在的有用方法。
我会稍微改变您的问题:
您会使用未初始化的指针吗?您知道,您没有设置为NULL还是没有分配它指向的内存吗?
在两种情况下可以跳过将指针设置为NULL的情况:
同时,将指针设置为NULL可能会向我隐藏错误,这听起来像是在争论您不应该修复错误,因为该修复可能会隐藏另一个错误。如果指针未设置为NULL,则可能显示的唯一错误就是尝试使用指针的错误。但是将其设置为NULL实际上会导致与您将其与释放的内存一起使用时所显示的错误完全相同,不是吗?
如果没有其他约束可以迫使您在删除指针后将其设置为NULL或不将其设置为NULL(Neil Butterworth提到了一个这样的约束),那么我个人倾向于保留它。
对我来说,问题不是“这是个好主意吗?” 但是“这样做会阻止或允许我成功什么行为?” 例如,如果这允许其他代码看到该指针不再可用,那么为什么其他代码甚至在释放它们之后尝试查看释放的指针?通常,这是一个错误。
它也做比必要的更多的工作,并且阻碍事后调试。不需要内存后,您触摸的内存越少,找出原因的原因就越容易。很多时候,我一直依靠内存处于与发生特定错误时相似的状态来诊断和修复该错误。
删除后显式的空为读者强烈暗示该指针代表某种在概念上是可选的东西。如果看到这样做,我将开始担心,在源代码中的所有地方都使用了指针,因此应该首先对NULL进行测试。
如果这就是您的实际意思,最好使用boost :: optional之类的东西在源代码中将其明确
optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();
但是,如果您真的希望人们知道指针已经“变坏”,那么我将与那些认为最好的方法是使指针超出范围的人达成100%的一致。然后,您将使用编译器来防止在运行时进行错误的取消引用的可能性。
那就是所有C ++浴池中的婴儿,不应该扔掉它。:)
在具有适当错误检查的结构良好的程序中,没有理由不将其分配为null。0
在这种情况下,它是公认的无效值。努力失败并很快失败。
许多反对赋值的论点0
表明,它可能隐藏一个错误或使控制流程复杂化。从根本上讲,这要么是上游错误(不是您的错误(对不起的双关语很抱歉)),要么是代表程序员的另一个错误-甚至可能表明程序流程变得过于复杂。
如果程序员希望引入一个可能为null的指针作为特殊值并围绕它编写所有必要的躲避,这就是他们故意引入的一个复杂问题。隔离越好,发现滥用案件的机会就越早,它们传播到其他程序的能力就越低。
可以使用C ++功能来设计结构合理的程序,以避免出现这些情况。您可以使用引用,也可以只说“传递/使用null或无效参数是一个错误”,这种方法同样适用于容器,例如智能指针。增加一致和正确的行为将阻止这些错误。
从那里开始,您只有非常有限的范围和上下文,其中可能存在(或允许使用)空指针。
相同的适用于not的指针const
。跟随指针的值是微不足道的,因为它的范围很小,并且检查和定义了不正确的用法。如果您的工具集和工程师在快速阅读后仍无法遵循程序,或者进行了不适当的错误检查或程序流程不一致/宽松,那么您还有其他更大的问题。
最后,当您想引入错误(乱写),检测对释放的内存的访问并捕获其他相关的UB时,您的编译器和环境可能会有一些防护措施。您也可以将类似的诊断程序引入程序,通常不会影响现有程序。
让我扩展您已经提出的问题。
这是您在问题中提出的要点形式:
在C ++中,删除后将指针设置为NULL并不是普遍的好习惯。有些时候:
但是,没有时候这是不好的!您不会通过显式将其归零来引入更多的错误,不会泄漏内存,也不会导致未定义的行为发生。
因此,如果有疑问,请将其为空。
话虽如此,如果您觉得必须显式地使某些指针为空,那么对我来说,这听起来好像您没有足够地拆分一个方法,应该查看称为“ Extract method”的重构方法以将该方法拆分为分开的部分。
是。
它唯一可以做的“危害”就是将低效率(不必要的存储操作)引入程序中-但是在大多数情况下,这种开销对于分配和释放内存块的成本而言是微不足道的。
如果您不这样做,则有一天您会遇到一些讨厌的指针解除引用错误。
我总是使用宏进行删除:
#define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; }
(和类似的数组,free(),释放句柄)
您也可以编写“自我删除”方法,该方法引用调用代码的指针,因此它们将调用代码的指针强制为NULL。例如,要删除许多对象的子树:
static void TreeItem::DeleteSubtree(TreeItem *&rootObject)
{
if (rootObject == NULL)
return;
rootObject->UnlinkFromParent();
for (int i = 0; i < numChildren)
DeleteSubtree(rootObject->child[i]);
delete rootObject;
rootObject = NULL;
}
编辑
是的,这些技术确实违反了一些有关使用宏的规则(是的,这些天您可能可以通过模板获得相同的结果)-但是通过使用多年,我从未访问过死内存-这是最令人讨厌,最困难和调试您可能会遇到的大多数时间。在多年的实践中,他们有效地消除了我所介绍的每个团队中的一类臭虫。
您也可以通过多种方式实现上述目的-我只是在试图说明这样的想法:如果人们删除对象,则迫使他们将指针设为NULL,而不是提供一种让他们释放不会使调用者的指针设为NULL的内存的方法。
当然,以上示例仅是迈向自动指针的一步。我不建议这样做,因为OP专门询问了不使用自动指针的情况。
anObject->Delete(anObject)
使anObject
指针失效那样的东西。那太可怕了。您应该为此创建一个静态方法,以便TreeItem::Delete(anObject)
至少被迫这样做。
“有时候这是一件好事,有时却毫无意义并可能掩盖错误”
我可以看到两个问题:简单的代码:
delete myObj;
myobj = 0
成为多线程环境中的主流:
lock(myObjMutex);
delete myObj;
myobj = 0
unlock(myObjMutex);
Don Neufeld的“最佳实践”并不总是适用。例如,在一个汽车项目中,即使在析构函数中,我们也必须将指针设置为0。我可以想象在安全性至关重要的软件中这种规则并不罕见。与尝试说服团队/代码检查器针对代码中使用的每个指针相比,跟踪它们要容易(明智),使该指针无效的行是多余的。
另一个危险是在使用异常的代码中依赖此技术:
try{
delete myObj; //exception in destructor
myObj=0
}
catch
{
//myObj=0; <- possibly resource-leak
}
if (myObj)
// use myObj <--undefined behaviour
在这样的代码中,您可能会导致资源泄漏并推迟问题或进程崩溃。
因此,这两个问题自发地贯穿我的脑海(Herb Sutter肯定会告诉更多),这使我过时了所有这样的问题:“如何避免使用智能指针并使用普通指针安全地完成工作”。
总是有悬空指针需要担心。
我可以想象在删除指针后将其设置为NULL的情况,在极少数情况下(在合理的情况下可以在单个函数(或对象)中重用)的情况下很有用。否则就没有意义了-指针需要指向有意义的东西-只要它存在-句点。
delete
不过,使用空指针是安全的,这是将指针归零可能很好的原因之一。