解引用指针后的后增操作?

71

我试图理解C语言中指针的行为,但对以下情况感到有些惊讶(下面是示例代码):

#include <stdio.h>

void add_one_v1(int *our_var_ptr)
{
    *our_var_ptr = *our_var_ptr +1;
}

void add_one_v2(int *our_var_ptr)
{
    *our_var_ptr++;
}

int main()
{
    int testvar;

    testvar = 63;
    add_one_v1(&(testvar));         /* Try first version of the function */
    printf("%d\n", testvar);        /* Prints out 64                     */
    printf("@ %p\n\n", &(testvar));

    testvar = 63;
    add_one_v2(&(testvar));         /* Try first version of the function */
    printf("%d\n", testvar);        /* Prints 63 ?                       */
    printf("@ %p\n", &(testvar));   /* Address remains identical         */
}

输出:

64
@ 0xbf84c6b0

63
@ 0xbf84c6b0

第二个函数(add_one_v2)中的*our_var_ptr++语句到底是什么意思,因为显然它与*our_var_ptr = *our_var_ptr +1不同?


13个回答

102

这是C和C++非常有趣的小陷阱之一。如果你想要绕晕自己的大脑,试试解决这个问题:

while (*dst++ = *src++) ;

这是一个字符串复制。指针不断地递增,直到复制一个值为零的字符。一旦你知道了这个技巧的原理,你就永远不会忘记指针上的++是如何工作的。

另外,你总是可以用括号覆盖操作符的顺序。以下代码将增加所指向的值,而不是指针本身:

(*our_var_ptr)++;

7
这个例子以及许多其他“不用手”的代码片段出现在C语言知名书籍《C程序设计语言》中。 - Benjamin Barenblat

53
由于运算符优先级规则和 ++ 是后缀运算符,因此 add_one_v2() 确实会解引用指针,但 ++ 实际上是应用于指针本身的。然而,请记住 C 语言始终使用按值传递: add_one_v2() 会增加其指针的 本地副本,这对存储在该地址处的值没有任何影响。
作为测试,请将 add_one_v2() 替换为以下代码片段,并查看输出如何受到影响:
void add_one_v2(int *our_var_ptr)
{
    (*our_var_ptr)++;  // Now stores 64
}

void add_one_v2(int *our_var_ptr)
{
    *(our_var_ptr++);  // Increments the pointer, but this is a local
                       // copy of the pointer, so it doesn't do anything.
}

10
关于操作顺序的说法不正确。在add_one_v2中,++应用于指针而非解引用操作。然而,由于这是后置递增,所以解引用操作会在递增之前发生。 - Torlack
你是在说原始的add_one_v2,还是我的带括号的示例? - hbw
我在谈论原始内容。我只是想指出你的说法“递增指针然后解引用...”是不正确的。 - Torlack
感谢您的快速回答(以及有关Torlack的信息性评论!)。++*our_var_ptr似乎与add_one_v1“工作”相同。 但有一件事我不明白:如果这些函数只使用指针的本地副本-->它们如何影响在主函数中打印的值?或者我误解了那个说法? - ChristopheD
2
指针本身是一个局部副本,但它所指向的值不是。 - Torlack
显示剩余3条评论

53

好的,

*our_var_ptr++;

它是这样工作的:

  1. 首先进行解引用操作,给你指向our_var_ptr所指示的内存位置(其中包含63)。
  2. 然后计算表达式,63的结果仍为63。
  3. 结果被丢弃(您没有对其执行任何操作)。
  4. 在评估之后,our_var_ptr将被递增。它正在改变指针指向的位置,而不是指向的内容。

这实际上与执行以下操作相同:

*our_var_ptr;
our_var_ptr = our_var_ptr + 1; 

明白了吗?Mark Ransom的回答有一个很好的例子,不过他实际上使用了结果。


GCC不接受:our_var_ptr++; 它接受的是:(our_var_ptr)++。 - a.saurabh
我认为应该是 *our_var_ptr; *our_var_ptr = *our_var_ptr + 1; - a.saurabh
@a.saurabh 我保证GCC会接受*our_var_ptr++。它可能会抛出一个警告,但如果你去掉它,你会破坏很多东西。使用*(our_var_ptr)++,括号内的内容首先被计算为our_var_ptr;这等于*our_var_ptr++ - BIBD
1
@a.saurabh 原帖作者想要的效果是 *our_var_ptr = *our_var_ptr + 1;,而不是我之前想要解释的内容。our_var_ptr = our_var_ptr + 1; 是这段 C 语言代码能够作为字符串复制运行的原因:while (*dst++ = *src++) ; - BIBD

8

这里有很多混淆,所以这是一个修改后的测试程序,让发生的事情更加清晰(或者至少更加清晰):

