C 语言数组内存布局与访问方式
在 C 语言中,数组是最基础也最重要的数据结构之一。理解它的内存布局,对于掌握指针、优化性能以及避免内存错误至关重要。
一维数组的连续存储
数组元素在内存中是连续存放的。这意味着第一个元素位于最低地址,后续元素依次递增。每个元素占用相同大小的空间。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("数组第一个元素的地址:%p\n", &arr[0]);
printf("数组第二个元素的地址:%p\n", &arr[1]);
printf("数组第三个元素的地址:%p\n", &arr[2]);
return 0;
}
运行这段代码,你会发现地址是依次递增的。因为 int 类型通常占用 4 字节,所以相邻元素的地址差就是 4。这种连续性使得我们可以通过简单的算术运算来定位任意元素。
多维数组的内存结构
二维数组可以看作是一维数组的嵌套。虽然逻辑上是行和列,但在物理内存上,它依然是按行优先顺序连续存储的。
#include <stdio.h>
int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
printf("二维数组的地址:%p\n", arr);
printf("第一行数组的地址:%p\n", arr[0]);
printf("第二行数组的地址:%p\n", arr[1]);
return 0;
}
注意看输出结果,arr 和 arr[0] 的地址是一样的,而 arr[1] 的地址则比 arr[0] 高出了 12 字节(3 个 int)。这说明数据是紧密排列的,中间没有空隙。三维及更高维度的数组同理,都是按照最右边的维度变化最快,整体保持连续。
两种访问方式:下标与指针
访问数组元素主要有两种方式,它们在底层往往殊途同归。
下标访问
这是最直观的方式,利用索引直接定位。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
指针访问
数组名本质上是一个指向首元素的常量指针。通过指针运算,我们可以灵活地遍历数组。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
// 指针加法会自动根据类型大小偏移
printf("ptr 指向的地址:%p\n", ptr);
printf("ptr+1 指向的地址:%p\n", ptr + 1);
// 解引用获取值
printf("ptr[0] = %d\n", ptr[0]);
return 0;
}
这里有个细节要注意:指针加 1,地址增加的是 sizeof(int) 即 4 字节,而不是 1 字节。这体现了类型安全的重要性。
指针与数组的微妙关系
数组名退化为指针后,就失去了部分'数组'的特性。比如,数组名是常量指针,不能自增(arr++ 会报错),但它可以被赋值给普通指针变量。
另外,区分指针数组和数组指针也很关键:
- 指针数组:存的是指针,如
int *ptr_arr[2]。 - 数组指针:指向整个数组,如
int (*arr_ptr)[5]。
#include <stdio.h>
int main() {
int arr1[5] = {1, 2, 3, 4, 5};
int arr2[5] = {6, 7, 8, 9, 10};
// 指针数组:每个元素都是指针
int *ptr_arr[2] = {arr1, arr2};
printf("ptr_arr[0][0] = %d\n", ptr_arr[0][0]);
// 数组指针:指向一个包含 5 个 int 的数组
int (*arr_ptr)[5] = arr1;
arr_ptr++; // 移动一个整行的大小
printf("arr_ptr[0][0] = %d\n", arr_ptr[0][0]);
return 0;
}
常见陷阱与注意事项
在实际开发中,数组操作最容易出问题的地方在于边界检查和资源管理。
越界访问
C 语言不会自动检查数组边界。访问 arr[5](假设长度为 5)会导致未定义行为,可能读取到垃圾值甚至导致程序崩溃。
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
// 危险操作:下标 5 已超出范围
printf("arr[5] = %d\n", arr[5]);
return 0;
}
动态内存泄漏
使用 malloc 分配数组后,必须记得释放。否则长期运行的程序会耗尽内存。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = (int *)malloc(5 * sizeof(int));
if (arr == NULL) return 1;
arr[0] = 1;
// ... 使用数组 ...
free(arr); // 务必释放
return 0;
}
总之,掌握数组的内存模型能让我们写出更安全、高效的代码。记住:连续存储是基础,指针运算是利器,边界检查是底线。


