1. 数组的作用
如果一个程序中需要同时定义一系列数据,单独分开定义显然是不合适的,这时候就需要用到数组。
编程语言设计需要考虑多个数据和变量如何处理,从下面三个方面来入手:
- 数据组织
- 资源管理
- 性能优化
其中最简单的一种方式就是数组。
2. 数组的初步使用
数组的定义就是在变量后面加一个方括号,里面填写数组的大小,例如:
1 | uint32_t numbers[10]; |
这样就定义了一个大小为10的数组,里面有十个数,每个数都是uint32_t
类型。
数组的初始化是使用一个大括号,里面分别填写数组中的每个值,用逗号隔开:
1 | uint32_t numbers[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; |
数组中的元素的取用,需要用到下标,数组的下标是从0
开始的,因此如果要取数组中的第五个数打印输出,就需要打印输出numbers[4]
:
1 | printf("numbers[4] = %" PRIu32 "\n", numbers[4]); // numbers[4] = 5 |
数组(Array)中特定下标(Index)中存放的值称为元素(Element)
数组的内存分布(Memory Layout)是连续不断的一片内存区域,中间不能有截断
如果要将数组中的所有元素都输出,需要用到for
循环:
1 | for (uint8_t index = 0; index < 10; index++) { |
如果要对数组中的某些元素进行修改,直接通过指定下标方式修改即可:
1 | numbers[4] = 666; |
3. 数组定义时下标需要常量
C语言程序在编译的时候,就要求此时数组的长度为一个确定的已知的常量。
因此定义数组时需要注意,数组的长度必须为常量,将使用scanf_s()
取到的一个值作为数组定义的长度值时,程序会直接报错。
1 | // 下面的写法会报错 |
甚至直接将定义并初始化的一个量作为数组的长度,或是将定义的一个常量值作为数组的长度,也同样会报错。
1 | // 下面的两种写法都会报错 |
这是因为C语言程序在编译时,使用const
定义的数据在编译的时候并不是常量,只是这个值会在这个局部的函数中被固定下来。
不过,定义数组时可以使用宏定义:
1 |
|
注意,此时的数组并没有被初始化,而在程序中使用时,和变量一样,一定要保证数组被正确初始化,这样程序在使用数组时才可以拿到正确的值。
最简单的初始化是大括号内只写一个
0
,这样所有元素的值都会被初始化为0
。
1 uint32_t numbers[10] = { 0 };但是当大括号里的这个值为其他不为零的值,只会将数组中的第一个元素初始化为这个值,其他元素的值都为
0
。因此,如果要将数组中的所有元素初始化为1,应该在大括号里面一一写出所有元素的初始化值。
4. 数组的注意事项
错误1:访问越界
如果定义数组时候的长度为n
,因为数组的下标是从0开始的,则数组中下标的最大值只能为n-1
,称为数组的边界(Boundary),如果访问数组的下标超过这个值,编译器会发出警告,访问到的值也会是一个随机的无意义值。
错误2:未初始化数组
定义数组时候必须初始化,访问未初始化的数组可能会得到一个随机的无意义值。
错误3:数组大小错误
定义数组时的长度必须是不为0的正整数,长度写负数或其他不符合要求的数时会报错。
警告1:部分初始化
一般来说,初始化数组要么使用{ 0 }
来将数组中的所有元素初始化为0,要么就在大括号中写清楚所有元素的初始化值,一定要避免出现部分初始化的情况,部分初始化时,未被初始化的那部分元素可能是意料之外的值而导致错误。
提示:总是初始化数组
初始化数组时,要习惯性的对数组进行初始化,例如下面这种写法:
1 | uint32_t numbers[10] = { 0 }; |
后面学到字符串时候,char*
类型的数组也必须初始化:
1 | char* str[10] = { "" }; |
5. 案例:分数
要求:
- 已知有五个学生的成绩(85、92、98、73、87),打印出所有学生的成绩
- 求出所有学生的总成绩、平均成绩
- 找出最高分和最低分
代码实现:
1 |
|
6. 案例:天气
要求:
- 给出一周七天每天的最高温度分别为:20、22、19、26、23、21、25
- 打印一周内每天的最高温度
- 求出这一周内最高温度的总和、平均值
- 找出最热和最冷的一天的最高温度
代码实现:
1 |
|
7. 案例:账户存款取款
要求:
- 模拟账户的存款取款操作,假设账户的金额都为整型
- 假设一共有三个账户,其中的金额分别为15000、18000、26000
- 第一个账户存款5000元,第二个账户取款2000元
- 在进行存取款操作前后分别打印出每个账户的金额数和总金额数
代码实现:
1 |
|
8. 案例:字母次数统计
要求:
- 给出一句固定的话,例如:“Example text for frequency analysis.”
- 程序自动统计这句话中每个字母出现的次数,并打印出来
- 任意找出出现频率最高和最低的字母中的一个
代码实现:
1 |
|
在C语言中,数组的长度可以在定义时不指定,编译器会根据初始化的元素数量自动推断长度。因此在定义数组时方括号中可以不写长度值,前提是必须对数组进行初始化,大括号中必须有数组初始化的数据。
也有动态数组这个概念,属于C语言新特性的范畴,放在最后面来介绍。
9. 二维数组
前面只有一个方括号定义的数组都称为一维数组,它其实是一种特殊的二维数组,一维数组只有一行,有n列,二维数组可以有n行,m列。
定义二维数组的方式非常简单,写两个方括号就行,需要注意的是二维数组初始化的标准写法,用于提高代码的可读性。例如定义一个五行六列的数组:
1 | uint32_t numbers[5][6] = { |
需要注意的是,数组下标的第一个数字指的是行,第二个数字指的是列,且行和列都是从0开始计算的。
例如取出数组中第3行第5列的元素:
1 | printf("numbers[2][4] = %" PRIu32 "\n", numbers[2][4]); // 输出:17 |
同样的,二维数组也可以使用for
循环遍历数组中的所有元素,只是此时应该使用两个for
循环嵌套来实现:
1 | for (uint32_t i = 0; i < 5; i++) { |
10. 隐式确定数组大小的初始化
对于一维数组:
数组定义时,如果不指定数组的长度,这时候的数组就是隐式定义,此时数组中所有的元素都应该被初始化,编译器会自动根据初始化的元素的个数来确定数组的长度。
因此,隐式确定数组时不可以使用{ 0 }
为所有的元素初始化为0,未指定长度时编译器不知道有多少元素被初始化为0,因此使用{ 0 }
隐式初始化时,编译器默认此时数组内只有一个为0的元素,访问下标为1的元素时就会出现数组越界的问题。
二维数组的情况比较特殊:
二维数组同样也可以被隐式定义,但此时可以不指定长度的下标只能为行数(第一个下标),列数(第二个下标)必须在定义时明确给出。
11. Unicode字符编码与wchar.h
、locale.h
接下来将制作一个五子棋棋盘的案例,有一些扩展知识需要提前了解。
计算机中的所有字符的显示都是通过字符编码实现的,字符编码的类型有很多,常见的有UTF-8、GBK等,GBK是“汉字内码扩展规范”的简称,主要服务于汉字的编码,而UTF-8属于Unicode编码的一种,Unicode是信息技术领域的业界标准,其整理、编码了世界上大部分的文字系统,使得电脑能以通用划一的字符集来处理和显示文字,不但减轻在不同编码系统间切换和转换的困扰,更提供了一种跨平台的乱码问题解决方案。
其具体编码方式等扩展知识可以自行搜索资料了解。
wchar.h
是C标准函数库中的头文件,提供了对宽字符的支持。宽字符(Wide character)是计算机抽象术语(没有规定具体实现细节),表示比1字节还宽的数据类型。不同于Unicode。
locale.h
是C程序设计语言标准函数库的一个头文件,声明了C语言本地化函数。这些函数用于在处理多种自然语言的软件编程设计时,把程序调整到特定的区域设置。这些区域设置影响到C语言标准库的输入/输出函数。
例如:
不同国家和区域对于小数点的处理是不同的,有的是1.234,56
,有的是1,234.56
;
不同国家的货币符号也有不同,例如美元($)、人民币(¥)、欧元(€)、英镑(£)等;
不同地区的时间表示也不同,例如中国一般是年/月/日 00:00 24h/12h制
,美国一般是MM/DD/YYYY
,欧洲一般是DD/MM/YYYY
;
不同国家和地区比较字符串的顺序时候的规则不同。
12. 案例:五子棋棋盘绘制
要求:
- 程序默认输出一个正方形的棋盘,每个位置有一个占位符,需要模拟两个人下棋的过程;
- 第一个用户通过输入一个坐标来下棋,判断输入的坐标合理并下棋成功后,在棋盘的对应位置将占位符换成一种棋子;
- 第二个用户还是通过输入一个坐标来下棋,不过这次下棋的棋子和第一次有所不同;
- 每次下棋后都会询问用户是否结束棋局;
- 只需要模拟棋盘和下棋的过程,不需要有判定输赢的逻辑。
代码实现:
一种比较基础的实现方式为:
1 |
|
这种实现方式较为简单,但是有几个问题:
- 每次打印棋盘都会使控制台输出向下滚动,可以在每次打印棋盘前使用头文件
Windows.h
中的system("cls");
进行清屏操作; - 玩家1下棋和玩家2下棋的逻辑几乎一样,没必要分开写,可以使用
? :
语句将两者合并起来,虽然这样会导致每次下完棋都需要确认是否继续,但本案例只作为演示。
改进后的程序如下:
1 |
|
同样的,这个改进后的程序也存在几个问题:
- 没有对
scanf_s()
的输入做检查和清空缓冲区的操作,当输入字符等非法值时,可能导致程序陷入死循环; - 目前的棋盘占位符和棋子都是用常规字符表示的,不太美观,可以使用前一节提到的头文件
wchar.h
和locale.h
来将棋盘和棋子变为宽字符。
改进后的最终程序如下:
1 |
|
13. 案例:农场作物成长
要求:
- 模拟一片矩形菜地,每个位置都有50%的概率有一颗未成熟的农作物;
- 在5秒内,每隔1秒里面的每个农作物都会有30%的概率成熟;
- 可以在初始化时候指定一些空地有石头,无法种植作物。
代码实现:
1 |
|
14. 第六章结束语
后面学函数和指针时也会用到数组。
对数组的操作无非就是和数据库类似的CRUD(增删改查)还有排序。
变量命名一定要养成规范的习惯。
多写!多练习!
本文链接: https://hanqingjiang.com/2025/07/25/20250725_C_array/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
