C 空指针算法

C null pointer arithmetic

我注意到来自 Clang 的这个警告:

warning: performing pointer arithmetic on a null pointer
has undefined behavior [-Wnull-pointer-arithmetic]

详细来说,触发此警告的是这段代码:

int *start = ((int*)0);
int *end = ((int*)0) + count;

转换为任何指针类型的常量文字零衰减为空指针常量,它不指向任何连续的内存区域但仍然具有类型指向类型的指针做指针运算。

当对从非零整数获得的非空指针执行相同操作不会触发任何警告时,为什么禁止对空指针进行算术运算?

更重要的是,C 标准是否明确禁止空指针运算


此外,此代码不会触发警告,但这是因为指针未在编译时求值:

int *start = ((int*)0);
int *end = start + count;

但是避免未定义行为的一个好方法是显式地将整数值转换为指针:

int *end = (int *)(sizeof(int) * count);

C 标准不允许。

6.5.6 Additive operators (emphasis mine)

8 When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the pointer operand points to an element of an array object, and the array is large enough, the result points to an element offset from the original element such that the difference of the subscripts of the resulting and original array elements equals the integer expression. In other words, if the expression P points to the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and (P)-N (where N has the value n) point to, respectively, the i+n-th and i-n-th elements of the array object, provided they exist. Moreover, if the expression P points to the last element of an array object, the expression (P)+1 points one past the last element of the array object, and if the expression Q points one past the last element of an array object, the expression (Q)-1 points to the last element of the array object. If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated.

出于上述目的,指向单个对象的指针被视为指向包含 1 个元素的数组。

现在,((uint8_t*)0) 指向数组对象的元素。仅仅是因为持有空指针值的指针不指向任何对象。说的是:

6.3.2.3 Pointers

3 If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

所以你不能对它进行算术运算。警告是有道理的,因为正如第二个突出显示的句子提到的,我们处于未定义行为的情况下。

不要被 offsetof 宏可能是这样实现的事实所迷惑。标准库不受用户程序的限制。它可以运用更深层次的知识。但是在我们的代码中这样做并没有明确定义。

编写 C 标准时,对于 anyvoid* 指针值 p,绝大多数 C 实现都支持 p+0p-0 都产生 p,而 p-p 将产生零。更一般地说,在大小为 N 的缓冲区上操作的大小为零 memcpyfwrite 之类的操作将在 N 为零时忽略缓冲区地址。这种行为将允许程序员避免编写代码来处理极端情况。例如,输出带有通过地址和长度参数传递的可选负载的数据包的代码自然会将 (NULL,0) 处理为空负载。

已发布的 C 标准基本原理中没有任何内容表明目标平台自然会以这种方式运行的实现不应像往常一样继续工作。但是,在 p 为 null 的情况下,在某些平台上维护此类行为保证的成本可能很高。

在大多数情况下,绝大多数 C 实现将以相同的方式处理构造,但可能存在这种处理不切实际的实现,标准将向空指针添加零描述为未定义行为。该标准允许实现作为“一致语言扩展”的一种形式,在不强加要求的情况下定义构造的行为,并允许一致(但不严格一致)的程序使用它们。根据已发布的基本原理,声明的意图是将对此类“流行扩展”的支持视为由市场决定的“实施质量”问题。可以以基本上零成本支持它们的实现会这样做,但是这种支持将很昂贵的实现将根据客户的需求免费支持或不支持此类构造。

如果您使用的编译器是针对普通平台的,并且旨在合理有效地处理最广泛的有用程序,那么围绕指针算法的扩展语义可能会让您比其他方式更有效地编写代码.但是,如果目标编译器不重视与高质量编译器的兼容性,则应该认识到它可能会将标准对古怪硬件的允许视为一种邀请,即使在普通硬件上也会做出荒谬的行为。当然,还应该意识到,此类编译器在极端情况下可能会表现出荒谬的行为,在这种情况下,遵守标准将要求它们放弃不合理但“通常”是安全的优化。