一维数组地址

一维数组地址一维

背景

1
2
3
假设有int a[5],请问表示数组a中首元素的地址是什么?
A: a
B: &a

答案: a

深究数组地址:

一、取地址运算符 &

1
int a;

如果 a 是一个 int 类型的变量,那么 &a 的结果类型是 **int \***(读作:指向 int 的指针)

怎么理解 & 符号?

你可以把它当成一个动作

  • a:代表“变量里的值”(在这个房间里住着谁?)。
  • &a:代表“变量的地址”(这个房间的门牌号是多少?)。

&a是一个地址,是什么类型的?

int * 而不是 int

虽然“地址”看起来像是一串数字(比如 6422300),在物理上它确实是个整数,但在 C 语言的规则里,它属于指针类型

二、怎么打印地址?

用什么符号打印?

新知识:%p

就像用 %d 来打印整数(decimal),用 %f 来打印小数(float)一样:

  • %p 是专门用来打印指针(Pointer)/地址的占位符。

它有什么特别的? 如果你用 %d 打印地址,它会显示成一个很长很乱的负数或整数。而 %p 会自动把地址显示成十六进制(比如 000000000061FE14),这是计算机里表示内存地址的标准方式。

1
2
a 的地址:   504363848
a 的地址: 0000009B1E0FFB48

怎么用printf打印格式?

printf 函数有点“洁癖”。对于 %p 这个占位符,C 语言的标准规定:你必须给我喂一个 void \* 类型的指针。

所以要使用强制类型转换

1
2
int a;
printf("a 的地址: %p\n", (void*)&a);

虽然直接传 &a(它是 int * 类型)在很多电脑上也能运行,也能打印出结果,但严格来说,类型不匹配。严谨的编译器(或者开启了严格警告模式)会报错或报警告。

所以,(void*)&a 的意思就是:

​ “编译器大哥,我知道 &a 是个 int 指针,但在打印这一刻,请把它当做一个通用指针(void*)传进去, 别报错了。”

当你想要打印任何变量的地址时,标准写法就是“三件套”:

  1. %p 做占位符。
  2. & 取出变量的地址。
  3. (void\*) 把地址转换成通用格式(为了不报错)。

三、数组中a和&a分别代表什么?

数值上,它们是同一个地址;但类型(Type)上,它们完全不同。

  • a:在大多数表达式中,它“退化”为数组首元素(第0个元素)的地址
  • &a:它是整个数组的地址
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
int a[5] = { 1, 2, 3, 4, 5 };

printf("a 的地址: %p\n", (void*)a);
printf("a 的地址: %p\n", (void*)&a);
// 你会发现上面打印的值完全一样

return 0;
}

虽然它俩打印出来的十六进制数字一模一样,但在 C 语言的法律(语法规则)里,它们的身份有着云泥之别。

想象你在网上买了一箱苹果(共 5 个),快递送到你家门口了。

  • 内存地址(那个数值):就是你家门口的地板坐标。
  • a (即 &a[0]):指的是“这箱子里的第一个苹果”
  • &a:指的是“这整个箱子”

问: 为什么坐标一样? 答: 因为箱子的起始位置,和箱子里第一个苹果的位置,物理上是在同一个点重合的!如果你把箱子拆了,第一个苹果就在箱子底部的最左边。

那首元素的地址到底是啥?

标准答案是 a (或者 &a[0])。

四、C语言数组退化(Array Decay)

我们弄清a和&a的区别后,会惊奇的发现a指的不是整个数组的地址,而是“退化”为数组首元素(第0个元素)的地址

那我们在函数调用的时候,按照惯性思维,在主函数中传入的不应该是整个数组的地址&a吗?

大家只传 a(首地址)而不传 &a(整个数组指针),是因为 C 语言在处理函数传参时,有一个非常“抠门”且“赖皮”的规则,即数组退化

怎么解释呢,我的说法是C语言在被创造出来的时候,压根就没想让函数接受一整个数组

不管你在函数定义里写得多像数组:

1
2
void work(int b[5]) { ... } // 看着像数组
void work(int b[]) { ... } // 看着也像数组

编译器在编译时,都会冷酷无情地把它们统统改成:

1
void work(int *b) { ... }   // 实质:就是一个普通的整数指针

说白了,数组退化指的就是数组退化成指针,在函数中通过地址进行对数组的操作

这意味着什么? 这意味着当你调用函数时,“箱子”的概念消失了。函数根本不知道这是一个由 5 个元素组成的整体。它只收到了一个起始坐标

打个比方,就像让工人(函数)削苹果

  • 惯性思维:把封装好的“苹果箱”递给工人。

  • C 语言的做法:你把工人带到仓库门口,指着地上的第一个苹果说:“从这开始削!”(传入 a)。

    • 如果你不告诉工人要削几个(传入长度),工人可能会顺着地址一直削下去,削完这 5 个,继续削后面的烂泥巴(内存越界),直到被保安(操作系统)抓走(程序崩溃)。

看看这个代码,更深层次地理解数组在函数中退化成指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

// 这里的 arr[] 看起来像个数组吧?
void test_size(int arr[]) {
// 试图在函数内部计算数组大小
printf("函数里的 sizeof: %zu\n", sizeof(arr));
}

int main() {
int a[5] = {1, 2, 3, 4, 5};

printf("main里的 sizeof: %zu\n", sizeof(a));

test_size(a); // 把数组传进去

return 0;
}

运行:

1
2
main里的 sizeof: 20
函数里的 sizeof: 8(缩水了!)

为什么会“缩水”?

这再次印证了我们刚才说的原理:数组退化

  1. main 函数里: 编译器非常清楚 a 是一个 int[5]。它手里握着图纸,知道这块地盘占了 20 个字节。所以 sizeof(a) 返回 20

  2. test_size 函数里: 虽然你写的是 int arr[],但编译器在编译那一瞬间,就已经把它偷梁换柱改成了 int *arr

    这时候,sizeof(arr) 测量的不再是那一整箱苹果,而是“存放地址的那张小纸条”(指针变量)的大小!

    • 在 64 位系统里,一个地址(指针)占 8 字节。
    • 在 32 位系统(如大多物联网芯片)里,一个地址(指针)占 4 字节。

五、指针运算

看看这两行代码:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main() {
int a;

printf("a + 1 的地址: %p\n", (void*)(a + 1)); // 前进 4 字节 (一个int)
printf("&a + 1 的地址: %p\n", (void*)(&a + 1)); // 前进 20 字节 (整个数组)

return 0;
}

运行得到:

1
2
a + 1 的地址:   0000009EE75EF76C
&a + 1 的地址: 0000009EE75EF77C

很明显,指针运算的+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个

一维数组地址
http://example.com/2025/12/23/一维数组地址/
作者
王柏森
发布于
2025年12月23日
许可协议