在指针使用之前检查null

时间:2009-12-17 05:15:43

标签: c++ pointers

大多数人都使用这样的指针......

if ( p != NULL ) {
  DoWhateverWithP();
}

但是,如果指针因任何原因为空,则不会调用该函数。

我的问题是,不检查NULL可能更有益吗?显然,在安全关键系统上,这不是一个选项,但是如果程序在没有它的情况下仍可以运行,那么你的程序崩溃的荣耀比没有被调用的函数更明显。

关于第一个问题,在使用指针之前,你总是检查NULL吗?

其次,考虑你有一个将指针作为参数的函数,并且在整个程序的多个指针上多次使用此函数。你是否发现在函数中测试NULL更有利(好处是你不必在整个地方测试NULL),或者在调用函数之前在指针上测试(这样做的好处是没有调用函数的开销) )?

13 个答案:

答案 0 :(得分:15)

你认为NULL指针经常导致立即崩溃是正确的,但是不要忘记,如果你通过NULL指针索引到一个大型数组,你可能确实得到一个有效的内存地址如果你的指数足够高。然后,你会得到内存损坏或不正确的内存读取,这将更难找到。

每当我可以假设调用一个带NULL的函数是一个错误,这在生产代码中永远不会发生,我更喜欢在函数中使用ASSERT防护,它们只在调试版本中编译成实际代码,而不是检查否则为NULL。

从我的观点来看,一般来说,函数应检查其参数,而不是调用者。您应该始终假设您的呼叫者可能对检查有点草率,或者他们可能包含错误......

道德:通过抛出的一些if()语句或使用一些ASSERT构造(可能带有明确的消息说明为什么会发生这种情况)来检查被调用函数中的NULL。同时检查调用者是否为NULL,但前提是调用者知道在正常的程序执行中可能发生这种情况,并采取相应的行动。

答案 1 :(得分:10)

当程序在NULL指针出现时崩溃是可以接受的,我偏爱:

assert(p);
DoWhateverWithP();

这只会检查调试版本中的指针,因为在预处理器级别定义NDEBUG通常#undef s assert()。它记录了您的假设并协助调试,但对已发布的二进制文件没有性能影响(但公平地说,检查NULL指针应该在绝大多数情况下对性能实际上没有影响)。

作为附带好处,这对C和C ++都是合法的,在后一种情况下,不需要在编译器/运行时启用异常。

关于你的第二个问题,我更喜欢把断言放在子程序的开头。同样,assert()的美妙之处在于,没有任何“开销”可言。因此,没有什么可以衡量在子程序定义中只需要一个断言的好处。

当然,需要注意的是,你永远不想断言带有副作用的表达式:

assert(p = malloc(1)); // NEVER DO THIS!
DoSomethingWithP();    // If NDEBUG was defined, malloc() was never called!

答案 2 :(得分:8)

我更喜欢这种风格:

if (p == NULL) {
    // throw some exception here
}

DoWhateverWithP();

这意味着,如果pNULL,此代码所处的任何功能都将很快失败。你是正确的,如果pNULLDoWhateverWithP无法执行但是使用空指针或者根本不执行该函数都是不可接受的方法来处理fack p 1}}是NULL

要记住的重要一点是提前退出并快速失败 - 这种方法产生的代码更容易调试。

答案 3 :(得分:8)

不要规定只检查空值,如果找不到则不做任何事情。

如果允许指针为null,那么在实际为空的情况下,您必须考虑代码的作用。通常,无所事事就是错误的答案。小心翼翼地定义可以像这样工作的API,但这需要的不仅仅是散布一些关于这个地方的NULL检查。

因此,如果允许指针为null,则必须检查null,并且必须执行任何适当的操作。

如果不允许指针为null,那么编写调用未定义行为的代码(如果它为null)是完全合理的。它与编写字符串处理例程没有什么不同,如果输入不是NUL终止的,则调用未定义的行为,或者如果调用者传入错误的值,则编写缓冲区使用的例程调用未定义的行为,或编写一个函数一个file*参数,如果用户将文件描述符reinterpret_cast传递给file*,则调用未定义的行为。在C和C ++中,您只需依靠调用者告诉您的内容即可。垃圾进,垃圾出。

但是,您可能希望编写代码,以便在传递最可能的垃圾类型时帮助您的调用者(毕竟可能是您)。断言和异常对此有好处。

