第七篇:C 语言数组全面解析与经典练习实战

本文Gitee——2025.5.27——:Blog code: 本仓库仅用于存放博客上的代码

这节课我们来讲解一下数组

一、数组基础概念

顾名思义,数组就是一组数,数组是由相同类型元素组成的集合,具有以下特点:

元素个数至少为 1 个。

所有元素类型相同。

数组分为一维数组和多维数组,常见的多维数组为二维数组。

如果你看过三体就一定对维度的概念记忆犹新,对应到C语言,元素就是0维,一维数组就是“线”,是一堆元素排成一排,二维数组就是“面”,是一堆一维数组排成一个“面”。

二、一维数组

(一)创建、初始化与数组类型

1.创建语法:type arr_name[常量值];

int math[20]---20个人的数学成绩
char ch[8]---包含8个字符的数组
double score[10]---10个双精度浮点数的数组

type:指定数组元素的数据类型(如 char、int 等)

arr_name数组名称,应使用具有实际意义的命名

常量值:定义数组元素数量,需根据实际应用需求确定

2.初始化方式

完全初始化:给定所有元素初始值。

int math[5]={1,2,3,4,5}
int arr[5] = {1,2,3,4,5}; // 每个元素依次初始化

不完全初始化:只初始化部分元素,剩余元素默认初始化为 0。

int math[5]={1}---第一个元素为1,其余为0
int arr2[6] = {1}; // 第一个元素为1,其余为0

错误初始化:初始化项数量超过数组大小。

int math[5]={1,2,3,4,5,6}---多了,一共就五个
int arr3[3] = {1,2,3,4}; // 错误,元素个数超出数组大小

省略数组大小:初始化时可省略数组大小,由初始值个数确定。

int math[]={1,2,3,4,5}---数组的大小为5
int arr[] = {1,2,3,4,5}; // 数组大小为5

这里要注意一点:

在 C 语言里,使用一维字符数组存储字符串常量时,会自动在结尾加上 \0 字符 。比如:

char ch[]="hello";
char str[] = "hello";

这里 "hello" 是字符串常量,编译器会在把它存储到 str 数组时,在 'o' 字符后面自动添加 \0 ,用来标识字符串结束。

但如果是通过逐个字符初始化数组,或者后续自行往数组写入字符等情况,若没有手动添加,数组结尾不会自动有 \0 。例如:

char ch[]={'h','e','l','l','o'}
char arr[5] = {'h', 'e', 'l', 'l', 'o'};

这里 arr 数组里就没有 \0 ,它只是普通字符数组,不是严格意义上的字符串 。要是后续想用类似 printf("%s", arr); 这样以字符串形式处理它,就会出错,因为程序找不到结束标识,可能会继续访问越界内存。 所以如果要将字符数组当作字符串用,通常得保证结尾有 \0 。

3.数组类型:数组类型由元素类型和数组大小共同决定,形式为type[大小]

int math[20];---类型为int[20]
char ch[10];---类型为char[10]
int arr1[10]; // 类型为int[10]
char ch[5]; // 类型为char[5]

(二)数组使用

1.下标规则:下标从 0 开始,若数组有 n 个元素,最后一个元素下标为 n-1,通过下标引用操作符[]访问元素.

int math[10]={1,2,3,4,5,6,7,8,9,10};
printf("%d\n",math[]);
printf("%d\n",math[]);
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("%d\n", arr[7]); // 输出8(下标7对应元素)
printf("%d\n", arr[3]); // 输出4(下标3对应元素)

上面我们知道,在 C 语言里,使用一维字符数组存储字符串常量时,会自动在结尾加上 \0 字符,那么在该字符数组中,字符串末尾的字符对应的下标该如何计算呢?

char ch[]={"asdfghjkl"}

前面我们说若数组有 n 个元素,最后一个元素下标为 n-1,我们这个字符串有14个字母,那n的下标是sizeof(jiewei)-1吗?不是,这是因为使用一维字符数组存储字符串常量时,会自动在结尾加上 \0 字符,因此n对应的下标=sizeof(jiewei)/sizeof(jiewei[0])-2,我们写个简单的代码运行一下:

#include <stdio.h>
int main()
{
	int s;
	char jiewei[] = { "abcdefghijklmn" };
	s = sizeof(jiewei) / sizeof(jiewei[0]) - 2;
	printf("%d", s);
	return 0;
}

运行一下,结果为13,证明我们上面的论述正确。

