什么时候应该在C中检查指针是否为NULL?


18

总结

C语言中的函数是否应该始终检查以确保未取消引用NULL指针?如果不是,何时跳过这些检查是否合适?

详细资料

我读过一些有关编程面试的书,我想知道在C语言中对函数参数进行输入验证的适当程度是多少?显然,任何需要用户输入的功能都需要执行验证,包括NULL在取消引用指针之前检查指针。但是,如果您不希望通过API公开同一文件中的函数,该怎么办?

例如,以下内容出现在git的源代码中:

static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
    if (!want_color(graph->revs->diffopt.use_color))
        return column_colors_max;
    return graph->default_column_color;
}

如果*graph为,NULL则将取消引用空指针,这可能会使程序崩溃,但可能导致某些其他不可预测的行为。另一方面,函数是static,因此程序员可能已经验证了输入。我不知道,我只是随机选择了它,因为它是用C编写的应用程序中的一个简短示例。我已经看到许多其他地方使用了指针而不检查NULL。我的问题不是针对此代码段的。

我在异常处理的上下文中看到了一个类似的问题。但是,对于不安全的语言(例如C或C ++),不会自动传播未处理的异常。

另一方面,我在开放源代码项目(例如上面的示例)中看到了很多代码,这些代码在使用它们之前不做任何指针检查。我想知道是否有人对何时将检查放入函数与是否假定使用正确参数调用函数的准则进行了思考。

对于编写生产代码,我通常对此问题感兴趣。但是我也对编程面试感兴趣。例如,许多算法教科书(例如CLR)倾向于以伪代码表示算法,而没有任何错误检查。但是,尽管这对于理解算法的核心很有帮助,但显然不是一种好的编程实践。因此,我不想告诉采访者我正在跳过错误检查以简化代码示例(就像教科书一样)。但是我也不想出现带有过多错误检查的低效率代码。例如,graph_get_current_column_color可以将其修改为检查*graph空值,但不清楚是否*graph为空值,除非不取消引用它。


7
如果您正在为API编写函数,而调用者不应该理解这些内脏,那么这里就是文档很重要的地方之一。如果您记录参数必须是有效的非NULL指针,则检查它成为调用者的责任。
Blrfl 2013年


回顾2017年,请记住这个问题,并且大多数答案都写于2013年,是否有任何答案都解决了由于优化编译器而造成的时间旅行未定义行为的问题?
rwong

对于期望有效指针参数的API调用,我想知道仅对NULL进行测试的价值是什么?被取消引用的任何无效指针将与NULL和segfault一样严重。
PaulHK '18年

Answers:


15

无效的空指针可能是由程序员错误或运行时错误引起的。运行时错误是程序员无法修复的,例如malloc由于内存不足,网络丢失数据包或用户输入愚蠢内容而导致的失败。程序员错误是由程序员错误使用该函数引起的。

我所看到的一般经验法则是,应始终检查运行时错误,但不必每次都检查程序员错误。假设有个白痴程序员直接叫graph_get_current_column_color(0)。它将在第一次调用时出现段错误,但是一旦您对其进行修复,则该修复程序将永久编译。无需每次都检查一次。

有时,尤其是在第三方库中,您会看到一个assert用于检查程序员错误的if语句,而不是语句。这样一来,您就可以在开发过程中编译检查,并将其保留在生产代码中。我还偶尔看到免费的检查,在检查中,潜在的程序员错误的来源与症状相去甚远。

显然,您总能找到一个比较古怪的人,但是我认识的大多数C程序员都比较喜欢简洁的代码,而不是比较安全的代码。“更安全”是一个主观术语。在开发过程中出现明显的段错误优于在现场进行细微的损坏错误。


这个问题有些主观,但这似乎是目前最好的答案。感谢所有对此问题发表想法的人。
加百利南部

1
在iOS中,malloc永远不会返回NULL。如果找不到内存,它将首先要求您的应用程序释放内存,然后要求操作系统(操作系统将要求其他应用程序释放内存并可能杀死它们),如果仍然没有内存,它将杀死您的应用程序。无需检查。
gnasher729

11

Kernighan&Plauger在“软件工具”中写道,他们将检查所有内容,并且,对于他们认为可能永远不会发生的条件,他们将中止并显示错误消息“无法发生”。

他们报告看到自己的终端出现“不可能发生”的次数迅速感到沮丧。

在(尝试)取消引用指针之前,应始终检查指针是否为NULL。 总是。您重复检查未发生的NULL的代码量以及处理器“浪费”的时间将比您不必通过崩溃转储进行调试而导致的崩溃数量要多得多-如果你那么幸运。

如果指针在循环内是不变的,则足以在循环外对其进行检查,但是您应随后将其“复制”到一个受作用域限制的局部变量中,以供循环使用,以添加适当的const装饰。在这种情况下,您必须确保从循环体调用的每个函数在原型上都必须包含必要的const装饰,即“全部向下”。如果你不这样做,还是不能(因为如供应商包装或顽固同事的),那么你必须检查它的NULL 每次都可以修改,因为肯定是COL墨菲是个坚定不移的乐观主义者,有人IS会不用的时候把它换掉。

