摘要 :数组把一组同类型元素连续放在内存里,下标从 0 开始。本文从「五个成绩要写五个变量」切入,先讲一维数组的创建、初始化和遍历,再看元素地址怎样逐个相差 4 字节,接着处理
sizeof与strlen、越界和二维数组的行优先布局,最后说明数组传参为什么会退化,并用二分查找串起下标操作。
文章目录
- [1. 五个成绩要几个变量](#1. 五个成绩要几个变量)
- [2. 创建与初始化](#2. 创建与初始化)
-
- [2.1 创建](#2.1 创建)
- [2.2 初始化](#2.2 初始化)
- [2.3 字符数组与 '\0'](#2.3 字符数组与 '\0')
- [3. 下标与遍历](#3. 下标与遍历)
-
- [3.1 下标从 0 开始](#3.1 下标从 0 开始)
- [3.2 遍历](#3.2 遍历)
- [3.3 越界](#3.3 越界)
- [4. 数组在内存里怎样排](#4. 数组在内存里怎样排)
- [5. 二维数组](#5. 二维数组)
-
- [5.1 创建与初始化](#5.1 创建与初始化)
- [5.2 内存布局](#5.2 内存布局)
- [6. 数组作函数参数](#6. 数组作函数参数)
- [7. 二分查找](#7. 二分查找)
- [8. 小结](#8. 小结)
1. 五个成绩要几个变量
存一个班的五份成绩,最直接的办法是写五个变量:
c
int s1 = 90;
int s2 = 82;
int s3 = 77;
int s4 = 95;
int s5 = 68;
int sum = s1 + s2 + s3 + s4 + s5;
五个变量还能接受,求平均也只是多写几个加号。人数换成 50,变量要写 50 个,循环也没有下标可以依赖,改一次统计规则就要改一大片代码。
数组把同类型的元素排在一起,用一个名字和一组下标访问。同一份成绩改成数组:
c
#include <stdio.h>
int main(void)
{
int scores[5] = {90, 82, 77, 95, 68};
int sum = 0;
for (int i = 0; i < 5; i++)
sum += scores[i];
printf("平均分 %.2f\n", sum / 5.0);
return 0;
}
输出:
text
平均分 82.40
scores 是一个数组,里面有 5 个 int。scores[i] 取出下标为 i 的元素,for 循环让下标从 0 走到 4。数组的元素类型相同,长度在创建时确定,元素在内存里连续存放。
2. 创建与初始化
2.1 创建
数组的创建格式是:
c
类型 数组名[常量值];
例如:
c
int math[20]; // 20 个 int,存放数学成绩
char ch[8]; // 8 个 char
double score[10]; // 10 个 double
方括号里的值表示元素个数,必须大于 0。长度在创建时确定,后面不能通过赋值改变。VS2026 的 MSVC 不接受用变量做数组长度,长度要写成常量或常量表达式。
数组的类型由元素类型和长度共同决定。int a[10] 的类型是 int [10],char c[5] 的类型是 char [5],两者不是同一种类型。
2.2 初始化
数组用大括号初始化,元素之间用逗号隔开:
c
int a[5] = {1, 2, 3, 4, 5}; // 完全初始化
int b[5] = {1, 2}; // 不完全初始化,其余元素补 0
int c[] = {7, 8, 9}; // 省略长度,由初始化项推算为 3
| 写法 | 结果 |
|---|---|
int a[5] = {1, 2, 3, 4, 5}; |
5 个元素依次为 1~5 |
int b[5] = {1, 2}; |
前两个为 1、2,后三个自动为 0 |
int c[] = {7, 8, 9}; |
长度为 3 |
int d[3] = {1, 2, 3, 4}; |
初始化项过多,编译错误 |
不完全初始化时,没有写到的元素补 0。只写 int a[5]; 而不初始化,局部数组里的值是残留的垃圾值,和未初始化的局部变量一样,读它属于未定义行为。需要全 0 时写 int a[5] = {0};。
把三种写法跑一遍:
c
#include <stdio.h>
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int b[5] = {1, 2};
int c[] = {7, 8, 9};
for (int i = 0; i < 5; i++)
printf("%d ", a[i]);
printf("\n");
for (int i = 0; i < 5; i++)
printf("%d ", b[i]);
printf("\n");
printf("c 有 %zu 个元素\n", sizeof(c) / sizeof(c[0]));
return 0;
}
输出:
text
1 2 3 4 5
1 2 0 0 0
c 有 3 个元素
sizeof(c) / sizeof(c[0]) 用整个数组的字节数除以一个元素的字节数,得到元素个数。这个写法在后面遍历数组时会反复用到。
2.3 字符数组与 '\0'
字符数组可以用大括号逐个字符初始化,也可以直接用字符串初始化:
c
#include <stdio.h>
#include <string.h>
int main(void)
{
char s1[] = "abc";
char s2[] = {'a', 'b', 'c'};
printf("sizeof(s1)=%zu sizeof(s2)=%zu\n", sizeof(s1), sizeof(s2));
printf("strlen(s1)=%zu\n", strlen(s1));
printf("s1=%s\n", s1);
return 0;
}
输出:
text
sizeof(s1)=4 sizeof(s2)=3
strlen(s1)=3
s1=abc
s1 用 "abc" 初始化,字符串末尾隐藏一个 '\0',所以占 4 字节;s2 用三个字符初始化,没有 '\0',只占 3 字节。
printf 的 %s 和 strlen 都从首字符开始读,直到遇到 '\0' 才停。s2 没有结束标志,用 %s 打印会继续读后面的内存,strlen(s2) 也会一直往后找,结果不确定,属于越界访问。字符数组要么留出 '\0' 的位置,要么只按元素逐个处理。
sizeof 和 strlen 的区别:
sizeof |
strlen |
|---|---|
| 操作符,不是函数 | 库函数,需要 <string.h> |
| 算操作数占多少字节 | 数 '\0' 之前的字符个数 |
| 不关心里面存什么 | 没有 '\0' 就一直往后找,可能越界 |
sizeof("abc") 为 4 |
strlen("abc") 为 3 |
3. 下标与遍历
3.1 下标从 0 开始
C 规定数组下标从 0 开始。长度为 n 的数组,合法下标是 0 到 n - 1,最后一个元素是 arr[n - 1]。
c
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printf("%d\n", arr[7]); // 8
arr[7] = 0; // 修改第 8 个元素
printf("%d\n", arr[7]); // 0
arr[7] 里的 [] 是下标引用操作符,7 是偏移量。arr[7] 不是第 7 个元素,而是下标为 7 的元素,也就是第 8 个。
3.2 遍历
遍历数组就是让下标走遍 0 到 n - 1。元素个数不要写死,用 sizeof 算:
c
#include <stdio.h>
int main(void)
{
int arr[10] = {0};
int sz = (int)(sizeof(arr) / sizeof(arr[0]));
for (int i = 0; i < sz; i++)
scanf("%d", &arr[i]);
for (int i = sz - 1; i >= 0; i--)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
依次输入 1 到 10,输出:
text
10 9 8 7 6 5 4 3 2 1
sz 由 sizeof(arr) / sizeof(arr[0]) 得到,数组长度改了,sz 跟着变。倒序循环从 sz - 1 开始,条件是 i >= 0,所以下标变量要用 int。如果写成 size_t i,i >= 0 永远成立,循环停不下来。
scanf 在 VS2026 会报 C4996。在文件第一行、任何 #include 之前加 #define _CRT_SECURE_NO_WARNINGS 1,后文代码省略这一行。
3.3 越界
下标超出 0 到 n - 1,就是数组越界。C 不做运行时边界检查,编译器通常也不报错,越界读和越界写都是未定义行为。
两种常见写法:
c
for (int i = 0; i <= 10; i++) // 错:访问了 arr[10]
for (int i = 1; i <= 10; i++) // 错:漏掉了 arr[0]
第一个多走一步,第二个从 1 开始。遍历长度 10 的数组,统一写 i < sz。
越界的后果取决于内存布局:可能读到垃圾值,可能改坏相邻变量,也可能程序直接崩溃。更麻烦的是一种「看起来正常」的越界,这次运行没出错,换台机器或换个优化级别就变了。不要把某次运行结果当成越界安全的证据。
4. 数组在内存里怎样排
依次打印每个元素的地址:
c
#include <stdio.h>
int main(void)
{
int arr[5] = {10, 20, 30, 40, 50};
for (int i = 0; i < 5; i++)
printf("&arr[%d] = %p\n", i, (void *)&arr[i]);
return 0;
}
某次运行的输出(地址每次运行都会变):
text
&arr[0] = 000000A69E5FFCF0
&arr[1] = 000000A69E5FFCF4
&arr[2] = 000000A69E5FFCF8
&arr[3] = 000000A69E5FFCFC
&arr[4] = 000000A69E5FFD00
相邻地址相差 4 字节,正好是一个 int 的大小。数组元素按下标顺序、从低地址到高地址连续存放。

连续性带来一个直接结果:下标访问不需要从头数。已知首地址和元素大小,第 i 个元素的地址可以算出来:
地址 ( a r r i ) = 地址 ( a r r 0 ) + i × s i z e o f ( i n t ) 地址(arri) = 地址(arr0) + i \times sizeof(int) 地址(arri)=地址(arr0)+i×sizeof(int)
所以 arr[i] 的访问时间是 O(1),和 i 多大无关。数组名在大多数表达式里会转换成首元素地址,这一点在指针部分会展开;现在只要记住,下标背后是一次地址计算。
5. 二维数组
5.1 创建与初始化
二维数组把一维数组作为元素,创建格式是:
c
类型 数组名[行数][列数];
int arr[3][5]; // 3 行,每行 5 个 int
double data[2][8]; // 2 行,每行 8 个 double
初始化时,行数可以省略,列数不能省略:
| 写法 | 行数 | 结果 |
|---|---|---|
int a[3][5] = {1, 2}; |
3 | 只有 a[0][0]、a[0][1] 为 1、2,其余补 0 |
int b[3][5] = {``{1, 2}, {3, 4}, {5, 6}}; |
3 | 每行前两列有值,其余补 0 |
int c[][5] = {1, 2, 3, 4, 5, 6, 7}; |
2 | 第一行 1~5,第二行 6、7、0、0、0 |
int d[][5] = {``{1, 2}, {3, 4}, {5, 6}}; |
3 | 按内层大括号推算行数 |
列数不能省,因为编译器要靠它计算一行占多少字节。行数可以由初始化项和内层大括号的数量推算,列数推不出来。
行下标和列下标都从 0 开始。arr[2][4] 是行下标 2、列下标 4,也就是第 3 行第 5 列:
c
#include <stdio.h>
int main(void)
{
int arr[3][5] = {
{1, 2, 3, 4, 5},
{2, 3, 4, 5, 6},
{3, 4, 5, 6, 7}
};
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
printf("%d ", arr[i][j]);
printf("\n");
}
printf("arr[2][4] = %d\n", arr[2][4]);
return 0;
}
输出:
text
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
arr[2][4] = 7
遍历二维数组用两层循环,外层走行,内层走列。每打印完一行,补一个换行。
5.2 内存布局
二维数组在内存里不是一张二维表格,而是一行接一行排成一条线。打印每个元素的地址:
c
#include <stdio.h>
int main(void)
{
int arr[2][3] = {0};
for (int i = 0; i < 2; i++)
for (int j = 0; j < 3; j++)
printf("&arr[%d][%d] = %p\n", i, j, (void *)&arr[i][j]);
printf("sizeof(arr)=%zu sizeof(arr[0])=%zu sizeof(arr[0][0])=%zu\n",
sizeof(arr), sizeof(arr[0]), sizeof(arr[0][0]));
return 0;
}
输出(地址每次运行会变):
text
&arr[0][0] = 000000390DDFFE10
&arr[0][1] = 000000390DDFFE14
&arr[0][2] = 000000390DDFFE18
&arr[1][0] = 000000390DDFFE1C
&arr[1][1] = 000000390DDFFE20
&arr[1][2] = 000000390DDFFE24
sizeof(arr)=24 sizeof(arr[0])=12 sizeof(arr[0][0])=4
同一行内相差 4 字节,换行处 arr[0][2] 到 arr[1][0] 也只相差 4 字节。二维数组按行优先存放:先放第 0 行,再放第 1 行。

arr[0] 可以看成一维数组的数组名,代表第 0 行。所以 sizeof(arr[0]) 是一整行的大小(3 × 4 = 12),sizeof(arr[0][0]) 是一个元素的大小。
行数是 sizeof(arr) / sizeof(arr[0]),列数是 sizeof(arr[0]) / sizeof(arr[0][0])。
6. 数组作函数参数
把数组交给函数处理,函数里要遍历,就必须知道元素个数。写两个函数:一个把数组元素全部改成指定值,一个打印数组:
c
#include <stdio.h>
void fill(int arr[], int sz, int value);
void print_arr(int arr[], int sz);
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int sz = (int)(sizeof(a) / sizeof(a[0]));
fill(a, sz, -1);
print_arr(a, sz);
return 0;
}
void fill(int arr[], int sz, int value)
{
for (int i = 0; i < sz; i++)
arr[i] = value;
}
void print_arr(int arr[], int sz)
{
for (int i = 0; i < sz; i++)
printf("%d ", arr[i]);
printf("\n");
}
输出:
text
-1 -1 -1 -1 -1
fill 修改的是调用方的数组,不是副本。数组传参时,传过去的是首元素地址,形参和实参指向同一块内存。
但元素个数不能靠 sizeof 在函数里算。看下面这个程序:
c
#include <stdio.h>
void show_size(int arr[])
{
printf("函数内 sizeof(arr)=%zu\n", sizeof(arr));
}
int main(void)
{
int a[10] = {0};
printf("main 内 sizeof(a)=%zu\n", sizeof(a));
show_size(a);
return 0;
}
输出:
text
main 内 sizeof(a)=40
函数内 sizeof(arr)=8
a 有 10 个 int,在 main 里占 40 字节。到了函数里,sizeof(arr) 只有 8 字节,这是 x64 下指针的大小。
形参写成 int arr[] 时,编译器把它按指针处理,这个转换叫数组退化(array decay)。函数里拿不到数组长度,sizeof(arr) / sizeof(arr[0]) 也算不出元素个数。所以数组传参时,长度要像 sz 一样单独传进去。
二维数组传参同理,形参写成 int arr[][5],行数可以省,列数不能省。列数决定了每一行跨多少字节,编译器需要它来计算 arr[i][j] 的地址。
7. 二分查找
数组下标不只用来遍历,也能用来缩小查找范围。二分查找要求数组已经升序排列:每次取中间元素,比目标小就到右半边找,比目标大到左半边找。
c
#include <stdio.h>
int binary_search(int arr[], int sz, int key)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] < key)
left = mid + 1;
else if (arr[mid] > key)
right = mid - 1;
else
return mid;
}
return -1;
}
int main(void)
{
int arr[] = {1, 3, 5, 7, 9, 11, 13};
int sz = (int)(sizeof(arr) / sizeof(arr[0]));
int index = binary_search(arr, sz, 11);
if (index != -1)
printf("找到了,下标是 %d\n", index);
else
printf("找不到\n");
return 0;
}
输出:
text
找到了,下标是 5
以 key = 11 为例,查找范围的变化是:
| 轮次 | left | right | mid | arrmid | 动作 |
|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 7 | 7 < 11,left 移到 4 |
| 2 | 4 | 6 | 5 | 11 | 相等,返回下标 5 |
每比较一次,范围缩小一半。长度为 n 的数组,最多比较 ⌊ log 2 n ⌋ + 1 \lfloor \log_2 n \rfloor + 1 ⌊log2n⌋+1 次,时间复杂度是 O(log n),额外空间是 O(1)。顺序查找最坏要看 n 个元素,数据量大时差距明显。
mid 写成 left + (right - left) / 2,而不是 (left + right) / 2。后者在 left 和 right 都很大时,相加可能溢出 int 的范围。
二分查找的前提是有序。数组无序时,要么先排序,要么改用顺序查找。
8. 小结
- 数组是相同类型元素的连续集合,长度在创建时确定,下标范围是 0 到 n - 1。
- 不完全初始化时其余元素补 0;局部数组不初始化时值是垃圾值。
- 字符串初始化会多一个
'\0';{'a', 'b', 'c'}没有结束标志,按%s打印或传给strlen会越界。 sizeof算字节数,strlen数'\0'前的字符;元素个数用sizeof(arr) / sizeof(arr[0])。- 越界读写是未定义行为,C 不做运行时检查;遍历统一写
i < sz。 - 一维数组连续存放,二维数组按行优先连续存放;下标访问的地址由首地址加偏移算出。
- 数组传参传的是首元素地址,函数内
sizeof得到指针大小,元素个数要另传;二维数组的列数不能省。 - 二分查找要求有序,每轮把范围减半,时间 O(log n),空间 O(1)。