2.遍历数组:使用循环遍历数组下标,实现元素的输入 / 输出操作。

输出所有元素

int i = 0;
for(i = 0; i < 10; i++)
{
    printf("%d ", arr[i]);
}

  输入元素值

for(i = 0; i < 10; i++)
{
    scanf("%d", &arr[i]); // 通过地址修改元素值
}

(三)内存存储

数组在内存中连续存放,元素地址随下标递增而递增,相邻元素地址差等于单个元素大小(如int型数组相邻元素地址差 4 字节)。

地址打印示例

for(i = 0; i < 10; i++)
{
    printf("&arr[%d] = %p\n", i, &arr[i]); // 输出各元素地址
}

(四)计算元素个数

使用sizeof关键字计算数组总大小和单个元素大小,两者相除得到元素个数。

int arr[10] = {0};
int sz = sizeof(arr) / sizeof(arr[0]); // sz为10

三、二维数组

(一)创建与初始化

1.创建语法type arr_name[常量值1][常量值2];

int arr[3][5];  3行5列的整型数组
double data[2][8];  2行8列的双精度浮点型数组

常量值1代表行数,常量值2表示列数

2.初始化方式

不完全初始化:部分元素赋值,其余默认 0。

int arr1[3][5] = {1,2};  第一行前两个元素为1、2,其余为0,后续行全为0
int arr2[3][5] = {0}; 所有元素初始化为0

完全初始化:按行依次赋值所有元素。

int arr3[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7}; 每行5个元素,共3行

按行初始化:用嵌套大括号区分行。

int arr4[3][5] = {{1,2},{3,4},{5,6}}; 每行前两个元素赋值,其余为0

省略行数:必须指定列数,行数由初始化数据量决定。

int arr5[][5] = {1,2,3}; 1行3列,其余元素为0
int arr6[][5] = {{1,2}, {3,4}, {5,6}};  3行2列,其余元素为0

3.二维数组的类型

二维数组的类型由元素类型和行列维度决定,格式为 元素类型[行数][列数],例如:

int arr[3][5] 的类型是 int[3][5](3 行 5 列整型数组)。

(二)数组使用

下标规则:行下标和列下标均从 0 开始,通过arr[row][col]访问元素。

printf("%d\n", arr[2][4]);

示例:访问 3 行 5 列数组中第 2 行第 4 列元素(值为 7)。

遍历数组:使用双重循环,外层循环行,内层循环列。

输入 / 输出示例

 输入
for(i = 0; i < 3; i++)
{
    for(j = 0; j < 5; j++)
    {
        scanf("%d", &arr[i][j]);  输入每个元素
    }
}
输出
for(i = 0; i < 3; i++)
{
    for(j = 0; j < 5; j++)
    {
        printf("%d ", arr[i][j]);  输出每个元素
    }
    printf("\n"); 换行分隔行
}

(三)内存存储

二维数组在内存中仍连续存储,先存储第一行所有元素,再存储第二行,依此类推,相邻元素(包括跨行元素)地址差等于单个元素大小。

地址打印示例

for(i = 0; i < 3; i++)
{
    for(j = 0; j < 5; j++)
    {
        printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]); 输出各元素地址
    }
}

四、C99 变长数组(VLA)

特性:允许使用变量指定数组大小,数组长度在运行时确定,不可初始化。

示例

int n;
scanf("%d", &n);  输入数组大小
int arr[n]; 变长数组
for(i = 0; i < n; i++)
{
    scanf("%d", &arr[i]); 输入元素
}

注意:部分编译器(如 VS2022)不支持变长数组,建议使用 gcc 等编译器测试。

但是我就爱用VS2022,所以我不接受建议,那应该怎么做才能解决这个问题呢?

很简单,之所以VS2022不支持,是因为它默认使用的是msvc这个编译器,msvc不支持变长数组,但是苹果的clang是支持的,因此我们可以使用苹果的clang 

首先,在系统搜索这个,点击,

然后点击修改,

默认情况中,这个是不会勾选的,我们把它勾选上然后下载

下载结束后点开VS2022,右键点击你的项目名称,

点击平台工具集,再点击右边的下拉窗口,

点击这个LLVM(clang-cl),最后点击确定就可以了

现在你的VS2022就支持变长数组了。

五、数组经典练习

(一)字符汇聚效果

需求:从两端向中间逐步填充字符,逐渐打印出完整的字符串,一秒变化一次。

