指针表达式:*ptr++,*++ptr和++*ptr

157

最近我遇到了这个问题,自己无法理解。

这三个表达式真正的意思是什么?

*ptr++
*++ptr
++*ptr

我曾经尝试过Ritchie,但是很遗憾,我无法理解他关于这三个操作的讲解。

我知道它们都用于增加指针/指向的值。我猜测可能有很多关于优先级和评估顺序的事情。比如,一个操作会先增加指针然后获取该指针的内容,而另一个则只获取内容然后再增加指针等等。正如你所看到的,我并没有清楚地理解它们实际的操作,我想尽快弄清楚。但是当我有机会在程序中应用它们时,我真的迷失了方向。例如:

int main()
{
    char *p = "Hello";
    while(*p++)
         printf("%c",*p);
    return 0;
}

给我这个输出:

ello

但我的期望是它会打印 Hello

最后一个请求--请给我一些示例,说明每个表达式在给定的代码片段中如何工作。因为大多数时候,只有简短的理论段落会让我头晕。


7
你漏掉了第四个:(*ptr)++(需要加括号以消除与 *ptr++ 的歧义)。 - user4815162342
18
因为你在打印指针前已经对其进行了自增操作。你想要的是 while(*p) 和 printf("%c", *p++);。 - dcaswell
5
如果这个问题让你的面试者感到困惑,请尝试以下方法:声明一个全局指针 char* p,指向一个由唯一字符组成的、以结束符结尾的字符串。然后定义一个函数 fn(char ch),该函数打印出 ch 参数和当前指向的字符。现在调用 fn(*p++)。问:fn 函数会打印出相同的字符两次吗?你会惊讶地发现,有多少教授会回答错误。 - WhozCraig
1
因为p指向一个字符串字面值,所以你应该写成const char* p = "Hello"; - hetepeperfan
@WhozCraig 我是受访者 :) 是的,这更加如此。 - Himanshu
显示剩余3条评论
11个回答

327

这里有一个详细的解释,希望能对您有所帮助。让我们从您的程序开始,因为它是最容易解释的。

int main()
{
    char *p = "Hello";
    while(*p++)
        printf("%c",*p);
    return 0;
}

第一条语句:

char* p = "Hello";

声明p为指向char的指针。当我们说“指向char的指针”时,这是什么意思?这意味着p的值是char的地址;p告诉我们在内存中有一些空间来存储char

该语句还将p初始化为指向字符串字面量"Hello"中的第一个字符。为了这个练习的目的,重要的是要理解p指向的只是整个字符串中的第一个字符'H',而不是整个字符串。毕竟,p是指向一个char,而不是整个字符串。p的值是"Hello"'H'的地址。

然后您设置了一个循环:

while (*p++)

循环条件*p++是什么意思?这里有三个因素使它变得复杂(至少在熟悉之前):

  1. 后缀++和间接*运算符的优先级
  2. 后缀递增表达式的值
  3. 后缀递增表达式的副作用

1. 优先级。快速查看运算符优先级表将告诉您,后缀递增具有比解除引用/间接更高的优先级。这意味着复杂表达式*p++将被分组为:*(p++)。也就是说,*部分将应用于p++部分的值。所以我们先来看p++部分。

2. 后缀表达式的值p++的值是递增之前p的值。如果您有:

int i = 7;
printf ("%d\n", i++);
printf ("%d\n", i);

输出将为:

7
8

因为i++在增量之前会先计算出i的值。同样地,p++也会计算出当前p的值。我们知道,p的当前值是'H'的地址。

所以现在*p++中的p++部分已经被计算出来了;它是p的当前值。然后进行*部分。*(p的当前值)的意思是:访问p所持有的地址上的值。我们知道该地址上的值是'H'。因此,表达式*p++的结果是'H'

现在等一下,你说什么?如果*p++的结果是'H',那么为什么上面的代码没有打印出那个'H'呢?这就是副作用的作用。

