C/C++:指针算术

C/C++: Pointer Arithmetic

本文关键字:指针 C++      更新时间:2023-10-16

我读了一点指针算术,发现了两件我无法理解的事情,也不知道它是如何使用的

address_expression - address_expression

以及

address_expression > address_expression

有人能向我解释一下吗?它们是如何工作的,何时使用。

编辑:

我想说的是,如果我只取两个地址并减去,它们会产生什么

如果我取两个地址进行比较,结果是什么?或者基于进行比较

编辑:我现在明白了减去地址的结果,但比较地址我仍然不明白

我理解1<2,但是一个地址如何比另一个地址大,在上它们的比较是什么

这里有几个答案表明指针是数字。这不是C标准所规定的指针的准确描述。

在很大程度上,您可以将指针视为数字和内存中的地址,前提是(a)您了解指针减法将字节的差异转换为元素(减去的指针类型),以及(b)您了解该模型的极限。

以下使用1999 C标准(ISO/IEC 9899,第二版,1999-12-01)。我希望以下内容比提问者要求的更详细,但鉴于这里的一些错误陈述,我认为应该提供准确无误的信息。

根据6.5.6第9段,可以减去两个指向同一数组元素的指针或指向数组最后一个元素后的指针。因此,如果您有int a[8], b[4];,您可以从指向a[2]的指针中减去指向a[5]的指针,因为a[5]a[2]是同一数组中的元素。您还可以从指向a[8]的指针中减去指向a[5]的指针,因为a[8]是数组最后一个元素之后的一个。(a[8]不在数组中;a[7]是最后一个元素。)不能从指向b[2]的指针中减去指向a[5]的指针,因为a[5]b[2]不在同一数组中。或者,更准确地说,如果你做这样的减法,行为是未定义的。请注意,这不仅仅是未指明的结果;您不能指望会得到一些可能毫无意义的数字:行为是未定义的。根据C标准,这意味着C标准没有说明由此产生的后果。你的程序可以给你一个合理的答案,也可以中止,或者删除文件,所有这些结果都符合C标准。

如果执行允许的减法,则结果是从第二个指向元素到第一个指向元素的元素数。因此,a[5]-a[2]为3,a[2]-a[5]为−3。无论a是什么类型,这都是正确的。C实现需要将距离从字节(或它使用的任何单位)转换为适当类型的元素。如果a是每个8字节的double的阵列,则对于3个元素,a[5]-a[2]是3。如果a是每个一个字节的char的阵列,则对于3个元素,a[5]-a[2]是3。

为什么指针不只是数字?在一些计算机上,尤其是较旧的计算机上,寻址内存更为复杂。早期的计算机地址空间很小。当制造商想要制造更大的地址空间时,他们也希望与旧软件保持一定的兼容性。由于硬件限制,他们还必须实现各种寻址存储器的方案,这些方案可能涉及在存储器和磁盘之间移动数据,或者改变处理器中控制地址如何转换为物理存储器位置的特殊寄存器。对于在这样的机器上工作的指针,它们必须包含比一个简单地址更多的信息。正因为如此,C标准不仅仅将指针定义为地址,还允许您对地址进行算术运算。只定义了合理数量的指针算术,并且C实现需要提供使该算术工作所需的操作,但不需要更多。

即使在现代机器上,也可能存在复杂性。在Digital的Alpha处理器上,指向函数的指针不包含该函数的地址。它是函数描述符的地址。该描述符包含函数的地址,还包含正确调用函数所需的一些附加信息。

关于关系运算符,如>,C标准在6.5.8第5段中规定,可以比较可以减去的指针,如上所述,还可以比较指向聚合对象(结构或联合)成员的指针。指向数组成员(或其结束地址)的指针按预期方式进行比较:指向索引较高元素的指针大于指向索引较低元素的指针。指向同一并集的两个成员的指针比较相等。对于指向结构的两个成员的指针,指向后面声明的成员的指针大于指向前面声明的成员。

只要你保持在上面的限制范围内,那么你就可以把指针看作是内存地址的数字。

通常,C实现很容易提供C标准所需的行为。即使计算机具有复合指针方案,例如基地址和偏移量,通常数组的所有元素都会使用彼此相同的基地址,结构的所有元素也会使用彼此一致的基地址。因此,编译器可以简单地减去或比较指针的偏移部分,以获得所需的差异或比较。

然而,如果你在这样一台计算机上减去指向不同数组的指针,你可能会得到奇怪的结果。由基地址和偏移量形成的位模式可能看起来比另一个指针更大(当解释为单个整数时),即使它指向内存中的较低地址。这就是您必须遵守C标准所设置的规则的原因之一。

指针减法产生相同类型的两个指针之间的数组元素数。

例如,

