数组
C语言包含4种基本数据类型:
- 整型
- 浮点型
- 指针
- 聚合类型
- 数组
- 结构
- ……
之前的文章中,我们已经学习了前面三种基本数据类型。
今天我们将学习聚合类型中的数组。
数组分为两种:
- 一维数组
- 多维数组
但是多维数组其实是一维数组的一种扩充,所以我们仅仅学习一维数组。
一维数组的声明
为了声明一个一维数组,我们需要在数组名后面跟一对方括号[],方括号里面是一个整数,指定数组中的元素的个数。
int values[20];
显而易见,我们声明了一个整型数组,数组包含20个整型元素。
数组下标总是从0开始,最后一个元素的下标是元素的数目减1。
一维数组初始化
值得我们注意的是,与整型,浮点型,指针类型的变量初始化不同的是,数组的初始化需要一系列的值。
这些用于初始化数组的值位于一对花括号{}中,每个值之间用逗号,分隔。
int vector[5] = {10,20,30,40,50};
初始化列表给出的值逐个赋值给数组的各个元素。
所以:
{0}. vector[0] = 10
{0}. vector[1] = 20
{0}. vector[2] = 30
{0}. vector[3] = 40
{0}. vector[4] = 50
静态初始化和自动初始化
变量的存储类型决定变量何时创建,何时销毁以及它的值保存多久。
同样,数组变量的声明位置也决定着数组变量的默认存储类型。
- 对于静态变量而言
- 存储于静态内存中的数组只初始化一次,也就是在程序开始执行之前。
- 链接器用包含可执行程序的文件中合适的值对数组元素进行初始化。
- 如果数组未被初始化,数组元素的初始值将会自动设置为0。
- 对于自动变量而言
- 由于自动变量位于运行时堆栈中,执行流每次进入它们所在的代码块时,这类变量每次所处的内存位置可能并不相同,所以在程序开始执行前,编译器没有办法对这些位置进行初始化。所以,自动变量在默认情况下是未初始化的。
- 如果自动变量的声明中给出了初始值,每当执行流进入自动变量声明所在的作用域时,变量就被一条隐式的赋值语句初始化。这条赋值语句需要时间和空间执行。
- 数组的问题在于初始化列表中可能有很多值,这就可能产生很多条赋值语句。对于那些非常庞大的数组,它的初始化时间可能非常可观。
所以,当数组的初始化局部于一个函数(或代码块)时,我们应该考虑——在程序的执行流每次进入该函数(或代码块)时,每次都对数组进行重新初始化是不是值得?
如果不值得,那么我们应该把数组声明为static,这样数组的初始化只需在程序开始前执行一次。
不完整的数组初始化
按理来说,数组的初始化应该如上面所示。
但是,在编译器中,存在几种不完整的数组初始化,且运行无误。
int list[5] = {1,2,3,4};
int deque[] = {1,2,3,4,5};
对于第一条初始化语句,它将前4个初始值分别赋予list[0],list[1],list[2],list[3],而对于list[4]则默认初始化为0。
对于第二条初始化语句,我们在声明中并没有给出数组的长度,这时编译器自动把数组的长度设置为刚好能够容纳所有初始值的长度。
但是,我们也不能盲目的使用不完整的初始化。
下面这两种情况,编译器在运行时则会报错或出现与我们希望情况不符合的现象:
int list1[5] = {1,2,3,4,5,6};
int list2[5] = {1,5};
对于第一条声明语句,编译器直接报错。这是因为初始化值的数目和数组元素的数目并不匹配,没办法把6个数据放到5个变量中。
对于第二条声明语句,我们本意是希望list[0] = 1,list[4] = 5。但是结果是list[0]=1,list[1] = 5,后面三个元素都为0。
即,我们在使用不完整初始化数组时,仅仅可以省略最后几个初始值。
字符数组初始化
对于数组中的每个元素,除了可以存储int或float型的数值之外,我们还可以存储char型的字符。
根据之前的知识,我们会对字符数组的初始化采用如下的方式:
int message[] = {'h','e','l','l','o',0};
显而易见,这种数组初始化方式比较麻瓜。
因此,C语言标准提供了一种快速初始化字符数组的方式:
int message[] = "hello";
尽管初始值看上去像是一个字符串常量,但实际上并不是。
数组与指针
接下来,就是我们的核心——数组与指针的关系。
我们再次研究下述声明:
int a;
char c;
double d;
int *p;
int b[10];
我们把前四个变量a、c、d、p称为标量,因为它们的类型单一、值单一。
变量名 | 值 | 类型 |
---|---|---|
a | 10 | int |
c | ‘H’ | char |
d | 3.14 | double |
p | &a | int* |
我们把最后一个变量b称为数组,因为它是一些值的集合。
如下图所示:
我们将数组名和下标一起使用,用于标识该集合中的某个特定的值。
注意,每个特定的值都是一个标量,可以用于任何可以使用标量的上下文中环境中。
变量名 | 值 | 类型 |
---|---|---|
b[0] | 1 | int |
b[1] | 1 | int |
b[2] | 1 | int |
b[3] | 1 | int |
b[4] | 1 | int |
b[5] | 1 | int |
b[6] | 1 | int |
b[7] | 1 | int |
b[8] | 1 | int |
b[9] | 1 | int |
但是,数组名b是什么类型呢?
在C中,几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址!
数组名的值是一个指针,也就是数组第一个元素的地址!?
大多数人看到这句话,第一反应会产生如下图所示的内存结构:
如图所示,数组名b是一个指针,值是数组第一个元素的地址,所以指针b应该指向数组的第一个元素。
乍一看,这种分析非常正确。
但是,如果数组是这种内存结构的华,那么其所占据的总内存空间是不是应该加上数组名所占据的这一块呢?
所以,这种内存结构的示意图是错误的!
究其原因,是由于我们忽略了句中的“的值”二字。C语言只是说数组名的值是一个指针,而没有说数组名就是一个指针。
所以,正确的内存结构示意图应该如下图所示:
数组名b占据的内存空间就是数组中各个元素所占空间连接而成的总的内存空间。
数组名的类型取决于数组元素的类型。
- 如果数组元素的类型是int型,那么数组名就是指向int的常量指针。
- 如果数组元素的类型是其他类型,那么数组名就是指向其他类型的常量指针。
数组名的值是一个指针常量!?
注意这个是指针常量,而不是指针变量。
指针常量所指向的是内存中数组的起始位置,如果修改这个指针常量,唯一可行的操作就是把整个数组移动到内存的其他位置。但是,在程序完成链接之后,内存中数组的位置是固定的。所以当程序运行之后,再想移动数组就为时已晚了。
因此,数组名的值是一个指针常量。
所以,如下的操作是非法的:
int a[10];
int *c;
a = c;
数组名的值是数组第一个元素的地址!?
如图所示,我们可以将数组名完全看做数组第一个元素的地址。
考虑下列代码:
int b[10];
int *c;
c = &b[0];
假设b[0]的地址为100。
在执行了赋值语句之后,指针变量c的值变成100,指针c指向数组b的第一个元素。
如下图所示:
由于我们可以将数组名完全看做数组第一个元素的地址,上面的赋值语句也可以改写为如下代码:
int b[10];
int *c;
c = b;
相当于把“类指针变量”b拷贝给了指针变量c。
数组赋值操作
对于一般的标量,我们可以使用赋值语句对其赋值:
int x = y;
很多时候,我们也希望将一个数组的所有元素赋值给另外一个数组,这时候应该怎么办呢?
如果我们采取了和普通标量一样的方式,采用赋值操作符=,那么编译器将会报错。
这也是很多C初学者容易犯的错误。
int m[10];
int n[10];
m = n;
/*错误*/
这是因为数组名不能代表整个数组元素,数组名类似于指针,数组名的值的其第一个元素的地址。
对于数组而言,如果我们希望将一个数组的所有元素赋值给另外一个数组,我们必须使用一个循环,一次复制一个元素。
下标引用操作符[]和解引用操作符*
首先需要声明的是,除了优先级之外,下标引用操作符[]和间接访问操作符*完全相同。
考虑下列代码:
int b[10];
*(b + 3)是什么???
不急,我们一步步分析:
- 数组名b的类型是一个指向整型的指针,所以3这个值需要根据整型值的长度进行调整。
- 指针加法运算的结果是另一个指向整型的指针,它指向的是数组第一个元素向后移3个整型长度的位置。
- 解引用操作符*访问这个位置,取得其值。
再考虑下列代码:
int b[10];
b[3]是什么???
对于b[3],我们之前分析过,这是一个标量,表示数组b中的第4个元素。
所以,我们能够得到如下的等式:
array[subscript]
等价于
*(array + subscript)
那么,
- 在使用下标引用的地方,我们完全可以使用对等的指针表达式来代替。
- 在使用上面这种形式的指针表达式的地方,我们也可以使用下标表达式来代替。
最后,我们通过一个案例,深度理解上面的内容。
代码如下:
int b[10] = {0,1,2,3,4,5,6,7,8,9};
int *ap = b + 2;
表达式 | 值 |
---|---|
ap | b +2 或 &b[2] |
*ap | *(b + 2) 或 b[2] |
ap + 6 | b + 8 或 &b[8] |
*ap + 6 | b[2] + 6 |
*(ap + 6) | *(b + 8)或 b[8] |
ap[0] | 等价于*(ap+0),即b[2] |
ap[-1] | 等价于*(ap-1),即b[1] |
ap[9] | 等价于*(ap+9),但是下标越界,非法!!! |
数组下标越界检查
最早的C编译器不检查下标,同时最新的编译器依然不检查下标。
这是由于数组和指针的密不可分的关系,导致编译器检查下标将是一项非常庞大的任务。
所以,我们的经验是:
- 如果下标值是从那些已知是正确的值计算得来,那么就无需检查它的值。
- 如果一个用作下标的值是根据某种方法从用户输入的数据产生而来的,那么在使用它之前必须进行检查,确保它们位于有效的范围之内!