C 数组
数组是 C 里最简单的"批量数据"结构——把一组同类型的数据连续存放在内存里,用下标访问。数组是后续学指针、字符串、动态内存的基础,必须吃透。本篇覆盖一维数组、二维数组、数组与指针的关系,以及新手最容易踩的坑:越界访问。
一、一维数组
数组声明语法:类型 名字[长度];。可以在声明时用花括号初始化:
#include <stdio.h>
int main(void) {
// 1. 声明 + 完全初始化
int nums[5] = {10, 20, 30, 40, 50};
// 2. 部分初始化(其余自动补 0)
int arr[5] = {1, 2}; // 等价 {1, 2, 0, 0, 0}
// 3. 全部初始化为 0
int zeros[5] = {0}; // {0, 0, 0, 0, 0}
// 4. 省略长度,由初始化列表决定
int xs[] = {1, 2, 3, 4}; // 长度自动是 4
// 访问:下标从 0 开始
printf("%d\n", nums[0]); // 10
printf("%d\n", nums[4]); // 50
nums[2] = 99; // 修改第 3 个元素
// 遍历
int len = sizeof(nums) / sizeof(nums[0]);
for (int i = 0; i < len; i++) {
printf("%d ", nums[i]); // 10 20 99 40 50
}
printf("\n");
return 0;
}几个要点:
- 下标从 0 开始:长度 5 的数组,有效下标是 0~4。
- 长度在编译期确定:C89 要求长度是常量(C99 的 VLA 例外)。
- 求长度经典技巧:
sizeof(arr) / sizeof(arr[0])。注意:这个技巧只在定义数组的作用域内有效,数组传给函数后就失效了(后面细讲)。 - 部分初始化时其余元素自动补 0——常用来把数组清零:
int a[100] = {0};
二、越界访问(危险!)
这是 C 数组最致命的特性:C 不做数组边界检查。你访问 arr[5](长度只有 5,有效下标是 0~4),编译器不报错,运行时也不会抛异常,而是默默地读写相邻的内存——可能是其他变量,可能是未分配的内存(段错误),也可能被黑客利用做缓冲区溢出攻击(很多安全漏洞就是这么来的)。
#include <stdio.h>
int main(void) {
int arr[5] = {1, 2, 3, 4, 5};
// 危险!越界访问 - C 不做边界检查!
printf("%d\n", arr[5]); // 未定义行为!可能输出垃圾值
printf("%d\n", arr[100]); // 极可能崩溃(段错误)
// 越界写更危险,可能破坏其他变量甚至被利用做攻击
// arr[5] = 999; // 经典的"缓冲区溢出"漏洞
// C 不像 Java/Python 会抛异常,它只是默默地读写相邻内存
// 这就是为什么很多安全漏洞(缓冲区溢出)出自 C 代码
// 防御:自己跟踪长度,传数组时一并传长度
void print_arr(int *a, int n); // 长度必须显式传
return 0;
}防御办法:自己时刻跟踪数组长度,把数组传给函数时同时传长度。这也是为什么 C 标准库的字符串函数(strncpy、snprintf)都要你传长度参数。
三、二维数组
二维数组就是"数组的数组",常用来表示矩阵、棋盘、表格:
#include <stdio.h>
int main(void) {
// 1. 二维数组声明 + 初始化
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 2. 也可以连续写
int m2[2][3] = {1, 2, 3, 4, 5, 6};
// 3. 部分初始化
int m3[3][3] = {{1}, {0, 1}, {0, 0, 1}}; // 对角矩阵
// 访问:matriy[row][col]
printf("%d\n", matrix[1][2]); // 7(第2行第3列)
// 遍历:双层循环
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%3d ", matrix[i][j]);
}
printf("\n");
}
// 行数和列数
int rows = sizeof(matrix) / sizeof(matrix[0]); // 3
int cols = sizeof(matrix[0]) / sizeof(matrix[0][0]); // 4
printf("%d 行 %d 列\n", rows, cols);
return 0;
}二维数组在内存里是按行存储的——matrix[0][0]、matrix[0][1]、... matrix[0][3]、matrix[1][0] 连续存放。访问 matrix[i][j] 的本质是 *(*(matrix + i) + j)。
四、数组作为函数参数
这是新手最容易困惑的点之一。当你把数组传给函数时,实际传递的是首元素指针,不是整个数组的拷贝。这带来两个后果:
#include <stdio.h>
// 数组作为函数参数,实际传递的是"首元素指针"
// 所以函数内无法用 sizeof 求数组长度!
// 必须显式传长度
// 三种等价写法,本质都是 int* arr
void print_arr(int arr[], int n) {
// sizeof(arr) 在这里是指针大小(8字节),不是数组大小!
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 另一种写法:显式写指针
void double_each(int *arr, int n) {
for (int i = 0; i < n; i++) {
arr[i] *= 2; // 修改会影响原数组(因为传的是地址)
}
}
// 二维数组作为参数:第二维大小必须指定
void print_matrix(int rows, int cols, int m[][4]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%3d ", m[i][j]);
}
printf("\n");
}
}
int main(void) {
int nums[] = {1, 2, 3, 4, 5};
int n = sizeof(nums) / sizeof(nums[0]);
print_arr(nums, n); // 1 2 3 4 5
double_each(nums, n);
print_arr(nums, n); // 2 4 6 8 10
int matrix[2][4] = {{1,2,3,4}, {5,6,7,8}};
print_matrix(2, 4, matrix);
return 0;
}- 函数内 sizeof(arr) 得不到数组长度——它是指针的大小(8字节),不是数组的大小。所以必须把长度作为额外参数传进去。
- 函数内修改数组会影响原数组——因为传的是地址。这跟普通变量值传递不同。
三种写法 int arr[]、int arr[10]、int *arr 在函数参数里完全等价,都是 int *。推荐第一种或第三种,第二种会误导(让人以为长度被检查了,其实没有)。
五、C99 变长数组 VLA
C99 引入了变长数组(Variable Length Array)——可以用变量作为数组长度:
#include <stdio.h>
// C99 起支持变长数组 VLA(Variable Length Array)
// 长度是运行时变量
int main(void) {
int n;
printf("输入数组大小:");
scanf("%d", &n);
int arr[n]; // 变长数组,长度由运行时决定
for (int i = 0; i < n; i++) {
arr[i] = i * i;
}
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 注意:VLA 在栈上分配,n 太大会栈溢出
// 而且 VLA 在 C11 改成可选特性,某些编译器可能不支持
// 大数组还是建议用 malloc(后续内存篇讲)
// 实际工程中,变长需求首选 malloc,不用 VLA
return 0;
}但 VLA 有争议:
- VLA 在栈上分配,长度太大(比如百万级)会导致栈溢出崩溃。
- C11 把 VLA 改成可选特性,某些编译器(如 MSVC)不支持。
- 实际工程中,需要运行时决定大小,首选
malloc(堆上分配),不用 VLA。
六、数组初始化技巧汇总
int a[5] = {1, 2, 3, 4, 5};:完全初始化int a[5] = {0};:全部清零(部分初始化,其余补 0)int a[] = {1, 2, 3};:省略长度,自动推断int a[100]; memset(a, 0, sizeof(a));:运行时清零- C99 指定初始化:
int a[5] = {[2] = 10, [4] = 20};——只初始化指定下标,其余补 0
七、数组与指针的关系(下篇细讲)
这里先剧透:C 里数组名在很多场景下会"退化"为首元素指针。所以 arr[i] 其实是 *(arr + i) 的语法糖。理解了这一点,你就能理解为什么数组传参后 sizeof 失效、为什么指针能像数组一样用下标访问。下一篇指针会深入讲这个关系。
小结
数组是连续同类型数据的集合,下标从 0 开始。C 不做边界检查,越界访问是未定义行为,自己要管好长度。数组传给函数实际传的是首元素指针,sizeof 失效。二维数组按行存储。下一篇我们看 C 的指针——这是 C 的灵魂,也是数组的进阶。
← 上一篇 C 函数
下一篇 C 指针(重点) →