采用Franci对这个问题的评论的类比:大多数人在穿越人行道时,或坐在沙发上之前都不寻找汽车。他们仍然可能被汽车击中。它发生了。但是在这种情况下花费任何力气检查汽车通常会被认为是偏执狂,或者说汤罐上的说法“首先,检查你厨房里的汽车。然后加热汤。”

您的代码也是如此。将无效值传递给函数要比将汽车意外驾驶到某人厨房要容易得多。但是如果他们这样做并打击某人,那仍然是司机的过错,而不是厨师未能做到应有的谨慎。您不一定希望厨师(或被调用者)使用应该多余的检查来混淆他们的食谱(代码)。

还有其他方法可以找到问题,例如单元测试和调试器。无论如何,除了必要的(道路)之外,创造一个无车环境要比在整个地方无所不能地驾驶汽车更安全,并希望每个人都可以随时应对它们。所以,如果你在不允许的情况下检查null,你不应该让这个让人们知道 毕竟是允许的。

[编辑 - 我真的只是一个错误的例子,检查null不会找到无效的指针。我将使用地图来保存一些物体。我将使用指向这些对象的指针(表示图形),这很好,因为map永远不会重定位其内容。但我还没有定义对象的排序(这样做会有点棘手)。因此,为了让事情发生变化并证明其他代码有效,我使用了向量和线性搜索而不是地图。没错,我不是指矢量,我的意思是deque。因此,在第一次向量调整大小之后,我没有将空指针传递给函数,但是我将指针传递给已被释放的内存。

我发出了无效垃圾的哑误差,大致和我做出无效错误传递空指针的哑错误一样。因此,无论我是否添加了对null的检查,我仍然需要能够诊断程序崩溃的问题,因为我无法检查。由于这也将诊断空指针访问,我通常不打扰检查null,除非我正在编写代码以通常检查进入函数的前提条件。在这种情况下,如果可能的话,应该做的不仅仅是检查null。]

答案 4 :(得分:4)

除了其他答案外,还取决于NULL表示的内容。例如,这段代码完全没问题,而且非常惯用:

while (fgets(buf, sizeof buf, fp) != NULL) {
    process(buf);
}

此处,NULL值不仅表示错误,还指示文件结束条件。同样地,strtok()会返回NULL来说“没有更多令牌”(尽管我应该首先避免使用strtok(),但我会离题)。在这种情况下,如果返回的指针不是NULL,则调用函数完全正常,否则不执行任何操作。

编辑:另一个例子,更接近问题:

const char *data = "this;is;a;test;";
const char *curr = data;
const char *p;
while ((p = strchr(curr, ';')) != NULL) {
    /* process data in [curr, p) */
    process(curr, p);
    curr = p + 1;
}

此处NULL再次strchr()表示;无法找到NULL,而我们应该停止进一步处理数据

话虽如此,如果NULL未被用作指示,则取决于:

  • 如果代码中此时指针不能为assert(p != NULL);,则在开发时有fprintf(stderr, "Can't happen\n");非常有用,abort()或等效声明,然后采取适当的任何行动(NULL或类似可能是此时唯一合理的选择。)
  • 如果指针可以是malloc(),并且它并不重要,那么绕过空指针的使用可能会更好。假设您正在尝试为写入日志消息分配内存,malloc()失败。你不应该因此而中止该计划。如果sprintf()成功,则需要调用函数(NULL / whatever)来填充缓冲区。
  • 如果指针可以是NULL,那么它很关键。在这种情况下,您可能希望失败,并希望这种情况不会经常发生。
  

其次,考虑你有一个功能   将指针作为参数,   你使用这个函数多次   遍及多个指针的时间   你的计划。你发现它了吗?   有利于测试中的NULL   功能(好处是你没有   必须测试全是NULL   地方),或之前的指针   调用函数(好处   没有来自呼叫的开销   功能)?

这取决于很多因素。如果我可以肯定有时或大多数时候指针传递给函数不能是NULL,那么额外检查函数是浪费的。如果传递的指针来自很多地方,并且在任何地方进行检查都很棘手,当然,检查在函数本身中是很好的。

标准库函数在大多数情况下不会检查str*mem*free()函数。例外是NULL 检查assert

关于assert的评论:NDEBUG如果定义了assert则是无操作,因此不应该将其用于调试 - 它的唯一用途是在开发过程中捕获编程错误。此外,在C89中,int需要assert(p != NULL),因此assert(p)在这种情况下比仅仅{{1}}更好。

