C语言基础知识指针与自增表达式详解*p、*(p)、*p、*(p)、(*p)、(*p)与*p适合已经学过变量、数组和指针希望彻底分清“移动指针”和“修改指针所指数据”的学习者。1. 先给结论真正要分清的是“谁在自增”面对这些表达式不要只盯着*和的位置猜结果。先补出括号再确认的操作数写法实际解析谁发生变化表达式得到什么*p*(p)指针p后移旧指针所指对象的值*(p)*(p)指针p后移旧指针所指对象的值*p*(p)指针p先前移新指针所指对象的值*(p)*(p)指针p先前移新指针所指对象的值(*p)(*p)p所指对象加 1对象加 1 之前的旧值(*p)(*p)p所指对象加 1对象加 1 之后的新值*p(*p)p所指对象加 1对象加 1 之后的新值最短记忆法p自增指针移动数组元素不变。*p自增数据改变指针位置不变。后置表达式结果取旧值。前置表达式结果取新值。2. 预备知识p、*p与p 1分别是什么先看一个统一的初始状态inta[]{10,20,30};int*pa;// 等价于 int *p a[0];此时p保存a[0]的地址。*p表示p指向的对象也就是a[0]。*p的值是10。p 1指向下一个同类型元素也就是a[1]。2.1 指针加 1不是简单地把地址数值加 1如果p的类型是int *那么p 1会移动到下一个int元素。地址通常增加sizeof(int)个字节。int*p;// p 1 跨过一个 intdouble*q;// q 1 跨过一个 doublechar*r;// r 1 跨过一个 char因此p的准确含义是“让p指向下一个同类型元素”而不是固定移动一个字节。2.2 数组名不是普通可修改指针inta[3]{10,20,30};int*pa;p;// 正确p 是可修改的指针变量a;// 错误数组名不是可修改左值数组名在很多表达式中会转换为首元素地址但它本身不能像指针变量那样自增。3. 语法基础优先级负责“分组”不等于运行时先后顺序本题涉及的优先级由高到低如下优先级运算符例子结合方向较高后缀自增exprp、(*p)从左向右较低前缀自增expr、解引用*exprp、*p从右向左括号会显式指定分组。因此*p// 后缀 先与 p 结合解析为 *(p)*p// 一元运算从右向左结合解析为 *(p)*p// 一元运算从右向左结合解析为 (*p)(*p)// 括号让后缀 作用于 *p重要提醒“优先级更高”只说明表达式怎样分组不自动规定所有子表达式在机器上按什么时间顺序执行。本文说“先取旧指针的值再后移”是一种可靠的语义理解方式编译器生成的机器指令可能采用不同顺序只要可观察结果符合 C 语言规则即可。4.*p与*(p)先使用旧指针随后移动指针4.1 为什么它们完全相同后缀的优先级高于一元解引用**p*(p)不能把它误读为(*p)// 这是另一种完全不同的表达式4.2 分步理解inta[]{10,20,30};int*pa;intx*p;可以按下面的语义步骤理解p提供p自增前的旧指针值也就是a[0]。*对这个旧地址解引用因此得到a[0]的值10。p完成自增最终指向a[1]。数组内容没有改变。最终状态x 10 p a[1] a {10, 20, 30}在这个普通赋值示例中可把它近似展开为intx*p;p;这里的“展开”用于帮助理解不是通用的文本替换规则。4.3 单独写*p;会发生什么*p;它仍然解析为*(p);表达式取得的值被丢弃但p的副作用仍然存在所以p会后移。为了提高可读性如果只是想移动指针直接写p;更清楚。4.4 典型用途读取当前元素并移动到下一个元素intsum0;while(pa3){sum*p;}每轮循环都会读取当前元素把它加到sum让p指向下一个元素。5.*p与*(p)先移动指针再读取新位置5.1 为什么它们完全相同前缀和解引用*都属于一元运算符并且从右向左结合*p*(p)5.2 分步理解inta[]{10,20,30};int*pa;intx*p;步骤如下p先把p从a[0]移到a[1]。p的表达式结果是更新后的指针值a[1]。*解引用新地址读取a[1]。数组内容没有改变。最终状态x 20 p a[1] a {10, 20, 30}在这个普通赋值示例中可按两句理解p;intx*p;5.3 与*p的核心差别intx1*p;// 使用旧 p 指向的元素然后 p 后移intx2*p;// p 先前移然后使用新 p 指向的元素如果两次都从p a[0]单独开始*p 得到 10 *p 得到 206.(*p)修改所指对象表达式结果是旧值括号让后缀作用于*p也就是p指向的对象inta[]{10,20,30};int*pa;intx(*p);步骤如下*p指定对象a[0]。后置自增把对象从10改为11。由于使用后置自增整个表达式提供的是修改前的旧值10。p本身没有移动。最终状态x 10 p a[0] a {11, 20, 30}可按下面两句理解intx*p;*p*p1;注意这里变化的是*p不是p。7.(*p)与*p修改所指对象表达式结果是新值7.1 为什么*p等于(*p)前缀和解引用*都是一元运算符并且从右向左结合*p(*p)7.2 分步理解inta[]{10,20,30};int*pa;intx(*p);步骤如下*p指定对象a[0]。前置自增先把a[0]从10改为11。整个表达式提供修改后的新值11。p本身没有移动。最终状态x 11 p a[0] a {11, 20, 30}可按下面两句理解*p*p1;intx*p;7.3 与(*p)的核心差别二者都会让p指向的数据加 1最终数组内容相同区别只在表达式产生的值intx1(*p);// x1 得到旧值intx2(*p);// x2 得到新值如果两次都从a[0] 10单独开始(*p) 让 a[0] 变成 11但表达式结果是 10 (*p) 让 a[0] 变成 11并且表达式结果是 118. 四类表达式的内存变化对比下面每一行都重新从同一个初始状态开始inta[]{10,20,30};int*pa;表达式指针最终位置数组最终内容x的值int x *p;a[1]{10, 20, 30}10int x *p;a[1]{10, 20, 30}20int x (*p);a[0]{11, 20, 30}10int x (*p);a[0]{11, 20, 30}11int x *p;a[0]{11, 20, 30}119. 完整可运行程序下面的程序让每个案例都从独立的{10, 20, 30}开始避免前一个案例影响后一个案例。#includestdio.hintmain(void){{inta[]{10,20,30};int*pa;intx*p;printf(*p : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}{inta[]{10,20,30};int*pa;intx*p;printf(*p : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}{inta[]{10,20,30};int*pa;intx(*p);printf((*p) : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}{inta[]{10,20,30};int*pa;intx(*p);printf((*p) : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}{inta[]{10,20,30};int*pa;intx*p;printf(*p : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}return0;}推荐使用下面的命令编译gcc-stdc17-Wall-Wextra-pedanticpointer_increment.c-opointer_increment ./pointer_increment预期输出*p : x10, p_index1, a{10,20,30} *p : x20, p_index1, a{10,20,30} (*p) : x10, p_index0, a{11,20,30} (*p) : x11, p_index0, a{11,20,30} *p : x11, p_index0, a{11,20,30}10. 进一步理解这些表达式也能出现在赋值号左边只要解引用得到的是可修改对象下面两种写法都合法10.1 修改当前元素然后移动指针inta[]{10,20,30};int*pa;*p99;// 解析为 *(p) 99;结果a {99, 20, 30} p a[1]10.2 先移动指针再修改新位置的元素inta[]{10,20,30};int*pa;*p99;// 解析为 *(p) 99;结果a {10, 99, 30} p a[1]这两个例子再次说明*p与*p的差别是使用旧地址还是新地址。11. 最容易犯的错误11.1 把*p错看成(*p)错误理解*p// 认为它会让 *p 的值加 1正确解析*(p)// 移动的是指针 p如果你确实想让所指对象加 1应写(*p);11.2 认为p只移动一个字节p移动一个“元素”地址跨越多少字节由指针类型决定int*p;// 通常跨 sizeof(int) 字节double*q;// 通常跨 sizeof(double) 字节11.3 忘记检查数组边界假设inta[]{10,20,30};int*pa[2];此时intx*p;// 合法读取最后一个元素随后 p 变成 a 3a 3是“尾后指针”可以用于比较但不能解引用printf(%d\n,*p);// 未定义行为p 已经指向尾后位置而下面的写法会先移动到尾后位置再立即解引用因此也是未定义行为pa[2];intx*p;// 未定义行为11.4 对只读对象使用(*p)constinta[]{10,20,30};constint*pa;(*p);// 错误不能通过 p 修改 const int(*p)和(*p)都要求*p是可修改左值。11.5 在同一个完整表达式里既修改p又读取p下面的参数求值顺序没有提供所需的先后保证printf(%d %d\n,*p,*p);// 未定义行为原因是一个参数读取p另一个参数修改p二者之间没有顺序保证。安全改写intfirst*p;p;printf(%d %d\n,first,*p);同理下面的写法在的两个操作数中多次修改同一个对象也是未定义行为inty(*p)(*p);// 未定义行为安全原则是一条语句只做一次关键状态修改复杂表达式拆成多句。12. 一个连续执行的综合例子这一次不重置状态按顺序执行inta[]{2,4,6};int*pa;intx*p;inty*p;intz*p;逐句分析第一句intx*p;读取a[0]所以x 2。p后移到a[1]。数组仍为{2, 4, 6}。第二句inty*p;此时p指向a[1]。a[1]从4增加为5。前置自增返回新值所以y 5。p仍指向a[1]。第三句intz*p;p先从a[1]移到a[2]。读取a[2]所以z 6。最终结果x 2 y 5 z 6 p a[2] a {2, 5, 6}13. 快速判断流程看到表达式时可以机械地做三步根据优先级和结合性补括号。找出的真正操作数是p还是*p。再判断是前置还是后置以确定表达式得到旧值还是新值。14. 自测题所有小题都独立执行。题 1inta[]{5,8,13};int*pa;intx*p;求x、p - a和数组内容。题 2inta[]{5,8,13};int*pa;intx*p;求x、p - a和数组内容。题 3inta[]{5,8,13};int*pa;intx(*p);求x、p - a和数组内容。题 4inta[]{5,8,13};int*pa;intx*p;求x、p - a和数组内容。题 5inta[]{5,8,13};int*pa;*p100;求p - a和数组内容。参考答案题号表达式结果p - a数组最终内容1x 51{5, 8, 13}2x 81{5, 8, 13}3x 50{6, 8, 13}4x 60{6, 8, 13}5无x1{100, 8, 13}15. 最终速查表想实现的操作推荐写法结果特征读取当前元素然后移动到下一个元素value *p;使用旧地址p后移先移动到下一个元素再读取value *p;使用新地址p先移动当前元素加 1但拿到旧值old (*p);p不动数据改变当前元素加 1并拿到新值now (*p);或now *p;p不动数据改变修改当前元素然后移动到下一个元素*p value;旧位置被修改p后移先移动再修改新位置的元素*p value;新位置被修改最后再记一遍*p *(p) 旧地址取值指针后移 *p *(p) 指针先移新地址取值 (*p) 数据后置自增结果是旧值 (*p) *p 数据前置自增结果是新值判断核心不是*写在左边还是右边而是最终作用于p还是*p。
C语言指针自增表达式详解*p++、*(p++)、*++p、*(++p)、(*p)++、++(*p) 与 ++*p
C语言基础知识指针与自增表达式详解*p、*(p)、*p、*(p)、(*p)、(*p)与*p适合已经学过变量、数组和指针希望彻底分清“移动指针”和“修改指针所指数据”的学习者。1. 先给结论真正要分清的是“谁在自增”面对这些表达式不要只盯着*和的位置猜结果。先补出括号再确认的操作数写法实际解析谁发生变化表达式得到什么*p*(p)指针p后移旧指针所指对象的值*(p)*(p)指针p后移旧指针所指对象的值*p*(p)指针p先前移新指针所指对象的值*(p)*(p)指针p先前移新指针所指对象的值(*p)(*p)p所指对象加 1对象加 1 之前的旧值(*p)(*p)p所指对象加 1对象加 1 之后的新值*p(*p)p所指对象加 1对象加 1 之后的新值最短记忆法p自增指针移动数组元素不变。*p自增数据改变指针位置不变。后置表达式结果取旧值。前置表达式结果取新值。2. 预备知识p、*p与p 1分别是什么先看一个统一的初始状态inta[]{10,20,30};int*pa;// 等价于 int *p a[0];此时p保存a[0]的地址。*p表示p指向的对象也就是a[0]。*p的值是10。p 1指向下一个同类型元素也就是a[1]。2.1 指针加 1不是简单地把地址数值加 1如果p的类型是int *那么p 1会移动到下一个int元素。地址通常增加sizeof(int)个字节。int*p;// p 1 跨过一个 intdouble*q;// q 1 跨过一个 doublechar*r;// r 1 跨过一个 char因此p的准确含义是“让p指向下一个同类型元素”而不是固定移动一个字节。2.2 数组名不是普通可修改指针inta[3]{10,20,30};int*pa;p;// 正确p 是可修改的指针变量a;// 错误数组名不是可修改左值数组名在很多表达式中会转换为首元素地址但它本身不能像指针变量那样自增。3. 语法基础优先级负责“分组”不等于运行时先后顺序本题涉及的优先级由高到低如下优先级运算符例子结合方向较高后缀自增exprp、(*p)从左向右较低前缀自增expr、解引用*exprp、*p从右向左括号会显式指定分组。因此*p// 后缀 先与 p 结合解析为 *(p)*p// 一元运算从右向左结合解析为 *(p)*p// 一元运算从右向左结合解析为 (*p)(*p)// 括号让后缀 作用于 *p重要提醒“优先级更高”只说明表达式怎样分组不自动规定所有子表达式在机器上按什么时间顺序执行。本文说“先取旧指针的值再后移”是一种可靠的语义理解方式编译器生成的机器指令可能采用不同顺序只要可观察结果符合 C 语言规则即可。4.*p与*(p)先使用旧指针随后移动指针4.1 为什么它们完全相同后缀的优先级高于一元解引用**p*(p)不能把它误读为(*p)// 这是另一种完全不同的表达式4.2 分步理解inta[]{10,20,30};int*pa;intx*p;可以按下面的语义步骤理解p提供p自增前的旧指针值也就是a[0]。*对这个旧地址解引用因此得到a[0]的值10。p完成自增最终指向a[1]。数组内容没有改变。最终状态x 10 p a[1] a {10, 20, 30}在这个普通赋值示例中可把它近似展开为intx*p;p;这里的“展开”用于帮助理解不是通用的文本替换规则。4.3 单独写*p;会发生什么*p;它仍然解析为*(p);表达式取得的值被丢弃但p的副作用仍然存在所以p会后移。为了提高可读性如果只是想移动指针直接写p;更清楚。4.4 典型用途读取当前元素并移动到下一个元素intsum0;while(pa3){sum*p;}每轮循环都会读取当前元素把它加到sum让p指向下一个元素。5.*p与*(p)先移动指针再读取新位置5.1 为什么它们完全相同前缀和解引用*都属于一元运算符并且从右向左结合*p*(p)5.2 分步理解inta[]{10,20,30};int*pa;intx*p;步骤如下p先把p从a[0]移到a[1]。p的表达式结果是更新后的指针值a[1]。*解引用新地址读取a[1]。数组内容没有改变。最终状态x 20 p a[1] a {10, 20, 30}在这个普通赋值示例中可按两句理解p;intx*p;5.3 与*p的核心差别intx1*p;// 使用旧 p 指向的元素然后 p 后移intx2*p;// p 先前移然后使用新 p 指向的元素如果两次都从p a[0]单独开始*p 得到 10 *p 得到 206.(*p)修改所指对象表达式结果是旧值括号让后缀作用于*p也就是p指向的对象inta[]{10,20,30};int*pa;intx(*p);步骤如下*p指定对象a[0]。后置自增把对象从10改为11。由于使用后置自增整个表达式提供的是修改前的旧值10。p本身没有移动。最终状态x 10 p a[0] a {11, 20, 30}可按下面两句理解intx*p;*p*p1;注意这里变化的是*p不是p。7.(*p)与*p修改所指对象表达式结果是新值7.1 为什么*p等于(*p)前缀和解引用*都是一元运算符并且从右向左结合*p(*p)7.2 分步理解inta[]{10,20,30};int*pa;intx(*p);步骤如下*p指定对象a[0]。前置自增先把a[0]从10改为11。整个表达式提供修改后的新值11。p本身没有移动。最终状态x 11 p a[0] a {11, 20, 30}可按下面两句理解*p*p1;intx*p;7.3 与(*p)的核心差别二者都会让p指向的数据加 1最终数组内容相同区别只在表达式产生的值intx1(*p);// x1 得到旧值intx2(*p);// x2 得到新值如果两次都从a[0] 10单独开始(*p) 让 a[0] 变成 11但表达式结果是 10 (*p) 让 a[0] 变成 11并且表达式结果是 118. 四类表达式的内存变化对比下面每一行都重新从同一个初始状态开始inta[]{10,20,30};int*pa;表达式指针最终位置数组最终内容x的值int x *p;a[1]{10, 20, 30}10int x *p;a[1]{10, 20, 30}20int x (*p);a[0]{11, 20, 30}10int x (*p);a[0]{11, 20, 30}11int x *p;a[0]{11, 20, 30}119. 完整可运行程序下面的程序让每个案例都从独立的{10, 20, 30}开始避免前一个案例影响后一个案例。#includestdio.hintmain(void){{inta[]{10,20,30};int*pa;intx*p;printf(*p : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}{inta[]{10,20,30};int*pa;intx*p;printf(*p : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}{inta[]{10,20,30};int*pa;intx(*p);printf((*p) : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}{inta[]{10,20,30};int*pa;intx(*p);printf((*p) : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}{inta[]{10,20,30};int*pa;intx*p;printf(*p : x%d, p_index%ld, a{%d,%d,%d}\n,x,(long)(p-a),a[0],a[1],a[2]);}return0;}推荐使用下面的命令编译gcc-stdc17-Wall-Wextra-pedanticpointer_increment.c-opointer_increment ./pointer_increment预期输出*p : x10, p_index1, a{10,20,30} *p : x20, p_index1, a{10,20,30} (*p) : x10, p_index0, a{11,20,30} (*p) : x11, p_index0, a{11,20,30} *p : x11, p_index0, a{11,20,30}10. 进一步理解这些表达式也能出现在赋值号左边只要解引用得到的是可修改对象下面两种写法都合法10.1 修改当前元素然后移动指针inta[]{10,20,30};int*pa;*p99;// 解析为 *(p) 99;结果a {99, 20, 30} p a[1]10.2 先移动指针再修改新位置的元素inta[]{10,20,30};int*pa;*p99;// 解析为 *(p) 99;结果a {10, 99, 30} p a[1]这两个例子再次说明*p与*p的差别是使用旧地址还是新地址。11. 最容易犯的错误11.1 把*p错看成(*p)错误理解*p// 认为它会让 *p 的值加 1正确解析*(p)// 移动的是指针 p如果你确实想让所指对象加 1应写(*p);11.2 认为p只移动一个字节p移动一个“元素”地址跨越多少字节由指针类型决定int*p;// 通常跨 sizeof(int) 字节double*q;// 通常跨 sizeof(double) 字节11.3 忘记检查数组边界假设inta[]{10,20,30};int*pa[2];此时intx*p;// 合法读取最后一个元素随后 p 变成 a 3a 3是“尾后指针”可以用于比较但不能解引用printf(%d\n,*p);// 未定义行为p 已经指向尾后位置而下面的写法会先移动到尾后位置再立即解引用因此也是未定义行为pa[2];intx*p;// 未定义行为11.4 对只读对象使用(*p)constinta[]{10,20,30};constint*pa;(*p);// 错误不能通过 p 修改 const int(*p)和(*p)都要求*p是可修改左值。11.5 在同一个完整表达式里既修改p又读取p下面的参数求值顺序没有提供所需的先后保证printf(%d %d\n,*p,*p);// 未定义行为原因是一个参数读取p另一个参数修改p二者之间没有顺序保证。安全改写intfirst*p;p;printf(%d %d\n,first,*p);同理下面的写法在的两个操作数中多次修改同一个对象也是未定义行为inty(*p)(*p);// 未定义行为安全原则是一条语句只做一次关键状态修改复杂表达式拆成多句。12. 一个连续执行的综合例子这一次不重置状态按顺序执行inta[]{2,4,6};int*pa;intx*p;inty*p;intz*p;逐句分析第一句intx*p;读取a[0]所以x 2。p后移到a[1]。数组仍为{2, 4, 6}。第二句inty*p;此时p指向a[1]。a[1]从4增加为5。前置自增返回新值所以y 5。p仍指向a[1]。第三句intz*p;p先从a[1]移到a[2]。读取a[2]所以z 6。最终结果x 2 y 5 z 6 p a[2] a {2, 5, 6}13. 快速判断流程看到表达式时可以机械地做三步根据优先级和结合性补括号。找出的真正操作数是p还是*p。再判断是前置还是后置以确定表达式得到旧值还是新值。14. 自测题所有小题都独立执行。题 1inta[]{5,8,13};int*pa;intx*p;求x、p - a和数组内容。题 2inta[]{5,8,13};int*pa;intx*p;求x、p - a和数组内容。题 3inta[]{5,8,13};int*pa;intx(*p);求x、p - a和数组内容。题 4inta[]{5,8,13};int*pa;intx*p;求x、p - a和数组内容。题 5inta[]{5,8,13};int*pa;*p100;求p - a和数组内容。参考答案题号表达式结果p - a数组最终内容1x 51{5, 8, 13}2x 81{5, 8, 13}3x 50{6, 8, 13}4x 60{6, 8, 13}5无x1{100, 8, 13}15. 最终速查表想实现的操作推荐写法结果特征读取当前元素然后移动到下一个元素value *p;使用旧地址p后移先移动到下一个元素再读取value *p;使用新地址p先移动当前元素加 1但拿到旧值old (*p);p不动数据改变当前元素加 1并拿到新值now (*p);或now *p;p不动数据改变修改当前元素然后移动到下一个元素*p value;旧位置被修改p后移先移动再修改新位置的元素*p value;新位置被修改最后再记一遍*p *(p) 旧地址取值指针后移 *p *(p) 指针先移新地址取值 (*p) 数据后置自增结果是旧值 (*p) *p 数据前置自增结果是新值判断核心不是*写在左边还是右边而是最终作用于p还是*p。