【问题标题】:When to check for nullptr inside functions with a pointer parameter?何时使用指针参数检查函数内部的 nullptr?
【发布时间】:2020-07-10 20:54:59
【问题描述】:

来自 C++,我们有 const 引用,我总是很难在 C 中解决这个问题。

如果我在 C 中有这样的东西:

struct Vector3 {
    float x,y,z;
};

void test(struct Vector3 *va, struct Vector3 *vb) {

    // Check for nullptr... 
    if (va == NULL || vb == NULL) {
       // log / exit program
    }
    // bla bla...
}

在 C++ 中,我可以使用 const 引用来不允许 NULL 指针,但在 C 中则不允许。你什么时候在函数中检查 null?或者你用什么规则来检查或不检查?因为如果指针是否为空,检查每个接收到指针的函数感觉是错误的。

我知道我可以传递Vectors 的值而不是指针,但这只是一个示例。假设我们正在处理大型结构。

【问题讨论】:

  • 就我个人而言,NULL 仅当函数的某些其他部分在没有它的情况下可以在逻辑上执行时才检查参数。我通常还会断言我期望有效的指针的有效性。我认为很多都归结为样式和文档。不幸的是,你确实失去了假设指针有效性的绝对权利,你只需要用常识来弥补它,并相信你的函数调用者也会这样做。

标签: c pointers


【解决方案1】:

逻辑错误和断言错误是有区别的。如果您的函数只允许使用有效指针运行,那么您的函数的用户有责任并应在进入您的函数之前检查它是否是有效指针。标准 C 函数记录了诸如“未定义行为”之类的状态,因为它实际上是未定义的

如果是这样,这就是assert() 发挥作用的地方。 assert 旨在仅在项目的调试配置中触发,并在项目的发布配置中被删除(因此不会影响性能) - 因此是 NDEBUG 宏。 NASA principles of safety-critical code 告诉“代码的断言密度应该平均到每个函数至少两个断言”。

因此,如果将 NULL 传递给您的函数是“无效状态”,在这种情况下,您的函数的行为是“未定义”,因为传递 NULL(或任何其他无效指针)是没有意义的,我通常写:

struct Vector3 {
    float x,y,z;
};

void test(struct Vector3 *va, struct Vector3 *vb) {
    assert(va != NULL);
    assert(vb != NULL);
    // bla bla...
}

(在 gcc 上,我会添加 __attribute__((__nonnull__)) 并使用最新的 gcc 和 -std=c2x 我们可以 [[gnu::nonnull]])。请注意,assert() 在定义 NDEBUG 时会扩展为“无”(即不计算表达式),因此不要将具有副作用的语句放在 assert() 中。

【讨论】:

  • 啊哈,如果用户传递了一个 nullptr,那么添加断言将有助于在调试模式下调试错误,但如果它在发布中发生,则将是未定义的行为(没有性能损失),这是有道理的。我接受这个答案是因为我认为它更完整并且有一个例子。
【解决方案2】:

如果函数承诺(在其文档中)处理空指针,则需要检查。否则它不会,并且当用户随后(错误地)提供指向此类函数的空指针时,行为变得未定义。

您可以让编译器通过将不接受空指针的指针参数声明为 type arg[static 1](不需要诊断,但当您通过此类参数传递 NULL 时,clang 会发出警告)而不是 type *arg 或使用非标准 __attribute((nonnull(arg_index))) 函数属性(当您通过此类参数传递 NULL 时会导致诊断参数),但这些不是正确性所必需的。

asserts(在发布版本中被省略)也很有帮助。

【讨论】:

  • 对于type arg[static 1] arg,编译器不会“强制”它为非空;它只是对编译器的一个指示,非空参数没有被传递。如果将空指针传递给此类函数,则不需要诊断。
  • @P.P.感谢您的评论。是的,通过NULL 传递type arg[static N] 的诊断完全是可选的。 clang 对此发出警告,gcc 没有(两者都有属性 nonnull,它可靠地发出警告)。
  • nonnull 属性也一样。如果编译器无法推断它是非空参数,则不会发出警告(此外,它是编译器扩展)。简而言之,空指针验证也不是替代。因此,它们的实际用途受到限制,因为在几乎所有情况下,您仍然需要进行空指针检查。
  • 在进程中包含静态代码分析以进一步检测 NULL 指针的取消引用可能不会有什么坏处。
  • @P.P 是的,它只警告静态已知的空指针。但是,如果提供的指针为空,您只需触发 UB 的路线是完全有效的。 C充满了UB的可能性。当上下文可以保证正确性而无需检查成本时,我看不到人们在每个签名的数学运算中检查溢出,对每个下标进行边界检查等。一切都是权衡。
【解决方案3】:

在用 C 编写时,我通常采用 C 方法:document 对函数参数的要求,并假设调用者提供符合文档的参数。因此,如果我记录一个指针参数必须是指向struct Vector3 的有效指针(例如),我通常不会执行空值检查,如果调用者不遵守,我只会让筹码落在可能的位置。

