C中void指针的指针算术

239

当指向特定类型(例如 int, char, float, ..) 的指针被递增时,它的值将增加该数据类型的大小。如果一个指向大小为 x 的数据的 void 指针被递增,它如何到达指向 x 字节之后的位置?编译器如何知道要将 x 添加到指针的值中?


1
可能是在MSVC中对void*执行指针算术运算的错误的重复问题。 - Carl Norum
4
这个问题似乎假定编译器(/运行时)知道指针所设定的对象类型,并将其大小添加到指针中。这是一个完全错误的理解:它只知道地址。 - PJTraill
3
如果一个指向大小为x的数据的void指针被递增,它如何指向后面x个字节?它不会。为什么那些有这种问题的人不能在提问之前测试一下呢,至少进行最基本的编译检查,而这个代码无法编译。给它负1分,真不敢相信这还获得了100个赞和0个踩。 - underscore_d
@underscore_d,这是一个好问题。我们必须知道为什么它不能编译。假设将1添加到void *会使地址向前移动1个字节(与char *相同)是完全合理的。然而,这种愚蠢不合逻辑的标准被无缘无故地强加给每个人,成为“非法”的。 - ScienceDiscoverer
@ScienceDiscoverer 是的,这是一个很好的问题。但愚蠢和不合逻辑是你的观点。在我看来,空指针算术运算无效是完全合乎逻辑的,因为在C语言中,p+n 显然必须将 n * sizeof(*p) 添加到地址 p 中,但 sizeof(void) 显然为 0。 - Steve Summit
10个回答

361

最终结论:在C语言和C++中,对 void* 进行算术运算是不合法的。

GCC 作为扩展允许它,参见指针算术运算(注意,该部分是手册中“C扩展”章节的一部分)。 Clang 和 ICC 可能允许对 void* 进行算术运算以与GCC保持兼容。 其他编译器(如MSVC)不允许对 void* 进行算术运算,如果指定了 -pedantic-errors 标志或 -Werror=pointer-arith 标志,则GCC也不允许(如果您的代码库还必须使用 MSVC 编译,则此标志很有用)。

C标准规定

引用自 n1256 草案。

该标准对加法操作的描述如下:

6.5.6-2: 对于加法,要么两个操作数都属于算术类型, 要么一个操作数是指向对象类型的指针, 另一个操作数是整数类型。

因此,问题在于 void* 是否是指向“对象类型”的指针, 或等效地说,void 是否是“对象类型”。 “对象类型”的定义为:

6.2.5.1:类型分成对象类型(完全描述对象的类型), 函数类型(描述函数的类型)和不完整类型 (描述对象但缺少确定其大小所需的信息)。

标准将 void 定义为:

6.2.5-19:void 类型包括 一组空值; 它是无法完成的不完整类型。

由于void是不完整类型,它不是对象类型,因此它不是加法运算的有效操作数。因此,您不能对void指针执行指针算术运算。
注:最初认为允许void*算术运算,因为C标准的以下部分:
6.2.5-27:指向void的指针应具有与指向字符类型的指针相同的表示和对齐要求。
然而,“具有相同的表示和对齐要求”意味着可以在函数参数、函数返回值和联合成员之间互换使用,但这并不意味着您可以在void*上进行算术运算。

10
根据C99标准(6.5.6.2),加法必须要求两个操作数都是算术类型,或者一个操作数是指向对象类型的指针,另一个操作数是整数类型。(6.2.5.19) void类型包含一组空值;它是一个不完整的类型,不能被完成。我认为这表明void*指针算术运算是不允许的。GCC有一个扩展,允许这样做。 - mtvec
1
尽管这个答案被证明是错误的,但它仍然很有用,因为它提供了确凿的证据表明空指针不适用于算术运算。 - Ben Flynn
1
这是一个很好的答案,它有正确的结论和必要的引用,但是那些来到这个问题的人因为没有读到答案底部而得出了错误的结论。我已经编辑过了,使其更加明显。 - Dietrich Epp
1
Clang和ICC默认情况下不允许void*算术运算。 - Sergey Podobry
3
对于指针的加法操作,现在规定:“一个运算对象必须是指向完整对象类型的指针,而另一个则应为整数类型。” 所以我猜使用 void* 指针进行指针加法仍然是未定义行为。 - Steve Siegel
显示剩余12条评论

71

void* 指针上不允许使用指针算术运算。


