【C语言】数组

摘要 :数组把一组同类型元素连续放在内存里,下标从 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)。
相关推荐
careathers1 小时前
【数据结构】基于比较的排序算法 - 选择排序
数据结构·算法·排序算法
FRing031 小时前
JavaScript入门到精通三十三(异常处理、call方法、apply方法、bind方法、防抖、节流)
开发语言·前端·javascript
听风若依1 小时前
C++模板初阶
c++·算法
geovindu1 小时前
rust: Semaphore Pattern
开发语言·后端·设计模式·rust·代理模式·企业级信号量模式·同步型模式
水饺编程1 小时前
第2章:Linux 基础,制作【.tar.xz】压缩包文件
linux·c语言·汇编
纪念 2292 小时前
C++ 算法(模拟)
开发语言·c++·算法
揽星河..2 小时前
【C语言】函数
c语言·算法
zwc32104320552 小时前
Day 2:图像内存、数据类型与stride
c++·人工智能·opencv·算法·计算机视觉
niucloud-admin2 小时前
NIUCLOUD PHP SAAS上门家政使用文档--服务订单:评价列表
开发语言·php