在删除之前检查空指针是否存在有什么原因吗?

57

我经常看到旧代码在删除指针之前先检查 NULL,类似于下面的代码:

if (NULL != pSomeObject) 
{
    delete pSomeObject;
    pSomeObject = NULL;
}

在删除指针之前检查它是否为NULL的原因是什么?把指针设置为NULL之后的原因是什么?


10
不知道为什么他们在删除之前检查是否为空指针,因为删除一个空指针是完全安全的,但他们之所以在删除后将其设置为NULL,是为了防止对象意外被第二次删除,这样可以避免程序崩溃(删除悬空指针是不好的)。 - mpen
10个回答

77

删除空指针是完全“安全”的,它实际上相当于无操作。

你可能想在删除之前检查是否为空指针,因为尝试删除空指针可能表示程序中存在错误。

编辑

注意:如果你重载了delete运算符,那么delete NULL可能就不再“安全”。


4
@Dbger: 我不确定你的意思,让我澄清一下我的帖子。如果你将空指针传递给删除机制,在底层确实会进行空指针检查,如果指针为空,则“函数”仅返回而不实际执行任何操作。但是你可能想在删除之前检查空指针的原因是,如果你试图删除已经设置为null的指针,那么这意味着你对该指针的假设是不正确的。如果你的假设是错误的,那么很可能存在一个错误(bug)。 - Randolpho
3
根据ISO/IEC 14882:2003的第5.3.5节第2段第2句话,"如果delete运算符的操作数是空指针,则不会发生任何操作。" - Randolpho
3
@Dbger: 你是从谷歌代码页面得到的吗?我也是。我太抠门了,不想在ISO网站上花30美元。在我看来,这就是当今世界的问题所在。 - Randolpho
3
“在删除指针之前检查其是否为 NULL 的原因是,尝试删除空指针可能会暗示您的程序存在错误。”请说明您的意思。 我看不到问题中所示的代码中的空指针检查如何帮助检测/避免/解决此类错误,所以它有什么用处?回答:在程序中使用指针时,如果该指针为null,则表示指针未指向任何有效的内存地址。如果在试图删除一个空指针时,程序会导致崩溃或其他不可预测的行为,这可能会使程序员难以定位和修复错误。因此,在删除指针之前检查其是否为null可以帮助程序员发现并修复这些类型的错误。但是,请注意,与代码中的空指针检查相比,更好的做法是在程序设计阶段就避免使用悬挂指针,以确保程序的稳定性和可靠性。 - Don Hatch
3
@Randolpho谢谢,我明白了。关键部分是“记录您尝试删除空指针的事实”,这并没有出现在代码示例中,我也不认为您在回答或后续评论中提到过(虽然也许我漏掉了,毕竟奇怪的事情时有发生)。如果您能将此澄清添加到您的回答中,那将很有帮助;否则我真的不知道您的意思。 - Don Hatch
显示剩余6条评论

41
C++标准保证在delete-expression(§8.5.2.5/2)中使用空指针是合法的。然而,未指定是否会调用释放函数(operator deleteoperator delete[]; §8.5.2.5/7, note)。

如果使用默认的释放函数(即标准库提供的),并且传入了空指针,则该调用不起作用(§6.6.4.4.2/3)。

但是如果释放函数不是由标准库提供的——即我们重载了operator delete(或operator delete[]),则未指定会发生什么。

一位称职的程序员应该在释放函数内部适当地处理空指针,而不是在调用之前,就像OP的代码所示。同样,在删除后将指针设置为nullptr/NULL只有非常有限的用途。一些人喜欢这样做是基于防御式编程的精神:在出现错误时,访问已删除的指针将导致对空指针的访问而不是访问随机的内存位置,这将使程序行为稍微更加可预测。虽然两种操作都是未定义的行为,但是空指针访问的行为在实践中更加可预测(它通常会直接崩溃而不是内存损坏)。由于内存损坏特别难以调试,重置已删除的指针有助于调试。

当然,这只是治疗症状而非根本原因(即错误)。您应该将重置指针视为代码异味。清晰、现代的C++代码将使内存所有权变得清晰,并通过使用智能指针或等效机制进行静态检查,从而可以证明避免此类情况。
奖励:对重载operator delete的解释:
尽管其名称如此,operator delete是一个可以像其他函数一样重载的函数。对于每个具有匹配参数的operator delete调用,都会在内部调用此函数。operator new也是如此。
在某些情况下,重载operator new(然后还有operator delete)是有意义的,当您想要精确控制内存分配方式时。这并不难,但必须采取一些预防措施以确保正确的行为。Scott Meyers在《Effective C++》中详细描述了这一点。
现在,让我们假设我们想要为调试重载全局版本的operator new。在此之前,请注意以下代码中发生的情况:
klass* pobj = new klass;
// … use pobj.
delete pobj;

