在C/C++中检查空指针

202

在最近的代码审查中,一位贡献者试图强制执行所有指针上的NULL检查以以下方式进行:

int * some_ptr;
// ...
if (some_ptr == NULL)
{
    // Handle null-pointer error
}
else
{
    // Proceed
}

而不是

int * some_ptr;
// ...
if (some_ptr)
{
    // Proceed
}
else
{
    // Handle null-pointer error
}

我同意他的方法更加清晰,因为它明确地表达了“确保此指针不为空”,但我想反驳说,任何在处理这段代码的人都会理解,在if语句中使用指针变量是隐式检查NULL。同时,我认为第二种方法出错的可能性更小,例如:

if (some_ptr = NULL)

找到并调试它非常困难。

您更喜欢哪种方式,为什么?


49
保持一致的风格有时比选择哪种风格更重要。 - Mark Ransom
20
@Mark:一贯性始终是你的风格中最重要的因素。 - sbi
17
我认为第二种方法更不容易引入错误,例如 if( some_ptr = NULL )。这也是为什么有些人使用 if(NULL == some_ptr),因为无法将NULL赋值给变量,所以会得到编译错误。 - user122302
17
现今大多数编译器会警告条件语句中的赋值操作,但遗憾的是很多开发者会忽略编译器的警告。 - Mike Ellery
13
我不同意上述说法,即一致性是最重要的。这与一致性无关,至少不是指好的那种一致性。在这个领域,我们应该让程序员表达他们自己的风格。如果有人不能立即理解任何形式,那么他们应该立刻停止阅读代码,并考虑换职业。我还要说更多:如果有化妆品一致性,你不能通过脚本自动执行它,那么请将其删除。耗费人类道德的代价比人们在会议上做出的那些愚蠢的决定重要得多。 - wilhelmtell
显示剩余9条评论
14个回答

274

根据我的经验,形式为 if (ptr)if (!ptr) 的测试是首选的。它们不依赖于符号 NULL 的定义。它们不会暴露意外分配的机会。而且它们清晰简洁。

编辑: 正如SoapBox在评论中指出的那样,它们与C++类兼容,例如 unique_ptr,shared_ptr,auto_ptr,这些对象充当指针并提供了转换为 bool 以启用正好这种习惯用法。对于这些对象,显式比较 NULL 将必须调用指针转换,这可能具有其他语义副作用或比 bool 转换更昂贵的代价。

我偏爱那些不需要不必要文字的代码。 if (ptr != NULL) 具有与 if (ptr) 相同的含义,但代价是冗余的明确性。下一个逻辑步骤是编写 if ((ptr != NULL) == TRUE),这条路通往疯狂。 C 语言清楚地表明,由 ifwhile 或类似方式测试的布尔值具有非零值为真,零为假的特定含义。冗余并不会使其更清晰。


28
此外,它们与指针包装类(如shared_ptr、auto_ptr、scoped_tr等)兼容,这些类通常会覆盖 operator bool(或safe_bool)。 - SoapBox
此外,即使在 Stroustrup 的第三版书中,他也建议使用 0 而不是 NULL(即使在赋值时)以获得更好的类型检查。 - Claudio
1
需要注意的一件重要事情是:直到最近,我倾向于写“if (ptr)”而不是“if (ptr != NULL)”或“if (ptr != nullptr)”,因为它更简洁,使代码更易读。 但是我刚刚发现,在最近的编译器中,与NULL/nullptr进行比较会引发(相应的)警告/错误。因此,如果您想检查指针,请使用“if (ptr != NULL)”而不是“if (ptr)”。如果您错误地将ptr声明为int,则第一个检查将引发警告/错误,而后者将在没有任何警告的情况下编译。 - Arnaud
1
@David天宇Wong 不,那不是意思。该页面上的示例是两行序列 int y = *x; if (!x) {...},其中优化器允许推断出,如果 x 为 NULL,则 *x 将未定义,因此它必须不为 NULL,因此 !x 必须为 FALSE。表达式 !ptr 不是 未定义行为。在该示例中,如果在使用 *x 之前进行测试,则优化器将错误地消除测试。 - RBerteig
1
Bjarne Stroustrup(C++的创造者)在他的官方C++指南中同意了该答案:http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-if。规则:**ES.87:不要在条件中添加冗余的==或!=**。请参考链接中的详细说明和几个示例。 - Gabriel Staples
显示剩余3条评论

64

if (foo)已经足够清晰了,使用它。


35

首先要说的是:在代码库中,一致性(consistency)至关重要,比决策本身更重要。

在C++中

在C++中,NULL被定义为00L

