7.2 深入理解数组

在概括性地了解了数组之后,下面将更全面、更深入地介绍数组。

7.2.1 数组是一种数据类型

数组是一种数据类型。这种数据类型和第一篇介绍过的基本数据类型不同的是,它并不是一种由C语言预先定义好了的数据类型,而是根据具体问题由程序编写者自己构造的一种数据类型。

数组是用来描述具体问题中一组有序且性质相同的数据对象的。这种“性质相同”反映在代码中的意义就是它们的数据类型相同。

由于这个数据类型可能是任何一种适合作为数组元素的类型,而且这组数据的数目也有各种可能,因此有多种数组类型。只有在数组定义完成后,这个具体的数组类型才成为完全确定的数组类型。

7.2.2 数组定义的含义

和定义变量一样,定义数组的目的是为某组数据申请内存空间并命名这块内存空间,以便在后面的代码中使用。和定义变量不一样的是,定义数组还意味着对这个数组进行构造。

在定义或构造一个数组时,需要为这组数据占据的内存空间取一个共同的名字,这个名字也就是这组数据共同的名字,这个名字需要满足标识符法则。

此外还要明确向编译器表明:

(1)数组元素的类型。

(2)数组元素总的个数。

数组定义的一般形式为

数组元素的类型数组名[数组元素的个数];

以“int bjs[4];”为例:

这个定义蕴涵着下列信息:

(1)[]说明bjs是个数组名(这里[]是一个类型说明符)。

(2)数组bjs中共有4个元素(4也属于类型说明符)。

(3)这4个元素皆为int类型(int也是类型说明符)。

在定义数组时,数组元素的个数必须是大于0的整数类型的表达式。在C99之前,必须是整数类型的常量表达式,而C99则允许使用变量表达式。使用变量表达式的数组叫做变量长度数组(Variable-length Array),缩写为VLA,将在后面的章节内专门介绍。

7.2.3 数组名是什么

如果在代码中加上一句“printf("%d",sizeof bjs == sizeof(int)*4);”,从这条语句的输出值为“1”中就不难体会到,数组名意味着一块连续的存储空间,而且这块内存空间的大小恰恰等于数组元素的个数与数组元素占据空间大小的乘积。

如图7-2所示,数组的各个元素是依次连续地存储在这块内存中的。了解这一点在后面的学习中特别重要。

7.2 深入理解数组 - 图1

图7-2 数组的各个数据是连续存放的

数组的名字可以表示数组这种数据对象占据的内存,这说明它具有左值含义。然而,C语言并没有定义对数组整体上的赋值运算。因此数组的名字不可以用来给数组整体进行赋值。换句话说,尽管数组的名字在sizeof运算中表示的是左值的含义,但是却绝对不像普通的基本类型变量名一样可以出现在赋值号的左面。这也没有什么好奇怪的,因为所谓运算都是针对具体的数据类型而言的。数组是一种新的数据类型,这种数据类型可以进行sizeof运算,但并没有(被)赋值这种运算。

7.2.4 一维数组元素的引用

定义数组解决了为一组类型相同数据对象的命名和安排内存的问题。由于数组的名字不是一个可以被赋值的左值表达式,所以除了极少数情况(如做sizeof运算),数组并没有太多的整体上的运算。对于具体的问题,一般需要把数组中的数据一个一个拿出来单独运算。

把数组中的数据单个地拿来运算,叫做对数组元素的引用。在C语言中,对数组中各个元素的引用是依据各个元素的编号进行的。引用数组元素的一般方法是:

数组名[类型表达式]

[]内的整数类型表达式即要引用数组元素的编号,叫做“下标”。

在C语言中,数组元素的编号永远是从0开始记数的,因此对于数组元素的个数为4的数组来说,其各元素的编号依次为0、1、2、3。

由于数组元素的编号是从0开始到数组元素的个数-1,因此在引用数组元素时要特别注意下标的范围不可以超出合理的界限。仍以前面定义的数组为例,bjs[-1]、bjs[4]都是合法的C语言表达式,但由于得到的数据对象不在数组所在的那块内存中,所以这两个引用毫无具体意义(不清楚引用的究竟是什么),而且会造成程序的错误和危害。这种情况叫做越界。

