【问题标题】:Post-increment on a dereferenced pointer?取消引用指针的后增量?
【发布时间】:2010-10-25 23:27:15
【问题描述】:

试图理解 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 不同?

【问题讨论】:

标签: c pointers operator-precedence


【解决方案1】:

这是让 C 和 C++ 如此有趣的小问题之一。如果你想弯曲你的大脑,想出这个:

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

这是一个字符串副本。指针不断增加,直到复制了一个值为 0 的字符。一旦你知道这个技巧为什么有效,你就永远不会忘记 ++ 是如何在指针上工作的。

附:您始终可以使用括号覆盖运算符顺序。以下将增加指向的值,而不是指针本身:

(*our_var_ptr)++;

【讨论】:

  • 感谢这个非常好的例子!
  • 这个例子,还有很多其他的“看,妈,没有手!”代码 sn-ps,出现在 C 知识的堡垒中,The C Programming Language。
【解决方案2】:

由于运算符优先级规则以及++ 是后缀运算符这一事实,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.
}

【讨论】:

  • 操作顺序不是这样的。在 add_one_v2 中,++ 应用于指针,而不是取消引用。但是,由于它是后增量,因此取消引用发生在增量之前。
  • 你说的是原始的 add_one_v2,还是我的带括号的例子之一?
  • 我说的是原版。我只是想指出您的“正在递增指针然后取消引用......”的说法是不正确的。
  • 感谢您的快速回答(以及信息丰富的评论 Torlack!)。 ++*our_var_ptr 似乎与 add_one_v1 一样“工作”。不过我不明白的一件事:如果函数仅使用指针的本地副本->它们如何实现影响 main 中打印的值?还是我误解了那句话?
  • 指针本身是一个本地副本,但它指向的值不是。
【解决方案3】:

好的,

*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)++
  • IMO 也应该是 *our_var_ptr; *our_var_ptr = *our_var_ptr + 1;
  • @a.saurabh 我保证 GCC 会接受*our_var_ptr++。它可能会发出警告,但如果你把它拿走,你会破坏很多东西。使用*(our_var_ptr)++,括号内部首先评估为our_var_ptr;等于*our_var_ptr++。
  • @a.saurabh *our_var_ptr = *our_var_ptr + 1; 是原海报想要的效果;不是我想要解释的。 our_var_ptr = our_var_ptr + 1; 是 while (*dst++ = *src++) ; 在 C 中用作字符串副本的原因。
【解决方案4】:

这里有很多混乱,所以这是一个修改后的测试程序,以使发生的事情变得清晰(或至少清晰er):

#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 中的增量适用于指针,而不是它指向的位置。
  • post 增量发生在对术语求值之后,因此取消引用获取数组中的第一个值(元素 0)。

【讨论】:

  • 感谢您抽出宝贵时间撰写此答案,正是这种东西使 StackOverflow 成为如此出色的环境!我在一小时内得到了比我在糟糕的 C 教程中找到的更有见地的答案;)
  • 谢谢!我认为这是最重要的一点——“因为 ++ 比 * 绑定得更紧密”
  • 另外,人们应该注意这一点: int q = *p++;不等价于 *p++; *p++ 仍然只有“第一个值”。而在 q 它是“第二个值”。(我想你在第二点提到过)
【解决方案5】:

正如其他人指出的那样,运算符优先级导致 v2 函数中的表达式被视为*(our_var_ptr++)。

但是,由于这是一个后自增运算符,因此说它先递增指针然后取消引用它并不完全正确。如果这是真的,我认为您不会得到 63 作为输出,因为它将返回下一个内存位置中的值。实际上,我认为操作的逻辑顺序是:

  1. 保存指针的当前值
  2. 增加指针
  3. 取消引用步骤 1 中保存的指针值

正如 htw 所解释的,您没有看到指针值的变化,因为它是按值传递给函数的。

【讨论】:

  • 感谢您的精彩解释!
【解决方案6】:

如果您不使用括号来指定操作顺序,则前缀和后缀增量都优先于引用和取消引用。但是,前缀递增和后缀递增是不同的操作。在 ++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;

【讨论】:

  • char *x = {'a', 'c'}; char y = *++x; //or *(++x); char z = *x; y 和 z 将是 a 还是 c?
  • @Shir 带“c”。谢谢你指出!我在答案中修复了它。
【解决方案7】:

our_var_ptr 是指向某个内存的指针。即它是存储数据的存储单元。 (在这种情况下,int 的二进制格式为 4 个字节)。

*our_var_ptr 是取消引用的指针 - 它指向指针“指向”的位置。

++ 增加一个值。

所以。 *our_var_ptr = *our_var_ptr+1 取消引用指针并将该位置的值加一。