如果在函数内部,并且传入的指针应该为非NULL,则应进行验证。

如果您从某个函数接收到它,并且发出的消息应该是非NULL,则应进行验证。为此,malloc()特别臭名昭著。(现在已经不复存在的北电网络公司已经制定了一套严格的书面编码标准。我不得不调试一下崩溃,我追溯到malloc()返回了一个NULL指针,而白痴编码器也没有去检查它在他写信之前,因为他只是知道他有足够的记忆力。当我终于找到它时,我说了一些非常讨厌的话。)


8
如果您使用的是需要非NULL指针的函数,但无论如何都要检查,并且它为NULL ...接下来怎么办?
2013年

1
@detly停止您正在做的事情并返回错误代码,或者触发一个断言
James

1
@James-没想到assert。如果您正在谈论更改现有代码以包括NULL检查,那么我不喜欢错误代码的想法。
2013年

10
@detly,如果您不喜欢错误代码,就不会对C开发人员产生太大的影响
James

5
@ JohnR.Strohm-这是C,它是断言或什么都没有:P
不错,

5

当您可以某种方式使自己确信指针不可能为空时,可以跳过检查。

通常,空指针检查是在代码中实现的,在该代码中,预期空值将显示对象当前不可用的指示符。Null用作标记值,例如终止链表,甚至终止指针数组。argv传递给字符串的向量main必须由指针以空值终止,类似于以空字符终止字符串的方式:argv[argc]是空指针,您可以在解析命令行时依靠它。

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

因此,用于检查null的情况是其中a为期望值的情况。空检查实现空指针的含义,例如停止搜索链表。它们防止代码取消引用指针。

在设计不希望使用空指针值的情况下,没有必要检查它。如果出现无效的指针值,它很可能会显示为非空值,无法以任何可移植的方式将其与有效值区分开。例如,通过读取未初始化的存储而获得的指针值被解释为指针类型,通过某种幕后转换获得的指针或超出范围的指针。

关于诸如这样的数据类型graph *:可以将其设计为使null值是有效图:没有边缘也没有节点的东西。在这种情况下,所有使用graph *指针的函数都必须处理该值,因为它在图形表示中是正确的域值。另一方面,a graph *可以是一个指向类似容器的对象的指针,如果我们拿着一个图,它永远不会为null。然后,空指针可能会告诉我们“该图形对象不存在;我们尚未分配它,或者我们释放了它;或者当前没有关联的图形”。指针的后一种用法是布尔值/卫星值的组合:指针为非null表示“我有这个姊妹对象”,并且它提供了该对象。

即使不释放对象,我们也可以将指针设置为null,仅仅是为了将一个对象与另一个对象分离:

tty_driver->tty = NULL; /* detach low level driver from the tty device */

我知道最有说服力的论点是,指针在某一点不能为null,是将该点包装在“ if(ptr!= NULL){”和相应的“}”中。除此之外,您还处于正式验证领域。
约翰·斯特罗姆

4

让我再向赋格曲添加声音。

我说,就像许多其他答案一样,现在不要打扰检查;这是呼叫者的责任。但是我有一个基础而不是简单的权宜之计(和C编程的傲慢)。

我尝试遵循Donald Knuth的原则,使程序尽可能脆弱。如果发生任何错误,请使其崩溃,并且引用空指针通常是实现此目的的一种好方法。通常的想法是崩溃或无限循环比创建错误数据好得多。它引起了程序员的注意!