有些语言的编译器会对下标是否越界进行检查,但C语言的编译器不进行这种检查。C语言认为把数组元素下标写正确是程序作者的责任而不是编译器的义务。而越界,恰恰是初学者最容易犯的错误之一。

7.2.5 数组元素引用是一个表达式

具体地,仍以定义:

7.2 深入理解数组 - 图2

为例,其各个元素依据其在内存中的顺序分别被叫做:

7.2 深入理解数组 - 图3

这里[]是一个运算符(数组下标运算符(Array Subscript Operator),属于后缀运算的一种,优先级16(1)),bjs与数组元素的编号进行[]运算得到该编号的数组元素。

可以用这些表达式去读、写这些数据或进行其他运算。这些表达式和基本类型的变量名的作用完全一样。比如:

7.2 深入理解数组 - 图4

现在可以发现一个新的C语言现象,那就是表达式可以出现在“=”运算符的左面作为被赋值的对象。这一点也不用奇怪,在C语言中一个表达式可能表示一个数据对象所占据的内存,这时这个表达式叫左值表达式(如“bjs[1] = 11”)。但在“printf("%d\n", bjs[1])”中,"bjs[1]"表示的是bjs[1]这块内存中的值,这时“bjs[1]”是一个右值表达式。

随之而来的是另一个问题,在表达式“bjs[1]”中,数组的名字bjs究竟是作为左值还是作为右值参与运算的呢?答案是,是作为右值(值)参与运算的。

7.2.6 数组名有值吗

可以肯定地说,数组名有值。如果在代码中加上一句“printf("%u",bjs);”,就会发现这会输出一个值。而且即使改变了数组各个元素的值,“printf("%u",bjs);”的输出结果依然如故。这暗示着bjs的值与所占据的内存中的数据并没有什么关系。

这是由于数组所占据的内存的内容在总体上并没有一个值的含义,而数组名的值也同样不表示数组所占据内存内容的值(因为压根没有)。数组名作为右值表示的是另外一种含义。这是和那些简单的基本数据类型变量最大的区别。

7.2.7 重复一遍

恐怕有些读者会有些糊涂了。但是,C语言就是如此地细腻而微妙。而且这些内容极其重要,所以在此把前面陈述的要点重复一遍是十分必要的。

首先,数组是一种新的数据类型,但这并不是一种具体的数据类型的名称,而是一类数据类型的总称。

数组的名字在作为左值表达式的时候(如作为sizeof运算符的运算对象)表示数组占据的总的内存空间。但是尽管数组的名字可以作为左值,C语言却并不允许对数组名进行赋值运算。

数组的名字具有一个值,这时数组的名字是一个右值表达式,数组名的值并不是数组占据的那块内存内容的值含义而是有其他含义(将在后面章节介绍)。数组占据的那块内存的内容在总体上没有任何值含义。

本书在后面提到数组的名字时将采用两种说法:数组和数组名。前者表示这个数组的名字是一个左值表达式、即表示的是数组所占据的那块内存;后者表示的是该数组的名字的右值含义。例如,对于:

7.2 深入理解数组 - 图5

称“sizeof bjs”的含义是求数组bjs占据多少内存空间,而称“bjs[0]”是数组名与0做“[]”(下标)运算。

数组的名字的数据类型的名称是什么呢?这个问题的答案也迥异于基本数据类型,答案竟然有两种可能。

(1)数组bjs的数据类型名称是“int[4]”,这可以通过“printf("%d", sizeof bjs == sizeof(int[4]));”输出的结果得到证实。

(2)数组名bjs的数据类型是一种指针,暂时可以用“int[]”来表示。对于目前的应用来说这已经足够了,更准确的名称将在指针一章介绍。

“int[]”这种数据类型,目前可以进行的计算只有“[]”(下标)运算。这种运算的结果,如同int类型的bjs[0]一样,可以作为左值,也可以作为右值。但是用“int[]”类型说明符定义的变量却并非数组名。

许多人认为C语言的指针难学,实际上是因为指针前面的不少内容并没有学好。最为突出的就是对数组这种数据类型没有透彻地理解。本小节内容的作用在数组这章里并不突出,即使没有完全掌握也不影响本章后面内容的学习。但是一旦接触到指针,本小节的内容就变得无比重要了。所以请务必在学习指针之前熟练掌握本小节的内容。