我注意到来自Clang的警告:
warning: performing pointer arithmetic on a null pointer
has undefined behavior [-Wnull-pointer-arithmetic]
详细来说,正是此代码触发了此警告:
uint8_t *end = ((uint8_t*)0) + sizeof(uint8_t) * count;
为什么对从非零整数获取的非空指针执行相同操作时,禁止对空指针进行算术运算不会触发任何警告?
更重要的是,C标准是否明确禁止空指针算法?
答案 0 :(得分:6)
C标准不允许。
6.5.6加法运算符(强调我的意思)
8将具有整数类型的表达式添加到或 从指针中减去,结果为指针的类型 操作数。 如果指针操作数指向数组的元素 对象,并且数组足够大,结果指向一个元素 与原始元素的偏移量,使得 结果数组元素和原始数组元素的下标等于 整数表达式。换句话说,如果表达式P指向 数组对象的第i个元素,表达式(P)+ N(等效地, N +(P))和(P)-N(其中N的值为n)分别指向 数组对象的第i + n个元素和第i-n个元素(如果存在)。 此外,如果表达式P指向数组的最后一个元素 对象,表达式(P)+1指向对象的最后一个元素 数组对象,如果表达式Q指向最后一个元素之后的一个 数组对象的表达式(Q)-1指向数组对象的最后一个元素 数组对象。 如果指针操作数和结果都指向 相同数组对象的元素,或者在最后一个元素之后 数组对象,求值不应产生溢出;除此以外, 行为是不确定的。如果结果指向最后一个 数组对象的元素,不得将其用作a的操作数 一元*运算符。
出于上述目的,指向单个对象的指针被视为指向1个元素的数组。
现在,((uint8_t*)0)
没有指向数组对象的元素。仅仅因为持有空指针值的指针不会指向任何对象。在下面这样说:
6.3.2.3指针
3如果将空指针常量转换为指针类型,则 结果指针(称为空指针)可以保证进行比较 不等于指向任何对象或函数的指针。
因此您无法对此进行算术运算。该警告是有道理的,因为正如突出显示的第二句所提到的,我们处于行为未定义的情况。
不要被offsetof
宏可能是这样实现的事实所愚弄。标准库不受用户程序约束的约束。它可以使用更深入的知识。但是在我们的代码中这样做的定义不明确。
答案 1 :(得分:0)
对此线程的澄清很少。
首先,由于StoryTeller引用的原因,这是C标准中未定义的行为:
如果指针操作数和结果都指向同一数组对象的元素,或者指向数组对象的最后一个元素,则求值不应产生溢出; 否则,行为是不确定的。
由于转换为任何指针类型的零常量文字会转换为空指针常量,该常量不指向内存的任何连续区域,因此行为是不确定的。
但是,对空指针执行算术运算以获取偏移量并不是什么新鲜事,offsetof
宏的C实现使用它:
#define offsetof(st, m) ((size_t)&(((st *)0)->m))
并且经常对指针执行相同的算术方式:
int *end = (int *)0 + array_size;
此行实际上与写作相同:
int *end = (int *)(sizeof(int) * array_size);
我认为偏移量计算是在实现中定义的,因为编译器“可以” 取消引用此类指针以获取实际的内存偏移量,这当然是不太可能的,但仍然可能。
>此外,请注意,针对空指针算法的警告仅针对Clang 6.0。即使使用-fsanitize=undefined
,GCC也不会触发它。
答案 2 :(得分:0)
在编写 C 标准时,对于 任何 非void*
指针值 p,绝大多数 C 实现都会支持 p+0
和 {{ 1}} 都产生 p-0
,而 p
将产生零。更一般地,当 N 为零时,像大小为零的 p-p
或 memcpy
之类的操作在大小为 N 的缓冲区上操作将忽略缓冲区地址。这种行为将允许程序员避免编写代码来处理极端情况。例如,输出带有通过地址和长度参数传递的可选负载的数据包的代码自然会将 (NULL,0) 处理为空负载。
已发布的 C 标准基本原理中没有任何内容表明目标平台自然以这种方式运行的实现不应该像往常一样继续工作。但是,在某些平台上,在 fwrite
为 null 的情况下,维护此类行为保证的成本可能会很高。
在大多数情况下,绝大多数 C 实现会以相同的方式处理构造,但可能存在这种处理不切实际的实现,标准将向空指针添加零的特征描述为未定义行为。该标准允许实现,作为“符合语言扩展”的一种形式,在不强加任何要求的情况下定义结构的行为,并允许符合(但不是严格符合)的程序使用它们。根据已发布的基本原理,声明的意图是将对此类“流行扩展”的支持视为由市场决定的“实施质量”问题。可以以基本上零成本支持它们的实现可以这样做,但是在这种支持昂贵的实现中,可以根据客户的需求自由地支持或不支持此类构造。
如果一个人使用的编译器针对普通平台,并且旨在合理有效地处理最广泛的有用程序,那么围绕指针算术的扩展语义可能允许一个人比其他方式更有效地编写代码。然而,如果一个编译器不重视与高质量编译器的兼容性,那么人们应该认识到,它可能会将标准对古怪硬件的允许视为一种邀请,即使在普通硬件上也可以做出荒谬的行为。当然,人们还应该意识到,在遵守标准要求他们放弃不健全但“通常”是安全的优化的极端情况下,此类编译器可能会表现得很荒谬。