3. 后缀表达式的副作用。后缀++具有当前操作数的,但它具有增加该操作数的副作用。什么?再看一下那个int代码:

int i = 7;
printf ("%d\n", i++);
printf ("%d\n", i);

如前所述,输出将为:

7
8

当在第一个printf()中评估i++时,它的值为7。但是C标准保证,在第二个printf()开始执行之前的某个时间点,++运算符的副作用将已经发生。也就是说,在第二个printf()发生之前,i将已经因第一个printf()中的++运算符而被增加。顺便说一下,这是标准关于副作用时间的少数保证之一。

因此,在您的代码中,当评估表达式*p++时,它的值为'H'。但是当您到达以下内容时:

printf ("%c", *p)

发生了讨厌的副作用。 p 已经被增加了。哇!它不再指向 'H',而是指向 'H' 后面的一个字符:也就是 'e'。这解释了你的口音输出:

ello

因此,在其他答案中出现了一系列有用(且准确)的建议合唱:要打印标准英语发音的 "Hello" 而不是其伦敦俚语版本,您需要类似以下的代码:
while (*p)
    printf ("%c", *p++);

说了这么多,那其他的呢?你问这些的意思:

*ptr++
*++ptr
++*ptr

我们刚刚讨论了第一个,现在来看看第二个:*++ptr

我们在之前的解释中看到后缀递增p++有一定的优先级副作用。前缀递增++p与其后缀对应物具有相同的副作用:它将其操作数增加1。然而,它具有不同的优先级和不同的

前缀递增的优先级低于后缀递增;它的优先级为15。换句话说,它具有与解引用/间接运算符*相同的优先级。在像下面这样的表达式中:

*++ptr

重要的不是优先级:这两个运算符在优先级上是相同的。所以关联性发挥作用。前缀递增和间接操作符具有右-左关联性。由于这种关联性,操作数ptr将与最右边的运算符++分组,然后才是更靠左的运算符*。换句话说,表达式将被分组为*(++ptr)。因此,就像*ptr++一样,但出于不同的原因,这里也会将*部分应用于++ptr部分的值。

那么这个值是什么呢?前缀递增表达式的值是操作数在递增之后的值。这使它与后缀递增运算符非常不同。假设你有:

int i = 7;
printf ("%d\n", ++i);
printf ("%d\n", i);

输出结果将是:

8
8

...跟后缀操作符看到的不一样。同样地,如果您有:

char* p = "Hello";
printf ("%c ", *p);    // note space in format string
printf ("%c ", *++p);  // value of ++p is p after the increment
printf ("%c ", *p++);  // value of p++ is p before the increment
printf ("%c ", *p);    // value of p has been incremented as a side effect of p++

输出结果将是:

H e e l                // good dog

你明白为什么吗?

现在我们来看你问过的第三个表达式,++*ptr。实际上,这是最棘手的一个。两个运算符具有相同的优先级和右-左结合性。这意味着表达式将被分组为 ++(*ptr)++ 部分将应用于 *ptr 部分的值。

所以如果我们有:

char q[] = "Hello";
char* p = q;
printf ("%c", ++*p);

出乎意料的自大输出将会是:

I

什么?好的,所以*p部分将被评估为'H'。然后++开始发挥作用,此时它将应用于'H',而不是指针!当您将1添加到'H'时会发生什么?您将得到1加上'H'的ASCII值,即72;您将得到73。将其表示为char,您将获得ASCII值为73的char'I'
这解决了您在问题中提到的三个表达式。这里还有另一个表达式,在您的问题的第一条评论中提到:
(*ptr)++ 

这个也很有趣。如果你有:

char q[] = "Hello";
char* p = q;
printf ("%c", (*p)++);
printf ("%c\n", *p);

它将会给你这样一个充满热情的输出:

HI
    

