指向 2D 数组项的指针可以为 NULL 吗?

Can pointer to 2D array item be NULL?

提问人:Thomas Matthews 提问时间:8/11/2023 最后编辑:Thomas Matthews 更新时间:8/11/2023 访问量:94

问:

静态分析程序 (LDRA) 存在以下语句问题:

const float    range_min = p_results_phase->range_min;

分析器表示需要测试指针是否为 NULL。p_results_phase

定义如下:

Cal_Ver_Results_t const * const     p_results_phase = &results[test_num][CAL_VER_PHASE];

数组在函数声明中定义:results

void DrawVACalibrationVerificationResultsScreen(Cal_Ver_Results_t results[][CAL_VER_SI_COUNT], unsigned int test_num)

我的理解是指针不能为 NULL,因为指针被指定指向数组中的某个位置。p_results_phaseresults

编辑 1:实际代码(按顺序):

Cal_Ver_Results_t const * const     p_results_phase = &results[test_num][CAL_VER_PHASE];
const float                         range_min = p_results_phase->range_min;
const float                         range_max = p_results_phase->range_max;

Screen snapshot as additioinal proof of no intervening code between definition and usage.

问题:

  1. 如果变量超出范围,指针是否会分配给 NULL?p_results_phasetest_num
  2. 如果 超出范围,指针是否会分配给 NULL?p_results_phaseCAL_VER_PHASE

定义:Cal_Ver_Results_t

typedef struct Cal_Ver_Results_s {
    float result1;
    float result2;
    float range_min;
    float range_max;
    _Bool pass;
}Cal_Ver_Results_t; 
数组 c 指针 多维数组 null

评论

1赞 Barmar 8/11/2023
我相信你是对的,这似乎是静态分析中的一个错误。
2赞 Remy Lebeau 8/11/2023
获取数组元素的地址永远不会产生 NULL 指针。但是,如果数组指针本身为 NULL,或者元素越界,则获取地址将是未定义的行为
1赞 Eugene Sh. 8/11/2023
您可能省略了代码的其他一些部分,这些部分可能会使分析器认为指针可以变为 NULL。
0赞 Thomas Matthews 8/11/2023
@RemyLebeau 指针怎么可能为 NULL?它被分配为指向数组中的插槽,而无需任何干预代码。我将编辑问题。
1赞 Eric Postpischil 8/11/2023
编辑问题以提供最小的可重现示例

答:

0赞 chqrlie 8/11/2023 #1

指针值是根据作为函数参数接收的数组的地址计算得出的。如果定义为局部或全局数组,并且假设元素位于数组边界内,则指针不能为 null,但尽管函数参数定义中有数组语法,但该指针仍作为指针传递,因此该函数可以接收 null 指针作为参数。在这种情况下,计算方式会生成一个无效的指针,取消引用它具有未定义的行为。resultsresultsp_results_phase&results[test_num][CAL_VER_PHASE]

计算出的指针可能为 null,但这将是巧合:例如,如果 和 都是 和 是 null 指针。比较的价值有限,但检查不是空指针是更一般的健全性检查。test_numCAL_VER_PHASE0resultsp_results_phaseNULLresult

至于你的其他问题:

如果变量越界,指针是否会被分配给?p_results_phaseNULLtest_num

不太可能,但在这种情况下将是一个无效的指针,不得取消引用。p_results_phase

如果 出界,指针会被分配给吗?p_results_phaseNULLCAL_VER_PHASE

同上,将是一个无效的指针。取消引用它将具有未定义的行为。指针可能指向数组中或数组外部的另一个条目,当您取消引用无效指针时,任何事情都可能发生。p_results_phase

当您确认这是数组的索引时,您可能需要检查它是否在正确的边界内,但未提供数组大小,因此您可能必须信任调用方。test_num

您可以非常简单地检查它不是空指针results

    if (!results) {
        /* signal some error */
        return;
    }