代码思路

整个过程超简单,分三个步骤就能搞定,保证你一看就懂:

第一步:准备材料

  1. 写一句你想说的话
    比如 "Hello, World!",把它存到一个数组里,就叫 target

    char target[] = "Hello, World!";
  2. 做一块 “彩票涂层”
    再准备一个数组,里面全是 # 号,长度要和刚才那句话一模一样。
    比如 "############",就像给真正的文字盖了一层 “涂层”

    char cover[] = "############";

第二步:从两边往中间 “刮涂层”

  1. 用两个手指(数组下标)标记位置

    int left = 0;  左手指(下标)从0开始
    int right = sizeof(target)/sizeof(target[0]) - 2;       
    右手指(下标)从最后一个字符开始(减2是因为字符串末尾有个'\0')
    
    • 左手指:从左边第一个 # 开始(下标是 0)。
    • 右手指:从右边第一个 # 开始(下标是 字符串长度 - 1,是sizeof(arr) / sizeof(arr[0])-2)。

右边的手指(字符数组下标)用sizeof(arr) / sizeof(arr[0])计算要减2!!!还记得我们之前说过“字符串的结尾隐藏着一个\0字符”吗?因此,字符串的sizeof(arr) / sizeof(arr[0])结果等于字符总数加一

这里,我们可以再次简单介绍一下strlen函数:

strlen 是 C/C++ 计算字符串长度的函数:

  • 原型:size_t strlen(const char *str);
  • 头文件:<string.h>(C)或 <cstring>(C++)
  • 功能:遍历字符串找 '\0' ,返回其前非空字符数,不含 '\0' 。
  • 注意:字符串需以 '\0' 结尾,不能用于未初始化指针 ,否则有未定义行为。

简言之:strlen是求除\0外的字符串的长度

示例:

#include <stdio.h>
#include <string.h>
int main() {
    char str[] = "Hi";
    size_t len = strlen(str);
    printf("长度:%zu\n", len); 
    return 0;
}

  1. 开始 “刮涂层”
    用一个循环,只要左手指还没碰到右手指,就一直重复下面三个动作:

    while (left <= right)
     {
        int left=0;
        int right=sizeof(target)/sizeof(target[0])-2;
        cover[left] = target[left];    左边“刮开”一个字符
        cover[right] = target[right];  右边“刮开”一个字符
        left++;                        左手指右移一格
        right--;                       右手指左移一格
        printf("%s\n", cover);         打印当前的样子
    }
    
    • 替换字符:把 target 里左手指和右手指对应的字符,填到 cover 里同样位置,替换掉 #
    • 移动手指:左手指往右挪一位,右手指往左挪一位。

第三步:让程序 “慢动作” 播放