现在添加运算符优先级 - 将其读取为 (*our_var_ptr) = (*our_var_ptr)+1,您会看到取消引用首先发生,因此您获取该值并递增它。

在您的另一个示例中,++ 运算符的优先级低于 *,因此它获取您传入的指针,向其添加一个(因此它现在指向垃圾),然后返回。 (记住在 C 中值总是按值传递的,所以当函数返回时,原来的 testvar 指针保持不变,你只改变了函数内部的指针)。

我的建议是,在使用解引用(或其他任何方法)时,请使用括号来明确您的决定。不要试图记住优先规则,因为有一天你只会使用另一种语言,它们会略有不同,你会感到困惑。或者老了,最终忘记了哪个优先级更高(就像我对 * 和 -> 所做的那样)。

【讨论】:

  • 感谢您的回答,我认为使用括号使其更明确的建议是可靠的!
【解决方案8】:

我会尝试从不同的角度来回答这个问题...... 第1步 让我们看一下运算符和操作数: 在这种情况下,它是操作数,并且您有两个运算符,在这种情况下,* 用于取消引用,++ 用于递增。 第2步 哪个优先级更高 ++ 优先于 * 第 3 步 ++在哪里,它在右边,表示POST增量 在这种情况下,编译器会记下“心理笔记”来执行增量AFTER,它与所有其他运算符一起完成...... 请注意,如果它是 *++p 那么它将在之前执行 所以在这种情况下,它相当于取两个处理器的寄存器,一个将保存取消引用 *p 的值,另一个将保存递增 p++ 的值,在这种情况下有两个原因,是发布活动... 在这种情况下,这就是棘手的地方,而且看起来很矛盾。 人们会期望 ++ 优先于 *,它确实如此,只是 POST 意味着它只会在所有其他操作数之后应用,在下一个 ';' 之前令牌...

【讨论】:

    【解决方案9】:

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

    【讨论】:

      【解决方案10】:
          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 个字节;)

      【讨论】:

        【解决方案11】:

        “++”运算符的优先级高于“*”运算符,这意味着指针地址将在取消引用之前递增。

        但是,“+”运算符的优先级低于“*”。

        【讨论】:

        • 啊!不。 ++ 绑定得更紧密,所以它是递增的指针。但它是后增量版本,所以它发生在 取消引用之后。所以:取消引用指针并返回一个值,然后递增指针(不是指向的值)。
        【解决方案12】:

        图片大约值一千字(给或取一百万左右)......符号可以是图片(反之亦然。)

        所以⸺对于我们这些寻求tl;dr's(优化数据消耗)但仍然想要“(大部分)无损”编码的人来说,矢量图像/图片/插图/演示是最重要的。

        换句话说,请忽略我的最后两条语句,然后看下面。


        有效表格

        *a++ ≣ *(a++)
             ≣ (a++)[0] ≣ a++[0]
             ≣ 0[a++]  // Don't you dare use this (“educational purposes only”)
             // These produce equivalent (side) effects;
               ≡ val=*a,++a,val
               ≡ ptr=a,++a,*ptr
               ≡ *(ptr=a,++a,ptr)
        
        *++a ≣ *(++a)
             ≣ *(a+=1)  ≣ *(a=a+1)
             ≣ (++a)[0] ≣ (a+=1)[0] ≣ (a=a+1)[0]  // ()'s are necessary
                        ≣ 0[++a]   // 0[a+=1], etc... “educational purposes only”
             // These produce equivalent (side) effects:
               ≡ ++a,*a
               ≡ a+=1,*a
               ≡ a=a+1,*a
        
        ++*a ≣ ++(*a)
             ≣ *a+=1
             ≣ *a=*a+1
             ≣ ++a[0] ≣ ++(a[0])
                      ≣ ++0[a]  // STAY AWAY
        
        (*a)++  // Note that this does NOT return a pointer;
                // Location `a` points to does not change
                // (i.e. the address `a` is unchanged)
                ≡ val=*a,++*a,val
        

        注意事项

        /* 间接/顺从运算符必须pr̲e̲放弃目标标识符:*/
        ❌ a++*; ❌
        ❌ a*++; ❌
        ❌ ++a*; ❌

        【讨论】:

          【解决方案13】:

          因为指针是按值传递的,所以只有本地副本会增加。如果你真的想增加指针,你必须像这样通过引用传递它:

          void inc_value_and_ptr(int **ptr)
          {
             (**ptr)++;
             (*ptr)++;
          }
          

          【讨论】:

          • 实际上并没有回答这个问题。他在问 *ptr++ 做什么。正确答案涉及描述优先级。
          猜你喜欢
          • 2016-12-01
          • 1970-01-01
          • 1970-01-01
          • 2012-12-31
          • 2013-01-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多