【问题标题】:Checking for null before pointer usage在使用指针之前检查 null
【发布时间】:2009-12-17 05:15:43
【问题描述】:

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

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

但是,如果指针由于某种原因为空,则不会调用该函数。

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

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

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

【问题讨论】:

  • 大多数人在过马路之前都会环顾四周,看看他们是否会被来车撞到。但这并不是绝对必要的,您只需过马路就可以知道您是否会被汽车撞到。 :-)
  • @Franci:大多数人都会检查汽车,当汽车在那里时,等待。许多程序会检查汽车,如果有汽车,就跳到下一个斑马线。
  • 我希望尽可能通过引用传递来避免这个问题。
  • @Steve 和其他人。显然这是理想的,但并不总是一种选择。

标签: c++ pointers


【解决方案1】:

您的想法是正确的,NULL 指针通常会导致立即崩溃,但是不要忘记,如果您通过 NULL 指针对大型数组进行索引,您可能确实会获得有效的内存地址如果你的指数足够高。然后,您会遇到内存损坏或不正确的内存读取,这将更难定位。

每当我可以假设使用 NULL 调用函数是一个错误,而这绝不应该在生产代码中发生,我更喜欢在函数中使用 ASSERT 保护,它只在调试构建中编译成实际代码,而不是检查否则为 NULL。

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

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

【讨论】:

  • 想了半天之后,我终于得出结论,在空指针上调用函数会导致未定义的行为,即使它会导致程序崩溃荣耀之光,是一种未定义的荣耀之光。我宁愿检查 null 而不是那样做。
【解决方案2】:

当出现NULL 指针时程序崩溃是可以接受的,我倾向于:

assert(p);
DoWhateverWithP();

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

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

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

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

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

【讨论】:

  • assert 的问题在于它会在调试模式下造成额外的延迟。因此,时间与发布版本不同。特别是在实时应用程序中,如果您有很多断言,整个程序可能会停止工作。
  • 真;我正在使用一些类似 assert(ValidGraph(g)); 的代码。其中 ValidGraph() 是一些昂贵的例程,用于检查图形的大量特征。这是在循环中调用的,导致调试版本异常缓慢(并且此应用程序不是实时的)。
  • assert() 不是灵丹妙药,但我相信在许多常见的现实情况下,它是一个有吸引力的解决方案。
  • "assert 的问题在于它在调试模式下会导致额外的延迟。因此,时间与发布构建中的时间不同。尤其是在实时应用程序中,如果您有很多断言整个程序可能会停止工作” --- 如果您依赖调试构建执行与发布构建相同的操作,那么您做错了。 STL 在调试模式下通常要慢得多,这在 3rd-party 库中很常见。
【解决方案3】:

不要将只检查 null 而不做任何事情作为规则。

如果指针被允许为空,那么你必须考虑你的代码在它实际为空的情况下会做什么。通常,什么都不做是错误的答案。小心地定义这样工作的 API 是可能的,但这需要的不仅仅是分散一些关于该位置的 NULL 检查。

所以,如果允许指针为空,那么你必须检查是否为空,并且你必须做任何适当的事情。

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

但是,您可能希望编写代码来帮助您的调用者(毕竟可能是您)当最可能的垃圾类型被传入时。断言和异常对此很有用。

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

您的代码也是如此。将无效值传递给函数比不小心将车开进某人的厨房要容易得多。但如果他们这样做并撞到人,这仍然是司机的错,而不是厨师没有行使应有的注意。您不一定希望厨师(或被调用者)用应该是多余的检查来弄乱他们的食谱(代码)。

还有其他方法可以找到问题,例如单元测试和调试器。无论如何,创造一个没有汽车的环境要安全得多,除非在必要的地方(道路),而不是在一个地方随意驾驶汽车并希望每个人都能随时应对它们。因此,如果您确实在不允许的情况下检查 null,您不应该让人们认为它毕竟 是允许的。

[编辑——我只是碰到了一个错误的例子,检查 null 不会找到一个无效的指针。我将使用地图来保存一些对象。我将使用指向这些对象的指针(以表示图形),这很好,因为 map 从不重新定位其内容。但是我还没有定义对象的顺序(这样做会有点棘手)。所以,为了让事情动起来并证明其他一些代码有效,我使用了向量和线性搜索而不是地图。没错,我不是说vector,我是说deque。所以在第一次调整向量大小后,我没有将空指针传递给函数,而是将指针传递给已释放的内存。

我犯的传递无效垃圾的愚蠢错误的频率与我犯的传递无效空指针的愚蠢错误的频率差不多。因此,无论我是否添加了对 null 的检查,我仍然需要能够诊断程序由于我无法检查的原因而崩溃的问题。因为这也将诊断空指针访问,所以我通常不会费心检查空,除非我正在编写代码来一般检查进入函数的先决条件。在这种情况下,如果可能的话,它应该做的不仅仅是检查 null。]

【讨论】:

  • 我在睡了之后稍微重新考虑了这件事。由于程序的性质,指针在任何情况下都不应该为 NULL。但是,在程序员未能将指针初始化为正确值或做了一些愚蠢的事情(如果 p = NULL )的情况下,检查 NULL 可能很有用。仅仅看到程序崩溃并没有太大帮助,因此在发生这种情况时打印一条消息似乎很理想(仅在调试模式下)。
  • “仅仅看到程序崩溃并没有太大帮助” - 如果您在调试器下运行测试,通常是这样。但根据您的平台,您可能没有可用的调试器。即使它在那里,你也可能并不总是使用它。所以我认为诸如断言之类的可开关检查很有用
  • 如果我的程序崩溃了,我通常会浏览一下我认为的潜在问题。如果我找不到任何东西,或者我错了,我会去调试器。但是,一条消息说“空指针错误”肯定会使搜索更容易,因为我知道要查找什么。
