ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言指针经典四式:*p++、*++p、++*p、(*p)++ 彻底解析

C语言指针经典四式:*p++、*++p、++*p、(*p)++ 彻底解析 1. 拆开揉碎为什么这组表达式是C语言的一座分水岭*p、*p、*p、(*p)这四个表达式几乎是每个学C语言的人都会遇到的坎。你随便在搜索引擎里搜“C语言指针难题”“C语言必背100代码”“翁恺C语言练习题”这组表达式一定会出现在结果里。我在带新人、帮同事Review代码、甚至自己在凌晨三点Debug的时候都无数次和它们打过照面。可以说能不能一次性把这四行表达式说清楚基本就反映了你对“运算符优先级”“指针自增”“左值”这三个概念掌握到了什么程度。为什么它这么经典因为它在极短的代码里集中了C语言最底层的两个核心机制指针的移动地址运算和内存单元内容的修改解引用后的操作。很多同学背结论——“*p是先取指再移动指针”——背得很熟但换个形式比如写成嵌套的函数指针调用或者配合二级指针立刻就懵了。这说明优先级这个东西光靠背没有用你得在语法树的层面上真正理解它。这篇东西我打算从运算符规则讲起把四个表达式的求值过程逐个拆开画清楚每一步发生了什么再补上一批能直接跑起来的验证代码和实战场景最后聊聊我实际踩过的那些坑。如果你正在刷C语言基础题或者准备计算机二级C语言考试又或者在工作中写嵌入式、写算法题时被指针绕晕这篇文章应该能帮你把这块地基彻底夯实。很多初学者会问一个问题题目里四个表达式*和都是单目运算符它们谁先算谁后算差别真的有那么大吗答案是差别大到你无法想象一个措辞可能让程序从“正常遍历数组”变成“修改数组内容”甚至“段错误”。接下来的章节我会一步步推导。2. 优先级与结合性的底层规则先把C语言运算规则这张表吃透2.1 运算符优先级速查单目运算符里藏着什么规律C语言的运算符优先级表是所有教材都会列的东西。初看很吓人一大堆符号按级别排成十几行但你真正需要重点记住的区间其实没那么多。我们关注的、--、*、全都属于“单目运算符”这个级别。我先把和本主题直接相关的规则拎出来后缀自增p、后缀自减p--优先级最高属于第二梯队第一梯队是圆括号()、数组下标[]、成员选择.和-。前缀自增p、前缀自减--p、解引用*p、取地址p、逻辑非!p等位于同一个优先级层级低于后缀自增但高于乘除法、加减法和绝大多数双目运算符。所有单目运算符的结合方向是自右向左。这条规则是关键。*p之所以等价于*(p)不是因为“后缀比*优先级高”这一句话就够了还要结合“单目运算符右结合”这个性质。所谓右结合就是在没有括号的情况下从右往左依次找操作数。于是*p先结合把p看成一个整体然后*再对这个整体解引用。这里我补充一个很多人忽略的细节优先级决定的是“谁的语法结合位置更紧”而不是“谁先执行”。在实际运行时p这个整体已经包含了“用旧值生成副本”和“修改p本身”两个动作而*作用于的是那个旧值副本。严格来说C标准只规定了运算结果并没有强制规定自增和解引用谁先落实到内存上但理解成“先取旧值副本再自增再解引用副本”对日常编程是安全且准确的。2.2 右结合到底怎么看出无括号版本的真实含义我们来做一个规范的语法分析。表达式*p中*和前缀在同一优先级层因为没有显式括号所以从右向左结合。哪个运算符离操作数p更“近”当然是前缀。它先和p绑定形成p这个整体然后*再作用于它最终解析为*(p)。同理*p的解析过程是操作数首先是*p前缀的优先级和*相同但从右向左结合于是先看p前面的运算符发现是*先绑定*p然后再绑定(*p)最终结果是(*p)。这里*p本身是一个左值可修改的内存单元所以可以直接作用在它上面。(*p)则完全是另一条路用括号强行把*p包起来让后缀作用在*p这个左值上。后缀自增的语义是“返回旧值然后把变量本身加1”所以(*p)做的事情是“取出指针p指向的值作为表达式结果然后将这个值自增1”。把四个表达式放一起对比你能看到一个很有意思的对称性。我整理成一张表表达式等价写法对指针p的影响对指针所指向变量的影响表达式返回的旧值*p*(p)p向后移动一个单位无影响p移动前指向单元的值*p*(p)p向前移动一个单位无影响p移动后指向单元的值*p(*p)无影响所指向的变量自增1自增后的值(*p)(*p)无影响所指向的变量自增1自增前的值有了这张表题目最核心的问题就已经解决了一半。但光看懂表格还不行你得会在实际代码里驱动它们。2.3 常见误区别把“优先级”和“运算次序”混为一谈这里必须单独说一个高频误区。很多教材把“优先级”描述成“谁先算”导致很多新手觉得*p是先执行p再执行*p。这个说法在结果上有点偏但在概念上非常误导人。实际情况是*p被解析成*(p)之后p这个表达式本身的求值会产生两个东西一个是修改前的p值旧地址另一个副作用是把p本身加1。然后*作用在“旧地址”上取的是旧地址处的值。所以从最终效果看表达式等价于tmp p; /* 先保存旧指针 */ p p 1; /* 指针自增副作用 */ result *tmp; /* 解引用旧指针 */看到没有是先解引用旧地址再修改指针。优先级只决定了“绑定在p上而不是绑定在*p上”并没有直接规定内存写入的先后次序。以后在面试、笔试、考试里遇到这类题不要在脑子里模拟“谁先执行”而是直接做语法解析把整个表达式拆成“哪部分是操作数、哪部分是运算符”然后再按每种运算符的语义去求值。3. 四种表达式逐个击破语义、场景和容易混淆的边界3.1*p最常用的“先取后移”遍历数组的常青树*p是四个人里出场率最高的一个。C语言里最常见的字符串复制、数组遍历、内存拷贝底层几乎都是它在驱动。它的语义是取出当前指针指向的值然后让指针后移一位。为什么要强调“取出当前值”而不是“先让指针移动”因为后缀自增的特性决定了p这个表达式的结果永远是被修改前的p值。我在前面已经推导过了这里不再重复。看一个最典型的使用场景用指针遍历数组。#include stdio.h int main(void) { int arr[] {10, 20, 30, 40, 50}; int *p arr; int i; printf(使用 *p 遍历数组\n); for (i 0; i 5; i) { printf(%d , *p); } printf(\n); /* 此时p已经指向arr[4]的下一个位置即arr5 */ printf(遍历结束后p指向的位置是%ld\n, (long)(p - arr)); return 0; }运行结果如下使用 *p 遍历数组 10 20 30 40 50 遍历结束后p指向的位置是5这个例子里*p循环了5次每次循环先取当前位置的值打印然后指针向后移动一个int最后一次移动后指针指向数组越界位置arr5。这在C语言里是允许的只要你不解引用越界之后的位置就没事。*p之所以比*p; p;两行写法更让人上瘾是因为它在语义上是一个原子操作既简洁又不容易遗漏。很多自研代码、开源项目、Linux内核代码里你能看到大量*dst *src;这样的写法它本质就是把复制和移动合并在一起循环体只剩一个while (n-- 0) *dst *src;。3.2*p先移后取用于“跳过第一个元素”或“从下一个位置开始处理”*p的语义是先把指针向后移动一个单位然后取新指向位置的值。它和*p相比就是把自增动作放到了取值之前。这玩意儿最典型的应用场景是当你想从第二个元素开始遍历或者在某些算法里先“跳过”当前节点。比如写一个单链表相关的递归展开处理熟悉指针的人就会写出first *head; second *head;这样的代码如果你要跳过第一个元素直接从第二个开始处理*p就派上用场。来看验证示例#include stdio.h int main(void) { int arr[] {10, 20, 30, 40, 50}; int *p arr; printf(跳过第一个元素从第二个开始取%d\n, *p); printf(此时p指向的元素下标%ld\n, (long)(p - arr)); return 0; }运行输出跳过第一个元素从第二个开始取20 此时p指向的元素下标1如果我把代码改成这样int *p arr; printf(%d\n, *p); /* 20 */ printf(%d\n, *p); /* 20 */ printf(%d\n, *p); /* 30 */你会看到第一次*p把p移到下标1处取了20第二次*p先取20再把p移动到下标2第三次*p取到的就是30。这个练习如果能在脑子里准确模拟出来优先级这块就算过关了。3.3*p修改目标值这才是“自增指向对象”的正确姿势前两个表达式都在移动指针后两个表达式作用的对象完全不同。*p的解析结果是(*p)它的含义是先取指针p指向的值然后把这个值自增1。整个过程不影响p本身。这个表达式经常会和*p搞混尤其是在面试题里题干故意写得很像让你误以为是在移动指针。实际上一句话就能区分*p改的是“针”指针的位置。*p改的是“线头”指针所指的实体内容。写个例子验证#include stdio.h int main(void) { int x 100; int *p x; printf(自增前 x %d\n, x); *p; printf(自增后 x %d\n, x); printf(p 本身的值地址没有变p %p\n, (void *)p); return 0; }运行结果自增前 x 100 自增后 x 101 p 本身的值地址没有变p 0x7ffc...注意*p写多了之后有人会习惯性给它加个空格区分层次。比如*p和 *p在C语言里其实是同一个东西但很多代码规范明确要求写*p这种紧凑格式因为 unary operator 紧跟操作数可读性更好也避免编译器和人类同一时期产生误会。这里我额外提一句顶层指针和底层指针可以相互赋值吗这个相关的热词。其实这个概念在C语言语境下和const修饰有关。顶层指针指的是指针本身是const底层指针指的是指针指向的对象是const。如果你在写*p时发现编译报错“increment of read-only location”那就是指针指向的对象被const限定了。反过来如果p本身是const指针你写*p就不行因为不能修改p本身。这三个概念经常在选择题里组合出现本质都是在考你自增的到底是“指针”还是“指向的值”。3.4(*p)后置自增对象值括号在这里决定了成败(*p)是四个表达式里最能体现“括号重要性”的一个。它和前两个表达式最大的区别是后缀的优先级极高如果不用括号把*p包起来写成*p含义就会变成“移动指针”而不是“修改值”。一旦加了括号就把“解引用”和“后置自增”这两个操作绑定在同一个对象上。语义上(*p)返回的是自增前的值然后才把指向的值加1。这个“返回旧值”的特征让它在很多算法场景中非常有用。比如你想把数组里的某个值取出后自增同时还需要旧值做后续判断#include stdio.h int main(void) { int x 100; int *p x; int old (*p); printf((*p) 返回的旧值: %d\n, old); printf(自增后 x 的值: %d\n, x); return 0; }运行结果(*p) 返回的旧值: 100 自增后 x 的值: 101这恰好和*p形成了一个对称关系*p返回的是新值自增后的值。(*p)返回的是旧值自增前的值。这个区别在嵌入C代码的时候会有直接的体现。比如你在写一个环形缓冲区的写指针更新逻辑写数据前你要用到旧写指针位置然后把写指针前进一格。如果某一步你需要“从缓冲区中的某个值读取后原地自增”那么选择(*p)和*p会导致完全不同的结果。我遇到过一位同事他写FIFO的状态计数时本来想这么写count (*fifo-tail);结果漏了括号变成count *fifo-tail;前者是先取回环缓冲区尾部元素的值然后原地自增后者是取完值后把tail指针本身后移完全不是一回事。排查了很久才发现是这里逻辑变了一个括号的位置硬生生让问题定位多花了两个钟头。这种教训很贵。4. 组合拳与真实场景从字符串拷贝到栈操作这套语法无处不在4.1 字符串与内存拷贝*dst *src背后的美学如果你写过或者看过高性能版的字符串拷贝一定见过这种循环void my_strcpy(char *dst, const char *src) { while ((*dst *src) ! \0) ; }你注意看这里*dst是“先取当前目标位置把源字符赋过去再把两个指针都后移”赋值表达式的结果是源字符本身当它是\0时循环停止。这里面埋着一个很容易出错的点循环体是空语句;真正干活的是 while 条件里的赋值。如果这里用的是*dst *src那初始时指针就已经领先了一个位置源字符串的首字符会拷贝到错误的位置最终结果必错无疑。这个例子就是*p语义最好的实战注解。我还常和刚入门的人说如果这段代码你能不看答案独立写出来且正确说明为什么终止条件写! \0而不是 0那你的指针基本功就过关了一半。再发散一个点如果用负数作为结束标志的“带长度前缀”的内存块拷贝代码就成了n src[0]; /* 长度存在首字节 */ src 1; while (n-- 0) { *dst *src; }这段代码里n-- 0这个条件同时完成了“判断旧值是否大于0”和“自减”两个任务。你能在这里看出后缀运算“返回旧值”的思想其实一脉相承。4.2 顺序栈与队列的指针操作后置自增是天然的生产者在很多嵌入式实时系统或算法题里顺序栈是用数组加栈顶指针top实现的。入栈操作如果写指针版往往长这样int stack[MAX]; int *top stack; void push(int val) { *top val; } int pop(void) { return *--top; }*top val做的事情是“把新元素写入当前栈顶位置然后top指针上移一格”。而*--top在弹出时先把top下移一格再取元素。你看这两个表达式一个是*p的写版本一个是*p的返回值版本正好覆盖了栈的入栈和出栈两个方向。我自己在笔试里出过这道变形题上面这个栈如果入栈写成*top val出栈写成*top--会有什么效果很多人的第一反应是“好像也能工作”但仔细推演就会发现如果初始top stack那么第一次*top val会写到stack[1]栈的第一个位置就被跳过了而*top--取完值后还把top再往下移一格指针边界会出问题。这种“看起来相似、实际上差之毫厘失之千里”的坑正是当年我对这个知识点印象最深的原因。4.3 链表的头插法到通用遍历结合二级指针的进阶用法除了基础的数据结构这套东西还会在更复杂的场景里出现。比如头插法创建链表时新人喜欢用一个临时指针在节点间穿梭Node *head NULL; Node *cur; while (/* 有数据 */) { cur malloc(sizeof(Node)); /* 填充cur的数据 */ cur-next head; head cur; }这个写法本身没毛病但如果写成“用二级指针操控头结点”的高阶版你会再次见到*pp和的组合void push(Node **head, int data) { Node *node malloc(sizeof(Node)); node-data data; node-next *head; *head node; }再看一个把(*pp)和结合用的例子如果要把当前指针指向的链表节点的值都加1并且移动指针有人会写出*p; /* 等价于 (*(p))先取旧对象自增再移动指针 */这种“自增对象值再移动指针”的复合行为其实可以拆成(*p); p;两行。我提倡在无人review且追求极限性能时用这种combo但团队合作阶段绝不要这么写。一眼能看懂优先级的老手当然没事可代码是给人读的工作里“可读性优先”才是王道。5. 验证与排错利用编译器、调试器和阅读汇编来吃透优先级5.1 断点验证用GDB打印每一步的中间状态我在实际带人的时候最推荐的验证方法是直接把代码放进GDB里观察每一步指针的值和地址的变化。下面是一段测试代码#include stdio.h int main(void) { int arr[] {10, 20, 30, 40, 50}; int *p arr; printf(*p %d\n, *p); /* 10 */ printf(*p %d\n, *p); /* 30 */ printf(*p %d\n, *p); /* 31 */ printf((*p) %d\n, (*p)); /* 31 */ printf(now x %d\n, arr[2]); /* 32 */ return 0; }这段代码是很好的一道手算题你先别急着跑结果自己往下推一遍初始p arr[0]。第一行*p取10p移动到下标1。第二行*pp先移动到下标2取值30。第三行*p对arr[2]进行自增arr[2]从30变为31表达式结果为31。第四行(*p)返回旧值31然后arr[2]变为32。最终输出*p 10 *p 30 *p 31 (*p) 31 arr[2] 32如果在GDB里跑你在每个printf之前设置断点打印p - arr和*p就能直观看到指针漂移和值的变化。我的经验是自己手算一遍之后再让机器打脸比看十遍教程都管用。很多人在这一步发现自己根本没懂优先级正好借此修正认知。5.2 观察反汇编编译器是怎么翻译这四条语句的前几年我在调试一个实时性要求比较高的模块为了抠执行周期特意去看了几条语句的汇编输出。其实用objdump -d或者直接gcc -S就能看到*p和*p的区别。用一段最简单的代码来做反汇编实验int test1(int *p) { return *p; } int test2(int *p) { return *p; } int test3(int *p) { return *p; } int test4(int *p) { return (*p); }在x86-64平台上用gcc -O2 -S编译后你会发现几个函数生成的汇编指令数量不同但核心就几条test1先movl (%rdi), %eax再把%rdi加4返回旧值。test2先把%rdi加4再movl (%rdi), %eax返回新地址处的值。test3incl (%rdi)同时影响内存然后movl (%rdi), %eax读取自增后的值。test4movl (%rdi), %eax保存旧值然后incl (%rdi)返回旧值。反汇编一眼看出来后缀返回旧值、前缀返回新值不是“概念上的约定”而是编译器实实在在生成的机器指令序列。理解到这个层面你对优先级的理解才算真正落地。5.3 用静态分析工具和Clang-Tidy约束团队书写前面说了这种复合表达式一旦写错排错成本极高。所以实际工程项目里我会把两个工具开起来开启编译器警告-Wall -Wextra让-Wsequence-point之类的警告帮我们盯住副作用相关的未定义行为。用clang-tidy的cppcoreguidelines-pro-bounds-pointer-arithmetic等规则提醒团队注意指针算术。在代码评审规范里明确规定出现*p这种复合表达式时必须有注释解释语义。尤其在涉及二级指针或者函数指针时强制加括号禁止挑战人类理解极限。我见过很多新人写代码喜欢在网上找各种短小精悍的写法比如一行遍历、一行拷贝。但我始终建议在性能允许的前提下优先考虑可读性。while (n-- 0) *dst *src;这种经典写法可以保留因为它足够通行但再往前多跨一步写出**dp这种三重复合表达式就是对队友审美和心智的考验了。6. 优先级之外那些容易被牵连的基础概念6.1 左值、右值与副作用为什么(*p)必须要求*p是左值后置自增要求它的操作数必须是可修改的左值也就是必须要有一个实实在在的内存地址可以被修改。在(*p)中*p就是左值因为它解引用后对应着一个具体的对象。但在*p中*的操作数是p的结果这个结果是一个“右值”是临时保存的旧地址所以它只是被读取不会被修改。很多人搞不懂“左值右值”我可以给一个粗浅但好用的理解方式左值是能出现在赋值号左边的东西右值是不能出现在左边的东西。*p能所以它是左值p的结果不能所以它是右值。*p合法是因为*p是左值前缀能在原处修改它(p)非法是因为p这个右值无法被自增。面试笔试里还有一种阴险考法给你int *p;然后问*p 1合法吗答案是合法因为*p解析为*(p)最终得到的还是一个左值*p_old赋值会作用在指针移动前的目标内存上。这种写法在旧式代码里有时会出现但我个人不建议主动使用因为逻辑太绕维护成本极高。6.2 顶层const、底层const与指针自增的兼容问题前面提到过热词“顶层指针和底层指针可以相互赋值吗”这里正式聊一下。C语言里虽然不叫顶层底层但大家习惯借用C的说法顶层const指针本身不能改如int *const p;此时需要对p自增的写法如*p里的指针自增都非法。底层const指向的对象不能改如const int *p;此时需要对对象自增的写法如*p、(*p)都非法。这两种指针之间的赋值规则可以简化记忆要么“权限从不收窄到收窄”要么“权限不变”。把非const指针赋给const指针是允许的因为你在限制自己把const指针赋给非const指针则不行因为这会绕过限制编译器会直接拒绝。回到本主题它和优先级的关联是*p编译报 “increment of read-only location” 时你得先怀疑是不是底层const*p报 “read-only variable is not assignable” 时你得先怀疑是不是顶层const。能同时看懂这两种报错说明你不是光记了优先级而是把指针的整个类型系统串起来了。6.3 序列点与未定义行为不要在一条表达式里过度发挥优先级能告诉你*p是“先取旧值再移动指针”但它不能告诉你多个副作用之间的先后顺序。C语言里有一个概念叫“序列点sequence point”在同一个序列点之间对同一个变量做多次未排序的修改属于未定义行为undefined behavior。举例*p *p; /* 危险左右两侧都对p进行了读取但自增与右侧读取的先后顺序未定义 */这种写法看起来想做什么“把当前值copy到下一个位置”但它到底是把p[0]复制到p[0]如果是先自增再取右侧还是把p[0]复制到p[1]如果是先取右侧再自增标准并没有保证。不同编译器不同优化级别下行为都可能不一样。正确的做法是拆成两步*p *p; p;或者如果你是想把当前值赋给后一个位置那就是p[1] *p;这里我强烈建议各位读者建立一条铁律一条语句里不要对同一个变量做两次未排序的修改也不要在一个副作用表达式里同时读取同一个变量的其他状态除非你有十足的把握。靠脸debug是没有前途的。7. 实战速查与记忆口诀把四兄弟彻底吃透讲到这里核心内容已经差不多全了。最后给你一套我总结的记忆方式方便你临考、刷题、coding时快速调用7.1 一眼定生死看到表达式先找“被自增的对象”面对任何复合表达式第一步不是看运算优先级表而是看“谁和谁先结合”。方法很简单如果没有括号就按照“后缀/--优先于前缀单目单目运算符从右向左结合”的规则强行绑对。绑对之后看自增符号是贴在指针变量p上、还是贴在解引用结果*p上。如果自增贴在p上那就是指针在移动如果自增贴在*p上那就是指向的值在变化。最后再看自增是前缀还是后缀来决定返回值是新值还是旧值。我把这四个表达式用一个表格做终极对比你也可以把这个表收藏起来面试前快速过一眼表达式解析结果操作对象返回的值口语化记忆*p*(p)指针pp旧位置的值先取值指针再走*p*(p)指针pp新位置的值指针先走再取值*p(*p)指向的对象自增后的新值给当前值加1结果返回新值(*p)(*p)指向的对象自增前的旧值给当前值加1但返回旧值7.2 常考易错题十连测你能答对几道下面这些题目都来自我平时带人时整理的错题集每道题目背后的考点都指向同一个优先级问题。你先自己做然后看解析。int a[] {1,2,3,4}; int *p a;问*p *p的值这题其实是未定义行为但很多考试题的作者没意识到会给出一个“预期答案”。严格来说同一表达式内对p多次后缀自增且求值顺序未排序属于未定义行为。如果非要按“从左到右”理解结果是1 2 3但我不建议在任何真实代码里这么写。接上题如果写成(*p)请问它自增的是谁通过括号先解析(* (p) )即先取p旧位置的值然后把那个值自增。整个表达式对a[0]自增返回旧值1最终a[0]变为2p也移动到a[1]。char *s abc; while (*s) ;执行完s指向哪里每次*s取一个字符直到取出末尾的\0注意\0的值是0while结束。此时s仍然会再执行一次所以s指向的是字符串末尾\0的下一个位置。很多人没注意“结束条件判断之后还多移动了一次”的细节。*p怎么解析后缀优先级最高先绑给p得p然后*和前缀同一优先级从右向左所以先*p再作用于*p的结果最终等于(*(p))。效果是取旧地址处的值并自增然后指针移动。这种题目在考试里很常见专门考你优先级顺序的叠加能力。*p 1和(*p)有区别吗若只看最终效果两者都让*p的值加1结果都是新值因为返回的是新值而(*p)返回旧值。左值要求上两者都要求*p是左值。区别在于返回值*p 1返回自增后的新值(*p)返回旧值。如果p指向const限定的变量*p合法吗不合法。“increment of read-only location” 就是典型报错。此时需要转型或另想办法但常规逻辑下不应该对const对象做修改。int *p NULL; *p;会怎样编译能过运行大概率崩溃。因为*p会对NULL地址解引用并写内存。这类题考的是“语法合法不等于运行时安全”。(*p)和(*p)在表达式中作为整个右值使用时区别是什么前者返回旧值后者返回新值。例如a (*p)得到的是加1前的值a (*p)得到的是加1后的值。p是int *p和p的区别表达式结果不同p产生旧地址p产生新地址。副作用都是一样的让p增加一个int的大小。C语言里单独写p;和p;作为语句效果完全一样没有区别。什么时候必须用括号但凡你想表达“让指针指向的对象自增”都建议用括号明确写(*p)或(*p)。同理任何“取指针指向的对象并指针自增”的歧义场景写*(p)也远比*p对新手更友好。7.3 我个人的习惯写读自己时怎么用说点私人经验。我写代码有一个不成文的习惯单独语句里能拆就拆复合表达式保留在热点循环里。比如*p这种“遍历移动”的二合一写法在字符串拷贝、循环处理数组时写起来非常舒服性能和可读性平衡得很好我用得很顺手。但一旦表达式中再叠加第二个运算符比如*p我几乎一定会拆成两行(*p); p;有人会觉得这样多写了两行不够“专业”。但我见过太多次因为一行过于聪明的代码导致的线上问题。代码首先是给人看的其次才是让机器跑的。如果你在面试的时候写*p并且加了一行注释面试官反而会觉得你理解得清楚如果你写了不拆但说不清语义那就是给自己挖坑。最后再分享一个记忆技巧你把这四个表达式想象成“取件”和“寻址”的游戏。*p是“取出当前包裹然后你移动到下一个货架”*p是“你先走到下一个货架再拿包裹”*p是“把当前包裹里的东西加一个新物件然后给你看加完后的结果”(*p)是“你先登记包裹现状然后往里面塞东西最后把登记表给你”。这个小类比帮我在最开始学指针的时候熬过了最难的一个星期希望对你也有帮助。指针这东西你躲是躲不掉的。C语言里有一半以上的坑要么和指针有关要么和“运算符优先级导致你原本想操作A结果操作了B”有关。把*和的关系在纸上推演一百遍不如亲手写一遍、编译一遍、看一遍反汇编。真正理解这四个表达式之后你再去看函数指针、二级指针、复杂声明解析会觉得思路通畅很多。那些用C语言写的文件读写操作、虚拟存储器管理实验、数据结构课设底层逻辑也就是这些基础语法的不断重复和组合。希望这篇拆解能帮你把那层窗户纸捅破。
返回列表