如果你读过《The C++ Programming Language》,Bjarne Stroustrup建议在赋值时显式使用0以避免使用NULL宏,但我不确定他在比较时是否也这样做。我已经有一段时间没有读这本书了,我认为他只是使用if(some_ptr)而没有进行明确的比较,但我有点模糊。

这样做的原因是NULL宏是欺骗性的(像几乎所有的宏一样),它实际上是一个0字面量,而不是名称所暗示的唯一类型。避免使用宏是C++中的一般指导方针之一。另一方面,0看起来像一个整数,但与指针进行比较时却不是整数。就我个人而言,我可以采取任何一种方式,但通常我跳过明确的比较(虽然有些人不喜欢这样做,这可能就是你有一个贡献者建议改变的原因)。

无论个人感受如何,这在很大程度上是选择最小恶的一种方式,因为没有一个正确的方法。

这是一个清晰且常见的习惯用法,我更喜欢它,因为在比较时没有意外赋值的风险,而且阅读起来很清晰:

if (some_ptr) {}

如果你知道some_ptr是一个指针类型,那么这是很清楚的,但它也可能看起来像一个整数比较:

if (some_ptr != 0) {}

这是比较清晰的,通常情况下它是有意义的... 但这是一个不安全的抽象,NULL实际上是0字面量,可能很容易被误用:

if (some_ptr != NULL) {}

C++11现在有nullptr,这是首选方法,因为它是明确且准确的,只需注意意外赋值:

if (some_ptr != nullptr) {}

在你能够迁移到C++0x之前,我认为担心使用哪种方法都是浪费时间,它们都不足以满足要求,这就是为什么nullptr被发明出来的原因(还有与完美转发相关的通用编程问题)。最重要的是保持一致性。

在C中

C是一个不同的东西。

在C中,NULL可以定义为0((void*)0),而C99允许实现定义空指针常量。所以实际上取决于实现对NULL的定义,你需要检查标准库。

宏非常常见,在一般情况下,它们经常用于弥补语言中通用编程支持的缺陷和其他东西。该语言更加简单,并且依赖预处理器更加普遍。

从这个角度来看,在C中我可能会推荐使用NULL的宏定义。


1
虽然你在C++中关于0的说法是正确的,但在C语言中它有意义:(void *)0。在C++中,使用0比使用NULL更好,因为类型错误可能会很麻烦。 - Matt Joiner
@Matt:但是NULL本来就是零。 - GManNickG
@Matt:在C++中,你说0是更好的选择,但NULL必须为零。 - GManNickG
这是一个很好的观点,我忘了提到它,现在我正在改进我的答案并建议它。 ANSI C 可以将 NULL 定义为 ((void *)0),C++ 将 NULL 定义为 0。我没有直接查阅标准来确认这一点,但我理解的是,在 C++ 中,NULL 可以是 0 或 0L。 - M2tM
@GMan: 对,没错。在C++中使用0没有问题,因为它不会在与指针的比较中引起类型错误,而且在C++中,NULL就是0(在这种情况下,使用宏有什么意义呢)。但在C语言中,我肯定会使用NULL。个人而言,在C++中我会使用nullptr并定义一个const class或者使用即将推出的标准。 - Matt Joiner
显示剩余2条评论

20

我使用 if (ptr),但这完全不值得争论。

我喜欢我的方式,因为它简洁,尽管其他人说 == NULL 使代码更易读和更明确。我知道他们的想法,但我不同意额外的东西使它任何更容易。 (我讨厌宏,所以我有偏见。) 由你决定。

我不同意你的观点。如果您没有收到条件语句分配的警告,请将警告级别调高。就这么简单。(而且为了万能的伟大,请不要交换它们。)

请注意,在 C++0x 中,我们可以使用 if (ptr == nullptr),对我来说它读起来更好。 (再一次,我讨厌宏。但是 nullptr 很好。) 我仍然使用 if (ptr),只是因为我习惯了它。


希望多5年的经验能改变你的想法 :) 如果C++/C有类似于if_exist()的运算符,那么它就有意义了。 - magulla

11

说实话,我并不明白这有什么关系。两者都非常清晰,任何一位有C或C++经验的人都应该能理解。只有一个评论:

如果你打算识别错误并且不继续执行函数(即,你将立即抛出异常或返回错误代码),那么你应该把它作为守卫条件:

int f(void* p)
{
    if (!p) { return -1; }

    // p is not null
    return 0;
}

这样做可以避免"箭头代码"的出现。


1
有人指出在这里 http://kukuruku.co/hub/programming/i-do-not-know-c if(!p) 是未定义行为。它可能有效,也可能无效。 - David 天宇 Wong
3
如果你所说的是链接中的例子#2,那么if(!p)并不是未定义行为,而是在它之前的一行。同时if(p==NULL)也会有完全相同的问题。 - Mark Ransom