发生了什么?再次,这是关于优先级表达式值副作用的问题。由于括号,*p部分被视为主要表达式。主要表达式胜过其他任何内容;它们首先得到计算。而且,如您所知,*p求值为'H'。表达式的其余部分,++部分应用于该值。因此,在这种情况下,(*p)++变为'H'++

'H'++的值是多少?如果您说'I',那么您已经忘记了(已经!)我们对后缀递增与值vs.副作用的讨论。请记住,'H'++求值为'H'当前值。因此,第一个printf()将打印'H'。然后,作为副作用,该'H'将递增为'I'。第二个printf()打印出那个'I'。因此,您有了欢快的问候。

好的,但在这最后两种情况下,我为什么需要呢?

char q[] = "Hello";
char* p = q;

为什么我不能只有像这样的东西

char* p = "Hello";
printf ("%c", ++*p);   // attempting to change string literal!

因为"Hello"是一个字符串字面量。如果你尝试++*p,你就试图将字符串中的'H'改变成'I',使整个字符串变成"Iello"。在C语言中,字符串字面量是只读的;试图修改它们会引发未定义的行为。"Iello"在英语中也是未定义的,但这只是巧合。

相反,你也不能有

char p[] = "Hello";
printf ("%c", *++p);  // attempting to modify value of array identifier!

为什么不行呢?因为在这个实例中,p是一个数组。数组不是可修改的左值;你不能通过前缀或后缀递增或递减来改变p指向的位置,因为数组的名称就像一个常量指针一样工作。(这并不是它实际上的含义;这只是一种方便的看法。)

总之,这里是你所问的三件事:

*ptr++   // effectively dereferences the pointer, then increments the pointer
*++ptr   // effectively increments the pointer, then dereferences the pointer
++*ptr   // effectively dereferences the pointer, then increments dereferenced value

这里还有第四个,和前三个一样有趣:

(*ptr)++ // effectively forces a dereference, then increments dereferenced value

如果ptr实际上是一个数组标识符,第一和第二个将会崩溃。如果ptr指向字符串字面值,第三和第四个将会崩溃。

以上就是全部内容。希望现在一切都清晰了。感谢您的聆听,我将在这里待上整整一周。


36
来到这个论坛之前,我查找了我拥有的3本“C”书籍,并尝试了一些知名的在线教程。但是,它们都无法与您的解释相媲美(特别是您将所有东西都整合在一起的方式)。您不仅回答了我提出的问题,而且还从基础层面讨论了更多事情。实际上,您今天教给我很多基础知识,这些知识以前我缺乏。我禁不住要切换我的采纳答案。:) 再次感谢。 - allocated
33
+1 我认为这是我在 Stack Overflow 上读过的最好的长回答。我认为每个人都可以从这个答案中学到很多东西。 - Shafik Yaghmour
1
@verbose 您好!我建议避免将重要文本作为注释和水平滚动条。 - Grijesh Chauhan
12
先生,您应该写一本关于C语言的书。 - Dillon Burton
10
先生,您已经名副其实地做到了您的称号。 :) - sleeping_dragon
显示剩余10条评论

49
假设ptr指向数组arr的第i个元素。

  1. *ptr++计算结果为arr[i],并将ptr设置为指向arr的(i + 1)个元素。它相当于*(ptr++)

  2. *++ptrptr设置为指向arr的(i + 1)个元素,并计算结果为arr[i+1]。它相当于*(++ptr)

  3. ++*ptrarr[i]增加1,并计算其增加后的值;指针ptr保持不变。它相当于++(*ptr)

还有一个例子,但需要使用括号才能写出:

  1. (*ptr)++arr[i]增加1,并计算其增加前的值;指针ptr仍然保持不变。

其余的你可以自己理解;@Jaguar也回答了这个问题。


15

*ptr++ :对指针 ptr 进行后置自增操作

*++ptr :对指针 ptr 进行前置自增操作

++*ptr :对指针 ptr 所指位置的值进行前置自增操作

这里了解有关前置自增和后置自增运算符的信息。


这将输出Hello

