第一部分:一个&引发的连锁反应:从取址符到数组退化
起因:在下面的代码中我看到一个曾在scanf中曾看见过的符号&,但在此前并不知道它存在的含义
#include<stdio.h>intmain(){inta=10;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 语言底层机制的深入理解。
第二部分:指针与内存的底层密码(核心重点)
怎么通过首地址确定存储的数据?-
通俗比喻:内存就像一排快递柜,地址是“门牌号”,数据类型决定了“每个包裹占几个格子”。
原则:首地址 + 数据类型 = 确定数据。首地址告诉你从哪里开始,数据类型(步长)告诉你读几个格子。
一开始觉得它给出的答案很废话,因为在数组学习的开始就讲了如何通过索引获取数组中的数据,由于指针的学习位于数组之后,我选择先去了解一下指针。学习之后最初的猜想得到了证实,通过将数组的首地址退化为指针,从而得到数组后面一系列数据。
指针法如下:
#include<stdio.h>intmain(){// 定义一个整型数组,内存中连续存放 5 个 intintarr[5]={10,20,30,40,50};int*p=arr;for(inti=0;i<5;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 求长度。
经典例题纠正:
#include<stdio.h>voidtest(int*arr,intlength){// 在函数内部,sizeof(arr) 输出为 8 字节(指针大小),而不是整个数组printf("在函数内部 sizeof(arr) = %zu 字节\n",sizeof(arr));for(inti=0;i<length;i++){printf("%d ",arr[i]);// 等价于 *(arr + i)}}intmain(){intmyArr[5]={10,20,30,40,50};// 在 main 中数组未退化,计算真实长度intlength=sizeof(myArr)/sizeof(myArr[0]);// 长度(5)=总长度(20)/一个数据类型占用的字节(4)test(myArr,length);// 必须额外传长度return0;}正文完。
额外部分:为何说“for 循环 / while 循环是语法糖,它们的底层本质就是 goto 语句加上条件判断。
现代 CPU(中央处理器)在硬件层面根本不知道什么是 for、while 甚至 if。它唯一的执行逻辑就是:读取指令 -> 执行指令 -> 默认执行下一行。
如果 CPU 只会顺着往下执行,那循环是怎么实现的?靠的就是“跳转指令”(Jump)。而 goto,恰恰就是 C 语言里唯一直接对应底层跳转指令的语句。
我们来一步步用代码剥开这个语法糖。
一、用 goto 模拟 while 循环
我们平时写的 while 循环,看起来像是一个“框”,让程序在里面转圈。但如果把它用 goto 撕开,本质非常简单。
C 语言的 while 写法:
inti=1;while(i<=5){printf("%d ",i);i++;}完全等价的 goto 写法:
inti=1;// 1. 条件判断标签loop_condition:if(i>5){gotoloop_end;// 条件不满足,跳出循环}// 2. 循环体printf("%d ",i);i++;// 3. 无条件跳回条件判断处gotoloop_condition;loop_end:// 循环结束后的代码printf("\n循环结束");二、用 goto 模拟 for 循环
for 循环其实只是把 初始化、条件判断、步进更新 三个部分写在一行了,底层逻辑和 while 一模一样。
C 语言的 for 写法:
for(inti=1;i<=5;i++){printf("%d ",i);}完全等价的 goto 写法:
inti=1;// 1. 初始化(只执行一次)for_condition:// 2. 条件判断标签if(i>5){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 模型,不仅能让你彻底理解循环的运行机制,更能让你深刻体会到计算机科学中‘结构化编程’的伟大智慧。”