指针算术

65

有没有对指针算术有良好文章或解释(博客,示例)?目标读者是一群学习C和C++的Java程序员。


1
http://www.c-faq.com第4节。 - Keith Thompson
7个回答

79
这是我学习指针的地方:https://cplusplus.com/doc/tutorial/pointers/ 一旦你理解了指针,指针算术就很简单。它与普通算术的唯一区别在于,你要添加到指针上的数字将会乘以指针所指向类型的大小。例如,如果你有一个指向int的指针,而int的大小为4字节,(pointer_to_int + 4) 将计算出一个内存地址,比原来的地址多16字节(4个int)。
所以当你写下这样的代码时:
(a_pointer + a_number)

在指针算术中,实际上发生的是什么?
(a_pointer + (a_number * sizeof(*a_pointer)))

在普通算术中。


但是指针地址不能被加到另一个指针地址上。为什么? - Martin Erhardt
链接的文章很棒,现在更加清晰易懂了! - CWitty
@MartinErhardt,你可能会喜欢这个 p = (T*)(((long)p) + ((long)p2)) - GreatAndPowerfulOz

56

首先,binky 视频可能会有所帮助。这是一个关于指针的好视频。至于算术运算,这里有一个示例:

int * pa = NULL;
int * pb = NULL;
pa += 1; // pa++. behind the scenes, add sizeof(int) bytes
assert((pa - pb) == 1);

print_out(pa); // possibly outputs 0x4
print_out(pb); // possibly outputs 0x0 (if NULL is actually bit-wise 0x0)

(请注意,严格来说对包含null指针值的指针进行递增操作是未定义的行为。我们使用NULL是因为我们只关心指针的值。通常情况下,只有在指向数组元素时才使用递增/递减)。

以下展示了两个重要概念:

  • 将整数加/减到指针上,意味着将指针向前/向后移动N个元素。因此,如果int类型的大小为4个字节,在递增1之后,pa可以在我们的平台上包含0x4。
  • 将一个指针从另一个指针中减去,意味着得到它们之间的距离,以元素为单位测量。因此,将pb从pa中减去将产生1,因为它们之间有一个元素距离。

拿一个实际的例子来说。假设你写了一个函数,人们给你提供了一个起始指针和一个结束指针(在C++中非常常见):

void mutate_them(int *begin, int *end) {
    // get the amount of elements
    ptrdiff_t n = end - begin;
    // allocate space for n elements to do something...
    // then iterate. increment begin until it hits end
    while(begin != end) {
        // do something
        begin++;
    }
}

ptrdiff_t 是指 (end - begin) 的类型。在某些编译器中,它可能是 "int" 的同义词,但在另一个编译器中可能是另一种类型。由于不确定,所以选择使用通用的 typedef ptrdiff_t


需要注意的是,<stddef.h>或<cstddef>定义了ptrdiff_t(根据标准)。我认为它不是一个特殊的类型,只是编译器输出的类型的名称(typedef)。 - strager
是的,确实,可能是一些有符号的整数类型。不想深入细节。 - Johannes Schaub - litb
8
不好意思提醒您,但是您的第一个例子属于未定义行为。 :) 不允许对空指针进行增量操作。 :) - jalf
jalf,谢谢你指出这个问题。我加了一条注释 :) 现在我的说法“如果NULL实际上是按位0”就不对了,因为0是一个零整数。但我想你知道我的意思哈哈 - Johannes Schaub - litb
2
我通常发现跟随视频教程很困难,但这个Binky的教程超级简单、棒极了,而且还很有趣 : )。只用3分钟就能掌握基础知识。 - RBT
该链接已经没有任何视频。 - Daniyal Yasin

7
应用自然语言处理技术,也称为地址计算。 '指针'通常会被害怕和误解,因为它们是由错误的人在错误的阶段以错误的方式进行教学的。难怪没有人能够理解它。
在教授指针时,教员会说“p是指向a的指针,p的值是a的地址”等等。这种方法行不通。以下是您可以建立的原材料。练习使用它,您的学生将理解。
'int a'中的'a'是一个整数,它存储整数类型的值。 'int *p'中的'p'是'int star',它存储'int star'类型的值。
'a'是获取存储在'a'中的整数的方法(尝试不要使用'value of a') '&a'是获取a本身存储的位置的方法(尝试说'address')
'b = a'为使其工作,两侧必须具有相同的类型。如果a是int,则b必须能够存储int。(因此______ b,空白填写'int')
'p =&a'为使其工作,两侧必须具有相同的类型。如果a是整数,则&a是地址,p必须能够存储整数的地址。 (因此______ p,空白填写'int *')
现在以不同的方式编写int *p以提取类型信息:
int * | p 'p'是什么?答案:它是'int *'。因此,'p'是一个整数的地址。
int | *p '*p'是什么?答案:它是'int'。因此,'*p'是一个整数。
现在进入地址算术:
int a; a = 1; a = a + 1;
“a = a + 1”中我们在做什么?将其视为“下一个”。因为a是数字,这就像说“下一个数字”。由于a保持1,说“下一个”会使它变成2。
// 谬误的例子。你已经被警告了!!! int *p int a; p =&a p = p + 1;
'p = p + 1'中我们在做什么?仍然在说“下一个”。这次,p不是数字而是地址。因此,我们所说的是“下一个地址”。下一个地址取决于数据类型,更具体地取决于数据类型的大小。 printf(“%d%d%d”,sizeof(char),sizeof(int),sizeof(float));
因此,地址的'下一个'将向前移动sizeof(数据类型)。
这对我和我曾经教过的所有人都起作用。