int main()
{
    const char *p = "Hello";
    while(*p)
         printf("%c",*p++);//Increment the pointer here 
    return 0;
}

@Nik-Lz 是的,输出将会是 Hello - Jainendra
简洁而有用。一个经过精心设计的答案。谢谢。 - mohammadsdtmnd

8
您的循环条件有误:
while(*p++)
    printf("%c",*p);

等同于

while(*p)
{
    ++p;
    printf("%c",*p);
}

这是错误的,应该是:

while(*p)
{
    printf("%c",*p);
    ++p;
} 

*ptr++*(ptr++) 是相同的,即:

const char  *ptr = "example";
char  value;

value = *ptr;
++p;
printf("%c", value); // will print 'e'

*++ptr*(++ptr)相同,即:

const char  *ptr = "example";
char  value;

++p;
value = *ptr;
printf("%c", value); // will print 'x'

++*ptr++(*ptr)相同,即:

const char  *ptr = "example";
char  value;

value = *ptr;
++value;
printf("%c", value); // will print 'f' ('e' + 1)

我完全同意答案的第一部分。在第二部分中,用整数初始化指针(指向整数!)对于那些正在努力理解指针使用的人来说是令人困惑的。 - nickie

4
我想补充一下,因为虽然其他答案是正确的,但我认为它们缺少了一些内容。
 v = *ptr++

means

 temp = ptr;
 ptr  = ptr + 1
 v    = *temp;

与之相反,当...
 v = *++ptr

means

 ptr = ptr + 1
 v   = *ptr

重要的是要理解后增量(和后减量)的含义。
 temp = ptr       // Temp created here!!!
 ptr  = ptr + 1   // or - 1 if decrement)
 v    = *temp     // Temp destroyed here!!!

为什么这很重要呢?在 C 语言中并不是那么重要。但在 C++ 中,ptr 可能是一个复杂的类型,比如迭代器。例如:

 for (std::set<int>::iterator it = someSet.begin(); it != someSet.end(); it++)

在这种情况下,由于it是一个复杂类型,it++可能会因为创建temp而产生副作用。当然,如果你很幸运,编译器会尝试丢弃不需要的代码,但如果迭代器的构造函数或析构函数执行任何操作,则it++在创建temp时会显示这些效果。
简而言之,我的意思是“写出你的意思”。如果你的意思是“增加指针”,那么就要写成++ptr而不是ptr++。如果你的意思是temp = ptr,ptr += 1,temp,那么就要写成ptr++

4

你提到了运算符优先级,需要注意的是*比前缀递增运算符具有更高的优先级,但不如后缀递增运算符。以下是它们的区别:

*ptr++ - 从左到右依次执行,先对指针进行解引用操作,然后再将指针的值加1(由于后缀递增运算符的优先级高于解引用操作,因此它并不会改变指向的内容)

*++ptr - 先将指针加1,然后再进行解引用操作,这是因为前缀递增运算符和解引用操作的优先级相同,所以它们按照从右到左的顺序进行计算

++*ptr - 与上述情况类似,按照从右到左的顺序进行解引用操作和递增操作。请注意,在您的情况下,这将导致未定义的行为,因为您正在尝试修改一个只读变量(char* p = "Hello";)。


0

指针表达式:*ptr ++、*++ptr 和 ++*ptr:

注意:指针必须初始化并具有有效地址。因为在 RAM 中除了我们的程序(a.out)之外,还有很多其他程序同时运行,即如果您尝试访问未为您保留的某些内存,则操作系统将抛出分段错误。

在解释这个问题之前,让我们考虑一个简单的例子?

#include<stdio.h>
int main()
{
        int num = 300;
        int *ptr;//uninitialized pointer.. must be initialized
        ptr = &num;
        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
        *ptr = *ptr + 1;//*ptr means value/data on the address.. so here value gets incremented
        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
        /** observe here that "num" got changed but manually we didn't change, it got modified by pointer **/
        ptr = ptr + 1;//ptr means address.. so here address got incremented
        /**     char pointer gets incremented by 1 bytes
          Integer pointer gets incremented by 4 bytes
         **/
        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
}