这里实际上发生了什么?那么以上内容大致可以翻译为以下代码:

// 1st step: allocate memory
klass* pobj = static_cast<klass*>(operator new(sizeof(klass)));
// 2nd step: construct object in that memory, using placement new:
new (pobj) klass();

// … use pobj.

// 3rd step: call destructor on pobj:
pobj->~klass();
// 4th step: free memory
operator delete(pobj);

注意第2步,我们使用了一个略微奇怪的语法来调用new。这是所谓的定位new的调用方式,它接受一个地址并在该地址构造一个对象。这个运算符也可以被重载。在这种情况下,它只是用来调用类klass的构造函数。

现在,不再拖延,以下是操作符重载版本的代码:

void* operator new(size_t size) {
    // See Effective C++, Item 8 for an explanation.
    if (size == 0)
        size = 1;

    cerr << "Allocating " << size << " bytes of memory:";

    while (true) {
        void* ret = custom_malloc(size);

        if (ret != 0) {
            cerr << " @ " << ret << endl;
            return ret;
        }

        // Retrieve and call new handler, if available.
        new_handler handler = set_new_handler(0);
        set_new_handler(handler);

        if (handler == 0)
            throw bad_alloc();
        else
            (*handler)();
    }
}

void operator delete(void* p) {
    cerr << "Freeing pointer @ " << p << "." << endl;
    custom_free(p);
}

这段代码内部只使用了自定义实现的malloc/free,大多数实现也是如此。它还创建了一个调试输出。考虑以下代码:

int main() {
    int* pi = new int(42);
    cout << *pi << endl;
    delete pi;
}

它产生了以下输出:
Allocating 4 bytes of memory: @ 0x100160
42
Freeing pointer @ 0x100160.

现在,这段代码与operator delete的标准实现有根本区别:它没有测试空指针!编译器不会检查这个问题,所以上面的代码可以编译,但是当你尝试删除空指针时,它可能在运行时产生严重错误。

然而,正如我之前所说,这种行为实际上是意外的,库编写者应该注意在operator delete中检查空指针。这个版本得到了很大改进:

void operator delete(void* p) {
    if (p == 0) return;
    cerr << "Freeing pointer @ " << p << "." << endl;
    free(p);
}

总之,虽然粗糙的operator delete实现可能需要在客户端代码中进行显式的空值检查,但这是非标准行为,仅应在遗留支持(如有必要)中容忍。

rajKumar: 看到更新了吗?希望这对你有所帮助。理解这些重载并不容易。我只能建议阅读Scott Meyers的《Effective C++》(不仅仅是关于C++的,它是事实上的C++指南)。 - Konrad Rudolph
2
将已删除的指针设置为NULL还有另一个好处:保证任何后续尝试对指针解除引用的操作将立即崩溃,而不是做出不可预知的行为。 - nobody
1
你应该知道,free(NULL)也是一个空操作,所以你的示例并不真正展示问题。除此之外,你指出了问题的精髓。 - Evan Teran
5
因为重载操作符有问题而在删除之前检查空值是错误的做法 - 修复该操作符! - paulm
@paulm 我完全同意。我只是想展示一下语言在技术上允许的内容。 - Konrad Rudolph
显示剩余2条评论

10

删除空值是无效操作。在调用delete之前检查null是没有必要的。

如果指针为null带有您关心的其他信息,则可能出于其他原因要检查null。


-1 只因为你想不出理由而说“没有理由”是不合适的。性能是一个可能的原因。 - Don Hatch
7
+1 来抵消那个荒谬的 -1(我希望我可以踩评论)。确实没有理由检查 NULL,而且性能也是不检查的原因……在删除之前检查 NULL 是多余的,而且编译器无法消除这种冗余。这个答案有用地指出了测试对于 if (p != NULL) {dosomethingwith(p); delete p; } 这种常见模式来说是不冗余的。 - Jim Balter

9

删除操作内部会检查 NULL。您的测试是多余的。