为了让人眼能看清每一步变化,需要在每次打印后暂停一下。因此我们需要简单了解一下sleep函数:

  1. Windows 系统:

    • 头文件:windows.h
    • 原型:Sleep(毫秒数);
    • 示例:Sleep(1000); // 暂停 1 秒
  2. Linux/Unix 系统

    • 头文件:unistd.h
    • 原型:sleep(秒数);
    • 示例:sleep(1); // 暂停 1 秒

    用途暂停程序执行,常用于延时或节奏控制。
    注意:参数单位不同,会阻塞当前线程

    注意不同系统下函数名的大小写区别,Windows 下是 Sleep ,Linux/Unix 下是 sleep ,不要写错。

    #include <stdio.h>
    #include <windows.h>
    
    
    char target[] = {"womenbijiangchenggong"};
    char cover[] =  {"#####################"};
    int left = 0;
    int right = sizeof(target) / sizeof(target[0]) - 2;
    int main()
    {
    	while (left<=right)
    	{
    		Sleep(1000);停一秒,以便观察
    		cover[left] = target[left];
    		cover[right] = target[right];
    		printf("%s\n", cover);
    		left++;
    		right--;
    	}
    	return 0;
    }
    

    添加system("cls") 清屏:

    运行一下:

    但是现在有一个问题,我不想要看到一行一行的打印,我想要的是逐渐变化逐渐取代的效果,这就需要简单了解一下system("cls"); 

    system("cls"); 是 C/C++ 语言中调用系统命令的函数语句 。作用是清空当前 Windows 控制台窗口内容,并将光标移到窗口左上角,实现清屏效果 。其中:

    • system函数 :是 C 语言标准库 <stdlib.h> 中的函数,作用是让程序执行操作系统命令 。通过它可调用系统命令来完成一些操作,像清屏、查看目录等 。
    • "cls" :是 Windows 命令提示符(CMD)里的清屏命令,是 “Clear Screen” 的缩写 。

    成品:(用上strlen函数)

    #include <stdio.h>
    #include <windows.h>
    #include <string.h>
    
    char target[] = { "womenbijiangchenggong" };
    char cover[] =  { "#####################" };
    int left = 0;
    
    int main()
    {
    	int right = strlen(cover) - 1;
    	while (left <= right)
    	{
    		
    		Sleep(1000);停一秒,以便观察
    		system("cls");
    		cover[left] = target[left];
    		cover[right] = target[right];
    		printf("%s\n", cover);
    		left++;
    		right--;
    	}
    	return 0;
    }
    
    

    运行之后,成功实现,哎这个视频添加进来太麻烦了,我搞了半天都没搞定,只能加一个截图了

    可以看到现在只有一行了

    全局变量的初始要求

    而如果我们细心一点就可以发现:left和right的定义在不同的位置,这是我有意安排的,如果按照这张图片书写就会报错:

    错误原因分析

    错误提示 E0059 常量表达式中不允许函数调用 和 E0028 表达式必须含有常量值 ,是因为在 C 语言中,数组的大小在编译期一般要求是常量表达式 。而我在第 63 行 int right = strlen(cover)-1; 中,使用 strlen 函数来计算数组大小并初始化变量 right ,strlen 函数调用是在运行期才能确定结果的,不是编译期常量,不符合语法规则。

    解决方案

    把计算 right 的操作放到 main 函数内部,这样在运行时进行计算就不会有问题了

    简言之:这个创建right时放到了全局,而全局变量初始要求给定的是常量,而这个strlen(cover)-1表达式不是常量,就报错了,将这一行代码放到main函数中就好了

    (二)二分查找

    需求:在升序数组中查找指定值,返回下标或提示未找到。

    代码思路

    定义左右指针,初始分别指向数组首尾。

    计算中间下标,比较中间元素与目标值,调整左右指针范围,直至找到或确定不存在。

    这个很简单,只用找到中间的数值,比较中间数值和要查找数的大小,如果大了就取左半段再次二分查找,如果小了就取右半段再次二分查找,以此类推,不断进行二分查找的操作可以用while循环做到,查找到了就停止并跳出循环,查找不到就一直查找到左下标大于右下标时,此时所有数据都排查完毕,循环结束并输出:“找不到”

    不过当while循环结束时,有两种情况,一种是找到了跳出循环,一种是找不到,循环判断条件为假时自然结束,怎么区分到底找没找到?可以利用标记,这个思想在上一篇博客的“打印123~456之间的素数”有过应用,这里同样可以使用,如果找到了,在跳出循环之前改变标记,在循环外检验标记是否被改变,如果改变了,就输出“找到了”,如果没有改变就输出:“找不到”。至此,思路彻底理清。

    第一步:确定查找范围和比较方式

    我们要有一堆排好序的数据。每次都先找到这堆数据中间那个数,然后和要找的数比大小:

    • 要是中间数比要找的数大,那目标数肯定在左边这半段,我们就对左半段继续二分查找。
    • 要是中间数比要找的数小,目标数就在右边这半段,接着对右半段二分查找。

    第二步:用 while 循环实现查找

    用 while 循环一直做上面的操作。只要左下标不大于右下标,就一直找。找到了就直接跳出循环;要是一直找不到,等左下标大于右下标了,所有数据都查过了,循环就结束,输出 “找不到”。

    第三步:用标记判断是否找到

    循环结束后,可能是找到了跳出的,也可能是没找到,循环条件不满足才结束的。咱用个标记来区分:

    • 要是找到了,在跳出循环前把标记改了。
    • 循环结束后检查标记,标记变了就输出 “找到了”;标记没变,就输出 “找不到”。

    一步一步实操:

    注意:虽然在 C 语言标准里用中文命名变量并非语法错误,但不符合常规代码编写规范,会降低代码的可读性和可维护性,也可能在不同开发环境中出现兼容性问题。但是我为了让初学者理解容易,在讲解过程中都用中文命名变量,直到最终成果时再改为英文

    #include <stdio.h>
    
    int 升序数组[] = {0,1,2,3,4,5,6,7,8,9};
    int main()
    {
    	int left = 0;
    	int right = sizeof(升序数组)/sizeof(肾虚数组[0])-1;
    
    
    
    
    
    	return 0;
    }
    
    

    到这一步之后我们需要细想一下:还需要什么变量?首先需要一个目标值并将其初始化为0~9的一个数字;其次还要一个中间值变量用来和目标值对比,同时还要有一个标记变量来确认循环结束时候的情况,我们将这些补齐:

    #include <stdio.h>
     
    int 升序数组[] = {0,1,2,3,4,5,6,7,8,9};
    int main()
    {
    	int left = 0;
    	int right = sizeof(升序数组)/sizeof(升序数组[0])-1;
    
    	int 目标值 = 7;
    	int 中间值 = 0;
    	int 标记 = 0;
    
    
    	return 0;
    }
    
    

    接着,就该编写while循环了,根据上面的思路分析,我们在没找到的时候要核查所有数据,而left和right是数组的下标,那么很容易想到while循环判断条件是:left<=right。这一步解决之后,根据我们的思路,此时就该处理中间值中间值=(left+right)/2,随后比较升序数组[中间值]目标值的大小

    #include <stdio.h>
     
    int 升序数组[] = {0,1,2,3,4,5,6,7,8,9};
    int main()
    {
    	int left = 0;
    	int right = sizeof(升序数组)/sizeof(升序数组[0])-1;
    
    	int 目标值 = 7;
    	int 中间值 = 0;
    	int 标记 = 0;
    	while (left <= right)
    	{
    		中间值 = (left + right) / 2;
    		if (升序数组[中间值] < 目标值)
    			;
    		else if (升序数组[中间值] > 目标值)
    			;
    		else
    		{
    			标记 = 1;
    			break;
    		}
    	}
    	if (标记 == 1)
    		printf("找到了,该目标数值在该升序数组中");
    	else
    		printf("找不到,该目标数不在该升序数组中");
    	return 0;
    }
    
    

    现在我们只需要编写while循环里面的嵌套if中控制的语句了,当(升序数组[中间值] < 目标值)时,此时舍弃左边的数组部分,那只需要将left=中间值+1;即可,这样下次循环中就会直接对右半部分开始二分查找,同理,当(升序数组[中间值] > 目标值)时,此时舍弃右边的数组部分,那只需要将right=中间值-1;即可,这样下次循环中就会直接对左半部分开始二分查找

    让我们看一下

    成品:

    #include <stdio.h>
    
    int ARR[] = {0,1,2,3,4,5,6,7,8,9};
    int main()
    {
    	int left = 0;
    	int right = sizeof(ARR)/sizeof(ARR[0])-1;
    	int target = 7;
    	int mid = 0;
    	int flag = 0;
    	while (left <= right)
    	{
    		mid = (left + right) / 2;
    
    		if (ARR[mid] < target)
    			left=mid+1;
    		else if (ARR[mid] > target)
    			right=mid-1;
    		else
    		{
    			flag = 1;
    			break;
    		}
    	}
    	if (flag == 1)
    		printf("找到了,该目标数值在该升序数组中,下标是%d",mid);
    	else
    		printf("找不到,该目标数不在该升序数组中");
    	return 0;
    }

    运行一下:

    OK,成功。

    六. 总结

    本文围绕 C 语言数组展开全面介绍:

    数组概念:数组是相同类型元素集合,分一维和多维,以二维常见。

    一维数组:创建需指定类型和元素数量;初始化有完全、不完全等方式,字符数组存字符串会自动加\0 ;通过下标访问元素,下标从 0 开始,可利用循环遍历 ;在内存连续存储,用sizeof 计算元素个数。

    二维数组:创建指定行列数;初始化方式多样,类型由元素类型和行列维度决定;行列下标均从 0 开始,用双重循环遍历,内存中连续存储。

    C99 变长数组:允许变量指定大小,运行时确定长度,不可初始化,部分编译器不支持,介绍了 VS2022 下利用 clang 支持的方法。

    经典练习:字符汇聚效果通过从两端向中间填充字符实现,借助Sleep 控制节奏、system("cls") 清屏 ;二分查找在升序数组中找指定值,用左右指针和中间值不断缩小区间,结合标记判断是否找到。

    嗯,希望能够得到你的关注,希望我的内容能够给你带来帮助,希望有幸能够和你一起成长。

    写这篇博客的时候天气很好,我走到阳台拍下了一张宿舍对面的照片作为本文的封面。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值