但是引用空指针(特别是对于大型数据结构)并不总是会导致崩溃。叹。确实如此。这就是Asserts的所在。它们很简单,可以立即使您的程序崩溃(这回答了问题:“如果遇到空值,该方法应该怎么做?”),并且可以在各种情况下打开/关闭(我建议不要关闭它们,因为对于客户来说,崩溃和看到一条隐秘的消息比丢失数据更好。

那是我的两分钱。


1

我通常只检查何时分配了指针,这通常是我唯一可以实际对其执行操作的步骤,并且如果它无效则可以恢复。

例如,如果我得到一个窗口的句柄,我将在正确的位置检查它是否为null,然后对null条件进行一些处理,但是我不会每次都检查它是否为null。我在每个指针传递的每个函数中都使用了指针,否则我将有大量重复的错误处理代码。

像这样的函数graph_get_current_column_color在遇到错误的指针时可能完全无法对您的情况做任何有用的事情,因此我将保留对调用方的NULL检查。


1

我会说这取决于以下几点:

  1. CPU利用率至关重要吗?每次检查NULL都会花费一些时间。
  2. 指针为NULL的几率是多少?它只是在以前的功能中使用过吗?指针的值可能已更改。
  3. 系统是抢占式的吗?意味着可能发生任务更改并更改值吗?ISR可以进来并改变价值吗?
  4. 代码的耦合程度如何?
  5. 是否有某种自动机制会自动检查NULL指针?

CPU利用率/奇数指针为NULL 每次检查NULL都需要花费时间。因此,我尝试将检查限制在指针可能更改其值的位置。

抢占式系统 如果您的代码正在运行,并且其他任务可能会中断它,并有可能更改该值,则最好进行检查。

紧密耦合的模块 如果系统紧密耦合,则需要进行更多检查。我的意思是,如果多个模块之间共享数据结构,则一个模块可能会更改另一个模块下的内容。在这种情况下,经常检查是有意义的。

自动检查/硬件辅助 最后要考虑的是,您所运行的硬件是否具有某种可以检查NULL的机制。具体来说,我指的是页面错误检测。如果您的系统具有页面错误检测功能,则CPU本身可以检查NULL访问。我个人认为这是最好的机制,因为它总是运行并且不依赖程序员进行显式检查。它还具有几乎零开销的好处。如果可以的话,我建议您这样做,调试会稍微困难一些,但也不太难。

要测试它是否可用,请创建一个带有指针的程序。将指针设置为0,然后尝试读取/写入它。


我不知道是否将段错误分类为执行自动NULL检查。我同意拥有CPU内存保护确实有帮助,因此一个进程不会对系统的其余部分造成太大的损害,但是我不会将其称为自动保护。
加布里埃尔南部

1

在我看来,验证输入(即前置条件或后置条件)对于检测编程错误是一件好事,但前提是它会导致响亮而令人讨厌的显示停止错误,而这种错误是无法忽略的。assert通常具有这种效果。

如果没有非常协调的团队,任何未能做到的事情都会变成一场噩梦。当然,理想情况下,所有团队都应在严格的标准下进行非常仔细的协调和统一,但是我所工作的大多数环境都远远不够。

举个例子,我与一些同事一起工作,他们相信人们应该认真检查空指针的存在,因此他们撒了很多这样的代码:

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

...有时甚至不返回/设置错误代码。这是在已有数十年历史的代码库中,其中包含许多收购的第三方插件。它也是一个受许多错误困扰的代码库,这些错误通常很难追踪到根本原因,因为它们倾向于在远离问题根源的站点中崩溃。

而这种做法就是原因之一。move_vertex向其传递空顶点违反了上述函数的既定先决条件,但是这样的函数只是默默地接受了它,却没有做任何响应。因此,趋向于发生的事情是,一个插件可能会遇到程序员错误,导致该插件将null传递给所述函数,只是无法检测到它,之后才做很多事情,最终系统将开始崩溃或崩溃。

但是,这里真正的问题是无法轻松检测到此问题。因此,我曾经尝试查看如果将上面的类比代码转换为assert,会发生什么情况,如下所示:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

...令我震惊的是,我发现断言即使在启动应用程序时也会左右失败。修复了最初的几个呼叫站点后,我做了更多的事情,然后又遇到了更多的断言失败。我一直进行着修改,直到修改了太多的代码,以致于我最终撤消了更改,因为它们变得过于侵入,并且勉强保留了空指针检查,而是记录了该函数允许接受空顶点的情况。

但这是危险,即使在最坏的情况下,也无法轻易检测到违反前置/后置条件的情况。然后,您可以多年来,在测试雷达下飞行时,静默积累大量违反此类前提条件的代码。在我看来,这样的空指针检查在公然而令人讨厌的断言失败之外实际上可以带来的危害远远大于好处。

至于何时应该检查空指针的基本问题,我相信可以自由地断言是否旨在检测程序员错误,而不是让其静默且难以检测。如果这不是编程错误,而是程序员无法控制的事情,例如内存不足故障,那么检查空值并使用错误处理是有意义的。除此之外,这是一个设计问题,并基于您的功能认为有效的前后条件。


0

一种实践是始终执行空检查,除非您已经检查过它;否则,请执行以下操作:因此,如果将输入从函数A()传递到B(),并且A()已经验证了指针,并且您确定B()在其他任何地方都没有被调用,那么B()可以相信A()具有清理数据。


1
...直到六个月后有人出现并添加了一些更多的代码来调用B()(可能假设编写B()的人一定会正确检查NULL)。然后你就被搞砸了,不是吗?基本规则-如果输入的功能存在无效条件,请检查该条件,因为输入不在功能的控制范围之内。
Maximus Minimus

@ mh01如果您只是粉碎随机代码(即进行假设而不阅读文档),那么我认为额外的NULL检查不会做很多事情。想一想:现在B()检查NULL并...做什么?返回-1?如果调用者不检查NULL,您对他们将如何处理-1返回值案例有什么信心?
2013年

1
这就是来电者的责任。您要承担自己的责任,包括不信任收到的任意/未知/潜在未经验证的输入。否则,您将身处城市。如果呼叫者不检查,则表明呼叫者已经搞砸了。您检查了一下,自己的屁股被掩盖了,您可以告诉写呼叫者的人至少您做对了。
Maximus Minimus
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.