对于在 C++ 中专门使用 const 引用的情况,您还可以选择按值传递结构、联合和标量(但不是数组)。对于小型结构,例如您的示例中的结构,在某些情况下甚至可能会产生较小的性能提升。示例:

void test(struct Vector3 va, struct Vector3 vb) {
    // bla bla...
}

【讨论】:

    【解决方案4】:

    让我们同意这一点:如果将 NULL 指针传递给函数并在其中取消引用,则程序会导致 分段错误(如果您使用的是嵌入式 uC,则会导致 CPU nullptr 异常 -它可以有不同的名称,具体取决于特定的设备)。这是我们通常不喜欢的。

    我们无法阻止调用者传递 invalid 指针,但通过此检查,我们至少可以防止很大比例的潜在崩溃。

    让我们总结一下主要场景:

    1. 我们想要检查我们的函数何时是我们提供给第三方的 API。必须有人进行检查,由于我们无法确定客户会编写安全代码,因此我们检查参数并在无效值的情况下返回错误。 (作为一个例外,我们实际上可以有一个有据可查的 API,其中警告客户 NULL 参数将导致崩溃)。
    2. 我们可以检查与否我们的函数是否是我们在自己的程序中使用的实用程序(例如嵌入式固件)。我们设计了我们的代码,所以我们可以决定参数有效性检查是总是负责调用者(在这种情况下我们不检查),或者它总是负责被调用的函数(在这种情况下,我们不检查)。
    3. 如果我们对自己的设计非常有信心,我们可以避免检查,我们确信每个函数调用都会有有效的参数。或者,当我们的嵌入式软件的闪存空间非常有限时,我们需要保存来自if (ptr != NULL) 检查的所有额外代码。在后一种情况下,即使重新启动系统也是可以接受的。

    【讨论】:

    • 要考虑的一个重要问题是,如果参数无效,该怎么做而不是崩溃。我在想出这样一种情况时遇到了麻烦,即传递无效或(意外)空指针参数是由编程错误以外的任何原因引起的,因此尚不清楚是否会有任何可行的恢复路径。
    • the program crashes 我有一种奇怪的感觉,我不会同意。我同意会发生依赖于实现/架构的信号。但不一定“崩溃”。
    • @JohnBollinger 在这种情况下存在非常典型的 BADPARAM 错误(或类似的错误)。即使是“跟踪错误并且什么都不做”选项也比崩溃要好。 ps:在我公司,设计蜂窝物联网设备,check是负责调用函数的。
    • @JohnBollinger 我喜欢将errno 设置为EINVAL 并可能返回一个指示错误的值。至少,它将帮助将来必须调试它的可怜的傻瓜。希望其他程序员在调用该函数并记录它或其他内容时注意该错误,以便更容易跟踪它。
    • 好吧,那么您错过了那部分,在 linux 上取消引用 NULL 会导致发送 SIGSEGV 信号,该信号可以被处理并在信号上下文中继续执行代码。这不是“崩溃”,而是一个信号。而且,在 stm32 上取消引用 NULL 会导致 HardFault 异常。这不是“崩溃”,只是代码跳转到不同的位置。我同意会发生一些事情,但这不一定是崩溃。
    【解决方案5】:

    一般规则是,如果你要取消引用指针,那么你要检查它是否不是空指针,原因很明显。

    您在哪里执行此操作取决于函数的作用以及指针是否是其功能的核心。如果您总是要取消引用它,那么从一开始就检查是有意义的。例如

    void func(mytype *ptr)
    {
        if (!ptr) return;
    
        /* important stuff */
    }
    

    void func(mytype *ptr)
    {
        if (ptr) {
            /* important stuff */
         }
    }
    

    如果它是一种“可选”的东西,那么您可以在“使用点”进行检查,例如:

    void func(mytype *ptr)
    {
        /* important stuff that's always done */
    
        /* Optionally, if the ptr is not null, set it */
        if (ptr) {
            *ptr = /* a value that indicates some status or a calculated result */;
        }
    }
    

    【讨论】:

      【解决方案6】:

      任何参数验证都应该在函数的开头进行,这样您就可以避免在验证之前意外使用参数。

      在 C 中,没有标准编译器保证某些内容不为空(与 C++ 引用不同),因此您无法确定。如果您知道只有您将调用某个函数,则可以合并输入检查,这样您就不必在同一指针通过不同函数时多次执行此操作。但是对于来自库的指针/代码,没有任何保证,所以如果你想避免段错误,你必须到处检查。

      【讨论】:

      • 那么你是说每个接收指针参数的函数都应该检查null?
      【解决方案7】:

      如果您确定始终使用struct Vector3 变量的地址调用您的函数,通常您不必检查它是否等于NULL

      否则,您必须检查并定义在这种情况下要执行的操作。

      如果你有一个遍历二叉树的递归函数,你必须检查根节点是否不为 NULL,因为即使第一次调用有一个非 NULL 指针,在多次调用同一个函数后,你可能/将会出现这种情况,即使用 NULL 指针调用函数。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-03-14
        • 2018-04-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-12-13
        相关资源
        最近更新 更多