C99 引入了一种语法,用于指定使用数组语法声明的函数参数的最小元素数。例如,该函数可以声明和定义为:

void DrawVACalibrationVerificationResultsScreen(
    Cal_Ver_Results_t results[static 10][CAL_VER_SI_COUNT],
    unsigned int test_num)
{
    ...
}

这告诉编译器指向至少包含对象数组的数组。results10CAL_VER_SI_COUNTCal_Ver_Results_t

使用此原型,它不应允许显式传递 null 指针,如果它无法判断函数参数是否确实是预期最小长度的数组,则可能会产生警告。

同样,静态代码分析器应该能够假定它不能是空指针,因此两者都不是,前提是使用的索引值在二维数组边界内。resultsp_results_phase

此功能尚未得到广泛接受,尤其是在面向嵌入式系统的开发人员中,这可能是由于编译器支持不佳。恕我直言,语法既繁琐又不直观:我不主张重写 的原型,以强调指针必须为非 null,更不用说那些 of 和 friends,它只会解决这些函数参数的约束之一。strlensize_t strlen(const char s[static 1])strcpy()

评论

0赞 Thomas Matthews 8/11/2023
是索引,而不是容量。test_num
0赞 Thomas Matthews 8/11/2023
请提供测试 NULL 的 2d 数组的示例。我从未测试过 NULL 的数组参数,只测试过指针。:-)
2赞 Eric Postpischil 8/11/2023
@ThomasMatthews:没有数组参数。根据 C 2018 6.7.6.3 7,任何声明为数组的参数都会自动调整为指针。
1赞 John Bollinger 8/11/2023
@ThomasMatthews,声明和的含义完全相同。在函数定义中也是如此。void f(int x[42]);void f(int *x);
1赞 Eric Postpischil 8/11/2023
@ThomasMatthews: Re “A function parameter”:这应该被表述为“声明为数组的参数”,因为 C 也有函数参数,这些参数是声明为函数的参数。它们也被调整为指针(指向函数)。Re “I can use both array notation and pointer notation and still be correct?”:如果你的意思是 和 : 下标运算符对指针进行操作,而不是对数组进行操作。如果数组用于 in,则会自动将其转换为指针,然后执行下标操作。对于 中的数组,也会发生相同的转换。a[i]a+iaa[i]a+i
0赞 jpalecek 8/11/2023 #2

答案是肯定的,如果你不走运的话。

特别是,您需要确保:

  1. 该参数不是 .虽然它在语法上声明为数组,但实际上它的行为与普通指针非常相似。resultsNULL

  2. 也就是说,正如你所说,在范围内。让它出界可能不会导致存在,那将是一个不幸的巧合,但它会指向任何地方并导致 UB。test_nump_result_phaseNULL

  3. CAL_VER_PHASE也应该有一个有效的值,但习惯上大写标识符是常量,所以它的值应该是OK(请检查)。

评论

0赞 John Bollinger 8/11/2023
尽管函数参数的声明方式类似于数组,但实际上将其声明指针。没有必要用“差不多”来模棱两可,我不知道除了“普通”的指针之外,你还能想到什么其他类型的指针。results
-1赞 akc5 8/11/2023 #3

越界访问数组是一个典型的问题。在 C 中,此行为是未定义的,因此变量可能为 NULL,也可能不是 NULL。p_results_phase

  1. 如果test_num变量越界,是否p_results_phase指针分配给 NULL?

是的,也不是。如果使用越界索引,则可能指向与预期不同的内存地址。例如:
被计算为 where 是数组的基址,是数组的索引。
现在,如果您正在访问越界索引,例如 ,它将只指向一个内存地址,如果该地址位于程序的虚拟地址空间内,那么您最终只会读取不正确的值,另一方面,写入这样的内存地址将覆盖该内存位置上存在的内容,这可能会导致其他严重问题。
此外,如果生成的内存地址位于程序虚拟地址空间的受限区域中,则对越界索引进行操作也可能导致分段错误。
a[i]*(a + i)aia[-100](a + (-100)) == (a-100)

