这个问题是关于术语的。
int main()
{
unsigned char array[10] = {0};
void *ptr = array;
void *middle = &ptr[5]; // <== dereferencing ‘void *’ pointer
}
Gcc发出警告取消引用无效指针。
我理解这个警告是因为编译器需要计算实际偏移量,而它不能,因为void
没有标准大小。
但我不同意这个错误信息。这不是取消引用。我找不到一个非引用的解释,因为它不是对某个东西的价值。
offsetof也是如此:
#define offsetof(a,b) ((int)(&(((a*)(0))->b)))
由于空指针取消引用,有很多关于这是否是UB的线程。但这不是一个空指针取消引用!是吗?
汇编代码中没有存储访问
mov rax, QWORD PTR [rbp-48]
add rax, 5
mov QWORD PTR [rbp-40], rax
取消引用和存储访问之间有什么区别?
但我不同意错误消息。这不是取消引用。我找不到一个非引用的解释,因为它不是对某个东西的价值。
该标准没有提供术语"去引用"的正式定义。它唯一使用它的地方是(非规范性)脚注102:
[…]一元
*
运算符取消引用指针的无效值包括空指针、与所指向对象类型不适当对齐的地址以及对象在其生存期结束后的地址。
不过,请注意,此注释将解引用描述为一元*
运算符的行为,而不是对结果执行其他操作的效果。您可以将该操作视为将指针转换为它所指向的对象,如果指针实际上没有指向指向类型的对象,或者指向类型是不完整的对象(如void
),您会意识到这会带来问题。即使生成的对象未被使用,这样的问题也会正式存在。
现在,我承认这里存在混淆的空间,因为在不使用结果对象的情况下执行解引用是无用的,但这与重点无关。考虑以下完整的C语句:
1 + 2;
你会因为结果未使用而否认它执行加法吗?
现在,您的(子)表达式ptr[5]
被定义为具有与(*((ptr)+(5)))
相同的含义。指针加法表达式的类型与所涉及的指针的类型相同,因此,从将一元*
运算符应用于该类型的表达式的意义上讲,这确实涉及到对void *
的解引用。
尽管如此,尽管我认为错误消息是正确的,我确实同意这是一个糟糕的选择。这里的一个更基本的问题,也是按求值顺序首先到达的问题,是违反了语言约束,即在添加指针时,指针必须指向完整的类型,而void
不是。事实上,很难将发出的消息解释为满足约束违反导致诊断的要求。这似乎是关于一个不同的问题——一个产生未定义行为,但不涉及违反约束的问题。
你还说:
offsetof也是如此:
#define offsetof(a,b) ((int)(&(((a*)(0))->b)))
[…]但这不是一个空指针取消引用!是吗?
小心。C语言没有定义offsetof()
宏的替换文本的具体形式;您介绍的是一个实现细节。
我们可以很容易地转移到语义上,因为"去引用"不是标准中定义的术语,所以我将解决一个类似的问题:当宏参数满足offsetof()
宏的要求时,所给出的定义是否扩展到具有定义良好的行为的表达式
当间接成员选择运算符(->
)的左侧操作数具有可接受的类型但不指向任何对象(例如为null时)时,该标准不会定义其行为。因此,该行为是未定义的。或者,如果我们认为a->b
完全等价于((*a).b)
,那么当a
不指向任何对象时,行为是明确未定义的。无论哪种方式,C语言都不会定义表达式的行为。
但这正是您的特定宏定义是实现细节变得重要的地方。从中提取它的实现可以自由地提供它想要的任何行为,特别是,它可以提供可靠地满足offsetof()
宏的C规范的行为。您自己不应该依赖这样的代码。即使在提供该表单的offsetof()
定义的实现中,您也不能确定它是否也使用了一些特殊的内部魔法(不能直接用于您自己的代码)来使其工作。