存储无效指针是否自动属于未定义行为?

25

显然,对无效指针进行解引用会导致未定义行为。但是,将无效内存地址简单地存储在指针变量中又如何呢?

考虑以下代码:

const char* str = "abcdef";
const char* begin = str;
if (begin - 1 < str) { /* ... do something ... */ }

表达式 begin - 1 的计算结果是一个无效的内存地址。请注意,我们实际上并没有引用该地址 - 我们只是在指针算术中使用它来测试它是否有效。尽管如此,我们仍然必须将一个无效的内存地址加载到寄存器中。

那么,这是未定义行为吗?我从来没有这样认为过,因为许多指针算术似乎都依赖于这种东西,而指针本质上只是一个整数。但最近我听说,即使是将无效指针加载到寄存器中的操作也是未定义的行为,因为某些体系结构会自动抛出总线错误或其他错误。有人能否向我指出C或C ++标准中解决这个问题的相关部分?


1
根据C/C++标准,这确实是未定义行为。但坦率地说,我从未见过在上述情况下是未定义行为的现实世界CPU/架构,即不允许任意指针算术运算的机器。我见过很多架构,包括嵌入式微控制器。因此,在我(谦虚)的观点中,只要限制自己使用现代非奇特的架构,代码就可以正常工作。 - valdo
请问能否扩展一下问题 - 如果您有一个for循环,其中您向后遍历数组怎么办?在这种遍历中,您肯定需要检查第一个元素之前的元素,而不需要对其进行解引用。我曾经有过类似的问题,但那是关于最后一个元素之后的元素的。 - Nick
7个回答

16

我手头有C草案标准,它通过未定义来规定了这种情况。在6.5.6/8中,它为ptr + I定义了以下情况:

  • 如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向原始元素的偏移量,使得结果和原始数组元素的下标之差等于整数表达式。
  • 此外,如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向数组对象的最后一个元素的下一个元素;如果表达式Q指向数组对象的最后一个元素的下一个元素,则表达式(Q)-1指向数组对象的最后一个元素。

您的情况不符合以上任何一种情况。您的数组大小不足以使-1将指针调整到指向不同的数组元素,也没有任何结果或原始指针指向超出末尾的位置。


1
这是未定义行为还是未指定行为。我期望代码能够运行并正常工作,没有任何不良后果,但无法确定它是否进入了if分支(根据标准)。 - Martin York
1
@Martin York:即使未被解引用,C++标准也将其定义为未定义行为。我希望我在帖子中引用了相关的语句。 - Chubsdad
1
这是一种行为,可能会导致验证指针寄存器内容的硬件出现故障。因此,它是未定义行为。某些实现可以规定程序执行引起未定义行为的各种操作时会发生什么。如果实现符合自己的规范,则该行为将被定义明确。但是,如果在符合C标准但不符合特定实现规范的不同实现上运行代码,则程序可能会以任意方式失败。 - supercat
@supercat,我不确定你认为我哪一部分是不同意的。我的观点是,即使指针陷阱的架构也可以做很多工作,使代码在假定指针适合通用寄存器并且您可以对它们进行整数运算时按预期运行。在我的示例中,size_t将是一个32位字,但ptrdiff_tintptr_t将在两个字中存储指针,并对它们进行long long int数学运算。但是,在该体系结构上,这将比测试选择器的相等性并仅对偏移量执行ALU操作效率低。 - Davislor
2
@Lorehead:现代使用“优化”一词是指编译器应积极识别可能调用未定义行为的情况,并得出结论,即变量不能保存会导致这种情况出现的值。例如,给定代码 if (p != 0) doSomething(p); debug_log(*p);,“现代”的优化编译器可以得出结论,如果“p”为null,则调用doSomething是安全的,即使在目标平台上读取空指针也只会产生无意义的值。 - supercat
显示剩余5条评论

11

由于另一个原因,您的代码是未定义行为:

表达式begin - 1并不会产生无效指针。这是未定义行为。您不能在超出正在操作的数组边界之外进行指针算术运算。因此,无效的是减法本身,而不是存储所得到的指针。


1
C99的解释(在我的答案中提供链接)特别提到了指针算术超出数组范围会产生无效指针。 - fizzer
2
ptrdiff_t 只能用于计算指向同一数据对象的两个指针。唯一的例外是指向数组“末尾”之后的一个指针,不在数组边界内也可以。 - DevSolar
@fizzer: 该理由提供了一个合理的原因,即为什么要允许实现对比以前有效指针之类的操作进行陷阱,但我想知道是否有人建议在标准中加入像__POINTER_EXTENSIONS这样的宏,实现可以将其设置为一种概念上与__FP_EVAL_MODE类似的值,以指示实现可以支持超出标准要求的操作类型。许多实现可以对指针行为提供远远超出标准要求的保证,并且算法可以使用... - supercat
@supercat 为什么操作系统需要那个?操作系统不会去测试您在程序中创建的对象... - jalf
@jalf:考虑一下mallocfree本身的设计。可以设计一个malloc/free系统,它在每个块的头部存储足够的信息,以便不必“搜索”该块,但如果有许多小对象,则此类开销可能会很大。在许多情况下,更实际的做法是使标头存储足够的信息,以便可以发现块与其他块的关系而不需要太多工作,但某些算法需要比较不相关的指针。至于memmove,也可以进行设计... - supercat
显示剩余14条评论

