C语言基础基础知识--数组

目录

  1. 数组的概念
  2. 一维数组的创建和初始化
  3. 一维数组的使用
  4. 一维数组在内存中的存储
  5. sizeof计算数组元素个数
  6. 二维数组的创建
  7. 二维数组的初始化
  8. 二维数组的使用
  9. 二维数组在内存中的存储
  10. C99中的变长数组
  11. 数组练习

1. 数组的概念

在C语言编程中,当我们需要处理多个同类型的数据时(比如一个班级所有学生的数学成绩、一段文字中的所有字符、一组测量得到的温度值等),如果为每个数据单独定义一个变量,不仅会让代码变得冗长繁琐,还会增加后期维护的难度。而数组正是为解决这类问题而生的一种数据结构,它的核心定义是“一组相同类型元素的集合”。从这个定义中,我们可以提炼出两个对实际编程至关重要的关键信息:

  • 首先,数组的核心作用是存储数据,因此它必须能容纳至少1个元素,元素个数绝对不能为0。如果数组元素个数为0,那么它就失去了存储数据的意义,而且在C语言的语法规则中,这样的“空数组”定义也是不合法的,会导致编译错误。

  • 其次,数组中所有元素的数据类型必须完全相同。这是因为数组在内存中会按照统一的类型规则分配存储空间,若同时存储不同类型的数据(比如在一个int数组中混入char类型的值),不仅会破坏内存分配的一致性,还会导致数据读取时出现类型不匹配的问题——可能是读取到错误的数值,甚至引发程序运行时的异常。

从结构复杂度来看,数组可以分为一维数组和多维数组。其中,一维数组是最基础、最常用的形式,它可以看作是“一条直线上排列的数据集合”;而多维数组则是在一维数组的基础上扩展而来,比如二维数组可以理解为“多个一维数组组成的表格”,三维数组可以理解为“多个二维数组组成的立体结构”。在实际开发中,二维数组是多维数组里最常见的类型,常用于处理表格类数据(如学生成绩表、矩阵运算等),更高维度的数组则因使用场景有限,较少出现在普通编程任务中。

2. 一维数组的创建和初始化

一维数组是数组的基础形式,掌握它的创建和初始化方法,是后续学习多维数组的重要前提。下面我们将从“创建语法”和“初始化方式”两个维度,详细讲解一维数组的基础用法。

2.1 数组创建

在C语言中,创建一维数组需要遵循固定的语法规则,其基本格式如下:

type arr_name[常量值];

这个语法格式中的每个部分都有明确的含义和使用要求,我们逐一拆解说明:

  • type(数据类型):该部分用于指定数组中存储元素的具体类型,它可以是C语言内置的基本数据类型(如存储整数的int、存储字符的char、存储单精度小数的float、存储双精度小数的double等),也可以是程序员自定义的数据类型(如后续会学到的结构体、枚举等)。选择type的核心依据是“数组要存储的数据类型”——比如要存储班级学生的成绩(整数),就用int;要存储一段文字的字符,就用char;要存储精确的科学计算数据(如圆周率、重力加速度),就用double。

  • arr_name(数组名):数组名是数组的“标识”,用于在代码中引用这个数组。给数组命名时,需要遵循C语言标识符的命名规则:只能由字母、数字和下划线组成,且不能以数字开头,同时不能使用C语言的关键字(如int、if、for等)。更重要的是,数组名应具备“语义化”——即通过名字能直观判断数组的用途,比如存储数学成绩的数组命名为math_scores,存储用户名的数组命名为user_names,这样能大幅提升代码的可读性,方便后续维护。

  • []中的常量值(数组大小):该部分用于指定数组能容纳的元素个数,它必须是一个“常量或常量表达式”(如10、3+5、sizeof(int)等),不能是变量(C99标准中的变长数组除外,后续会单独讲解)。确定数组大小的核心原则是“按需分配”——既不能过大(否则会浪费内存空间,比如只需要存储20个学生成绩,却定义了大小为100的数组),也不能过小(否则会导致数据无法完全存储,出现“数组越界”的风险)。

为了让大家更直观地理解一维数组的创建,我们举几个实际场景的例子:

  • 场景1:存储某个班级20名学生的数学成绩(整数类型),创建数组如下:
    int math_scores[20];  // int类型,数组名math_scores,大小20,可存20个整数
    
  • 场景2:存储一个8字符的密码(字符类型),创建数组如下:
    char password[8];  // char类型,数组名password,大小8,可存8个字符
    
  • 场景3:存储10个产品的重量(双精度小数类型,需精确到小数点后3位),创建数组如下:
    double product_weights[10];  // double类型,数组名product_weights,大小10,可存10个双精度小数
    

2.2 数组的初始化

“数组初始化”指的是在创建数组的同时,为数组中的元素赋予初始值的操作。在C语言中,数组的初始化需要使用大括号{}来包裹初始值,根据初始化的完整程度,可分为以下三种常见方式:

完全初始化

“完全初始化”指的是为数组中的每一个元素都明确指定初始值,初始值的个数与数组大小完全一致。这种方式的优点是元素值清晰可控,适用于数组大小较小且每个元素值都确定的场景。

示例1:创建一个大小为5的int数组,为每个元素赋予1~5的初始值:

int arr[5] = {1, 2, 3, 4, 5};  // 元素依次为arr[0]=1、arr[1]=2、arr[2]=3、arr[3]=4、arr[4]=5

示例2:创建一个大小为3的char数组,存储字符’a’、‘b’、‘c’:

char char_arr[3] = {'a', 'b', 'c'};  // 元素依次为char_arr[0]='a'、char_arr[1]='b'、char_arr[2]='c'
不完全初始化

“不完全初始化”指的是只为数组中的部分元素指定初始值,未指定初始值的元素会由C语言编译器自动赋予默认值——对于数值类型(如int、float、double),默认值为0;对于字符类型(char),默认值为’\0’(空字符)。这种方式适用于数组大小较大,但大部分元素值为0的场景(如统计数据的初始数组、缓存数组等),可以减少代码的冗余。

示例1:创建一个大小为6的int数组,仅为第一个元素赋予1,其余元素默认初始化为0:

int arr2[6] = {1};  // 元素实际为arr2[0]=1、arr2[1]=0、arr2[2]=0、arr2[3]=0、arr2[4]=0、arr2[5]=0

示例2:创建一个大小为4的float数组,为前两个元素赋予3.14和2.71,后两个元素默认初始化为0.0:

float float_arr[4] = {3.14, 2.71};  // 元素实际为float_arr[0]=3.14、float_arr[1]=2.71、float_arr[2]=0.0、float_arr[3]=0.0
错误的初始化(初始化项过多)

