C 语言(一)|数组:从一维到二维、存储与越界
详细介绍C语言数组的基础概念、一维数组、二维数组的定义、初始化、访问和内存布局。
版权声明:本文为作者原创,版权归作者所有。如需转载或引用,请联系作者获得授权,并在使用时注明出处。
大家好呀,好久不见~转眼假期已经到啦,大一上半程的校园生活伴着期末复习备考的忙碌告一段落,我也总算对大学的学习节奏慢慢熟悉了。这段时间跟着课程啃完了 C 语言指针的内容,也刷了不少对应的练习题,虽说能跟着做题目,但总觉得学编程不能一直往前赶,适时停下来回顾总结,把基础打牢才更重要。思来想去,就从数组这个 C 语言的核心基础开始,重新梳理一遍之前的知识点,再走一趟 C 语言的学习旅程~
一、C语言数组的基础认知
1.1 什么是数组(核心定义)
在 C 语言里,数组是一组相同数据类型元素的连续存储集合,核心作用是批量管理同类数据,比如用一个数组存 10 个学生的成绩,比定义 10 个单独变量更简洁。
1.2 核心特点
- 类型严格统一:数组里所有元素必须是同一种 C 语言基础类型(如 int、char、float),不能混存(比如不能在 int 数组里放字符串)
- 内存连续:数组在内存中是"挨着放"的
- 索引从 0 开始:访问数组元素靠"索引(下标)",第一个元素是 arr[0],最后一个是 arr[长度-1],越界访问(如 arr[10])会导致程序出错
1.3 数组的使用价值(解决什么问题)
批量处理同类型数据,替代大量单独定义的变量,让代码更简洁、易维护,是处理表格、成绩、序列等批量数据的核心工具。
二、一维数组(核心重点,C语言数组基础)
2.1 一维数组的定义与初始化
2.1.1 标准定义格式
type arr_name[常量值];
type是数组中存放数据的类型(如 int、char、float 等),也可以是自定义类型;arr_name是数组的名字;[]内是数组的大小,代表元素个数。
2.1.2 3种常见初始化方式
// 1. 完全初始化:给所有元素赋值
int num1[3] = {1, 2, 3}; // 数组num1:[1, 2, 3]
// 2. 部分初始化:未赋值元素默认是0
int num2[3] = {1}; // 数组num2:[1, 0, 0]
// 3. 省略长度:编译器按初始化元素个数自动算长度(此时长度是2)
int num3[] = {5, 6}; // 数组num3:[5, 6]
2.2 一维数组的访问与遍历
2.2.1 元素访问方式
1. 基础方式:索引(下标)访问(最常用)
C 语言数组通过"索引(下标)"定位元素,索引从 0 开始(对应数组第一个元素),最大值为"数组长度-1"。
2. 进阶方式:指针偏移访问
C 语言中,数组名本质是"指向数组第一个元素的常量指针"(即数组名 == &数组[0])。
#include<stdio.h>
int main()
{
int num1[3] = { 1, 2, 3 };
printf("%d", num1[0]); // 输出:1
return 0;
}
2.2.2 2种遍历方法(for循环 + sizeof通用求长)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int num1[3] = { 0 };
int sum = sizeof(num1) / sizeof(num1[0]);
printf("请输入元素的值:");
for(int i = 0; i < sum; i++) // 数组元素的输入
{
scanf("%d", &num1[i]);
}
printf("数组元素的打印\n");
for (int i = 0; i < sum; i++) // 数组元素的打印
{
printf("%d ", num1[i]);
}
return 0;
}
2.2.3 数组越界问题(核心风险)
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 10; // 越界!合法下标仅0-4
for(int i=0; i<=5; i++){} // 越界!条件应写i<5
2.3 一维数组的内存布局
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int num1[3] = { 1, 2, 3 };
for (int i = 0; i < 3; i++)
{
printf("%d ", num1[i]);
printf("&arr[%d] = %p\n", i, &num1[i]);
}
return 0;
}
运行结果:
1 &arr[0] = 000000A0F54FF828
2 &arr[1] = 000000A0F54FF82C
3 &arr[2] = 000000A0F54FF830
内存存储特征:
- 数组元素在内存中连续不间断存储,同类型元素占字节数一致,地址随下标依次递增(递增步长 = 单个元素字节数)
- 数组名是数组首元素的地址常量,不可修改(如
arr++属于语法错误) sizeof(数组名)计算的是整个数组的总字节数
下标访问的底层逻辑:
- 数组下标访问
arr[i]的本质是指针偏移解引用,等价于*(arr + i) - 以首元素地址为基准,向后偏移 i 个元素的字节数,再取对应内存的值
三、二维数组(一维数组的延伸,多维数组基础)
3.1 二维数组的核心理解(本质是"一维数组的数组")
二维数组可以看作是由多个相同长度的一维数组组成的数组,C 语言中按"行优先"规则存储,本质上还是一维数组的嵌套,用于处理表格、矩阵等二维结构数据。
3.2 二维数组的定义与初始化
3.2.1 标准定义格式
type arr_name[常量值1][常量值2];
int num1[3][6]; // 3行6列的二维数组
double num2[3][6];
解释:
- 3 表示数组有 3 行
- 6 表示每一行有 6 个元素
3.2.2 4种灵活初始化方式
1. 按行完全初始化(最清晰,推荐)
把每一行的元素用 {} 包裹,结构一目了然。
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
2. 按行部分初始化
未初始化的元素会自动补 0。
int arr[2][3] = {{1}, {4, 5}};
// 结果:arr[0] = {1, 0, 0},arr[1] = {4, 5, 0}
3. 省略行长度初始化
可以省略行数,编译器会根据初始化的元素个数自动计算行数,但列长度不能省略。
int arr[][3] = {1, 2, 3, 4, 5, 6};
// 编译器自动推断行数为 2,等价于 int arr[2][3]
4. 紧凑初始化
不分行包裹,按"行优先"顺序依次填充元素,可读性稍差。
int arr[2][3] = {1, 2, 3, 4, 5, 6};
// 效果与方式 1 完全一致
3.2.3 关键注意事项
- 列长度不可省略:无论是否省略行数,二维数组的列长度必须显式指定,否则编译器无法确定每行的元素个数
- 默认补 0 规则:如果初始化元素不足,未赋值的元素会被默认初始化为 0
- 全局 vs 局部数组:全局/静态二维数组默认所有元素为 0;局部二维数组未初始化时,元素为随机值
3.3 二维数组的访问与遍历
3.3.1 元素访问方式
索引(下标)访问:这是最常用的访问方式,行、列索引均从 0 开始,最大值为"对应维度长度-1"。
进阶方式:指针偏移访问:二维数组名本质是"指向第一行一维数组的常量指针",即 数组名 == &数组[0]。
3.3.2 双层 for 循环遍历
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int num2[2][3] = {0};
for (int i = 0; i < 2; i++) // 二维数组的输入
{
for (int j = 0; j < 3; j++)
{
scanf("%d", &num2[i][j]);
}
}
printf("二维数组的输出如下\n");
for (int i = 0; i < 2; i++) // 二维数组的输出
{
for (int j = 0; j < 3; j++)
{
printf("%d ", num2[i][j]);
}
printf("\n");
}
return 0;
}
3.4 二维数组的内存布局
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int num2[2][3] = { 0 };
int num = 1;
for (int i = 0; i < 2; i++) // 遍历赋值并打印地址
{
for (int j = 0; j < 3; j++)
{
printf("第%d个元素的地址为:%p \n", num, &num2[i][j]);
num++;
}
}
return 0;
}
物理存储核心特征(打破"二维"误区):
- 我们主观认为的存储方式:按行、列的二维表格形式分散存储
- 实际的存储方式:所有元素在内存中连续不间断存储,按"行优先"顺序依次排列,无行、列的物理分隔
四、C99中的变长数组
4.1 核心定义
变长数组(Variable Length Array,简称 VLA)是 C99 标准新增的特性,允许用变量指定数组的长度,数组长度可在运行时根据变量值动态确定,打破了 C99 前数组长度必须是「编译期常量」的限制。
4.2 示例
#include <stdio.h>
int main() {
int n;
printf("请输入数组长度:");
scanf("%d", &n);
int arr[n]; // 用变量n定义数组长度,这就是变长数组
// 遍历赋值+打印
for(int i=0; i<n; i++){
arr[i] = i+1;
printf("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
4.3 注意事项(避坑重点)
- 编译器支持问题:GCC、Clang 完全支持 VLA,但微软 MSVC(Visual Studio 默认编译器)不支持 C99 VLA
- 不可初始化:定义变长数组时不能直接初始化(
int arr[n] = {1, 2, 3};语法错误) - 栈溢出风险:栈空间较小,若变量值过大(如
int arr[1000000];),会直接导致栈溢出,程序崩溃 - sizeof 适配 VLA:
sizeof(变长数组)在运行时计算(普通数组 sizeof 在编译期计算),可正常获取 VLA 的实际总字节数
五、C语言数组高频坑点与避坑技巧
| 坑点 | 说明 | 避坑技巧 |
|---|---|---|
| 下标从 1 开始 | 新手最易犯,牢记数组下标从 0 开始 | 最后一个元素下标为「长度 - 1」 |
| 数组越界 | C 语言不检查,后果严重 | 拒绝硬编码长度,用 sizeof 通用求长公式,循环条件严格写「i < 长度」 |
| 定义数组时长度用变量 | C99 前编译器不支持 | 如需动态长度可使用堆内存分配或 C99 变长数组 |
| 未初始化局部数组直接使用 | 局部数组未初始化元素为随机值 | 使用前必须手动初始化或赋值 |
| 字符数组初始化的 '\0' 遗漏 | 字符数组作为字符串使用时,末尾必须有 '\0' | 确保字符串末尾有终止符 |
| 二维数组初始化省略列长度 | 语法错误 | 二维数组列长度不可省略 |
六、二分查找练习
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int arr[] = { 1, 2, 3, 5, 6, 7, 8, 9, 11, 13 };
int right = sizeof(arr) / sizeof(arr[0]) - 1; // 右下标
int left = 0; // 左下标
int m = 0; // 中间下标
int k = 7; // 目标值
int flag = 0; // 标记判断能否找到,0为找不到,1为找到
while (left <= right)
{
m = (right + left) / 2;
if (arr[m] > k)
{
right = m - 1;
}
if (arr[m] < k)
{
left = m + 1;
}
else
{
flag = 1;
break;
}
}
if (flag == 1)
{
printf("下标为%d\n", m);
}
else
printf("找不到");
return 0;
}
七、补充内容
7.1 sizeof求长原理再梳理
sizeof(数组名) // 计算整个数组占的总字节数
sizeof(数组名[0]) // 计算数组中单个元素的字节数
相除结果就是数组实际的元素个数。
7.2 C语言数组进阶用法
- 数组与指针的关联(核心等价公式)
- 数组作为函数参数(传参本质 + 长度丢失问题)
- 字符数组与字符串的区别(C 语言无字符串类型)
- 一维数组的经典操作(逆序、查找、去重)
7.3 C语言数组实战案例
- 一维数组:统计班级学生成绩(平均分/最高分/最低分)
- 一维数组:实现简单冒泡排序(排序基础)
- 二维数组:存储并打印矩阵(行/列求和)
- 字符数组:简单字符串遍历与统计(字符个数/数字个数)
八、总结
本篇我们学习了 C 语言数组的核心知识:
- 数组的基础认知:数组是一组相同类型元素的连续存储集合
- 一维数组:定义、初始化、访问、遍历、内存布局
- 二维数组:本质是"一维数组的数组",按行优先存储
- 变长数组:C99 新增特性,支持运行时动态指定长度
- 常见易错点:数组越界、下标边界、数组名本质
- 实战应用:二分查找、成绩统计、排序算法
数组是后续学习数据结构的基础,掌握这些核心知识点,为指针和更复杂的数据结构学习打下坚实基础!
感谢大家对我文章的支持,也很高兴与各位共同努力学习,我会继续坚持分享总结,共同奔赴下一个山海!