9
只是再为“foo == NULL”的做法辩护一个观点: 如果“foo”是一个“int *”或“bool *”,那么“if(foo)”检查有可能被读者误解为测试指针的值,即作为“if(*foo)”来理解。这里的“NULL”比较提醒我们正在谈论一个指针。 但我想一个良好的命名约定会使得这个论点不重要。

如果读者对间接级别如此迷失,我不确定他作为程序员是否会成为安全隐患。 - Deduplicator

9

个人而言,我一直使用if (ptr == NULL),因为它可以明确表达我的意图,但现在这只是一个习惯。

使用=代替==会被任何有正确警告设置的编译器捕获。

重要的是为你的团队选择一种一致的风格并坚持下去。无论你选择哪种方式,你最终都会习惯它,并且在处理其他人的代码时减少摩擦感将是受欢迎的。


7

25
K&R也会问:“什么是智能指针?”C++世界正在发生变化。 - Alex Emelianov
2
事实上,在C++领域,有些事情已经发生了变化 ;) - jalf
3
K&R在哪里声明(ref)?他们从未使用过这种风格。在K&R2第2章中,他们甚至建议使用if (!valid)而不是if (valid == 0) - schot
7
大家安静下来。他说的是k&r,而不是K&R。显然他们不太出名,因为他们的首字母甚至没有大写。 - jmucchiello
1
如果我问什么是K&R,那会暴露出我的C ++新手吗...但是总得有人问才能让大家学习。来吧,点燃争论的火焰! - Ali Akdurak
显示剩余3条评论

5

实际上,我两种方法都使用。

有些情况下,您首先检查指针的有效性,如果为NULL,则直接返回/退出函数。(我知道这可能会引起“函数是否应该只有一个退出点”的讨论)

大多数情况下,您会检查指针,然后执行所需操作,最后解决错误情况。结果可能是丑陋的多次缩进代码和多个if语句。


2
我个人不喜欢使用一个退出点会导致箭头代码。如果我已经知道答案,为什么不直接退出呢? - ruslik
1
@ruslik:在C语言(或类C++风格的C语言)中,有多个返回值会使清理工作更加困难。在“真正”的C++中,这显然不是问题,因为RAII会处理您的清理工作,但一些程序员(出于更或少合理的原因)仍停留在过去,要么拒绝依赖RAII,要么不被允许这样做。 - jalf
在这种情况下,@jalf可能会非常方便。此外,在许多情况下,需要清理的malloc()可以被更快的alloca()替换。我知道他们并不受推荐,但它们因某种原因而存在(至少对于我来说,为goto命名良好的标签比模糊的if/else树更加清晰)。 - ruslik
1
alloca是非标准的且完全不安全的。如果它失败了,你的程序就会崩溃。没有恢复的机会,而且由于堆栈相对较小,失败的可能性很大。在失败时,有可能破坏堆并引入特权泄漏漏洞。除非你对将要分配的大小有一个极小的限制(然后你可以使用普通数组),否则永远不要使用alloca或vla。 - R.. GitHub STOP HELPING ICE

3
如果样式和格式是你的评论中的一部分,那么应该有一个约定好的样式指南来衡量。如果有这样一个指南,请按照指南操作。如果没有指南,则应将此类细节保留为原样。这是浪费时间和精力,并且会分散对代码审查真正应该发现的问题。严重的是,如果没有样式指南,我会坚持不改变代码,即使它没有使用我喜欢的约定作为原则。
并不是很重要,但我的个人偏好是if (ptr)。对我来说,这种方式的含义更加明显,甚至比if (ptr == NULL)还要明显。
也许他想说的是在处理正确路径之前先处理错误情况更好?在这种情况下,我仍然不同意审阅者的观点。我不知道是否有一种公认的惯例,但我认为任何if语句中最“正常”的条件都应该排在第一位。这样我就不必深入挖掘函数的所有内容以及它是如何工作的。
唯一的例外是如果错误导致我退出函数,或者在继续之前可以从错误中恢复。在这些情况下,我会首先处理错误:
if (error_condition)
  bail_or_fix();
  return if not fixed;

// If I'm still here, I'm on the happy path

通过提前处理异常情况,我可以处理它,然后忘记它。但如果我无法通过提前处理来回到正常流程,则应在主要情况处理之后再处理它,因为这样可以使代码更易于理解。在我看来。
但是,如果它不在样式指南中,那么这只是我的观点,您的观点同样有效。要么标准化,要么不要标准化。不要让评审者仅仅因为他有观点就伪造标准化。

你的if语句缺少{} - Jason S

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