在数组初始化时,必须保证“初始值的个数 ≤ 数组大小”。如果初始值的个数超过了数组能容纳的元素个数,就会出现“初始化项过多”的错误——C语言编译器会检测到这种内存越界的风险,直接拒绝编译,并提示错误信息。

示例:创建一个大小为3的int数组,却为其赋予4个初始值(错误示范):

int arr3[3] = {1, 2, 3, 4};  // 错误:数组大小为3,初始化项为4个,超出数组容量

上述代码在编译时,编译器会报错(如GCC编译器会提示“too many initializers for ‘int [3]’”),因为数组arr3只能容纳3个元素,而4个初始值会导致内存溢出,破坏其他变量的存储空间。

2.3 数组的类型

很多初学者会误以为“数组没有类型”,但实际上,在C语言中数组也是一种“自定义类型”——它的类型由“元素类型”和“数组大小”共同决定,去掉数组名后剩余的部分就是数组的类型。理解数组的类型,对于后续学习“数组传参”“指针与数组的关系”等知识点至关重要。

我们通过具体示例来理解数组的类型:

// 示例1:int类型,大小10的数组
int arr1[10];  
// 去掉数组名arr1,剩余“int [10]”,因此arr1的数组类型是“int [10]”

// 示例2:int类型,大小12的数组
int arr2[12];  
// 去掉数组名arr2,剩余“int [12]”,因此arr2的数组类型是“int [12]”

// 示例3:char类型,大小5的数组
char ch[5];    
// 去掉数组名ch,剩余“char [5]”,因此ch的数组类型是“char [5]”

从上述示例可以看出,即使两个数组的元素类型相同(如arr1和arr2都是int类型),只要数组大小不同,它们的数组类型就不同。这种类型差异在函数参数传递中会体现得尤为明显——比如一个接收“int [10]”类型数组的函数,无法接收“int [12]”类型的数组作为参数,因为两者的类型不匹配。

3. 一维数组的使用

创建和初始化数组的最终目的是“使用数组中的元素”——比如读取元素值、修改元素值、遍历所有元素等。C语言为数组的使用提供了简洁的语法,核心是“数组下标”和“下标引用操作符”,下面我们详细讲解一维数组的使用方法。

3.1 数组下标

在C语言中,数组的元素是通过“下标”来定位的,就像我们通过“座位号”找到教室中的某个座位一样。关于数组下标,有两个必须牢记的规则:

  • 首先,数组下标的起始值是0,而不是1。这是C语言(以及很多编程语言)的设计传统,源于早期指针操作的高效性——数组名本质上是数组第一个元素的地址,通过“下标0”可以直接访问第一个元素(无需额外计算),若下标从1开始,反而需要多一次减法运算(如访问第一个元素需要用下标1,实际地址计算为“数组地址 + (1-1)*元素大小”),影响访问效率。

  • 其次,数组下标的最大值是“数组大小 - 1”。假设数组的大小为n(即能容纳n个元素),那么下标范围是0~n-1。如果访问下标为n的元素,就会出现“数组越界”的问题——这是C语言中非常常见的错误,因为C语言编译器不会主动检查数组下标是否越界,越界访问可能会读取到内存中的随机值,或修改其他变量的存储空间,导致程序运行异常(如输出错误结果、程序崩溃等)。

为了让大家更直观地理解数组下标,我们以一个大小为10的int数组为例:

int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

这个数组的10个元素对应的下标如下表所示:

数组元素arr[0]arr[1]arr[2]arr[3]arr[4]arr[5]arr[6]arr[7]arr[8]arr[9]
元素值12345678910
下标0123456789

C语言提供了“下标引用操作符[]”来通过下标访问数组元素,其使用格式为“数组名[下标]”。通过这个操作符,我们可以轻松地读取或修改数组元素的值。

示例:访问上述数组中下标为7和下标为3的元素,并打印它们的值:

#include <stdio.h>  // 包含标准输入输出库,用于printf函数
int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 
    // 访问下标7的元素(值为8),并打印
    printf("下标7对应的元素值:%d\n", arr[7]);
    // 访问下标3的元素(值为4),并打印
    printf("下标3对应的元素值:%d\n", arr[3]);
    return 0;
}

上述代码的运行结果如下:

下标7对应的元素值:8
下标3对应的元素值:4

如果我们想修改数组元素的值,也可以通过“数组名[下标]”直接赋值,比如:

arr[2] = 30;  // 将下标2的元素值从3修改为30
arr[9] = 100; // 将下标9的元素值从10修改为100

修改后,数组中arr[2]的值变为30,arr[9]的值变为100。

3.2 数组元素的打印

在实际编程中,我们经常需要“遍历数组”——即依次访问数组中的所有元素并进行操作(如打印、求和、筛选等)。对于一维数组,遍历的核心思路是“通过循环生成所有合法的下标”,然后通过下标访问每个元素。

由于数组下标的范围是0~n-1(n为数组大小),我们可以使用for循环来生成这个范围内的下标:循环变量i从0开始,每次递增1,直到i < n时停止(此时i的最大值为n-1,刚好是最后一个元素的下标)。

示例:遍历一个大小为10的int数组,打印所有元素的值:

#include <stdio.h>
int main()
{
    // 定义并初始化一个大小为10的int数组
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 
    int i = 0;  // 循环变量,用于生成数组下标
    // for循环:i从0开始,i < 10时继续循环(下标范围0~9)
    for(i = 0; i < 10; i++)
    {
        // 通过下标i访问数组元素,并打印(%d用于输出int类型,末尾加空格使输出更整齐)
        printf("%d ", arr[i]);
    }
    return 0;
}

上述代码的运行结果如下:

1 2 3 4 5 6 7 8 9 10 

需要注意的是,循环条件必须是“i < 10”,而不是“i <= 10”——如果写成“i <= 10”,当i=10时,会访问下标为10的元素,而数组的最大下标是9,此时会出现数组越界,可能导致程序输出错误结果(如随机值)。

除了for循环,我们也可以使用while循环来遍历数组,逻辑完全相同,只是循环结构略有差异:

#include <stdio.h>
int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 
    int i = 0;
    // while循环:i从0开始,i < 10时继续循环
    while(i < 10)
    {
        printf("%d ", arr[i]);
        i++;  // 循环变量递增,避免死循环
    }
    return 0;
}

这段代码的运行结果与for循环版本完全一致,具体使用哪种循环,可根据个人代码习惯和实际场景选择。

3.3 数组的输入

除了使用初始值,我们还可以在程序运行过程中,通过键盘输入为数组元素赋值——这需要结合循环和scanf函数(C语言中的标准输入函数)来实现。

scanf函数的核心作用是“从键盘读取数据并存储到指定变量中”,它需要接收“变量的地址”作为参数(因为只有通过地址,才能修改变量的值)。对于数组元素来说,“数组名[下标]”是元素的值,而“&数组名[下标]”(&是取地址符)才是元素的地址,因此在使用scanf为数组元素赋值时,必须加上&符号。

