【C语言】一维数组地址
一维数组地址一维
背景
1 | 假设有int a[5],请问表示数组a中首元素的地址是什么? |
答案: a
深究数组地址:
一、取地址运算符 &
1 | int a; |
如果 a 是一个 int 类型的变量,那么 &a 的结果类型是 int \*(读作:指向 int 的指针)
怎么理解 & 符号?
你可以把它当成一个动作 。
a:代表“** 变量里的值**”(在这个房间里住着谁?)。&a:代表“** 变量的地址**”(这个房间的门牌号是多少?)。
&a是一个地址,是什么类型的?
是 int * 而不是 int
虽然“地址”看起来像是一串数字(比如 6422300),在物理上它确实是个整数,但在 C 语言的规则里,它属于指针类型 。
二、怎么打印地址?
用什么符号打印?
新知识: %p
就像用 %d 来打印整数(d ecimal),用 %f 来打印小数(f loat)一样:
%p是专门用来打印指针(Pointer)/地址 的占位符。
它有什么特别的? 如果你用 %d 打印地址,它会显示成一个很长很乱的负数或整数。而 %p 会自动把地址显示成十六进制 (比如 000000000061FE14),这是计算机里表示内存地址的标准方式。
1 | a 的地址: 504363848 |
怎么用printf打印格式?
printf 函数有点“洁癖”。对于 %p 这个占位符,C 语言的标准规定:你必须给我喂一个void \* 类型的指针。
所以要使用强制类型转换
1 | int a; |
虽然直接传 &a(它是 int * 类型)在很多电脑上也能运行,也能打印出结果,但严格来说,类型不匹配 。严谨的编译器(或者开启了严格警告模式)会报错或报警告。
所以,(void*)&a 的意思就是:
“编译器大哥,我知道 &a 是个 int 指针,但在打印这一刻,请把它当做 一个通用指针(void*)传进去, 别报错了。”
当你想要打印任何变量的地址时,标准写法就是“三件套 ”:
- 用
%p做占位符。 - 用
&取出变量的地址。 - 用
(void\*)把地址转换成通用格式(为了不报错)。
三、数组中a和&a分别代表什么?
数值上,它们是同一个地址;但类型(Type)上,它们完全不同。
a:在大多数表达式中,它“退化”为** 数组首元素(第0个元素)的地址**。&a:它是** 整个数组的地址**。
1 |
|
虽然它俩打印出来的十六进制数字一模一样,但在 C 语言的法律(语法规则)里,它们的身份有着云泥之别。
想象你在网上买了一箱苹果(共 5 个),快递送到你家门口了。
- 内存地址(那个数值) :就是你家门口的地板坐标。
a(即&a[0]):指的是** “这箱子里的第一个苹果”**。&a:指的是** “这整个箱子”**。
问: 为什么坐标一样? 答: 因为箱子的起始位置 ,和箱子里第一个苹果的位置 ,物理上是在同一个点重合的!如果你把箱子拆了,第一个苹果就在箱子底部的最左边。
那首元素的地址到底是啥?
标准答案是 a (或者 &a[0])。
四、C语言数组退化**(Array Decay)**
我们弄清a和&a的区别后,会惊奇的发现a指的不是 整个数组的地址,而是“退化”为数组首元素(第0个元素)的地址 。
那我们在函数调用的时候,按照惯性思维,在主函数中传入的不应该是整个数组的地址&a吗?
大家只传 a(首地址)而不传 &a(整个数组指针),是因为 C 语言在处理函数传参时,有一个非常“抠门”且“赖皮”的规则,即数组退化
怎么解释呢,我的说法是C语言在被创造出来的时候,压根就没想让函数接受一整个数组
不管你在函数定义里写得多像数组:
1 | void work(int b[5]) { ... } // 看着像数组 |
编译器在编译时,都会冷酷无情地把它们统统改成:
1 | void work(int *b) { ... } // 实质:就是一个普通的整数指针 |
说白了,数组退化指的就是数组退化成指针 ,在函数中通过地址进行对数组的操作
这意味着什么? 这意味着当你调用函数时,“箱子”的概念消失了 。函数根本不知道这是一个由 5 个元素组成的整体。它只收到了一个起始坐标 。
打个比方,就像让工人(函数)削苹果
- 惯性思维:把封装好的“苹果箱”递给工人。
- C 语言的做法 :你把工人带到仓库门口,指着地上的第一个苹果 说:“从这开始削!”(传入
a)。- 如果你不告诉工人要削几个(传入长度),工人可能会顺着地址一直削下去,削完这 5 个,继续削后面的烂泥巴(内存越界),直到被保安(操作系统)抓走(程序崩溃)。
看看这个代码,更深层次地理解数组在函数中退化成指针:
1 |
|
运行:
1 | main里的 sizeof: 20 |
为什么会“缩水”?
这再次印证了我们刚才说的原理:数组退化 。
- 在
main函数里: 编译器非常清楚a是一个int[5]。它手里握着图纸,知道这块地盘占了 20 个字节。所以sizeof(a)返回 20 。 - 在
test_size函数里: 虽然你写的是int arr[],但编译器在编译那一瞬间,就已经把它偷梁换柱改成了int *arr。
这时候,sizeof(arr)测量的不再是那一整箱苹果 ,而是**“存放地址的那张小纸条”(指针变量)** 的大小!- 在 64 位系统里,一个地址(指针)占 8 字节。
- 在 32 位系统(如大多物联网芯片)里,一个地址(指针)占 4 字节。
五、指针运算
看看这两行代码:
1 |
|
运行得到:
1 | int a; |
0
很明显,指针运算的+1并不是数学里,1 + 1 = 2
在 C 语言的指针世界里,1 + 1 的结果取决于你是谁 。
指针的“一步”有多大?
在指针的世界里,+1 不代表“加 1 个字节”,而是代表**“加 1 个单位(Item)”** 。或者更通俗地说:“下一个” 。
这就好比:
- 对于蚂蚁(char)来说 :走一步(+1)就是挪动 1 厘米。
- 对于人类(int)来说 :走一步(+1)就是挪动 4 厘米。
- 对于巨人(array)来说 :走一步(+1)就是挪动 20 厘米。
计算机怎么知道“步长”是多少? 就是靠类型 !
a的类型是int *(指向一个整数)。电脑知道一个整数占 4 字节。所以a+1意味着:地址往后跳 4 个字节 。&a的类型是int (*)[5](指向一个包含5个整数的数组)。电脑知道这个大数组总共占5 * 4 = 20字节。所以&a+1意味着:地址往后跳 20 个字节 。
如果把数组改成 char a[5](char占1个字节),a+1和&a+1` 分别会增加多少字节?
- 答:1个和5个




