C语言教程-10-数组
什么是数组
数据往往不是各不相关的,我们需要处理的数据往往是一系列大量的相同类型的值,有着完全相同行为和作用.
例如一个班级的所有学生的各个学号,它们都是一个个固定长度的正整数,均用于唯一识别一名学生;
再例如一家超市的所有商品名,它们都是一个个的字符串.
对于这些同类型的,并且往往是大量的数据,我们显然不能像过去那样分别声明一个个单独的变量去存储:
1 |
|
显然,这样的存储非常繁琐,并且限制非常大,各个变量都是互相独立的(尽管他们的变量名都相似),如果我们重新编排学号,例如从100开头变为200开头,我们只能一个个的去设置:
1 |
|
这样显然不现实,幸运的是,高级语言提供了各种用于存储这种一系列值的功能.
C语言中使用数组
来存储大量的相同类型的值,我们可以在声明数组的时候设置其长度,即可以存储的具体数量:
1 |
|
C语言的数组
事实上,数组是线性分配的,也就是说,一个数组的每个元素在内存中是紧挨着的存储(分配)的.
例如一个长度为10的int数组,一共占用4x10个,也就是40个字节,其中4为一个int变量占用的空间.
下面介绍数组的最基本的内容.
已知常量大小数组
C语言中一般的数组(已知常量大小数组)
的声明语法如下:
1 | <元素类型> <数组名>[数组长度]; |
-
元素类型
可以是任意基本类型,例如
int
,double
,char
等也可以是像结构体这样的自定义类型
元素类型指定了整个数组的每一个元素的类型
-
数组名
数组名一样也是标识符,同样要遵循标识符的命名规范,和单个变量一样,数组名唯一标识了整个数组
-
数组长度
数组长度必须是
整数常量表达式
,说简单点,必须是能直接算出来的常数,而且必须是整数例如
2*3
,100
这些都是允许的,反之,2*i
这样的表达式就是不允许的(但不是绝对,后面提到的VLA就允许)
对数组元素进行访问十分简单:
1 | <数组名>[下标] |
C语言中(所有的编程语言)数组的下标都是从0开始的,这种设计十分合理,读者自行百度了解
我们要访问数组a的第1个元素,则它的下标(也就是编号)就是0,我们要访问它,只需要a[0]
即可
例如,我可以定义长度为10的int数组来存1~10这10个数:
1 |
|
输出结果为:
上面这种使用循环变量对数组进行逐个的访问的方法叫做遍历
,顾名思义,就是一个一个有序地对数组这个序列进行访问,我们可以在遍历时仅仅获取它们的值,或者可以对它们进行修改.
对数组进行初始化
在上一节的例子中,我们使用循环对数组的每一个元素进行了赋值.
我们也可以在声明时就使用一些特定的值对数组进行初始化
,这里涉及到初始化器
这个概念,具体可以自行百度了解.
初始化列表
我们在声明时对数组的各个元素进行初始化,可以使用初始化列表
来实现,语法如下:
1 | <元素类型> <数组名>[数组长度] = {表达式,...}; |
可以看到,除了原先的声明,我们在[]
后面紧跟一个={}
,其中包含有若干以,
分隔的表达式,这些表达式的值用于对数组的每个元素依次进行赋值.
例如,我们上面的例子可以改写为:
1 |
|
运行结果不变.
注意:
-
{}初始化列表
中的表达式个数不能多于数组元素的个数(数组长度),它们是一一对应来赋值的 -
如果表达式个数少于数组元素的个数,则后面的值被填充为整型的0或浮点型的+0
需要注意的是,自C23起,C语言才支持了使用
={}
这样的空初始化器
来达成与 C++ 中的值初始化相同的语义 -
特别常用于int数组,我们可以使用
={0}
来用0填充整个数组,不过,对于浮点型,个人建议还是不要依赖这种填充 -
如果没有对数组进行初始化,那么数组的各个元素将会是垃圾值,我们必须对其赋初值后才能"使用"它们.
注意:放在全局变量的数组和普通变量一样,会用0去填充,而不是垃圾值
指定初始化器
C99新增了一个指定初始化器
的特性,这让我们可以初始化指定的数组元素:
1 |
|
运行结果:
对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为0.
并且需要注意上面的例子中,[5]之后的6,7继续顺延到对[6],[7]进行赋值.
未知大小数组
如果我们忽略数组长度,那么这就是一个不完整的类型.关于这个问题不好解释,可以去找标准参阅.
如果我们这样写,我们需要使用初始化列表来进行初始化,以便编译器确定数组的长度,否则,编译器会因无法得知数组的大小而无法分配空间,导致报错.
初始化列表
中表达式的个数就会成为数组长度,另一方面,如果使用了指定初始化器
,则会保证数组能够容纳下所有的元素,例如有={[5]=3}
,则数组的长度为6,刚好能容纳下元素[5].
例如:
1 |
|
数组a的长度为10.
字符数组的初始化
字符数组的初始化有个特例,我们不仅可以像这样正常的为字符数组进行逐个赋值:
1 |
|
我们还可以直接使用一个字符串常量对其进行赋值:
1 |
|
效果是一样的,因为逐个赋值依旧是初始化列表,后面的'\0'
字符串结束标志,也就是整数0同样被默认填充.
而使用字符串常量,进行的是复制操作,结尾的'\0'
同样会被添加.
关于这方面的内容,后面讲解字符串时会进行详细讨论.
非常量长度数组
必须首先强调的是,非常量长度数组
,或者叫变长数组
,再或者缩写为VLA
,目前的兼容性不好,例如VS默认的msvc编译器就不支持这种用法.
另外VLA
是在C99引入的.
简单的说就是可以使用变量来声明数组,不是很建议使用,这里仅给出一个简单的例子:
1 |
|
运行结果:
二维数组
二维数组实际上,就是"数组的数组",即一个数组的每个元素都是数组.读者自行想象,最终结果就是实现一个NxM的矩阵.
有些问题光有一维(线性)的数组是不够的,我们需要"二维的"空间来存储,例如一张迷宫的地图.此时,就需要所谓的二维数组.
必须指出的是,所谓的二维,包括可能用到的更多维数组,都是逻辑上的划分,实际上数组全部都是线性的,也就是说,它们仍然在内存上排成一列,只不过是把这一长串的内存划分成几个大块,每一块都是一个子数组,作为整个数组的一个元素来使用.
二维数组的声明
我们十分容易能够将一维推广到二维:
1 | int a[3][4]; |
这将声明一个"3行4列"的二维数组,类似这样:
事实上,在底层,它仍然是一段连续的内存单元,也就是长为3x4xsizeof(int),也就是3x4x4=48个字节的连续内存:
只不过我们从逻辑上将其转换为一个相对"立体"的矩阵而已,底层上,仍然是线性存储的.
所以有一句话:“C语言没有二维数组”,这句话就是针对C的底层逻辑来描述的,当然,这不影响我们简单地将其视为二维来解释,事实上,既然C能够有这种写法,当然就是为了满足我们对多维数组的需求.
关于这方面的详细内容,我们将会在学习指针之后展开详细讨论(注:这是难点)
回到声明,我们可以看到,第一个[]代表第一维(可以理解为行),第二个[]代表第二维(可以理解为列),int a[3][4]
就声明了一个3行4列的二维数组.
对其访问也十分简单,例如我们要访问第2行第3列的元素,其下标为[1][2]
—下标仍然从0开始
则该元素就是a[1][2]
二维数组的初始化
对于初始化列表,同样可以应用于二维数组,我们既可以直接用一个花括号初始化所有的元素:
1 | int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; |
我们还可以进行嵌套,这样更加直观,可以很容易地看出我们正在初始化的是哪一行:
1 | int a[3][4] = { |
这两种写法等价,不过建议使用第二种方法,更为清晰.
例如我们可以输出1,2,3,4,…,12这些数组成的3x4的矩阵:
1 |
|
运行结果:
至于多维数组,均同理:
1 | int a[3][4][5]; // 声明一个三维数组 |
但是一般用的比较少,大多数情况下,二维数组就已经能满足各种需求了.
本章简单讲解了数组的使用方法,至于更加深入的内容,在没有讲解指针之前,都无法特别清楚的进行讨论.
---WAHAHA
下一篇:C语言教程-11-字符串