示例:创建一个大小为10的int数组,通过键盘输入为其赋值,然后遍历数组打印所有元素:

#include <stdio.h>
int main()
{
    // 定义一个大小为10的int数组(此处未初始化,后续通过输入赋值)
    int arr[10];  
    int i = 0;

    // 第一部分:通过循环和scanf输入数组元素
    printf("请输入10个整数(用空格或回车分隔):\n");
    for(i = 0; i < 10; i++)
    {
        // &arr[i]:获取下标i的元素地址,scanf将输入的值存储到该地址
        scanf("%d", &arr[i]);
    }

    // 第二部分:遍历数组,打印输入的元素
    printf("你输入的10个整数是:\n");
    for(i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }

    return 0;
}

上述代码的运行过程如下(假设输入的10个整数为2 1 4 3 6 5 8 7 0 9):

请输入10个整数(用空格或回车分隔):
2 1 4 3 6 5 8 7 0 9
你输入的10个整数是:
2 1 4 3 6 5 8 7 0 9 

在使用scanf为数组输入时,有两个注意事项:

  1. 输入的数据类型必须与数组元素类型匹配——比如int数组只能输入整数,若输入小数,会导致数据截断(如输入3.14,实际存储为3)。
  2. 输入的数据个数应与数组大小一致——若输入的数据个数少于数组大小,未输入的元素会保留随机值;若输入的数据个数多于数组大小,多余的数据会被后续的变量读取(若有),或导致程序异常。

4. 一维数组在内存中的存储

要深入理解数组的特性(如为什么能通过指针访问数组、为什么数组越界会导致错误),就必须了解数组在内存中的存储方式。通过分析数组元素的地址,我们可以清晰地看到数组的存储规律。

4.1 验证数组内存存储的代码

为了观察数组元素在内存中的地址分布,我们可以编写一段代码,通过循环打印每个数组元素的地址(使用printf函数的%p格式符,以十六进制形式输出地址):

#include <stdio.h>
int main()
{
    // 定义并初始化一个大小为10的int数组
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 
    int i = 0;
    // 循环打印每个元素的下标和对应的地址
    for(i = 0; i < 10; i++)
    {
        // %d:输出下标i;%p:输出元素arr[i]的地址
        printf("&arr[%d] = %p\n", i, &arr[i]);
    }
    return 0;
}

4.2 数组元素的地址分布结果

在32位Windows系统的Microsoft Visual Studio编译器中,上述代码的运行结果(地址值可能因每次运行而不同,这是正常现象)如下:

&arr[0] = 0133F8D0
&arr[1] = 0133F8D4
&arr[2] = 0133F8D8
&arr[3] = 0133F8DC
&arr[4] = 0133F8E0
&arr[5] = 0133F8E4
&arr[6] = 0133F8E8
&arr[7] = 0133F8EC
&arr[8] = 0133F8F0
&arr[9] = 0133F8F4

4.3 数组内存存储的核心规律

通过分析上述地址分布结果,我们可以总结出一维数组在内存中的两个核心存储规律:

规律1:数组元素的地址随下标增长而递增

从输出的地址值可以看出,随着数组下标的从0到9逐步增大,元素的地址也从0133F8D0逐步增大到0133F8F4。这说明数组在内存中是“按顺序排列”的,下标越小的元素,存储在内存中地址越小的位置;下标越大的元素,存储在内存中地址越大的位置。

规律2:相邻元素的地址差等于元素类型的字节数

观察相邻元素的地址差(如arr[0]和arr[1]的地址差、arr[1]和arr[2]的地址差),可以发现它们的差值都是4。这是因为数组的元素类型是int,而在32位系统中,int类型的变量占用4个字节的内存空间。因此,每个元素都会占用4个连续的字节,下一个元素的地址自然比前一个元素的地址大4。

如果数组的元素类型是char(占用1个字节),那么相邻元素的地址差会是1;如果是double(占用8个字节),相邻元素的地址差会是8。这进一步验证了“相邻元素地址差等于元素类型字节数”的规律。

4.4 数组连续存储的意义

数组在内存中的“连续存储”特性,是C语言中很多数组操作的基础,其核心意义体现在两个方面:

  1. 高效的元素访问:由于数组元素连续存储,只要知道数组第一个元素的地址(即数组名)和元素的下标,就可以通过公式“元素地址 = 数组名地址 + 下标 × 元素类型字节数”直接计算出任意元素的地址,从而实现“随机访问”——访问任意元素的时间复杂度都是O(1),效率极高。
  2. 为指针访问数组奠定基础:在C语言中,指针和数组有着密切的关系。由于数组元素连续存储,我们可以通过指针的“自增”“自减”操作来遍历数组——比如一个指向数组第一个元素的指针,每次自增1,就会指向 next 一个元素(因为指针自增的步长等于其指向类型的字节数)。这也是后续学习“指针与数组”的关键前提。

5. sizeof计算数组元素个数

在遍历数组、传递数组参数等场景中,我们经常需要知道数组的元素个数。如果直接将数组大小“写死”(如for循环条件中的i < 10),当数组大小修改时,所有用到数组大小的地方都需要手动修改,不仅繁琐,还容易出错。而C语言中的sizeof关键字,可以帮助我们动态计算数组的元素个数,提升代码的灵活性和可维护性。

5.1 sizeof关键字的基本作用

sizeof是C语言中的一个关键字(不是函数),其核心作用是“计算变量、数据类型或数组所占用的内存空间大小”,单位是“字节”。它的使用格式主要有两种:

  • 计算变量/数组的大小:sizeof(变量名/数组名)sizeof 变量名/数组名(括号可省略)。
  • 计算数据类型的大小:sizeof(数据类型)(括号不可省略)。

示例:计算不同变量和类型的大小(32位系统下):

#include <stdio.h>
int main()
{
    int a = 10;
    char b = 'a';
    double c = 3.14;
    int arr[10] = {0};

    // 计算变量a(int类型)的大小
    printf("sizeof(a) = %d 字节\n", sizeof(a));
    // 计算变量b(char类型)的大小
    printf("sizeof(b) = %d 字节\n", sizeof b);  // 省略括号
    // 计算double类型的大小
    printf("sizeof(double) = %d 字节\n", sizeof(double));
    // 计算数组arr的大小
    printf("sizeof(arr) = %d 字节\n", sizeof(arr));

    return 0;
}

运行结果如下(32位系统):

sizeof(a) = 4 字节
sizeof(b) = 1 字节
sizeof(double) = 8 字节
sizeof(arr) = 40 字节

5.2 计算数组元素个数的原理

