数组是C语言中最基础且核心的数据结构,本质是相同类型元素的连续集合,广泛用于存储批量数据(如成绩、坐标、字符串等)。
一、数组的核心概念
1.1 数组的定义
- 定义:数组是一组具有相同数据类型的元素,在内存中连续存储的集合。
- 核心特性:
- 元素类型统一:不能在一个int数组中存储char或float类型数据;
- 长度固定:创建时必须指定大小(常量值),后续无法动态扩容(C99出现的VLA边长数组针对此做出了便利性更新);
- 连续存储:元素在内存中紧密排列,无间隙,便于快速访问。
- 分类:按维度分为一维数组(最常用)、二维数组(矩阵形式)、多维数组(极少使用)。
1.2 为什么用数组?
- 替代多个独立变量:存储100个学生成绩,用
int scores[100]比100个独立变量简洁; - 支持随机访问:通过下标直接定位元素(时间复杂度O(1));
- 便于批量处理:结合循环可快速遍历、修改所有元素。
二、一维数组
一维数组是最常用的形式,核心围绕“创建→初始化→访问→遍历”展开,同时需注意下标、内存等关键细节。
2.1 一维数组的创建
语法格式:type arr_name[常量值];
type:元素数据类型(int、char、float等);arr_name:数组名(符合标识符规则);常量值:数组长度(必须是编译时确定的常量,不能是变量)。
示例:
int arr1[5]; // 定义int型数组,长度5,元素默认随机值
char arr2[10]; // 定义char型数组,长度10
float arr3[3]; // 定义float型数组,长度3
// int arr4[n]; // 错误:n是变量,C语言不支持动态长度数组(C99变长数组除外)
2.2 一维数组的初始化
初始化是创建数组时赋予初始值,避免默认随机值,有多种灵活方式:
// 1. 完全初始化:所有元素显式赋值
int arr1[5] = {1, 2, 3, 4, 5};
// 2. 不完全初始化:未赋值元素默认置0
int arr2[5] = {1, 2}; // 等价于 {1,2,0,0,0}
// 3. 省略长度:编译器根据初始化元素个数自动推断长度
int arr3[] = {1, 2, 3}; // 长度为3
// 4. 指定位置初始化(C99及以上):仅给特定下标赋值,其余为0
int arr4[5] = {[0]=10, [3]=20}; // arr4[0]=10, arr4[3]=20,其余为0
// 错误示例:初始化元素个数不可以超过数组长度
int arr5[3] = {1,2,3,4};
2.3 一维数组的访问与遍历
- 访问方式:通过下标引用运算符[] 访问,下标从0开始(第一个元素下标0,最后一个元素下标
长度-1); - 遍历方式:结合for循环生成下标,批量访问所有元素
示例:
int arr[] = {10, 20, 30, 40, 50};
int len = sizeof(arr) / sizeof(arr[0]); // 通用计算数组长度:总大小/单个元素大小
// 1. 直接访问指定元素
printf("arr[0] = %d\n", arr[0]); // 输出:10(第一个元素)
printf("arr[3] = %d\n", arr[3]); // 输出:40(第四个元素)
// 2. 遍历数组(正序)
printf("正序遍历:");
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]); // 输出:10 20 30 40 50
}
printf("\n");
// 3. 遍历数组(逆序)
printf("逆序遍历:");
for (int i = len - 1; i >= 0; i--) {
printf("%d ", arr[i]); // 输出:50 40 30 20 10
}
printf("\n");
2.5 内存布局:连续存储
一维数组的元素在内存中连续排列,每个元素占用的字节数等于其数据类型大小(如int占4字节),下标递增时地址依次递增。
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3};
for (int i = 0; i < 3; i++) {
printf("arr[%d] 的地址:%p\n", i, &arr[i]);
}
// 输出示例(地址值因系统而异,但相邻元素差4字节):
// arr[0] 的地址:006FFD7C
// arr[1] 的地址:006FFD80(+4字节)
// arr[2] 的地址:006FFD84(+4字节)
return 0;
}
2.6 数组名的本质:arr、&arr[0]、&arr的区别
数组名arr的行为特殊,不同场景下含义不同,核心区别如下:
| 表达式 | 地址值 | 类型 | sizeof结果(64位系统) | +1运算效果 |
|---|---|---|---|---|
arr | 首元素地址 | int[3](sizeof中)/int*(其他场景) | 12(数组总大小) | 移动4字节(一个int) |
&arr[0] | 首元素地址 | int*(指向首元素) | 8(指针大小) | 移动4字节(一个int) |
&arr | 首元素地址 | int(*)[3](数组指针) | 8(指针大小) | 移动12字节(整个数组) |
示例验证:
#include <stdio.h>
int main() {
int arr[3] = {1,2,3};
printf("arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr = %p\n", &arr);
printf("\n");
printf("arr + 1 = %p\n", arr + 1);
printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
printf("&arr + 1 = %p\n", &arr + 1);
return 0;
}

