1.数组
(1)数组的声明
我们知道,一个基本数据类型的变量只能存储一个数据,比如:
int data = 0x64;
如果需要存储一组int型数据呢?比如,1、2、3,则至少需要3个变量data0、data1、data2。比如:
int data0 = 1, data1 = 2, data2 =3;
由于数据的表现形式多种多样,还有字符型和其它的数值类型,因此仅有基本数据类型是不够的。是否可以通过基本数据类型的组合抽象构造其它的数据类型呢?答案是可以的,构造数据类型数组就是这样产生的。
从概念的视角来看,int型整数1、2和3都是相同的数据类型,data0、data1和data2三个变量的共性是data,其差异性是下标不一样。因此可以将data0、data1和data2抽象为一个名字,然后用下标区分这些变量的集合——data[0]、data[1]和data[2]。如果有以下声明:
intdata[3]; //解读为data是int数组(元素个数3)
那么data[3]就成了存放3个int型数据1、2、3的data[0]、data[1]和data[2]所组成的数组,即可分别对data[0]、data[1]和data[2]赋值:
data[0] = 1, data[1] =2, data[2] = 3;
当然,也可以按照以下方式声明一个数组并进行初始化:
intdata[3] = {1, 2, 3};
通常将data称为数组(变量)名,data[0]、data[1]和data[2]被称为变量。因而可以说,数组是将相同类型数据的若干变量按有序的形式组织起来,用一个名字命名,然后用下标区分这些变量的集合。
由于数组是建立在其它类型的基础上,因此C将数组看作构造类型,在声明数组时必须说明其元素的类型。比如,int类型的数组、float类型的数组或其它类型的数组。而其它类型也可以是数组类型,在这种情况下,创建的是数组类型的数组,简称数组的数组。
(2)下标与变量的值
在这里,定义了一个名为data的数组类型变量,它是由存放3个int型数据1、2、3的变量data[0]、data[1]和data[2]组成的。通常又将数组的各个变量称为数组的元素,而数组的元素是按照顺序编号的,这些元素的编号又称为数组元素的下标。
由于有了下标,因此数组元素在内存中的位置就被唯一确定下来了。下标总是从0开始的,最后一个元素的下标为元素的个数减1(即2),data[0]叫第1个元素,data[1]叫第2个元素,data[2]叫第3个元素,也就意味着所有的元素在内存中都是连续存储的。
直观上,数组是由下标(或称为索引)和值所组成的序对
value>集合,对于每个有定义的下标都存在一个与其关联的值,在数学上称为映射。除了创建新数组外,大多数语言对数组只提供两种标准操作:一个操作是检索一个值,另一个操作是存储一个值。
函数Create(data,
size)创建一个新的具有适当大小的空数组,初始时数组的每一项都没有定义。Retrieve操作接受一个数组data和一个下标index,如果下标合法,则该操作返回与下标关联的值,否则产生一个错误。Store操作接受一个数组data、一个下标index和一个项item的集合,即项是value值的集合,有时也将值(value)称为项(item),返回在原来数组中增加新的序对
value>后的数组。
显然,int系的任何常量表达式都可以作为数组元素的下标。比如:
int array[3+5]; // 合法
int array['a']; //表示int array[97];
上述定义之所以合法,因为表示元素个数的常量表达式在编译时就具有确定的意义,与变量的定义一样明确地分配了固定大小的空间。
虽然使用符号常量增强了数组的灵活性,但如果定义采用了以下的形式:
int n = 5;
int array[n]; //非法
因为标准C认为数组元素的个数n不是常量,虽然编译器似乎已经“看到”了n的值,但intarray[n]要在运行时才能读取变量n的值,所以在编译期无法确定其空间大小。使用符号常量定义数组长度的正确形式如下:
#define N 10
int array[N];
即可根据实际的需要修改常量N的值。
由于数组元素下标的有效范围为0~N-1,因此data[N]是不存在的,但C语言并不检查下标是否越界。如果访问了数组末端之后的元素,访问的就是与数组不相关的内存。它不是数组的一部分,使用它肯定会出问题。C为何允许这种情况发生呢?这要归功于C信任程序员,因为不检查越界可以使运行速度更快,所以编译器没有必要检查所有的下标错误。因为在程序运行之前,数组的下标可能尚未确定,所以为了安全起见,编译器必须在运行时添加额外代码检查数组的每个下标值,但这样会降低程序的运行速度。C相信程序员能编写正确的代码,这样的程序运行速度更快。但并不是所有的程序员都能做到这一点,越界恰恰是初学者最容易犯的错误,因此要特别注意下标的范围不能超出合理的界限。
(3)变量的地址与类型
当将变量data[0]、data[1]和data[2]作为&的操作数时,&data[0]是指向变量data[0]的指针,&data[1]是指向变量data[1]的指针,&data[2]是指向变量data[2]的指针。data[0]、data[1]和data[2]变量的类型为int,&data[0]、&data[1]和&data[2]指针的类型为int
*const,即指向常量的指针,简称常量指针,其指向的值不可修改。比如:
int a;
int * const ptr = &a;
ptr = NULL; //试图修改,则编译报警
&a = NULL; //试图修改,则编译报警
同理,&data是指向变量data的指针,那么data是什么类型?
按照声明变量的规约,将标识符data取出后,剩下的“int
[3]”就是data的类型,通常将其解释为由3个int组成的数组类型,简称数组类型。其目的是告诉编译器需要分配多少内存?3个元素的整数数组,data类型测试程序详见程序清单
1.20。
程序清单 1.20 data类型测试程序
1 #include
2
3 void f(int x);
4 int main(int argc, char *argv[])
5 {
6 int data[3];
7 f(data);
8 return 0;
10 }
通过编译器提示的警告,“funtion: 'int' differ in levels ofindirection from 'int
[3]'”,说明数组变量data的类型为不是int而是int
[3]数组类型。由于在设计C语言时,过多地考虑了开发编译器的便利。虽然设计编译器更方便了,却因为概念的模糊给初学者造成了理解上的困难。实际上数组应该这样定义:
int[3] data;
即int是与[3]结合的。&data到底是什么类型?
当data作为&的操作数时,则&data是指向data的指针。由于data的类型为int [3],因此&data是指向“int
[3]数组类型”变量data的指针,简称数组指针。其类型为int (*)[3],即指向int [2]的指针类型。为何要用“()”将“*”括起来?
如果不用括号将星号括起来,那么“int (*)[3]”就变成了“int
*[3]”,而int*[3]类型名为指向int的指针的数组(元素个数3)类型,这是设计编译器时约定的语法规则。
&data的类型到底是不是“int (*)[3]”?其验证程序范例详见程序清单 1.21。
程序清单 1.21 &data类型测试程序
1 #include
2 int main(int argc, char *argv[])
3 {
4 int data[3];
5 int b = &data;
6 return 0;
7 }
通过编译器提示的警告,“'int' differ in levels ofindirection from 'int
(*)[3]'”,说明&data的类型为int (*)[3]。
(4)sizeof(data)
将如何寻找相应的数组元素呢?常用的方法是通过“数组的基地址+偏移量”算出数组元素的地址。在这里,第一个元素&data[0]的地址称为基地址,其偏移量就是下标值和每个元素的大小sizeof(int)相乘。假设数组元素&data[0]
的地址为A,且在内存中的实际地址为0x22FF74,那么&data[1]的值为:当data作为sizeof的操作数时,其返回的是整个数组的长度。在这里,sizeof(data)的大小为12,即3个元素占用的字节数为4×3=12,系统会认为&data+1中的“1”,偏移了一个数组的大小,因此&data
+1是下一个未知的存储空间的地址(即越界)。在小端模式下,数组在内存中的存储方式详见图 1.10。
A + 1×sizeof(int) = (unsignedint)data + 4 = 0x22FF74 + 4 = 0x22FF78
&data[2]的值为:
A + 2×sizeof(int) = (unsigned int)data+ 8 = 0x22FF74 + 8 = 0x22FF7C
实际上,当在C语言中书写data[i]时,C将它翻译为一个指向int的指针。Data是指向data[0]的指针,data+i是指向data[i]的,因此不管data数组是什么类型,总有data+i等于data[i],于是*(data+i)等于data[i],其相应的测试范例程序详见程序清单
1.22。
程序清单 1.22变量的地址测试程序
1 #include
2 int main(int argc, char *argv[])
3 {
4 int data[3]= {1, 2, 3};
5 printf("%x, %x, %x, %x, %x",&data[0], &data[1], &data[2],
&data, &data+1);
6 return0;
7 }
实践证明,虽然&data[0]与&data的类型不一样,但它们的值相等。同时也可以看出,数组的元素是连续存储的。如果将数组变量占用内存的大小除以数组变量中一个元素所占用空间的大小,便可得到数组元素的个数。即:
int numData = sizeof(data) / sizeof(data[0]);
当然,也可以使用宏定义计算数组元素的个数:
#define NELEMS(data)(sizeof(data) / sizeof(data[0]))
当数组作为函数的参数时,C语言函数的所有参数必须在函数内部声明。但是,由于在函数内部并没有给数组分配新的存储空间,因此一维数组的容量只在主程序中定义。显然,如果函数需要得到一维数组的大小,则必须将它以函数参数的形式传入函数中,或将它作为全局变量访问。
2. 规约
标准C规定:除了“在声明中”或“当一个数组名是sizeof或&的操作数”之外,只要数组名出现在表达式中,这编译器总是将数组名解释为指向该数组的第一个元素的指针。
虽然data在表达式中解读为指向该数组首元素data[0]的指针,但实际上data被解读为&data[0]或等价于“*data==data[0]”,因此data与&data[0]的值相等,且它们的类型都是int
*const,即一个数组名是一个不可修改的常量指针(左值)。
根据指针运算规则,当将一个整型变量i和一个数组名相加时,其结果是指向数组第i个元素的指针,即data+index==&data[index],*(data+index)==data[index],因此习惯性地将:
int * ptr = &data[0];
写成下面这样的形式:
int *ptr = data;
由于data的类型是不可修改的常量指针int*const,因此任何试图使数组名指向其它地方的行为都是错误的。比如:
data ++; //错误
虽然data有地址,但其类型为int *const,因此不能对data++赋值,同样也不能反过来给data赋值。比如:
data = ptr;
类型地,象下面这样的表达式也是非法的:
intdata[3];
data= {1, 2, 3};
但可以将data复制给指针变量ptr,通过改变指针变量达到目的。比如:
#define N 10
int data[N];
int *ptr;
for(ptr = data; ptr < data + N; ptr ++)
sum+= *ptr;
for语句中的条件p
int sum = 0;
while(ptr < data +N)
sum += *ptr++;
其中,*ptr++等价于*(ptr++),它的含义为自增前表达式的值是*ptr,即ptr当前指向的对象,以后再自增ptr。