数组的元素个数 = 数组总占用内存大小 ÷ 单个元素占用内存大小。这个公式的依据是:

  • 数组总占用内存大小:通过sizeof(数组名)计算,等于“元素个数 × 单个元素字节数”。
  • 单个元素占用内存大小:通过sizeof(数组名[0])计算(数组名[0]是数组的第一个元素,所有元素类型相同,因此第一个元素的大小代表所有元素的大小)。

因此,数组元素个数的计算公式为:

int 元素个数 = sizeof(数组名) / sizeof(数组名[0]);

5.3 计算数组元素个数的示例

下面通过一段代码,演示如何使用sizeof计算数组的元素个数,并结合for循环遍历数组:

#include <stdio.h>
int main()
{
    // 定义一个大小为10的int数组(初始化为0,方便观察)
    int arr[10] = {0};  
    // 计算数组的总大小(sizeof(arr))和单个元素的大小(sizeof(arr[0]))
    int arr_total_size = sizeof(arr);
    int arr_element_size = sizeof(arr[0]);
    // 计算数组的元素个数
    int arr_length = arr_total_size / arr_element_size;

    // 打印计算结果
    printf("数组总大小:%d 字节\n", arr_total_size);
    printf("单个元素大小:%d 字节\n", arr_element_size);
    printf("数组元素个数:%d\n", arr_length);

    // 使用计算出的元素个数遍历数组(循环条件为i < arr_length)
    printf("遍历数组(所有元素初始化为0):\n");
    for(int i = 0; i < arr_length; i++)
    {
        printf("%d ", arr[i]);
    }

    return 0;
}

运行结果如下:

数组总大小:40 字节
单个元素大小:4 字节
数组元素个数:10
遍历数组(所有元素初始化为0):
0 0 0 0 0 0 0 0 0 0 

从结果可以看出,通过sizeof计算出的数组元素个数是10,与数组的实际大小完全一致。

5.4 使用sizeof计算数组个数的注意事项

虽然sizeof计算数组元素个数非常方便,但在使用时需要注意一个关键场景——数组作为函数参数时,sizeof无法正确计算元素个数

这是因为在C语言中,数组作为函数参数传递时,并不会传递整个数组,而是只传递数组第一个元素的地址(即“数组名退化为指针”)。此时,在函数内部使用sizeof(数组名)计算的,实际上是“指针变量的大小”(32位系统下指针大小为4字节,64位系统下为8字节),而不是数组的总大小。

示例(错误场景):

#include <stdio.h>
// 函数参数为int类型数组
void print_arr_length(int arr[])
{
    // 错误:此时arr是指针,sizeof(arr)计算的是指针大小(4字节)
    int length = sizeof(arr) / sizeof(arr[0]);
    printf("函数内部计算的数组长度:%d\n", length);  // 结果错误
}

int main()
{
    int arr[10] = {0};
    int length = sizeof(arr) / sizeof(arr[0]);
    printf("主函数中计算的数组长度:%d\n", length);  // 结果正确(10)
    print_arr_length(arr);  // 传递数组名(退化为指针)
    return 0;
}

运行结果(32位系统):

主函数中计算的数组长度:10
函数内部计算的数组长度:1  // 错误,因为4/4=1

因此,若需要在函数中使用数组的元素个数,不能在函数内部通过sizeof计算,而应在数组定义的地方(如主函数)计算好后,作为参数传递给函数。

6. 二维数组的创建

二维数组是在一维数组的基础上扩展而来的,它可以看作是“由多个一维数组组成的集合”,常用于处理具有“行”和“列”结构的数据(如学生成绩表、矩阵、表格数据等)。掌握二维数组的创建方法,是处理复杂结构化数据的重要基础。

6.1 二维数组的概念

在理解二维数组之前,我们可以先回顾一维数组的概念:一维数组是“一条直线上排列的元素集合”,只有一个维度(下标)。而二维数组则是“一个平面上排列的元素集合”,有两个维度——“行”和“列”。

更严谨地说,二维数组的本质是“数组的数组”——即一个一维数组中的每个元素,本身也是一个一维数组。例如,一个3行5列的int二维数组,可以理解为:

  • 它首先是一个“包含3个元素的一维数组”;
  • 这个一维数组中的每个元素,又都是一个“包含5个int元素的一维数组”。

我们可以通过一个直观的表格来理解3行5列的二维数组(假设数组名为arr):

行/列列0(下标0)列1(下标1)列2(下标2)列3(下标3)列4(下标4)
行0(下标0)arr[0][0]arr[0][1]arr[0][2]arr[0][3]arr[0][4]
行1(下标1)arr[1][0]arr[1][1]arr[1][2]arr[1][3]arr[1][4]
行2(下标2)arr[2][0]arr[2][1]arr[2][2]arr[2][3]arr[2][4]

从表格可以看出,二维数组中的每个元素都需要通过“行下标”和“列下标”共同定位,这也是它与一维数组的核心区别。

6.2 二维数组的创建语法

在C语言中,创建二维数组的基本语法格式如下:

type arr_name[常量值1][常量值2];

其中,每个部分的含义和使用要求如下:

  • type(数据类型):与一维数组相同,用于指定二维数组中所有元素的具体类型,可是int、char、float、double等内置类型,也可以是自定义类型。选择type的依据是“数组要存储的数据类型”——比如存储学生成绩表(整数),用int;存储字符矩阵(如游戏地图),用char。

  • arr_name(数组名):二维数组的标识,用于在代码中引用该数组。命名规则与一维数组一致:遵循C语言标识符规则,且具备语义化——比如存储3个班级、每个班级5名学生成绩的数组,命名为class_scores,便于理解其用途。

  • 常量值1(行数):用于指定二维数组包含的“一维数组个数”,即二维数组的“行数”。它必须是常量或常量表达式(如3、2+1等),不能是变量(C99变长数组除外)。

  • 常量值2(列数):用于指定二维数组中每个“一维数组”包含的元素个数,即二维数组的“列数”。它同样必须是常量或常量表达式,且在任何情况下都不能省略(后续初始化部分会详细说明)。

6.3 二维数组创建的示例

为了让大家更直观地理解二维数组的创建,我们举几个实际场景的例子:

示例1:创建一个3行5列的int二维数组,存储3个班级各5名学生的数学成绩
int class_math_scores[3][5];  
// type:int(成绩为整数);arr_name:class_math_scores(班级数学成绩);
// 常量值1:3(3个班级,即3行);常量值2:5(每个班级5名学生,即5列)

这个数组包含3个“一维数组”,每个一维数组有5个int元素,总元素个数为3×5=15个,可存储15个学生的数学成绩。