结论:
arr在大多数场景下退化为指向首元素的指针(int*),但在sizeof(arr)中保持数组类型,返回总大小;&arr[0]是明确的首元素指针(int*),与退化后的arr功能一致;&arr是数组指针,指向整个数组,+1运算移动整个数组的长度。
三、二维数组
二维数组可理解为每个元素是一个一维数组的一维数组,常用于存储矩阵、表格等二维数据。
4.1 二维数组的创建与初始化
(1)创建语法
type arr_name[行数][列数];
- 行数:一维数组个数;
- 列数:每个一维数组的元素个数;
- 行数可省略(编译器自动推断),但列数必须指定,列数是二维数组类型的一部分
(2)初始化
// 1. 完全初始化(按行排列)
int arr1[2][3] = {1,2,3,4,5,6}; //等价于 {{1,2,3},{4,5,6}}
// 2. 按行显式初始化(推荐,可读性高)
int arr2[2][3] = {{1,2}, {4,5}}; // 等价于 {{1,2,0},{4,5,0}}
// 3. 省略行数(编译器根据列数和元素个数推断)
//连续初始化:行数=总元素数量/列数1,向上取整
int arr3[][4]={1};// 等价于 {1,0,0,0}
//嵌套初始化:行数=内层嵌套{}数量
int arr4[][3]={{1},{2},{3}}; // 等价于 {{1,0,0},{2,0,0},{3,0,0}}
int arr3[][3] = {1,2,3,4,5}; // 推断为2行3列(5个元素,需2行容纳)
// 错误示例:省略列数,二维数组初始化中,列数不可省略
// int arr4[2][] = {1,2,3,4};
注意事项:赋值时区分 () 和 {},()会执行逗号表达式取尾值,{}则会确定当前一维数组内部情况;
4.2 二维数组的访问与遍历
- 访问方式:
arr[行下标][列下标],行、列下标均从0开始; - 遍历方式:嵌套for循环(外层遍历行,内层遍历列),优先按行遍历(利用内存局部性,效率更高)。
示例:
#include <stdio.h>
int main() {
int arr[2][3] = {{1,2,3}, {4,5,6}};
int rows = sizeof(arr) / sizeof(arr[0]); // 计算行数:总大小/一行的大小
int cols = sizeof(arr[0]) / sizeof(arr[0][0]); // 计算列数:一行大小/一个元素大小
// 1. 直接访问指定元素
printf("arr[1][2] = %d\n", arr[1][2]); // 输出:6(第2行第3列)
// 2. 按行遍历(高效)
printf("按行遍历:");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", arr[i][j]); // 输出:1 2 3 4 5 6
}
}
printf("\n");
// 3. 按列遍历
printf("按列遍历:");
for (int j = 0; j < cols; j++) {
for (int i = 0; i < rows; i++) {
printf("%d ", arr[i][j]); // 输出:1 4 2 5 3 6
}
}
printf("\n");
/*行的遍历是快于列的遍历,这一点将在4.3提及*/
// 4. 按地址遍历
printf("按列遍历:");
int total = sizeof(arr) / sizeof(arr[0][0]);
int* p = &arr[0][0];
for (int i = 0; i < total; i++) {
printf("%d ", p[i]); // 使用数组索引,避免p++操作
}
printf("\n");
4.3 二维数组的内存布局
二维数组的元素在内存中依然是连续存储的,按“行优先”顺序排列(先存第0行所有元素,再存第1行,以此类推)。
示例验证:
#include <stdio.h>
int main() {
int arr[2][3] = {{1,2,3}, {4,5,6}};
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("arr[%d][%d] 的地址:%p\n", i, j, &arr[i][j]);
}
}
// 输出示例(相邻元素差4字节,跨行元素也连续):
// arr[0][2] 的地址:006FFD68
// arr[1][0] 的地址:006FFD6C(+4字节,连续存储)
return 0;
}
4.3.plus:硬件层面上的读取
地址: 100 104 108 112 116 120 124 128
数据: 1 2 3 4 5 6 7 8
1.按行遍历(快)
for(i=0;i<2;i++)
for(j=0;j<4;j++)
printf("%d", arr[i][j]); // 1,2,3,4,5,6,7,8
访问顺序:100→104→108→112→116→120→124→128
特点:连续访问,缓存友好
2.按列遍历(慢)
for(j=0;j<4;j++)
for(i=0;i<2;i++)
printf("%d", arr[i][j]); // 1,5,2,6,3,7,4,8
访问顺序:100→116→104→120→108→124→112→128
特点:跳跃访问,缓存不友好
CPU加载数据的局部性原理:会将内存邻近的存储数据也加载到CPU缓存中
- 时间局部性:若一个位置被访问,则可能短时间内再次被访问
- 空间局部性:若一个位置被访问,则可能短时间内邻近位置被访问
由于二维数组数据存储的按行连续性,行遍历时,局部性原理使预读的CPU缓存容易命中,故更为高效
按行:访问arr[0][0]时,CPU把1,2,3,4,5,6,…都加载到缓存,后续访问都在缓存中
按列:访问arr[0][0]加载1-16到缓存,但接着访问arr[1][0](第5个元素),然后访问arr[0][1]时可能已不在缓存中,需要重新加载
4.4arr、&arr[0]、&arr[0][0]的区别与联系
#include <stdio.h>
int main()
{
// 定义一个3行4列的二维数组
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
printf("============== 二维数组内存布局分析 ==============\n\n");
// 1. 打印数组内容
printf("1. 数组内容:\n");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("arr[%d][%d] = %-2d ", i, j, arr[i][j]);
}
printf("\n");
}
printf("\n============== 地址对比分析 ==============\n\n");
// 2. 打印各种地址
printf("2. 地址值(十六进制):\n");
printf("arr = %p\n", (void*)arr);
printf("&arr[0] = %p\n", (void*)&arr[0]);
printf("&arr[0][0] = %p\n", (void*)&arr[0][0]);
printf("\n3. 地址值的数值分析:\n");
printf("arr = %lu\n", (unsigned long)arr);
printf("&arr[0] = %lu\n", (unsigned long)&arr[0]);
printf("&arr[0][0] = %lu\n", (unsigned long)&arr[0][0]);
printf("\n============== 类型分析 ==============\n\n");
// 3. 类型分析
printf("4. 类型分析:\n");
printf("sizeof(arr) = %lu bytes\n", sizeof(arr));
printf("sizeof(&arr[0]) = %lu bytes(指针大小)\n", sizeof(&arr[0]));
printf("sizeof(&arr[0][0]) = %lu bytes(指针大小)\n", sizeof(&arr[0][0]));
printf("\n5. 指针类型详细分析:\n");
printf("arr 的类型是:int (*)[4](指向含4个int的数组的指针)\n");
printf("&arr[0] 的类型是:int (*)[4](同上)\n");
printf("&arr[0][0] 的类型是:int * (指向int的指针)\n");
printf("\n============== 指针运算演示 ==============\n\n");
// 4. 指针运算演示
printf("6. 指针运算(+1操作):\n");
printf("arr + 1 = %p (移动了 %lu bytes)\n",
(void*)(arr + 1), (unsigned long)(arr + 1) - (unsigned long)arr);
printf("&arr[0] + 1 = %p (移动了 %lu bytes)\n",
(void*)(&arr[0] + 1), (unsigned long)(&arr[0] + 1) - (unsigned long)&arr[0]);
printf("&arr[0][0] + 1 = %p (移动了 %lu bytes)\n",
(void*)(&arr[0][0] + 1), (unsigned long)(&arr[0][0] + 1) - (unsigned long)&arr[0][0]);
printf("\n7. 字节计算:\n");
printf("一个int大小:%lu bytes\n", sizeof(int));
printf("一行大小(4个int):%lu × %lu = %lu bytes\n",
sizeof(int), 4ul, sizeof(int) * 4);
printf("整个数组大小:3 × 4 × %lu = %lu bytes\n",
sizeof(int), sizeof(arr));
printf("\n============== 访问方式演示 ==============\n\n");
// 5. 不同访问方式
printf("8. 访问arr[1][2]元素(值为7):\n");
printf("arr[1][2] = %d\n", arr[1][2]);
printf("*(*(arr + 1) + 2) = %d\n", *(*(arr + 1) + 2));
printf("*(arr[1] + 2) = %d\n", *(arr[1] + 2));
printf("*(&arr[0][0] + 1*4 + 2) = %d\n", *(&arr[0][0] + 1 * 4 + 2));
printf("\n9. 不同访问方式解析:\n");
printf("arr + 1 指向第1行(跳过一行)\n");
printf("*(arr + 1) 指向第1行第0列元素\n");
printf("*(arr + 1) + 2 指向第1行第2列元素\n");
printf("\n============== 内存连续性验证 ==============\n\n");
// 6. 内存连续性验证
printf("10. 所有元素的内存地址:\n");
printf("行 列 地址 偏移量\n");
printf("--------------------------------\n");
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("[%d][%d] %p +%ld\n",
i, j,
(void*)&arr[i][j],
(long)(&arr[i][j] - &arr[0][0]) * sizeof(int));
}
}
printf("\n11. 通过指针遍历所有元素:\n");
int* ptr = &arr[0][0];
printf("使用 &arr[0][0] 指针:");
for (int i = 0; i < 12; i++) {
printf("%d ", *(ptr + i));
}
printf("\n\n============== 关键总结 ==============\n\n");
printf("重要结论:\n");
printf("1. arr 和 &arr[0] 值相同,但 arr 是数组名(特殊)\n");
printf("2. arr 和 &arr[0] 类型相同(int (*)[4])\n");
printf("3. &arr[0][0] 是 int* 类型,指向单个元素\n");
printf("4. arr + 1 移动一行大小(4个int = %lu字节)\n", sizeof(int) * 4);
printf("5. &arr[0][0] + 1 移动一个int大小(%lu字节)\n", sizeof(int));
printf("6. 二维数组在内存中是连续存储的\n");
printf("\n============== 验证 &arr 的区别 ==============\n\n");
// 额外:&arr 的演示
printf("12. &arr 的特殊性:\n");
printf("&arr = %p\n", (void*)&arr);
printf("&arr 的类型是:int (*)[3][4](指向整个二维数组的指针)\n");
printf("&arr + 1 = %p (移动了整个数组大小:%lu bytes)\n",
(void*)(&arr + 1), sizeof(arr));
return 0;
}

五、数组常见问题
1. 下标越界
- 表现:程序崩溃、数据错乱、无报错但结果异常;
- 避免:用
sizeof计算数组长度,遍历下标严格控制在0~len-1。
2. 数组名误用
- 误区:将数组名当作指针修改(如
arr++); - 原因:数组名是“常量指针”,不能被赋值或自增/自减。
3. 数组作为函数参数退化
- 现象:数组作为函数参数时,会退化为指针,
sizeof(arr)返回指针大小(8字节,64位系统); - 解决:手动传递数组长度作为参数,而非在函数内计算。
示例:
```c
// 错误:函数内sizeof(arr)返回指针大小,无法得到数组长度
void test(int arr[]) {
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出:8(指针大小)
}
// 正确:传递长度参数
void test(int arr[], int len) {
// 遍历数组
}
4. 二维数组列数不可省略
- 误区:定义二维数组时省略列数(如
int arr[2][]); - 原因:编译器需要通过列数计算每行的元素个数,进而推断内存布局。
6703

被折叠的 条评论
为什么被折叠?



