
第一部分一个引发的连锁反应从取址符到数组退化起因在下面的代码中我看到一个曾在scanf中曾看见过的符号但在此前并不知道它存在的含义#includestdio.hintmain(){inta10;printf(%p\n,a);return0;}一元运算符叫取址符取址符的哲学scanf 与 printf 的区别使用场景是否加 代码写法原因普通变量printf 输出地址✅printf(%p, a);需要获取变量 a 的门牌号首地址 取出变量在内存中的地址普通变量输入scanf 写入数据✅scanf(%d, a);必须给出门牌号scanf 才能把数据塞进变量 a 的内存空间漏写 会导致段错误数组名作为参数传递❌scanf(%s, str);数组名本身就是首地址已经是指针无需再取址后续会提到已分配内存的指针变量❌scanf(%d, p);指针变量 p 本身存储的就是地址直接传递即可后续会提到对数组名取地址arr✅ 特殊arr此时代表整个数组的地址arr 1会跨过整个数组的大小与arr[0]数值相同但步长不同核心本身是地址时不加本身不是地址时才加正是不经意间的查询让我第一次真正接触到「地址」与「指针」这两个核心概念也由此开启了对 C 语言底层机制的深入理解。第二部分指针与内存的底层密码核心重点怎么通过首地址确定存储的数据-通俗比喻内存就像一排快递柜地址是“门牌号”数据类型决定了“每个包裹占几个格子”。原则首地址 数据类型 确定数据。首地址告诉你从哪里开始数据类型步长告诉你读几个格子。一开始觉得它给出的答案很废话因为在数组学习的开始就讲了如何通过索引获取数组中的数据由于指针的学习位于数组之后我选择先去了解一下指针。学习之后最初的猜想得到了证实通过将数组的首地址退化为指针从而得到数组后面一系列数据。指针法如下#includestdio.hintmain(){// 定义一个整型数组内存中连续存放 5 个 intintarr[5]{10,20,30,40,50};int*parr;for(inti0;i5;i){printf(%d,*p);//先取出*p的值然后p再向前一步}return0;}第三部分C语言——数组名的“退化”1.什么是数组退化为什么退化定义在 C 语言中除了少数例外数组名会经历“数组到指针的转换”退化为指向首元素的指针。为什么退化效率至上如果传参时把整个数组复制一份开销巨大。退化成指针后传参成本永远是复制一个地址8字节这也是C语言“零开销抽象”的体现。“退化”丢掉了什么丢失了数组的“长度”信息只知道起点不知道终点。2.数组名 本身大多数情况下就代表数组的首地址两个特殊情况遇到 sizeof(arr)此时 arr 代表整个数组。常用来计算数组长度int length sizeof(arr) / sizeof(arr[0]);遇到取地址符 arr此时代表整个数组的地址。虽然 arr 和 arr[0] 打印出的数值一样但它们的类型和步长完全不同。3.退化了怎么找后面的元素数组名退化丢的仅仅是长度但留下了首地址和步长。不通过索引直接通过首地址获取数据*(arr i)。使用临时指针遍历int *p arr; p;指针加法会自动乘以 sizeof(类型) 的步长。第四部分函数传参实战1.为什么数组传参不需要 因为数组名本身就是首地址。2.为什么必须传长度函数内部数组已彻底退化为指针无法用 sizeof 求长度。经典例题纠正#includestdio.hvoidtest(int*arr,intlength){// 在函数内部sizeof(arr) 输出为 8 字节指针大小而不是整个数组printf(在函数内部 sizeof(arr) %zu 字节\n,sizeof(arr));for(inti0;ilength;i{printf(%d ,arr[i]);// 等价于 *(arr i)}}intmain(){intmyArr[5]{10,20,30,40,50};// 在 main 中数组未退化计算真实长度intlengthsizeof(myArr)/sizeof(myArr[0]);// 长度5总长度20/一个数据类型占用的字节4test(myArr,length);// 必须额外传长度return0;}正文完。额外部分为何说“for 循环 / while 循环是语法糖它们的底层本质就是 goto 语句加上条件判断。现代 CPU中央处理器在硬件层面根本不知道什么是 for、while 甚至 if。它唯一的执行逻辑就是读取指令 - 执行指令 - 默认执行下一行。如果 CPU 只会顺着往下执行那循环是怎么实现的靠的就是“跳转指令”Jump。而 goto恰恰就是 C 语言里唯一直接对应底层跳转指令的语句。我们来一步步用代码剥开这个语法糖。一、用 goto 模拟 while 循环我们平时写的 while 循环看起来像是一个“框”让程序在里面转圈。但如果把它用 goto 撕开本质非常简单。C 语言的 while 写法inti1;while(i5){printf(%d ,i);i;}完全等价的 goto 写法inti1;// 1. 条件判断标签loop_condition:if(i5){gotoloop_end;// 条件不满足跳出循环}// 2. 循环体printf(%d ,i);i;// 3. 无条件跳回条件判断处gotoloop_condition;loop_end:// 循环结束后的代码printf(\n循环结束);二、用 goto 模拟 for 循环for 循环其实只是把 初始化、条件判断、步进更新 三个部分写在一行了底层逻辑和 while 一模一样。C 语言的 for 写法for(inti1;i5;i){printf(%d ,i);}完全等价的 goto 写法inti1;// 1. 初始化只执行一次for_condition:// 2. 条件判断标签if(i5){gotofor_end;}printf(%d ,i);// 3. 循环体i;// 4. 步进更新gotofor_condition;// 跳回条件判断for_end:三、编译器在背后做了什么当你按下“编译”按钮时Visual Studio 或者 GCC 编译器做的事情就是把你写的 while 和 for 翻译成上面那种 goto 逻辑然后再把 goto 翻译成 CPU 能看懂的汇编跳转指令。在汇编层面goto 对应的是 jmp无条件跳转、je如果相等就跳转、jne如果不相等就跳转。机器眼里只有“跳”和“不跳”根本没有“循环”四、所以for和while本质是什么 他们是一种受限制的、结构化的goto“我们以为for和while是编程语言天生就有的魔法但扒开底层它们不过是编译器的优雅封装。goto 像是一把锋利的开山刀威力极大但容易伤到手而 for 和 while 则是带着安全护手的精钢厨刀虽然限制了你砍伐的方式却能让你安全、高效地完成99% 的切菜工作日常开发。懂得看透for/while 底层的goto 模型不仅能让你彻底理解循环的运行机制更能让你深刻体会到计算机科学中‘结构化编程’的伟大智慧。”