分析上述代码的输出,希望您已经得到了上述代码的输出。从上述代码中可以清楚地看出指针名称(ptr)表示我们正在谈论地址,而*ptr表示我们正在谈论/数据。

CASE 1*ptr++,*++ptr,*(ptr++)和*(++ptr):

上述提到的所有4个语法都是相似的,在所有情况下,地址会被递增,但是地址如何递增是不同的。

注意:解决任何表达式时,找出表达式中有多少个运算符,然后找出运算符的优先级。如果有多个具有相同优先级的运算符,则检查演变的顺序或结合性,可能是从右到左(R)或从左到右(L)。

*ptr++:这里有两个运算符,即反引用(*)和++(递增)。两者具有相同的优先级,然后检查结合性,即从右到左。因此,从右到左开始解决,无论哪个运算符先出现。

*ptr++:首个 ++ 是从右到左解决的,因此地址被增加,但是这是后缀递增。

*++ptr:在这里与第一个相同,地址也被增加了,但这是前缀递增。

*(ptr++):这里有三个运算符,在这些运算符中,分组 () 优先级最高,所以首先解决了 ptr++,即地址被递增,但是是后缀递增。

*(++ptr):与上述情况相同,在这里地址也被递增,但是是前缀递增。

CASE 2:++*ptr、++(*ptr)、(*ptr)++:

上述所有四种语法都是类似的,在所有值/数据递增,但是值的变化方式不同。

++*ptr:首个 * 是从右到左解决的,因此值被更改,但是这是前缀递增。

++(*ptr):与上述情况相同,值被修改。

(*ptr)++:这里有三个运算符,在这些运算符中,分组 () 优先级最高,括号内为 *ptr,所以首先解决了 *ptr,即值被递增,但是是后缀递增。

注意:++*ptr 和 *ptr = *ptr + 1 都是相同的,在两种情况下值都会被更改。 ++*ptr:只使用一个指令(INC),可以直接在单个步骤中更改值。 *ptr = *ptr + 1:这里首先增加值(INC),然后再分配(MOV)。

为了理解上述指针增量的不同语法,让我们考虑简单的代码:

#include<stdio.h>
int main()
{
        int num = 300;
        int *ptr;
        ptr = &num;
        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
        *ptr++;//address changed(post increment), value remains un-changed
//      *++ptr;//address changed(post increment), value remains un-changed
//      *(ptr)++;//address changed(post increment), value remains un-changed
//      *(++ptr);//address changed(post increment), value remains un-changed

//      ++*ptr;//value changed(pre increment), address remains un-changed
//      (*ptr)++;//value changed(pre increment), address remains un-changed
//      ++(*ptr);//value changed(post increment), address remains un-changed

        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
}

在上面的代码中,尝试注释/取消注释并分析输出。
指针作为常量:有许多方法可以将指针作为常量,这里我提到了一些。
1)const int *p或int const *p:这里value是常量,地址不是常量,即p指向哪里?某个地址?在该地址上的值是什么?某个值对吧?该值是常量,您不能修改该值,但指针指向哪里?某个地址对吧?它也可以指向其他地址。
为了理解这一点,让我们考虑下面的代码:
#include<stdio.h>
int main()
{
        int num = 300;
        const int *ptr;//constant value, address is modifible
        ptr = &num;
        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
        *ptr++;//
//      *++ptr;//possible bcz you are trying to change address which is possible
//      *(ptr)++;//possible
//      *(++ptr);//possible

//      ++*ptr;//not possible bcz you trying to change value which is not allowed
//      (*ptr)++;//not possible
//      ++(*ptr);//not possible

        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
}

尝试分析上述代码的输出

2)int const *p:它被称为'**常量指针**',即地址是常量但值不是常量。在这里,您不允许更改地址,但可以修改值。