示例2:创建一个2行8列的double二维数组,存储2个小组各8个实验数据的精度值
double group_experiment_data[2][8];  
// type:double(实验数据需高精度);arr_name:group_experiment_data(小组实验数据);
// 常量值1:2(2个小组,即2行);常量值2:8(每个小组8个数据,即8列)

这个数组包含2个“一维数组”,每个一维数组有8个double元素,总元素个数为2×8=16个,可存储16个实验数据的精度值。

示例3:创建一个4行3列的char二维数组,存储4个字符串(每个字符串最多3个字符,含结束符’\0’)
char str_array[4][3];  
// type:char(存储字符);arr_name:str_array(字符串数组);
// 常量值1:4(4个字符串,即4行);常量值2:3(每个字符串最多3个字符,即3列)

这个数组包含4个“一维数组”,每个一维数组有3个char元素,总元素个数为4×3=12个,可存储4个短字符串(如"ab"、"cd"等)。

6.4 二维数组与一维数组的区别

二维数组和一维数组虽然都是数组,但在结构和使用上有明显区别,核心区别如下表所示:

对比维度一维数组二维数组
维度数量1个维度(下标)2个维度(行下标、列下标)
元素定位方式仅需1个下标(如arr[3])需2个下标(如arr[2][4])
内存存储逻辑一条直线上的连续存储平面结构的连续存储(行优先)
适用场景单一维度的数据(如一组成绩)表格类数据(如成绩表、矩阵)

7. 二维数组的初始化

与一维数组类似,二维数组也可以在创建时进行初始化——即通过大括号{}为数组元素赋予初始值。由于二维数组有“行”和“列”两个维度,其初始化方式比一维数组更灵活,主要分为以下四种常见方式。

7.1 不完全初始化

“不完全初始化”指的是仅为二维数组中的部分元素指定初始值,未指定初始值的元素会由编译器自动赋予默认值(数值类型默认0,字符类型默认’\0’)。这种方式适用于大部分元素为0的场景(如初始化为零矩阵、空成绩表等),可以减少代码冗余。

二维数组的不完全初始化遵循“行优先”原则——即从第一行的第一个元素开始,依次为元素赋值,第一行填满后再填充第二行,以此类推,未填充的元素全部为默认值。

示例1:创建一个3行5列的int二维数组,仅为前2个元素赋予1和2,其余元素默认0:

int arr1[3][5] = {1, 2};  

初始化后,数组元素的实际值如下表所示(未赋值元素为0):

行/列列0列1列2列3列4
行012000
行100000
行200000

示例2:创建一个2行4列的float二维数组,为前3个元素赋予3.14、2.71、1.41,其余元素默认0.0:

float arr2[2][4] = {3.14, 2.71, 1.41};  

初始化后,数组元素的实际值如下表所示:

行/列列0列1列2列3
行03.142.711.410.0
行10.00.00.00.0

7.2 完全初始化

“完全初始化”指的是为二维数组中的每一个元素都明确指定初始值,初始值的总个数与数组的总元素个数(行数×列数)完全一致。这种方式的优点是元素值清晰可控,适用于数组大小较小且每个元素值都确定的场景(如已知的小矩阵、固定的表格数据等)。

完全初始化同样遵循“行优先”原则——初始值按“行顺序”依次排列,第一行的所有元素值在前,第二行的所有元素值在后,以此类推。

示例:创建一个3行5列的int二维数组,为所有15个元素赋予明确值(第一行15,第二行26,第三行3~7):

int arr3[3][5] = {1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7};  

初始化后,数组元素的实际值如下表所示:

行/列列0列1列2列3列4
行012345
行123456
行234567

这种方式虽然直观,但当初始值较多时,代码会显得冗长,且容易出错(如漏写、多写初始值)。因此,对于元素值有明显“行划分”的场景,更推荐使用“按行初始化”的方式。

7.3 按照行初始化

“按行初始化”是二维数组特有的初始化方式,它通过嵌套的大括号{{}} ,明确区分每一行的初始值——外层大括号包裹所有行的初始值,内层每个大括号包裹一行的初始值。这种方式的优点是结构清晰,便于阅读和维护,尤其适用于初始值有明显行划分的场景(如矩阵、成绩表等)。

在按行初始化时,若某一行的初始值个数少于列数,该行未赋值的元素会默认为0;若某一行没有初始值(即内层大括号为空),则该行所有元素都默认为0。

示例1:创建一个3行5列的int二维数组,按行指定初始值(第一行12,第二行34,第三行5~6):

int arr4[3][5] = {{1, 2}, {3, 4}, {5, 6}};  

初始化后,数组元素的实际值如下表所示(每行未赋值元素为0):

行/列列0列1列2列3列4
行012000
行134000
行256000

示例2:创建一个2行3列的char二维数组,按行初始化,第二行无初始值:

char arr5[2][3] = {{'a', 'b'}, {}};  

初始化后,数组元素的实际值如下表所示(第二行所有元素为’\0’):

行/列列0列1列2
行0‘a’‘b’‘\0’
行1‘\0’‘\0’‘\0’

7.4 初始化时省略行,但不能省略列

在二维数组初始化时,C语言允许我们省略“行数”(即常量值1),但绝对不能省略“列数”(即常量值2)。这是因为编译器需要通过“列数”来计算行数——行数 = 初始值总个数 ÷ 列数(若有余数,行数向上取整);若省略列数,编译器无法确定每行有多少个元素,也就无法正确分配内存,会导致编译错误。

省略行数的初始化方式,同样适用于不完全初始化、完全初始化和按行初始化。

示例1:不完全初始化,省略行数

创建一个int二维数组,列数为5,初始值为1、2、3,编译器自动计算行数:

int arr6[][5] = {1, 2, 3};  

初始值总个数为3,列数为5,因此行数 = 3 ÷ 5 = 0.6 → 向上取整为1。初始化后,数组实际为1行5列,元素值如下:

行/列列0列1列2列3列4
行012300
示例2:完全初始化,省略行数

创建一个int二维数组,列数为5,初始值总个数为10(2×5),编译器自动计算行数为2:

int arr7[][5] = {1,2,3,4,5, 6,7,8,9,10};  

初始化后,数组实际为2行5列,元素值如下:

行/列列0列1列2列3列4
行012345
行1678910
示例3:按行初始化,省略行数

创建一个int二维数组,列数为5,按行指定3行的初始值,编译器自动计算行数为3:

int arr8[][5] = {{1,2}, {3,4,5}, {6}};  

初始化后,数组实际为3行5列,元素值如下:

行/列列0列1列2列3列4
行012000
行134500
行260000
错误示例:省略列数

若省略列数,编译器无法确定每行元素个数,会直接报错:

int arr9[3][] = {1,2,3,4,5};  // 错误:省略了列数,编译器无法计算每行元素个数