1
Delete.cpp 调用 Free.c,它检查是否为 NULL,然后返回。QED。void __cdecl _free_base (void * pBlock) { PHEADER pHeader; if (pBlock == NULL) return; - EvilTeach
-1 是的,对 NULL 进行测试在逻辑上是多余的,但这并不能回答为什么它可能是有用的这个问题。 - Don Hatch

4
根据C++03 5.3.5/2,删除空指针是安全的。以下内容引用自标准:
如果delete的操作数是空指针,则无论哪种情况,该操作都不会有任何效果。

1
-1 是的,虽然这并不能回答为什么进行测试可能有用的问题,但是它是安全的。 - Don Hatch

3

如果 pSomeObject 是 NULL ,delete 不会做任何事情。所以,不需要检查是否为 NULL。

我们认为,在删除后将指针赋值为 NULL 是一种良好的习惯,如果有可能某些笨蛋会尝试使用该指针。使用 NULL 指针要比使用指向未知内存的指针略好一些(NULL 指针会导致崩溃,指向已删除内存的指针可能不会)


不仅要使用指针,而且那个笨蛋可能会再次尝试删除它。不允许双重删除。 - Magnus Hoff
2
将已删除的指针设置为 NULL 可能会隐藏严重问题,如果周围的代码非常防御性地检查所有指针是否针对 NULL 进行解引用。 - Wolf

1
没有必要在删除之前检查是否为NULL。 如果代码中的某个地方通过执行NULL检查来检查某个对象是否已分配,则可能需要在删除后分配NULL。一个例子是一些按需分配的缓存数据。每当您清除缓存对象时,都会将NULL分配给指针,以便分配对象的代码知道它需要执行分配。

-1 不要仅仅因为你想不出理由就说“没有理由”。性能是一个可能的原因。 - Don Hatch
因为这个问题又+1了。 - underscore_d

0

我相信之前的开发人员编写代码时使用了“冗余”来节省一些毫秒时间: 在删除对象后将指针设置为NULL是一个好习惯,所以你可以在删除对象后使用以下代码:

if(pSomeObject1!=NULL) pSomeObject1=NULL;

但是delete操作本身就会进行这种比较(如果为NULL则不执行任何操作)。为什么要重复做一遍呢?无论pSomeObject的当前值如何,您都可以在调用delete之后将其赋值为NULL-但是如果它已经具有该值,则这可能略显冗余。

因此,我认为这些代码的作者试图确保pSomeObject1在被删除后始终为NULL,而不会产生潜在的不必要的测试和赋值成本。


2
如果操作员最终会删除它,那么如何节省时间呢?在大多数情况下,由于需要进行双重检查,因此使用的时间会增加50%,除非该值为空。 - paulm
我猜测意图是为了加速指针确实为空的情况。想象一下它不进行比较:delete将与NULL进行比较,立即退出,然后指针再次被赋值(不必要地)为NULL。由于函数调用比与常量进行比较更昂贵 - 通过首先进行比较(无论如何delete都会这样做),您可以从调用删除代码调用以及(此时不必要的)NULL分配的其他毫秒中节省一些毫秒。 - Joe Pineda
+1,因为目前为止它是唯一一个指出测试可能有效的原因的答案,假设删除操作被正确实现。 - Don Hatch
@JimBalter 调用函数并不是免费的 - 需要将参数推入、远程调用、推出参数,然后将返回值推入、返回、推出返回值。除非编译器可以将其内联(我承认现在大多数都使用“删除”),否则仅调用“free”将意味着执行那个可怕的比较 加上 函数调用,加上 将 NULL 分配给已经具有 NULL 值的变量 - 浪费了太多毫秒。 - Joe Pineda

-2

这取决于你在做什么。例如,一些旧版本的free实现如果传递了一个NULL指针将会出现问题。一些库仍然存在这个问题。例如,在Xlib库中的XFree函数如下所述:

描述

XFree函数是一个通用的Xlib例程,用于释放指定的数据。除非为对象明确指定了替代函数,否则必须使用它来释放由Xlib分配的任何对象。不能向此函数传递空指针。

因此,将释放NULL指针视为错误,这样你就会更加安全。


3
这个问题是关于删除(delete),而不是释放(free)或XFree。自至少UNIX V7以来,free就已经检查了NULL,并且根据语言标准需要这样做,就像delete一样。 - Jim Balter

-5
根据我的观察,在类Unix的机器上,如PARISC和Itanium,使用delete删除空指针是安全的。但在Linux系统中,这样做是相当不安全的,因为进程会崩溃。

在Linux上不安全是不正确的说法;标准保证它是安全的,而且我已经做了无数次。 - Don Hatch
如果你的进程崩溃了,编译器也出了问题,我希望你现在已经换了一个新的。 - underscore_d

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接