本文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就支持变长数组了。
五、数组经典练习
(一)字符汇聚效果
需求:从两端向中间逐步填充字符,逐渐打印出完整的字符串,一秒变化一次。
代码思路:
整个过程超简单,分三个步骤就能搞定,保证你一看就懂:
第一步:准备材料
-
写一句你想说的话
比如"Hello, World!"
,把它存到一个数组里,就叫target
char target[] = "Hello, World!";
-
做一块 “彩票涂层”
再准备一个数组,里面全是#
号,长度要和刚才那句话一模一样。
比如"############"
,就像给真正的文字盖了一层 “涂层”。char cover[] = "############";
第二步:从两边往中间 “刮涂层”
-
用两个手指(数组下标)标记位置
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;
}
-
开始 “刮涂层”
用一个循环,只要左手指还没碰到右手指,就一直重复下面三个动作: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函数:
-
Windows 系统:
- 头文件:
windows.h
- 原型:
Sleep(毫秒数);
- 示例:
Sleep(1000);
// 暂停 1 秒
- 头文件:
-
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")
清屏 ;二分查找在升序数组中找指定值,用左右指针和中间值不断缩小区间,结合标记判断是否找到。
嗯,希望能够得到你的关注,希望我的内容能够给你带来帮助,希望有幸能够和你一起成长。
写这篇博客的时候天气很好,我走到阳台拍下了一张宿舍对面的照片作为本文的封面。