不同意“next”的解释。不清楚下一个指针是否相隔4b,或者它是指可以指向的下一个4b(重用之前的低3b)。这种情况很少见,但是这里给出的类比对我来说不起作用。 - mafu
这实际上是一个非常清晰的答案。我真的很喜欢解释“_*p”是整数的可视化部分。 - j3141592653589793238

3

我认为指针算术的一个很好的例子是下面的字符串长度函数:

int length(char *s)
{
   char *str = s;
   while(*str++);
   return str - s;
}

3
这个例子并没有表明指针一定会每次增加1个字节,这一点经常被忽视。 - mafu

1

有几种方法可以解决这个问题。

直觉的方法,这是大多数C/C++程序员想到的,即指针是内存地址。litb的例子采用了这种方法。如果你有一个空指针(在大多数机器上对应地址0),并且加上一个int的大小,你会得到地址4。这意味着指针基本上只是花哨的整数。

不幸的是,这种方法存在一些问题。首先,它可能行不通。 不能保证空指针实际上使用地址0。(尽管将常量0分配给指针会产生空指针)。

此外,您不允许增加空指针,或者更一般地说,指针必须始终指向已分配的内存(或一个元素之后),或特殊的空指针常量0。

因此,更正确的思考方式是指针只是迭代器,允许您迭代分配的内存。 这实际上是STL迭代器背后的关键思想之一。它们被建模为非常像指针,并提供专门化的补丁来使原始指针作为适当的迭代器工作。

例如,这里给出了更详细的解释here

但是这种后一种观点意味着你应该真正解释STL迭代器,然后简单地说指针是这些迭代器的特殊情况。你可以增加一个指针以指向缓冲区中的下一个元素,就像你可以使用std::vector<int>::iterator一样。它可以指向数组末尾的一个元素,就像任何其他容器中的结束迭代器一样。你可以减去两个指向同一缓冲区的指针,以得到它们之间的元素数量,就像你可以使用迭代器一样,如果指针指向不同的缓冲区,你不能有意义地比较它们。(对于为什么不能,请考虑在分段内存空间中会发生什么。两个指向不同段的指针之间的距离是多少?)

当然,在实践中,CPU地址和C/C++指针之间有非常密切的关联。但它们并不是完全相同的东西。指针有一些限制,在你的CPU上可能并不是严格必要的。

当然,大多数C++程序员都在第一种理解上混淆,即使它在技术上是不正确的。它通常足够接近你的代码最终的行为,人们认为他们理解了它,并继续前进。

但对于一个来自Java的人,刚开始学习指针的人来说,后一种解释可能同样容易理解,并且以后不会给他们带来太多惊喜。


重要的是要意识到指针的长度并不总是4个字节。在某些系统(64位)上,它可能是8个字节长。例如,永远不要假设指针与整数的大小相同。因为程序员将指针隐藏在整数中而导致了太多的错误! - Waxhead
没错,这是一个重要的观点。我本意只是举了一个4字节的例子,但我应该更清楚地表达,因为指针通常并不保证是4个字节,而且它们经常不是。 - jalf

1
所以,要记住的关键是指针只是一个为解引用而类型化的字长变量。这意味着无论它是void*、int*、long long**,它仍然只是一个字长变量。这些类型之间的区别在于编译器认为解引用类型是什么。只是为了澄清,字长意味着虚拟地址的宽度。如果你不知道这是什么意思,只需记住在64位机器上,指针是8个字节,在32位机器上,指针是4个字节。理解指针的一个超级重要的概念是地址。地址是一个能够唯一标识内存中某个位置的数字。内存中的所有内容都有一个地址。对于我们的目的,我们可以说每个变量都有一个地址。这并不总是正确的,但编译器让我们假设这是正确的。地址本身是字节粒度的,这意味着0x0000000指定了内存的开始,而0x00000001是内存中的一个字节。这意味着通过将指针加一,我们向前移动一个字节到内存中。现在,让我们来看看数组。如果你创建一个类型为quux的32个元素的数组,它将从它的分配开始,到它的分配加上32*sizeof(quux)的开始,因为数组的每个单元格都是sizeof(quux)大。所以,当我们用array[n]指定数组的一个元素时,那只是*(array+sizeof(quux)*n)的简写。指针算术实际上只是改变你引用的地址,这就是为什么我们可以使用它来实现strlen的原因。
while(*n++ != '\0'){
  len++;
}

因为我们只是沿着字节一点一点地扫描,直到遇到零为止。希望这有所帮助!


0

这是一个关于指针算术的链接这里非常不错。

例如:

指针和数组

计算 ptr + i 的地址公式,其中 ptr 的类型为 T *。则地址公式为:

addr( ptr + i ) = addr( ptr ) + [ sizeof( T ) * i ]

对于32位平台上int类型,addr(ptr+i) = addr(ptr)+4*i;

减法

我们也可以计算ptr-i。例如,假设我们有一个名为arr的int数组。 int arr[ 10 ] ; int * p1, * p2 ;

p1 = arr + 3 ; // p1 == & arr[ 3 ] 
p2 = p1 - 2 ; // p1 == & arr[ 1 ] 

这可能是真的 - 在32位平台上,int并不保证是32位宽度 - 规则是它必须能够表示[−32767,+32767],这意味着它至少必须是16位宽度。您可能有一个32位平台,在该平台上编译器将int视为16位或64位宽度。 - Clearer
循环定义@Clearer-通常当我们谈论平台的“位宽”时,我们指的是int的大小(它应该是“自然”的整数类型)。 - Toby Speight
@TobySpeight,“通常”是无关紧要的。 - Clearer

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