如果指针p_results_phase是否被分配给 NULL CAL_VER_PHASE越界了?

同样的答案。越界数组索引意味着您正在读取/写入非预期的内存位置。对这些内存地址执行读/写操作将导致未定义的结果。

评论

1赞 H.S. 8/11/2023
a[i]评估为 和 不是 .*(a + i)(a + i)
3赞 Eric Postpischil 8/11/2023
Re “现在,如果您正在访问越界索引,例如,那将只指向内存地址”:不,使用越界索引(例如未指定)指向内存地址。未定义行为。这包括它没有被定义为引用的事实。虽然一个简单的“直接语义”C实现可以指向,但常见的现代实现并不简单。它们包括以令人惊讶的方式转换程序的积极优化,以及可能导致各种其他行为。a[-100](a + (-100)) == (a-100)a[-100]a-100a-100a-100
0赞 Eric Postpischil 8/11/2023
回复“不。“:由于行为未定义,答案是肯定的,而不是否定的。计算具有越界数组索引的表达式可能会导致将 null 指针分配给变量。
0赞 akc5 8/11/2023
@EricPostpischil 对 OP 的问题“是”意味着所有越界数组访问都将是 NULL 访问,但由于其未定义的行为,我们无法确定它。我认为,由于未定义的行为,它需要同时说“是”或“否”才能捕获所有可能的结果。
1赞 Eric Postpischil 8/11/2023
@akc5:对不起,我错过了编辑:答案应该是“是的,它可能会发生。断言它总是发生或永远不会发生是错误的。
0赞 John Bollinger 8/11/2023 #4

我的理解是指针不能为 NULL,因为指针被指定指向数组中的某个位置。p_results_phaseresults

嗯,是的,但更一般地说,是用一元运算符中的表达式的结果初始化的。只要操作数是有效的左值,操作就会计算其地址,该地址需要与每个 null 指针进行比较。如果操作数不是有效的左值,那么与其说是操作可能产生空指针的问题,不如说是操作具有未定义行为的问题。添加 null 检查不会以任何方式解决这个问题。p_results_phase&&

特别

  1. 如果变量超出范围,指针是否会分配给 NULL?p_results_phasetest_num
  2. 如果 超出范围,指针是否会分配给 NULL?p_results_phaseCAL_VER_PHASE

C 语言规范没有回答这两个问题。尝试通过越界索引引用数组元素会产生未定义的行为。在实践中,该 UB 不太可能表现为被计算为 null 指针的地址,但它可以。然而,这在很大程度上是无关紧要的,因为未定义的行为不是本地化的。如果存在越界访问,则这本身就是问题所在,而不是生成的 UB 允许程序表现得好像计算的指针值为 null。

分析器表示需要测试指针是否为 NULL。p_results_phase

分析器错误。但是,如果您需要安抚它,那么您可能需要添加无用的 null 检查。更有可能的是,您的编译器比分析器更智能,并且在启用优化的情况下进行编译时会删除额外的检查。

或者,可能有一种方法(例如,特殊构造的注释)来指示分析器忽略此感知到的问题。如果是这样,这可能值得考虑,特别是如果你的编译器碰巧警告 null 检查是无关紧要的,这是可以想象的。

0赞 Clifford 8/11/2023 #5

静态分析工具通常是关于应用/执行编码标准,而不是直接检测错误。编码标准的目的是减少维护和重用中的错误。

在这种情况下,在取消引用任何指针之前测试 null 是一种编码标准。它允许代码在其他使用上下文或维护中保持可靠,其中在其他地方所做的更改可能会导致指针为 null。

您可以从分析配置文件中删除该规则,也可以使用工具使用的任何消息抑制注释格式禁止显示此特定实例。