#include <stdio.h>

void add_one_v1(int *p){
  printf("v1: pre:   p = %p\n",p);
  printf("v1: pre:  *p = %d\n",*p);
    *p = *p + 1;
  printf("v1: post:  p = %p\n",p);
  printf("v1: post: *p = %d\n",*p);
}

void add_one_v2(int *p)
{
  printf("v2: pre:   p = %p\n",p);
  printf("v2: pre:  *p = %d\n",*p);
    int q = *p++;
  printf("v2: post:   p = %p\n",p);
  printf("v2: post:  *p = %d\n",*p);
  printf("v2: post:   q = %d\n",q);
}

int main()
{
  int ary[2] = {63, -63};
  int *ptr = ary;

    add_one_v1(ptr);         
    printf("@ %p\n", ptr);
    printf("%d\n", *(ptr));  
    printf("%d\n\n", *(ptr+1)); 

    add_one_v2(ptr);
    printf("@ %p\n", ptr);
    printf("%d\n", *ptr);
    printf("%d\n", *(ptr+1)); 
}

得到的输出如下:

v1: pre:   p = 0xbfffecb4
v1: pre:  *p = 63
v1: post:  p = 0xbfffecb4
v1: post: *p = 64
@ 0xbfffecb4
64
-63

v2: pre:   p = 0xbfffecb4
v2: pre:  *p = 64
v2: post:  p = 0xbfffecb8
v2: post: *p = -63
v2: post:  q = 64

@ 0xbfffecb4
64
-63

需要注意以下四点:

  1. 对指针的本地副本所做的更改不会反映在调用指针中。
  2. 对本地指针目标的更改确实会影响调用指针的目标(至少在更新目标指针之前如此)。
  3. add_one_v2 中指向的值未被递增,其后面的值也未被递增,但指针已经被递增。
  4. add_one_v2 中指针的递增发生在解除引用之后。

为什么?

  • 因为 ++ 的绑定比 *(作为解除引用或乘法)更紧密,所以 add_one_v2 中的递增应用于指针而不是指针所指的内容。
  • 后置 递增发生在术语评估之后,因此解除引用获取数组中的第一个值(元素0)。

感谢您抽出时间撰写这个答案,正是这种精神让StackOverflow成为如此优秀的环境!在这里,我一个小时内得到的启发性答案比我在那些糟糕的C语言教程中找到的还要多。;) - ChristopheD
谢谢!我认为这是最重要的一点 - "因为++比*更紧密地绑定" - kumar
此外,大家应该注意:int q = *p++; 不等同于 *p++; *p++ 仍然只有“第一个值”,而在 q 中是“第二个值”。(我想你在第二点中提到了它) - kumar

7
正如其他人所指出的那样,运算符优先级导致在v2函数中的表达式被视为*(our_var_ptr++)
然而,由于这是一个后置递增运算符,所以不能完全说它会递增指针并解引用它。如果是这样的话,我认为你不会得到63作为输出,因为它将返回下一个内存位置的值。实际上,我相信操作的逻辑顺序是:
1. 保存指针的当前值 2. 增加指针 3. 解引用步骤1中保存的指针值
正如htw所解释的那样,你没有看到指针值的变化,因为它是按值传递给函数的。

我认为这很荒谬。如果以新手的眼光来看(我不是),我会非常期望增加后的值被取消引用。感谢您在此澄清情况。 - orion elenzil

5

如果您没有使用括号来指定操作的顺序,那么前缀和后缀增量都优先于引用和解引用。但是,前缀增量和后缀增量是不同的操作。在++x中,该运算符获取您的变量的引用,将其加一,并按值返回它。在x++中,运算符增加您的变量,但返回其旧值。它们的行为有点像这样(想象它们被声明为类内方法):

//prefix increment (++x)
auto operator++()
{
    (*this) = (*this) + 1;
    return (*this);
}

//postfix increment (x++)
auto operator++(int) //unfortunately, the "int" is how they differentiate
{
    auto temp = (*this);
    (*this) = (*this) + 1; //same as ++(*this);
    return temp;
}

(请注意,后置自增运算涉及到一次复制,导致效率较低。这就是为什么在循环中应该优先使用++i而不是i++的原因,尽管现在大多数编译器都会自动进行优化。)

从代码中可以看出,后置自增运算会被首先执行,但由于其行为方式,您将引用指针的旧值。

以下是一个例子:

char * x = {'a', 'c'};
char   y = *x++; //same as *(x++);
char   z = *x;

在第二行中,指针x将在解引用之前被递增,但是解引用将会在旧值x(后缀递增返回的地址)上进行。因此y将被初始化为'a',而z将被初始化为'c'。但如果你这样做:

char * x = {'a', 'c'};
char   y = (*x)++;
char   z = *x;