【解决方案4】:

我更喜欢这种风格:

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

DoWhateverWithP();

这意味着如果pNULL,则此代码所在的任何函数都会快速失败。你是对的,如果pNULL,则DoWhateverWithP 无法执行,但使用空指针或根本不执行函数都是处理pNULL 的不可接受的方法。

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

【讨论】:

  • +1 用于抛出异常。异常使代码更具可读性和调试更容易。
  • -1 用于抛出异常。这真的是一种特殊情况,还是您的代码必须能够处理的常规事情?
  • @mch,如果程序必须能够处理它,那么当然,崩溃不是一种选择。这就是正在讨论的内容。
  • @mch,是的,这是一种特殊情况。
【解决方案5】:

除了其他答案之外,这取决于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()/whatever) 来填充缓冲区。
  • 如果指针可以是NULL,这很关键。在这种情况下,您可能希望失败,希望这种情况不会经常发生。

其次,考虑你有一个功能 将指针作为参数, 并且您多次使用此功能 整个过程中多个指针的时间 你的程序。有没有发现更多 有利于在 功能(好处是你没有 必须在整个范围内测试 NULL 地方),或在指针之前 调用函数(好处 调用 函数)?

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

在大多数情况下,标准库函数不检查NULL:例如str*mem* 函数。 free() 是一个例外,它确实检查NULL

关于assert 的评论:如果NDEBUG 被定义,assert 是无操作的,因此不应将其用于调试——它的唯一用途是在开发期间捕获编程错误。此外,在 C89 中,assert 采用 int,因此在这种情况下,assert(p != NULL) 比简单的 assert(p) 更好。

【讨论】:

    【解决方案6】:

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

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

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

    Param instance;
    f(instance);
    

    不需要非空性检查。

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

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

    更新:正如用户Crashworks 所说,仍然有可能使您的程序崩溃。然而,当使用引用时,传递一个有效的引用是客户端的责任,正如我在示例中所展示的,这很容易做到。

    【讨论】:

    • 引用可能为空。例如,如果我调用 int *foo =NULL; f(*foo);。这将编译并调用到 f 中,然后在实际使用引用时发生段错误。
    • @Crashworks:你是对的,但这没有多大意义,不是吗?
    • 你只得到一个分段错误?你很幸运。当我运行该代码时,恶魔从我的鼻子里飞了出来!
    • 好吧,你显然不会故意像那样取消引用 NULL,但它可能发生在一个大型程序中,其中一些变量通常存储为指针,然后你取消引用它以传递给这样的函数.我已经修复了很多这样的崩溃。
    • @Crashworks:在这些极少数情况下,我建议您在客户端代码中进行非空检查。
    【解决方案7】:

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

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

    因此,我通常会在“no-op else”中添加注释,除非它是“如果完成,则返回/中断”的形式。

    【讨论】:

      【解决方案8】:

      当您检查 NULL 时,跳过函数调用不是一个好主意。你应该有一个 else-part 在 NULL 的情况下做一些有意义的事情,例如抛出错误或返回错误代码到上层。

      另一方面,NULL 并不总是错误。例如,它通常用于指示已到达数据末尾。在这种情况下,您将不得不按照正常的程序流程来处理这种情况。

      【讨论】:

        【解决方案9】:

        第一个问题的答案是:你说的是理想情况,我看到的大多数使用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】:

          不检查 NULL 是否更有益?

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

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

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

          其次,假设您有一个将指针作为参数的函数...

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

          【讨论】:

            【解决方案11】:

            我想我见过更多。这样一来,如果你知道它无论如何都会爆炸,你就不会继续。

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

            【讨论】:

            • Erm...Dijkstra 不会同意:u.arizona.edu/~rubinson/copyright_violations/…
            • 我一般不赞成 C++ 掌握在人类手中。这正是我在野外看到的......因为他/她被 C++ 咬了很多次,以至于他不相信任何传入的数据......永远。因此,在调用链的每一步都会检查相同的参数。
            • 也可能是因为例外是更多的工作,更多的是你的脸 :).. 这正是作者想要避免的。
            • 我认为常见的标签称为return;
            • 这正是析构函数和 RAII 应该做的。
            【解决方案12】:

            我认为最好检查 null。不过,您可以减少需要进行的检查。

            在大多数情况下,我更喜欢在函数顶部使用简单的保护子句:

            if (p == NULL) return;
            

            也就是说,我通常只检查公开公开的函数。

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

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

            【讨论】:

              【解决方案13】:

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

              由于取消引用空指针的行为是未定义的,它可以做任何事情。崩溃,损坏的记忆,创造一个通往另一个维度的虫洞,让上古之神出现并吞噬所有人类......任何东西。当错误发生时,根据定义,取决于未定义的行为是错误。所以不要这样做刻意

              【讨论】:

                猜你喜欢
                • 2010-10-11
                • 1970-01-01
                • 2010-10-07
                • 2021-10-05
                • 2010-12-27
                • 2016-05-24
                • 1970-01-01
                • 2011-09-05
                • 2013-08-20
                相关资源
                最近更新 更多