8
一些架构有专门的寄存器用于保存指针。将未映射地址的值放入这样的寄存器是允许崩溃的。整数溢出/下溢是允许崩溃的。因为C旨在在各种平台上工作,指针提供了一种安全编程不安全电路的机制。
如果你知道自己不会在具有这种棘手特性的奇异硬件上运行,那么你就不需要担心语言中未定义的内容。它在平台上是明确定义的。
当然,这个例子的风格很差,没有好的理由去这样做。

1
它在一个平台上被定义得很好,并不意味着针对该平台的所有实现都能定义得很好。编译器的作者更关注“优化”而不是支持低级编程,因此即使底层平台可以,也不能指望这样的代码能够可靠地运行。 - supercat
@supercat 是的,这是另一个经常引起抱怨的问题。不过,这种错误更容易被发现。越界计算有时很难避免,并且在代码中很难看到。C++正在引入std::launder来选择性地赋予这些值,但实际上指定该函数与您预期的一样奇怪。 - Potatoswatter
C语言别名规则的另一个问题是它们基于内存的动态内容,而不是程序结构的静态方面。如果规则指定当从T*转换为U*指针时,这种转换创建了一个“窗口”,在此期间可以使用指针来访问类型为T*U*的元素,则此类规则可以允许比当前规则更多的优化,同时还允许使用许多否则需要-fno-strict-aliasing的代码。 - supercat
请问您能否给出您回答中提到的一种架构的名称? - Evg
1
@Evg m68k有地址寄存器,我不确定但未映射地址加载注释可能是指IA64。 - Potatoswatter
显示剩余3条评论

4

如果是这样的话,那么在进行指针运算时,你不就可以将所有指针强制转换为 ptrdiff_t 吗?换句话说,如果我将上面的代码示例更改为 if ((ptrdiff_t)begin - 1),那么这个行为就不再是未定义的了吗? - Channel72
不是未定义行为,而是实现定义的结果。也就是说,你的实现将记录一些合理的行为,但它不会是可移植的,可能也不会有用。 - fizzer
comp.lang.c FAQ有相关解答:http://c-faq.com/ptrs/int2ptr.html。就像我说的,我手头没有标准。 - fizzer
3
请注意,ptrdiff_t 类型将保存指针之间的差值,而不是指针本身。这两者并不相同。 - fizzer

2
$5.7/6 - “除非两个指针都指向同一个数组对象的元素,或者指向该数组对象的最后一个元素之后的位置,否则行为是未定义的。75)”
简而言之,即使您不对指针进行解引用,行为也是未定义的。

1
该文本涉及从指针中减去另一个指针;而该操作者正在将整数从指针中减去。 - James McNellis
@James McNellis:我想这是关于指针算术的。最终它关乎的是指针值的结果。 - Chubsdad
我对你的推理不确定,从不同数组中减去两个指针可能会出现问题,因为这些指针指向不同的内存区域(在16位架构中考虑远/近内存)。这里没有关于干涉指针本身的内容,事实上,使用64位指针的高位来存储附加标志是非常常见的。 - Matthieu M.

1
多年前就已经给出了正确答案,但我觉得有趣的是C99 rationale [sec. 6.5.6, last 3 paragraphs]解释了为什么标准支持将指向数组最后一个元素的指针(p+1)加1:

对广泛实践的重要认可是指针可以始终被递增到数组的结尾,而不必担心溢出或环绕

并且解释了为什么不支持p-1

另一方面,在p-1的情况下,必须在p遍历的对象数组之前分配整个对象,因此可能会失败。这种限制允许分段架构在可寻址内存范围的开头放置对象。

因此,如果指针p指向可寻址内存范围的起始对象,这是由该注释支持的,那么p-1将生成下溢。
请注意,整数溢出是标准中未定义行为的示例[sec. 3.4.3],因为它取决于翻译环境和操作环境。我相信很容易看出这种环境依赖性也适用于指针下溢。
这就是为什么标准明确将其定义为未定义行为[in 6.5.6/8],正如其他答案所指出的那样。引用该句话:
如果指针操作数和结果都指向同一数组对象或数组对象的最后一个元素之一,则评估不会产生溢出;否则,行为是未定义的。
请参见C99理由[sec. 6.3.2.3,最后4段],其中详细描述了如何生成无效指针以及可能产生的影响。

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接