在这里,x将被解引用,指向的('a')将被递增(变成'b')。由于后缀递增返回旧值,因此y仍将初始化为'a'。并且由于指针没有改变,z将初始化为新值'b'。

现在让我们来看一下前缀情况:

char * x = {'a', 'c'};
char   y = *++x; //same as *(++x)
char   z = *x;

在这里,解引用将发生在增加后的x值上(即由前缀递增运算符立即返回的值),因此y和z都将初始化为'c'。要获得不同的行为,可以更改运算符的顺序:

char * x = {'a', 'c'};
char   y = ++*x; //same as ++(*x)
char   z = *x;

在这里,您确保先增加x的内容,指针的值不会改变,因此y和z将被赋值为“b”。在strcpy函数(在其他答案中提到)中,也是先进行增量操作:

char * strcpy(char * dst, char * src)
{
    char * aux = dst;
    while(*dst++ = *src++);
    return aux;
}

每次迭代中,src++首先被处理。作为后缀递增运算符,它返回旧值src的值。然后,将旧值src(即指针)解引用并分配给赋值运算符左侧的变量。然后,dst被递增,其旧值被解引用成为左值并接收旧的src值。这就是为什么dst[0]=src[0]、dst[1]=src[1]等,直到*dst被赋值为0,跳出循环。

附加说明:

本答案中的所有代码都是使用C语言测试的。在C++中,您可能无法列出初始化列表式指针。因此,如果要在C++中测试示例,应先初始化一个数组,然后将其降级为指针:

char w[] = {'a', 'c'};
char * x = w;
char   y = *x++; //or the other cases
char   z = *x;

1
char *x = {'a', 'c'}; char y = *++x; //或 *(++x); char z = *x;y 和 z 会是 a 还是 c? - Shir
1
@Shir,感谢您指出这个问题!我已经在答案中进行了修正。 - Jango

3
我们的变量指针our_var_ptr是指向某块内存的指针。也就是说,它是存储数据的内存单元(在这种情况下,是int类型的4个字节的二进制格式)。
*our_var_ptr是解引用指针——它指向指针“指向”的位置。
++操作符增加一个值。
因此,*our_var_ptr = *our_var_ptr+1 解引用指针并将其位置上的值加一。
现在加入运算符优先级——将其视为(*our_var_ptr) = (*our_var_ptr)+1,您会发现解引用首先发生,因此您获取该值并将其增加。
在您的另一个示例中,++运算符的优先级低于*,因此它获取您传入的指针,将其加一(因此现在指向垃圾),然后返回。(请记住,在C中始终按值传递值,因此当函数返回时,原始testvar指针保持不变,您只改变了函数内部的指针)。
我的建议是,在使用解引用(或任何其他内容)时,请使用括号使您的决策明确。不要试图记住优先级规则,因为您可能会有一天使用另一种具有略微不同规则的语言,从而感到困惑。或者老了,忘记哪个具有更高的优先级(就像我对*和->一样)。

谢谢回答,我认为使用括号使其更明确的建议是可靠的! - ChristopheD

2
我将尝试从不同的角度回答这个问题... 步骤1 让我们看一下运算符和操作数: 在这种情况下,它是操作数,您有两个运算符,在这种情况下*表示解除引用,++表示增量。 步骤2 哪一个具有更高的优先级 ++比*具有更高的优先级 步骤3 ++在哪里,它在右边,这意味着增量 在这种情况下,编译器会“心理记录”在完成所有其他运算符之后执行增量之后的操作... 请注意,如果是* ++p,则会在之前执行 所以在这种情况下,它等效于取两个处理器寄存器,一个将保存解除引用*p的值,另一个将保存递增的p ++的值,之所以在这种情况下有两个,是因为POST活动... 这就是在这种情况下棘手的地方,它看起来像是一个矛盾。 人们会期望++优先于*,它确实是这样,只是POST意味着它仅在所有其他操作数之后,在下一个';'标记之前应用...

1
    uint32_t* test;
    test = &__STACK_TOP;


    for (i = 0; i < 10; i++) {
        *test++ = 0x5A5A5A5A;
    }

    //same as above

    for (i = 0; i < 10; i++) {
        *test = 0x5A5A5A5A;
        test++;
    }

因为test是一个指针,test++(未对其进行解引用)将会增加指针的值(它增加了test的值,该值恰好是被指向的目标地址)。由于目标类型为uint32_t,test++将会增加4个字节,如果目标是该类型的数组,则test现在将指向下一个元素。在进行这些操作时,有时需要先将指针转换为所需的内存偏移量。
        ((unsigned char*) test)++;

这将仅使地址增加1个字节;)

1

来自K&R,第105页:“*t++的值是t指向的字符在t被递增之前的值”。


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