上述代码在编译时,编译器会提示错误(如GCC编译器会提示“array type has incomplete element type ‘int[]’”),因为列数是计算每行元素个数的关键,不能省略。

8. 二维数组的使用

二维数组的使用核心是“通过行下标和列下标访问元素”,在此基础上可以实现元素的读取、修改、遍历(输入/输出)等操作。由于二维数组有两个维度,其遍历方式比一维数组更复杂,需要使用嵌套循环来实现。

8.1 二维数组的下标

与一维数组类似,二维数组的下标也用于定位元素,但二维数组需要“行下标”和“列下标”两个下标共同定位,这两个下标都遵循以下规则:

  • 下标起始值为0:行下标的起始值是0,列下标的起始值也是0。
  • 下标最大值为“维度大小 - 1”:若二维数组有M行N列,则行下标的范围是0M-1,列下标的范围是0N-1。访问超出该范围的下标,会导致数组越界,引发程序异常。

二维数组元素的访问格式为:数组名[行下标][列下标],通过这个格式,我们可以读取或修改数组元素的值。

示例:访问二维数组的指定元素

创建一个3行5列的int二维数组,访问并打印行下标2、列下标4的元素(即第三行第五列):

#include <stdio.h>
int main()
{
    // 定义并初始化一个3行5列的二维数组
    int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7}; 
    // 访问行下标2(第三行)、列下标4(第五列)的元素
    int target_element = arr[2][4];
    // 打印该元素的值(预期为7)
    printf("arr[2][4] = %d\n", target_element);

    // 修改行下标1(第二行)、列下标2(第三列)的元素值(从4改为40)
    arr[1][2] = 40;
    printf("修改后,arr[1][2] = %d\n", arr[1][2]);

    return 0;
}

上述代码的运行结果如下:

arr[2][4] = 7
修改后,arr[1][2] = 40

从结果可以看出,通过“行下标+列下标”,我们可以准确地访问和修改二维数组中的任意元素。

8.2 二维数组的输入和输出

二维数组的“输入”指的是通过键盘为数组元素赋值,“输出”指的是遍历数组并打印所有元素。由于二维数组有两个维度,输入和输出都需要使用嵌套循环——外层循环控制“行下标”,遍历每一行;内层循环控制“列下标”,遍历当前行的每一列。

8.2.1 二维数组的输入

二维数组输入的核心逻辑是:

  1. 外层循环:行下标从0开始,到“行数-1”结束,依次遍历每一行。
  2. 内层循环:针对当前行,列下标从0开始,到“列数-1”结束,通过scanf为每个元素赋值(注意使用&获取元素地址)。

示例:创建一个3行5列的int二维数组,通过键盘输入为其赋值:

#include <stdio.h>
int main()
{
    // 定义一个3行5列的int二维数组
    int arr[3][5];  
    int i = 0;  // 外层循环变量,控制行下标
    int j = 0;  // 内层循环变量,控制列下标

    printf("请输入3行5列的整数(每行5个,用空格或回车分隔):\n");
    // 外层循环:遍历3行(行下标0~2)
    for(i = 0; i < 3; i++)
    {
        // 内层循环:遍历当前行的5列(列下标0~4)
        for(j = 0; j < 5; j++)
        {
            // &arr[i][j]:获取当前元素的地址,scanf将输入值存储到该地址
            scanf("%d", &arr[i][j]);
        }
    }

    printf("输入完成!\n");
    return 0;
}

运行过程示例(输入3行5列的整数):

请输入3行5列的整数(每行5个,用空格或回车分隔):
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
输入完成!

输入完成后,数组arr中的元素会被赋值为输入的15个整数。

8.2.2 二维数组的输出

二维数组输出的核心逻辑与输入类似,只是将内层循环中的scanf替换为printf,用于打印当前元素的值。为了让输出结果更直观(符合二维数组的表格结构),通常会在每一行遍历结束后(即内层循环结束后),打印一个换行符\n

示例:承接上述输入示例,遍历数组并打印所有元素:

#include <stdio.h>
int main()
{
    int arr[3][5];  
    int i = 0;
    int j = 0;

    // (省略输入部分,与上述示例相同)
    printf("请输入3行5列的整数(每行5个,用空格或回车分隔):\n");
    for(i = 0; i < 3; i++)
    {
        for(j = 0; j < 5; j++)
        {
            scanf("%d", &arr[i][j]);
        }
    }

    // 输出部分:遍历数组并打印
    printf("你输入的3行5列整数如下:\n");
    for(i = 0; i < 3; i++)
    {
        // 内层循环:打印当前行的5个元素
        for(j = 0; j < 5; j++)
        {
            printf("%d ", arr[i][j]);  // 每个元素后加空格,使输出整齐
        }
        printf("\n");  // 当前行打印完毕,换行
    }

    return 0;
}

运行过程示例(输入与上述相同):

请输入3行5列的整数(每行5个,用空格或回车分隔):
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
你输入的3行5列整数如下:
1 2 3 4 5 
6 7 8 9 10 
11 12 13 14 15 

从输出结果可以看出,元素按“行”整齐排列,符合二维数组的表格结构,便于查看和验证输入是否正确。

8.2.3 结合sizeof动态遍历

与一维数组类似,我们也可以使用sizeof关键字动态计算二维数组的行数和列数,避免将行数和列数“写死”,提升代码的灵活性。

二维数组行数和列数的计算公式如下:

  • 行数 = sizeof(二维数组名) / sizeof(二维数组名[0]) → 二维数组总大小 ÷ 第一行的大小(每个行的大小相同)。
  • 列数 = sizeof(二维数组名[0]) / sizeof(二维数组名[0][0]) → 第一行的大小 ÷ 单个元素的大小。

示例:使用sizeof动态计算行数和列数,遍历二维数组:

#include <stdio.h>
int main()
{
    int arr[3][5] = {1,2,3,4,5, 6,7,8,9,10, 11,12,13,14,15};  
    // 动态计算行数:数组总大小 ÷ 第一行大小
    int rows = sizeof(arr) / sizeof(arr[0]);
    // 动态计算列数:第一行大小 ÷ 单个元素大小
    int cols = sizeof(arr[0]) / sizeof(arr[0][0]);

    printf("二维数组的行数:%d\n", rows);  // 输出3
    printf("二维数组的列数:%d\n", cols);  // 输出5

    // 动态遍历:循环条件使用rows和cols,而非固定值
    printf("动态遍历二维数组:\n");
    for(int i = 0; i < rows; i++)
    {
        for(int j = 0; j < cols; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }

    return 0;
}

运行结果如下:

二维数组的行数:3
二维数组的列数:5
动态遍历二维数组:
1 2 3 4 5 
6 7 8 9 10 
11 12 13 14 15 

这种方式的优势在于,当二维数组的行数或列数修改时,无需手动修改循环条件,代码会自动适应新的数组大小,大幅提升可维护性。

9. 二维数组在内存中的存储

与一维数组类似,二维数组在内存中的存储方式也是理解其特性的关键。虽然二维数组在逻辑上是“行和列”的表格结构,但在物理内存中,它依然是连续存储的,遵循“行优先”的存储规则。通过分析二维数组元素的地址,我们可以清晰地看到这一规律。

9.1 验证二维数组内存存储的代码

为了观察二维数组元素的地址分布,我们编写一段代码,通过嵌套循环打印每个元素的行下标、列下标和对应的地址:

#include <stdio.h>
int main()
{
    // 定义一个3行5列的int二维数组,初始化为0(便于观察)
    int arr[3][5] = {0};  
    int i = 0;  // 行下标循环变量
    int j = 0;  // 列下标循环变量

    // 嵌套循环:遍历每一行、每一列,打印元素地址
    for(i = 0; i < 3; i++)
    {
        for(j = 0; j < 5; j++)
        {
            // %d:输出行下标和列下标;%p:输出元素地址
            printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
        }
    }

    return 0;
}

9.2 二维数组元素的地址分布结果

在32位Windows系统的Microsoft Visual Studio编译器中,上述代码的运行结果(地址值可能因每次运行而不同)如下:

&arr[0][0] = 00CFF860
&arr[0][1] = 00CFF864
&arr[0][2] = 00CFF868
&arr[0][3] = 00CFF86C
&arr[0][4] = 00CFF870
&arr[1][0] = 00CFF874
&arr[1][1] = 00CFF878
&arr[1][2] = 00CFF87C
&arr[1][3] = 00CFF880
&arr[1][4] = 00CFF884
&arr[2][0] = 00CFF888
&arr[2][1] = 00CFF88C
&arr[2][2] = 00CFF890
&arr[2][3] = 00CFF894
&arr[2][4] = 00CFF898

9.3 二维数组内存存储的核心规律

通过分析上述地址分布结果,我们可以总结出二维数组在内存中的两个核心存储规律:

规律1:同一行内的元素连续存储,地址随列下标递增而递增

观察同一行(如行0)的元素地址:arr[0][0]的地址是00CFF860,arr[0][1]是00CFF864,arr[0][2]是00CFF868……相邻元素的地址差都是4字节(因为元素类型是int,占用4字节)。这说明在同一行内,元素是连续存储的,列下标每增加1,元素地址就增加4字节,与一维数组的存储规律完全一致。

规律2:跨行元素同样连续存储,地址随行下标递增而递增

观察跨行元素的地址(如行0的最后一个元素arr[0][4]和行1的第一个元素arr[1][0]):arr[0][4]的地址是00CFF870arr[1][0]的地址是00CFF874,两者的地址差也是4字节(与int类型的字节数一致)。这说明二维数组的“行”并非独立分段存储,而是遵循“行优先”的连续存储规则——行0的所有元素存储完毕后,紧接着存储行1的元素;行1存储完毕后,再连续存储行2的元素,整个二维数组在内存中是一个连续的“线性块”,不存在“行与行之间的内存间隙”。

以3行5列的int二维数组为例,其内存存储的线性顺序可表示为:
arr[0][0] → arr[0][1] → arr[0][2] → arr[0][3] → arr[0][4] → arr[1][0] → arr[1][1] → ... → arr[2][3] → arr[2][4]

9.4 二维数组连续存储的意义

二维数组的“连续存储”特性,与一维数组类似,是后续学习“指针访问二维数组”的关键基础,其核心意义体现在两点:

  1. 支持高效的指针访问:由于二维数组元素连续存储,我们可以将二维数组视为“长度为(行数×列数)的一维数组”,通过一个指针从数组起始地址开始,逐步自增(每次自增步长为元素类型字节数),即可遍历所有元素。例如,指向arr[0][0]的指针pp++后会指向arr[0][1]p += 5(跳过行0的5个元素)后会指向arr[1][0],这种访问方式在处理大型二维数组(如矩阵运算)时效率极高。
  2. 避免内存碎片:连续存储意味着二维数组的所有元素占用一块完整的内存空间,而非分散的多个小块,这能减少内存碎片的产生,提高内存利用效率,同时降低CPU缓存失效的概率(连续内存的缓存命中率更高)。

10. C99中的变长数组

在C99标准之前,C语言对数组的创建有严格限制——数组大小必须由“常量或常量表达式”指定,无法直接使用变量定义数组大小,这在处理“大小需运行时确定”的数据时(如用户输入的数值、文件读取的长度)显得非常不便。C99标准新增的“变长数组(Variable-Length Array,简称VLA)”特性,正是为解决这一问题而生。

10.1 传统数组的局限性(C99之前)

C99标准之前,创建数组时,[]中的大小必须是“编译时可确定的常量”,以下是合法与非法的示例对比:

数组定义合法性(C99前)原因分析
int arr1[10];合法10是常量,编译时可确定数组大小
int arr2[3+5];合法3+5是常量表达式,编译时可计算为8
int arr3[] = {1,2,3};合法初始化时可省略大小,编译器按初始值个数确定
int n=5; int arr4[n];非法n是变量,编译时无法确定数组大小

这种限制导致数组创建缺乏灵活性:若预估的数组大小过大,会浪费内存;若预估过小,又会导致数据无法存储(数组越界)。

10.2 变长数组(VLA)的核心特性

C99中的变长数组,允许使用“运行时可确定的变量”指定数组大小,其核心特性如下:

  1. 大小的动态确定性:变长数组的大小由变量决定,而变量的值需在程序运行时确定(如用户输入、函数返回值等)。例如:
    int a = 2, b = 3;
    int n = a + b;  // n的值在运行时计算为5
    int arr[n];     // 变长数组,大小为5(运行时确定)
    
  2. 不可初始化:由于变长数组的大小在运行时才确定,编译器无法在编译阶段为其分配固定的初始值空间,因此变长数组不能进行初始化(如int arr[n] = {1,2,3};是非法的)。
  3. 大小不可修改:“变长”仅指“数组大小由变量指定”,而非“数组大小可动态变化”。一旦数组创建(变量n的值确定),数组的大小就固定不变,后续无法通过修改变量n来改变数组大小。
  4. 编译器兼容性:需要注意的是,并非所有编译器都完全支持C99的变长数组特性——例如微软的Visual Studio 2022(及更早版本)虽支持大部分C99语法,但明确不支持变长数组;而GCC、Clang等编译器则完整支持该特性。

10.3 变长数组的使用示例(GCC编译器)

以下代码在GCC编译器中可正常运行,演示了通过用户输入确定变长数组大小、输入元素并打印的过程:

#include <stdio.h>
int main()
{
    int n = 0;
    // 步骤1:运行时获取用户输入,确定数组大小
    printf("请输入数组的大小(正整数):");
    scanf("%d", &n);  // 例如用户输入5,n的值为5

    // 步骤2:定义变长数组,大小为n
    int arr[n];
    int i = 0;

    // 步骤3:为变长数组输入元素
    printf("请输入%d个整数(用空格分隔):", n);
    for (i = 0; i < n; i++)
    {
        scanf("%d", &arr[i]);  // 读取用户输入的n个整数
    }

    // 步骤4:遍历并打印变长数组的元素
    printf("你输入的数组元素为:");
    for (i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }

    return 0;
}
运行示例1(输入大小5):
请输入数组的大小(正整数):5
请输入5个整数(用空格分隔):11 22 33 44 55
你输入的数组元素为:11 22 33 44 55 
运行示例2(输入大小10):
请输入数组的大小(正整数):10
请输入10个整数(用空格分隔):1 2 3 4 5 6 7 8 9 10
你输入的数组元素为:1 2 3 4 5 6 7 8 9 10 

10.4 变长数组的适用场景

变长数组主要适用于“数组大小无法在编译时确定”的场景,例如:

  • 处理用户动态输入的数据(如用户上传的文件内容长度、用户自定义的列表大小);
  • 科学计算中根据实验数据动态生成的数组(如测量次数由实验设备返回的数值决定);
  • 避免内存浪费(仅分配实际需要的内存空间,无需预估过大的固定大小)。

11. 数组练习

文档中提供了两个经典的数组练习案例,分别对应“字符移动动画”和“高效查找算法”,通过实践帮助巩固数组的使用技巧。

练习1:多个字符从两端移动,向中间汇聚

功能描述

定义两个字符数组:arr1存储目标字符串(如"welcome to bit..."),arr2存储占位符(如"#################");通过循环控制arr1的字符从arr2的“两端”向“中间”逐步替换占位符,配合Sleep函数实现动画效果,最终arr2完全变为arr1的内容。

核心思路
  1. 初始化两个字符数组:目标数组arr1(已知字符串)、占位符数组arr2(长度与arr1一致,全为#);
  2. 定义两个指针变量:left(初始指向数组起始下标0)、right(初始指向数组末尾下标strlen(arr1)-1);
  3. 循环替换:每次循环中,将arr1[left]arr1[right]的值赋给arr2的对应位置,然后left右移、right左移,直到left > right(所有字符替换完成);
  4. 动画效果:通过Sleep(1000)让程序暂停1秒(1000毫秒),每次替换后打印arr2,观察字符汇聚过程。
完整代码(需包含相关头文件)
#include <stdio.h>
#include <string.h>  // 用于strlen函数(计算字符串长度)
#include <windows.h> // 用于Sleep函数(Windows系统的延时函数)

int main()
{
    char arr1[] = "welcome to BOK...";  // 目标字符串
    char arr2[] = "#################";  // 占位符数组(长度与arr1一致)
    int left = 0;                       // 左指针:初始指向数组开头
    int right = strlen(arr1) - 1;       // 右指针:初始指向数组末尾(strlen计算字符串长度,不含'\0')

    printf("初始状态:%s\n", arr2);  // 打印初始占位符
    while (left <= right)             // 循环条件:左指针未超过右指针
    {
        Sleep(1000);                  // 延时1秒,观察动画效果
        arr2[left] = arr1[left];      // 左指针位置:用目标字符替换占位符
        arr2[right] = arr1[right];    // 右指针位置:用目标字符替换占位符
        left++;                       // 左指针右移,准备下一次替换
        right--;                      // 右指针左移,准备下一次替换
        printf("当前状态:%s\n", arr2); // 打印每次替换后的结果
    }

    return 0;  
}
运行效果(示例)
初始状态:#################
当前状态:w################.
当前状态:we##############..
当前状态:wel#############...
当前状态:welc###########g...
当前状态:welco#########sg...
当前状态:welcom#######msg...
当前状态:welcome#####tmsg...
当前状态:welcome #to msg...
当前状态:welcome to BOK...

练习2:二分查找(折半查找)

功能描述

升序排列的数组中查找指定的“关键字(key)”,通过“不断缩小查找范围”的方式提升效率。相较于“遍历数组逐个比较”(时间复杂度O(n)),二分查找的时间复杂度仅为O(log₂n),适用于数据量较大的升序数组。

核心思路
  1. 初始化指针:left(指向数组起始下标0)、right(指向数组末尾下标sizeof(arr)/sizeof(arr[0])-1);
  2. 计算中间下标:mid = left + (right - left) / 2(避免left+right数值过大导致溢出,优于mid=(left+right)/2);
  3. 比较与调整范围:
    • arr[mid] > key:关键字在左半部分,调整right = mid - 1
    • arr[mid] < key:关键字在右半部分,调整left = mid + 1
    • arr[mid] == key:找到关键字,标记并退出循环;
  4. 结果判断:循环结束后,若标记为“找到”,则输出关键字下标;否则输出“未找到”。
完整代码
#include <stdio.h>
int main()
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};  // 升序排列的数组
    int left = 0;                                   // 左指针:数组起始下标
    // 右指针:数组末尾下标(通过sizeof计算元素个数,再减1)
    int right = sizeof(arr) / sizeof(arr[0]) - 1;  
    int key = 7;                                    // 要查找的关键字(可修改)
    int mid = 0;                                    // 中间下标:每次循环重新计算
    int find = 0;                                   // 查找标记:0=未找到,1=找到

    // 循环查找:当左指针 <= 右指针时,查找范围有效
    while (left <= right)
    {
        mid = left + (right - left) / 2;  // 计算中间下标,避免溢出
        if (arr[mid] > key)
        {
            // 关键字在左半部分,缩小右边界
            right = mid - 1;
        }
        else if (arr[mid] < key)
        {
            // 关键字在右半部分,扩大左边界
            left = mid + 1;
        }
        else
        {
            // 找到关键字,标记并退出循环
            find = 1;
            break;
        }
    }

    // 输出查找结果
    if (find == 1)
    {
        printf("找到了,关键字%d的下标是:%d\n", key, mid);
    }
    else
    {
        printf("未找到关键字%d\n", key);
    }

    return 0;
}
运行示例
  1. 查找key=7(存在于数组中):
    找到了,关键字7的下标是:6
    
  2. 查找key=11(不存在于数组中):
    未找到关键字11
    
注意事项
  • 二分查找仅适用于升序(或降序)排列的数组,若数组无序,需先排序再查找(但排序会改变原数组元素的下标,需根据需求判断);
  • 当数组中存在多个相同的关键字时,二分查找只能找到“其中一个”(通常是中间位置的那个),无法定位所有相同元素的下标。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值