不同类型指针之间的减法
我试图找到两个变量之间的内存距离。 具体来说,我需要找到char []数组和int之间的距离。
char data[5]; int a = 0; printf("%p\n%p\n", &data[5], &a); long int distance = &a - &data[5]; printf("%ld\n", distance);
当我在没有最后两行的情况下运行我的程序时,我得到了两个变量的正确内存地址,如下所示:
0x7fff5661aac7 0x7fff5661aacc
现在我明白了,如果我没错,两者之间有5个字节的距离(0x7fff5661aac8,0x7fff5661aac9,0x7fff5661aaca,0x7fff5661aacb,0x7fff5661aacc)。
为什么我不能减去类型(int *)和类型(char *)之一的指针。 两者都是指内存地址..我该怎么做才能计算两者之间的距离,以字节为单位? 我尝试了两个指针中的一个,但它不起作用。
我得到:“ 错误:’char *’和’int *’不是兼容类型的指针 ”。 谢谢大家都会帮助我
不,这是不可能的。
首先,你只能减去(到)“兼容”类型的指针,这里的int
和char
不是兼容的类型。 因此减法是不可能的。
也就是说,即使两者都是兼容类型的指针,那么下面也会出现以下情况。
所以,其次,你不能只减去两个任意指针,它们必须基本上是同一个数组的(元素的地址)的一部分。 Othweise,它调用未定义的行为 。
引用C11
,章节§6.5.6, 附加算子
当减去两个指针时,两个指针都指向同一个数组对象的元素,或者指向数组对象的最后一个元素的元素; 结果是两个数组元素的下标的差异。 [….]
第三,另一个重点是,减去两个指针的结果是ptrdiff_t
类型,一个有符号整数类型。
[…]结果的大小是实现定义的,其类型(有符号整数类型)是在
头中定义的
ptrdiff_t
。 […]
因此,要打印结果,您需要使用%td
格式说明符。
指针减法仅针对同一数组内的指针(或刚刚超过数组的最后一个元素)定义。 任何其他用途都是未定义的行为。 让我们忽略你的实验。
当减去相同类型的两个指针到同一数组对象的元素时,结果是数组索引的差异。 您可以将有符号整数结果(类型为ptrdiff_t
)添加到第一个指针并获取第二个指针的值,或者从第二个指针中减去结果并获取第一个指针的值。 因此,实际上,结果是两个指针的字节地址的差异除以被指向的对象的大小。 这就是为什么允许减去不兼容类型的指针是没有意义的,特别是当引用的对象类型具有不同的大小时。 当减去指针指向不同大小的对象时,如何将字节地址的差异除以指向的对象的大小?
尽管如此,出于实验目的,您可以将两个指针(指向不同的对象)转换为char *
并减去它们,并且许多编译器只会将字节地址的差异作为数字给出。 但是,结果可能溢出ptrdiff_t
的整数。 或者,您可以将两个指针转换为intptr_t
类型的整数,并减去整数以获得字节地址的差异。 同样,从理论上讲,减法的结果可能会溢出intptr_t
类型的整数。
在标准PC上,没有什么能阻止你将指针转换为可以保存指针值的整数类型,并减去两个整数。
这样的整数类型并不保证存在于所有体系结构中(但在许多常见系统上都存在) – 想象一下分段存储器的信息多于单个数字。 如果整数类型不适合,则转换的行为未定义。
从标准草案n1570,6.3.2.3/6:
任何指针类型都可以转换为整数类型。 除了之前指定的以外,结果是实现定义的。 如果结果无法以整数类型表示,则行为未定义。 结果不必在任何整数类型的值范围内。
通常地址之间的差异将是人们期望的(连续声明的变量在内存中彼此相邻),并且可以用来告诉堆栈增长的方向等。
探索使用整数和指针可以做些什么可能会很有趣。
Olaf评论说,如果你“将[算术计算结果]转换回指针,你就会调用UB。” 这不一定是这样; 它取决于整数值。 标准草案在6.3.2.3/5中说明如下:
整数可以转换为任何指针类型。 除非先前指定,否则结果是实现定义的,可能未正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示
(由我强调。)如果我们通过向结构地址添加偏移量来计算结构成员的地址,我们显然已经处理了上述问题,因此需要实现。 肯定不是UB; 如果我们不能使用整数 – >指针转换,并通过结果指针访问该内存,很多嵌入式系统都会失败。 我们必须确保系统允许它,并且地址是合理的。
该段有一个脚注:
用于将指针转换为整数或整数到指针的映射函数旨在与执行环境的寻址结构一致。
也就是说,它们不会让用户感到惊讶 。 虽然理论上在存储器中相邻的不相关对象的地址可以被投射到完全不同的整数值,但它们不应该被认为是。 例如,用户可以合理地期望将线性存储器投影到线性整数空间中,从而保持排序和距离。
我还应该强调(正如我在一篇评论中所做的那样)标准不是世界。 它必须适应并保证各种机器。 因此,标准只能是最小的共同标准。 如果我们可以缩小我们考虑的架构范围,我们可以做出更好的保证。
一个常见的例子是整数寄存器中可能存在陷阱值,或者表示从未初始化寄存器读取的标志,该寄存器也会陷阱; 这些是造成标准中广泛的UB案例的原因,这些案例根本不适用于您的PC。
uint8_t * ptr = ...; uint8_t * ptr2 = ptr + 5;
现在,如果ptr
为100
, ptr2
会是什么? 正确,它将是105
。 但现在看看那段代码:
uint32_t * ptr = ...; uint32_t * ptr2 = ptr + 5;
再说一次,如果ptr
是100
, ptr2
会是什么? 错误! 它不会是105
,它将是120
。
为什么? 指针算术不是整数运算!
ptr2 = ptr + 5;
实际上意味着:
ptr2 = int_to_ptr(ptr_to_int(ptr) + (sizeof(ptr) * 5));
函数int_to_ptr
和ptr_to_int
并不存在,我只是将它们用于演示目的,因此您可以更好地理解场景之间的情况。
因此,如果你减去两个指针,结果不是它们的地址的差异,它是它们之间的元素数量:
uint32_t test[50]; ptrdiff_t diff = &test[20] - &test[10];
diff
将是10,因为它们之间有10个元素(一个元素是一个uint32_t
值)但这并不意味着test[10]
和test[20]
之间有10个字节,它们之间有40个字节。 uint32_t
值占用4个字节的内存。
现在你可能会理解为什么减去不同类型的指针毫无意义,因为不同的类型具有不同的元素大小,然后这样的减法会返回什么?
如果你想要两个指针之间有多少字节,你需要将它们都转换为具有一个字节元素的数据类型(例如uint8_t *
或char *
可以工作)或者将它们转换为void *
(GNU扩展但很多编译器)同样支持),这意味着数据类型是未知的,因此元素大小也是未知的,在这种情况下,编译器将是字节大小的元素。 所以这可能有效:
ptrdiff_t diff = (void *)ptr2 - (void *)ptr1;
但是这个
ptrdiff_t diff = (char *)ptr2 - (char *)ptr1;
更便携。
它将编译,它将提供结果。 如果结果有意义,那就是另一个主题。 除非两个指针指向相同的内存“对象”(相同的结构,相同的数组,相同的已分配的内存区域),否则它不是,正如标准所说,在这种情况下,结果是未定义的。 这意味着diff
可以(合法地)具有任何值,因此编译器也可以在这种情况下始终将diff
设置为0,这将由标准允许。
如果您想要定义行为,请尝试以下方法:
ptrdiff_t diff = (ptrdiff_t)ptr2 - (ptrdiff_t)ptr1;
这是合法的和明确的。 每个指针都可以转换为int值,而ptrdiff_t
是一个int值,保证足够大,以便每个指针都可以适合它(不要为了那个目的而使用int
或long
,它们不会产生任何这样的保证!)。 此代码将两个指针转换为整数,然后减去它们。 我现在仍然没有看到你可以用diff
做什么有用的东西,但是这个代码至少会提供一个定义的结果,但可能不是你可能期望的结果。
尝试将每个地址强制转换为void *
long int distance = (void *)&a - (void *)&data[5];
正如其他人会指出的那样,这是危险的,未定义的,但如果你只是在探索记忆是如何工作的,那应该没问题。
int的大小和char指针的大小是不同的。在int size为4个字节的系统中,如果你将int_pointer ++,它将增加4个字节的地址,而在char_ptr的情况下,它将地址增加1个字节。 因此,您可能会收到错误。
这是因为指针算术是关于偏移的。 例如,如果您有一个数组和指向该数组的指针,如:
int array[3] = { 1, 2, 3}; int *ptr = array;
然后你增加ptr,你期望数组中的下一个值,例如array [1]之后的array [0],无论它存储在哪个类型中。 因此,当您减去指针时,您不会获得例如字节,而是偏移量。
不要减去不属于同一数组的指针。