int buf[10] = /* initializer here */;
&buf[10] - &buf[0];  // yields 10, the difference is 10 elements

指针比较。例如,对于>关系运算符:如果左手侧的尖数组元素或结构成员在右手侧的尖阵列元素或结构构件之后,则>运算产生1,否则产生0。请记住,数组和结构是有序的序列。

&buf[10] > &buf[0];  // 1, &buf[10] element is after &buf[0] element

减去两个指针地址将返回该类型的元素数。

因此,如果你有一个整数数组和两个指针,减去这些指针将返回介于之间的int值,而不是字节数。与字符类型相同。因此,您需要小心这一点,尤其是当您使用字节缓冲区或宽字符时,您的表达式正在计算正确的值。如果您需要基于字节的缓冲区偏移量来处理不使用单个字节进行存储的东西(int、short等),则需要首先将指针强制转换为char*。

第一个表达式从另一个指针中减去一个指针。作为为什么这可能有用的一个简单例子,考虑一个C字符串。字符串在连续内存中,因此如果您有字符串的第一个字符的地址和最后一个字符的名称,您可以通过以下操作找到字符串的长度:

int strLength = (last_char_address - first_char_address) + 1;

这种指针算术是类型感知的,这意味着算术的结果表示两个指针之间特定类型的元素数量。在使用char的上述示例中,差异在于字符数。这对于例如指向两个CCD_ 30的指针类似地起作用。

类似地,您的第二个表达式只是比较指针,结果将是1或0。作为一个非常简单的例子,数组的元素5的地址始终为>,元素4:&string[4] > &string[5]为真。

在解释指针算术时,我喜欢用一个类比来解释它的工作原理和局限性,那就是考虑街道地址。

假设在埃尔姆街上有一堆大小相同的房子,所有的地块都有50英尺宽。假设我想知道从埃尔姆街12号到埃尔姆街46号有多远,假设我想了解这个距离,因为有很多房子,而不是以英尺为单位的距离。很明显,我可以从46中减去12,得到34套房子的答案。(事实上,当然,这比这要复杂一点,因为街道两侧可能都有房子,但我们暂时忽略这个问题。)

假设在第十大道上,有一堆工业建筑在更大的地块上,都有100英尺宽。我仍然可以减去街道编号,然后以建筑物的数量(而不是英尺)来计算距离。

这类似于C中的指针减法,在这里你可以得到按被指向对象的大小缩放的差异。您可以而不是以原始字节的形式获得答案(类似于街道地址类比中的英尺)。

但街道地址类比帮助我们理解的另一件事是,为什么我们不能使用指针算术来处理不同数组中的指针。假设我想知道从埃尔姆街12号到第十大道30号有多远。减去地址不起作用!这毫无意义。你不能有意义地减去或比较不同街道上的地址,就像你不能有意思地减去或对比不同数组中的指针一样。

指针通常可以被认为只是表示内存地址的数字,如0x0A31FCF20(十进制为2736770848)或0xCAFEDEAD(有时系统会用它来表示错误,我不记得详细信息了。)

指针比较通常用于对指针数组进行排序。当您需要检查指针是否在指针列表中时,排序的指针数组非常有用;如果对列表进行了排序,就不必查看列表中的每个元素来确定指针是否在该列表中。您需要使用比较来对列表进行排序。

当您有一个指向数据块的指针,并且需要访问不在数据块开头的内容时,通常会使用指针算术。例如:

const char *string = "hello world!"
const char *substring = string+6;
std::cout << string << "n";
std::cout << substring << std::endl;

这将输出:

hello world!
world!

在这里,我们得到了"helloworld!"或"world!"的前6个字符之后的字符串。请记住,如果可能的话,您应该在可用的地方使用std::string。一个与指针算术非常相似的概念是随机访问迭代器。

减去指针可以帮助您找到这两个指针之间的距离。如果您有一个指向数组第一个元素的指针,并且有一个指针指向数组最后一个元素之后的一个元素,那么减去这两个指针可以帮助您找到数组的大小。

另一种可能将指针视为整数的情况是链表的优化版本,称为XOR链表。你可以在这里找到更多的细节。如果你愿意的话,我可以详述;请在评论中告诉我。

您可以通过多种方式将地址视为int。唯一的区别是int表示该地址中的大小数量。例如,如果int * p恰好具有值,例如234(来自例如p = new int[12];的某个安全指令),则它表示地址234。如果我们做p += 1;,就int大小而言,它只是在添加。现在p是(假设本例为4字节int)238,也就是p[1]。事实上,p[x]相当于*(p+x)。你可以像int一样比较和。在某些情况下,这很有用,例如在给定的例子中,p[0]现在指的是p[1]。这样就避免了像p = &p[1]这样不必要地取消引用的操作。