注意:常量指针(上面的情况)必须在声明时初始化。

为了理解这一点,让我们检查简单的代码。

#include<stdio.h>
int main()
{
        int x = 300;
        int* const p;
        p = &x;
        printf("x = %d p =%p and *p = %d\n",num,p,*p);
}

在上面的代码中,如果你注意到没有++*p或*p++,那么你可能会认为这是一个简单的情况,因为我们没有改变地址或值,但它会产生错误。为什么?我在注释中提到了原因。
#include<stdio.h>
int main()
{
        int x = 300;
        /** constant pointer must initialize while decaring itself **/
        int* const p;//constant pointer i.e its pointing to some address(here its pointing to garbage), it should point to same address(i.e garbage ad
dress only 
        p = &x;// but here what we are doing ? we are changing address. we are making p to point to address of x instead of garbage address.
        printf("x = %d p =%p and *p = %d\n",num,p,*p);
}

那么这个问题的解决方案是什么?

     int* const p = &x;

为了更好地理解这个案例,让我们考虑下面的例子。

#include<stdio.h>
int main()
{
        int num = 300;
        int *const ptr = &num;//constant value, address is modifible
        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
        *ptr++;//not possible
//      *++ptr;//not possible bcz you are trying to change address which is not possible
//      *(ptr)++;//not possible
//      *(++ptr);//not possible

//      ++*ptr;// possible bcz you trying to change value which is allowed
//      (*ptr)++;// possible
//      ++(*ptr);// possible
        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
}

3)const int* const p:这里地址和值都是常量

为了理解这一点,让我们看下面的代码:

#include<stdio.h>
int main()
{
        int num = 300;
        const int* const ptr = &num;//constant value,constant address 
        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
        *ptr++;//not possible
        ++*ptr;//not possible
        printf(" num = %d ptr = %p and data on ptr : %d \n",num,ptr,*ptr);
}

0
*ptr++    // 1

这与以下内容相同:

    tmp = *ptr;
    ptr++;

因此,检索指向ptr的对象的值,然后增加ptr

*++ptr    // 2

这与以下内容相同:

    ++ptr;
    tmp = *ptr;

因此指针ptr被递增,然后读取由ptr指向的对象。

++*ptr    // 3

这与以下内容相同:

    ++(*ptr);

因此,指向ptr的对象被增加;ptr本身不会改变。


0
  • 后缀++的优先级高于一元*
  • 前缀++和一元*具有相同的优先级,同时具有从右到左的运算符结合性,这意味着右侧的运算符在左侧之前绑定到操作数。

因此:

  • *ptr++ 将指针增加1项,然后反引用它在增加之前的内存位置。
  • *++ptr 将指针增加1项,然后反引用它现在所指向的内存位置。
  • ++*ptr 先反引用内存位置,然后将其中的内容(值)增加1。

-1
const char *p = "Hello";   

*p means "Hello"
          ^
          | 
          p

*p++ means "Hello"
             ^
             | 
             p

*++p means "Hello"
            ^
            |     (WHILE THE STATEMENT IS EXECUTED)
            p

*++p means "Hello"
             ^
             |     (AFTER THE STATEMENT IS EXECUTED)
             p

++*p 表示你试图增加 *p 的 ASCII 值

   is "Hello"
       ^
       | 
       p

你不能增加常量的值,否则会出现错误。

至于你的 while 循环,循环会一直运行,直到 *p++ 到达字符串末尾,即 '\0'(NULL)字符。

现在,由于 *p++ 跳过了第一个字符,因此你只能从第二个字符开始得到输出。

以下代码不会输出任何内容,因为 while 循环有 '\0'

const char *p = "Hello";
    while('\0') 
         printf("%c",*p);

以下代码将会输出与下一个代码相同的结果,即 "ello"。
const char *p = "Hello";
    while(*++p)
         printf("%c",*p);

...................................

const char *p = "Hello";
    while(*p++)
         printf("%c",*p);

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