20
指针的算术运算仅适用于(完整的)对象类型指针。void是一种不完整类型,根据定义永远无法完成。 - schot
1
@schot:没错,而且指针算术运算只能在指向数组对象的元素的指针上进行,并且只有当操作的结果是指向该同一数组或该数组最后一个元素之一的指针时才定义。如果不满足这些条件,行为就是未定义的。(摘自C99标准6.5.6.8) - mtvec
1
显然,gcc 7.3.0不是这样的。编译器接受void*类型的p + 1024,并且结果与((char *)p) + 1024相同。 - uuu777
1
@zzz777 这是GCC的扩展,可以查看排名第一的答案中的链接。 - Ruslan

29

将其转换为char指针并将指针向前移动x个字节。


14
如果您正在编写自己的排序函数,根据man 3 qsort中的描述,应该具有void qsort(void *base, size_t nmemb, size_t size, [snip])参数。那么您就无法知道“正确的类型”。 - alisianoi
此外,如果您正在编写类似于Linux内核的container_of宏的内容,则需要一种方法来补偿不同编译器对结构体的打包。例如,给定此结构体:... typedef struct a_ { x X; y Y; } a; ...如果您有一个变量y *B =(something)并且您想要指向B的封闭a结构的指针(假设存在),那么您最终需要执行类似以下操作:... a *A =(a *)(((char *)B) - offsetof(a,Y));...如果您改为执行以下操作:... a *A =(a *)(((x *)B)-1);...那么您可能会遇到一些非常令人讨厌的意外! - chadjoan

22
C标准不允许使用void指针进行算术运算。然而,GNU C允许使用,因为它将void视为大小为1的类型。

C11标准§6.2.5

第19段

void类型包含一组空值;它是一个不完全的对象类型,无法完成。

以下程序在GCC编译器中运行良好。

#include<stdio.h>

int main()
{
    int arr[2] = {1, 2};
    void *ptr = &arr;
    ptr = ptr + sizeof(int);
    printf("%d\n", *(int *)ptr);
    return 0;
}

其他编译器可能会生成错误。


14

正因为这个原因,您不能对 void * 类型进行指针运算!


11

空指针可以指向任何内存块。因此,当我们尝试对空指针进行指针算术运算时,编译器不知道要增加/减少多少字节。因此,在空指针参与任何指针算术运算之前,必须首先将其转换为已知类型。

void *p = malloc(sizeof(char)*10);
p++; //compiler does how many where to pint the pointer after this increment operation

char * c = (char *)p;
c++;  // compiler will increment the c by 1, since size of char is 1 byte.

8

在进行指针运算之前,您需要将其强制转换为另一种指针类型。


2
在void指针上进行算术运算是一种有争议的、非标准的扩展。如果你考虑的是汇编语言,那么指针只是地址,对void指针进行算术运算就有意义,加1就是加1。但如果你按照C语言的模型来思考指针算术运算,那么给任何指针p加1实际上会将地址加上sizeof(*p),这正是你希望指针算术运算所做的,但由于sizeof(void)等于0,这在void指针上会出问题。
如果你按照C语言的模型来思考,你不介意它出现问题,并且也不介意插入显式转换为(char *),如果这是你想要的算术运算。但如果你考虑的是汇编语言,你希望它能够正常工作,这就是为什么这种扩展(虽然与C语言中指针算术运算的正确定义有所偏离)在某些圈子中受欢迎,并由一些编译器提供的原因。

-1

指针算术在空指针中不允许。

原因:指针算术与普通算术不同,因为它是相对于基地址发生的。

解决方案:在进行算术运算时使用类型转换运算符,这将使表达式执行指针算术的基本数据类型得到确定。 例如:point 是空指针。

*point=*point +1; //Not valid
*(int *)point= *(int *)point +1; //valid

-3
编译器通过类型转换来知道。给定一个 `void *x`:
- `x+1` 将一个字节添加到 `x`,指针移动到 `x+1` 字节处 - `(int*)x+1` 将 `sizeof(int)` 个字节添加,指针移动到 `x + sizeof(int)` 字节处 - `(float*)x+1` 添加 `sizeof(float)` 个字节,等等
虽然第一项不具有可移植性并且违背了C/C++的规范,但它仍然是符合C语言的正确写法,这意味着它将在大多数编译器上编译为某种形式,可能需要使用适当的标志(如 -Wpointer-arith)。

3
虽然第一项不可移植且违反了C/C++的Galateo规则,但这是正确的C语言吗?这是双重思考!在void *上进行指针算术运算在语法上是非法的,不应该编译,并且如果编译了会产生未定义的行为。如果一个粗心的程序员可以通过禁用某些警告来使其编译,那也不能成为借口。 - underscore_d
1
@underscore_d:我认为一些编译器曾经允许它作为扩展使用,因为这比必须将其转换为 unsigned char* 更方便,例如为指针添加 sizeof 值。 - supercat

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