答案 5 :(得分:2)

使用引用而不是指针可以避免这种非NULLness检查。这样,编译器确保传递的参数不为NULL。例如:

void f(Param& param)
{
    // "param" is a pointer that is guaranteed not to be NULL      
}

在这种情况下,由客户端进行检查。但是,大多数客户情况都是这样的:

Param instance;
f(instance);

不需要进行非NULLness检查。

使用堆上分配的对象时,可以执行以下操作:

Param& instance = *new Param();
f(*instance);

更新:作为用户Crashworks备注,仍然可以让您的程序崩溃。但是,在使用引用时,客户端有责任传递有效的引用,正如我在示例中所示,这很容易做到。

答案 6 :(得分:1)

怎么样:澄清意图的评论?如果意图是“这不可能发生”,那么可能一个断言是正确的,而不是if语句。

另一方面,如果一个空值是正常的,也许是一个“其他评论”解释了为什么我们可以跳过“然后”步骤将是有序的。 Stevel McConnel在“代码完成”中有一个关于if / else语句的一个很好的部分,以及缺少的其他是一个非常常见的错误(分心,忘了吗?)。

因此,我通常会对“no-op else”发表评论,除非它的形式为“if done,return / break”。

答案 7 :(得分:1)

当您检查NULL时,仅跳过函数调用并不是一个好主意。您应该有一个 else -part,在NULL的情况下执行某些有意义的操作,例如抛出错误或将错误代码返回到上一级。

另一方面,NULL并不总是错误。它通常用于表示例如已达到数据结束。在这种情况下,您将不得不像正常的程序流程一样处理这种情况。

答案 8 :(得分:0)

我想我已经看到了更多。这样,如果你知道它会爆炸,你就不会继续。

if (NULL == p)
{
  goto FunctionExit; // or some other common label to exit the function.
}

答案 9 :(得分:0)

第一个问题的答案是:你在谈论理想情况,我看到的大多数使用if ( p != NULL )的代码都是遗留的。还假设,您希望返回一个求值程序,然后使用数据调用求值程序,但是说该数据没有求值程序,在调用求值程序之前返回NULL并检查NULL是合理的。

第二个问题的答案是,它取决于情况,如删除检查NULL指针,而许多其他功能则没有。有时,如果在函数内部测试指针,则可能需要在许多函数中测试它,如:

ABC(p);
a = DEF(p);
d = GHI(a);
JKL(p, d);

但是这段代码会更好:

if(p)
{
 ABC(p);
 a = DEF(p);
 d = GHI(a);
 JKL(p, d);
}

答案 10 :(得分:0)

  

不检查NULL可能更有益吗?

我不会这样做,我赞成前线的断言和身体过去的某种形式的恢复。断言会为您提供什么,不检查null会不会?类似的效果,更容易解释和正式承认。

  

关于第一个问题,在使用指针之前,你总是检查NULL吗?

这实际上取决于代码和可用时间,但我非常擅长它;在我的程序中,“实现”的很大一部分包括程序应该做什么,而不是通常的'它应该做什么'。

  

其次,考虑你有一个将指针作为参数的函数......

我在函数中测试它,因为函数(希望是)更频繁地重用的程序。我也倾向于在打电话之前测试它,如果没有那个测试,错误会失去本地化(对于报告和隔离很有用)。

答案 11 :(得分:0)

我认为最好检查一下null。虽然,您可以减少需要进行的检查。

对于大多数情况,我更喜欢功能顶部的简单保护条款:

if (p == NULL) return;

尽管如此,我通常只会对公开曝光的功能进行检查。

但是,当空指针出现意外时我会抛出异常。 (有些函数用null调用没有任何意义,消费者应该负责任地使用它。)

构造函数初始化可以用作始终检查null的替代方法。当类包含集合时,这尤其有用。该集合可以在整个类中使用,而无需检查它是否已初始化。

答案 12 :(得分:0)

取消引用空指针是未定义的行为。如果你想在指针为空时崩溃,可以使用一个断言或类似的东西(并且,根据你所定义的类的行为,这可能是一个完全有效的响应 - 当人们可能期待某事时,它肯定比继续运行更好已经完成了!)。

由于取消引用空指针的行为未定义,因此它可以执行任何。崩溃,腐败的记忆,创造一个虫洞到另一个维度,允许长老神出来吞噬所有人类......任何事情。虽然发生了错误,但根据定义,未定义的行为是一个错误。所以不要故意