C Primer Plus 第十章——数组和指针
与普通变量相似,在初始化之前数组元素的数值是不定的。编译器使用的数值是存储单元中已有的数值。初始化数组元素时(int),当数值数目少于数组元素数目时(部分初始化),多余的数组元素被初始化为0。如果初始化列表中项目的个数大于数组大小,编译器则会认为这是一个错误。可以在初始化时使用空的方括号来让编译器根据列表中的数值数目确定数组大小。
C99增加了一种新特性:指定初始化项目。此特性允许选择对某些元素进行初始化。例如,要对数组的最后一个元素初始化。按照传统的C初始化语法,需要对每一个元素都初始化之后,才可以对最后的元素初始化。而C99规定,在初始化列表中使用带有方括号的元素下标可以指定某个特定的元素:
int arr[6]={0,0,0,0,0,212}; //传统语法
int arr[6]={[5]=212}; //把arr[5]初始化为212
此外,指定初始化项目具有两个特性:第一,如果在一个指定初始化项目后跟有不止一个值,例如在序列[4]=31,30,31中这样,则这些数值将用来对后续的数组元素初始化。第二,如果多次对一个元素进行初始化,则最后的一次有效。
为数组赋值:声明完数组后,可以借助数组的索引(即下标)对数组成员进行赋值。但C不支持把数组作为一个整体来进行赋值,也不支持用花括号括起来的列表形式进行赋值(初始化的时候除外)。即只能通过初始化时对数组赋值或者创建数组后对数组元素进行单独赋值。
/*无效的数组赋值*/
#define SIZE 5
int main(void)
{
int oxen[SIZE]={5,3,2,6}; //这是可以的 int yaks[SIZE];yaks=oxen; //不允许 yaks[SIZE]=oxen[SIZE]; //不正确 yaks[SIZE]={5,3,2,6}; //不起作用
在使用数组的是时候,需要注意数组索引不能超过数组的边界。编译器不检查索引的合法性。在标准C中,如果使用了错误的索引,程序执行结果是不可知的,取决于不同编译器。C99出现之前,声明数组时方括号内只能使用整数常量表达式。C99引入了变长数组(variable-length array,简称VLA),使得变量也是可用的。
二维数组:声明方法:
float rain[5][12];
理解这个声明的一种方法使首先查看位于中间的那部分(rain[5]),这部分说明rain是一个包含5个元素的数组。至于每个元素的情况,需要查看声明的剩余部分(float [12]),这部分说明每个元素的类型是float[12];也就是说,rain具有5个元素,并且每个元素都是包含12个float数值的数组。也可以把rain数组看成是一个二维数组,它包含5行,每行12列。数组是顺序存储的,前12个元素之后,跟着就是第二个包含12个元素的数组,依此类推。
数组标记实际上是一种变相使用指针的形式。数组名同时也是该数组首元素的地址。也就是说,如果flizny是一个数组,下面的式子是正确的:
flizny==&flizny[0]
指针的定义:
1、指针的数值就是它所指向的对象的地址。地址的内部表示方式是由硬件来决定的。很多种计算机都是以字节编址的,这意味着对每个内存字节顺序进行编号。对于包含多个字节的数据类型,比如double类型的变量,对象的地址通常指的是其首字节的地址。
2、在指针中用运算符*就可以得到该指针所指向的对象的地址。
3、对指针加1,等价于对指针的值加上它指向的对象的字节大小(增加一个存储单元)。对于数组来说,地址会增加到下一个元素的地址。
dates+2==&dates[2]; //相同的地址
*(dates+2)==dates[2]; //相同的值
可以看出数组和指针之间具有密切的关系:可以用指针表示数组的每个元素,并得到每个元素的数值。从本质上说,对同一个对象有两种不同的符号表示方法。C语言标准在描述数组时,确实借助了指针的概念。例如,定义ar[n]时,意思是*(ar+n),即“寻址到内存中的ar,然后移动n个单位,再取出数值。另外,应该注意区分*(dates+2)和*dates+2,简介运算符(*)的优先级高于+,因此后者等价于(*dates)+2。即:在C中恒有 *(ar+n)==ar[n]。要编写对数组进行操作的函数,可以在相应的函数定义中将指针作为形式参量传递给函数:
int sum(int *ar);
函数sum()从该参数中得到数组首元素的地址,并且可以知道从此地址找到一个int类型元素。在函数原型或定义函数头的场合中(并且也只有在这两种场合中),可以用int [ar]代替int *ar。
C保证在为数组分配存储空间的时候,指向数组之后的第一个位置的指针也是合法的(但对该地址存储的内容不作任何保证)。
指针操作:
1、赋值(assignment)——可以把一个地址赋给指针。通常使用数组名或地址运算符&来进行地址赋值。地址应该和指针类型兼容。
2、求值(value-finding)或取值(dereferencing)——运算符*可取出指针指向地址中存储的数值。
3、取指针地址——指针变量同其他变量一样具有地址和数值,使用运算符&即可得到存储指针本身的地址。
4、将一个整数加给指针——可以使用+运算符来把一个整数加给一个指针,或者把一个指针加给一个正数。两种情况下,这个整数都会和指针所指类型的字节数相乘,然后所得的结果会加到初始地址上。如果相加的结果超出了初始指针所指向的数组的范围,那么这个结果是不确定的。
5、增加指针的值——可以通过一般的加法或增量运算符来增加一个指针的值。对指向某数组元素的指针做增量运算,可以让指针指向该数组的下一个元素。
6、从指针中减去一个整数。
7、减小指针的值。
8、求差值(Differencing)——可以求两个指针间的差值。通常对分别指向同一个数组内两个元素的指针求差值,以求出元素之间的距离。差值的单位是相应类型的大小。例如指向int数组的两个指针相减的值如果是2,指的是它们所指的对象之间的距离是两个int数值大小,而不是2字节。有效指针差值运算的前提是参加运算的两个指针是指向同一个数组(或是其中之一指向数组后面的第一个地址)。指向两个不同数组的指针之间的差值运算可能会得到一个数值结果,但也可能导致一个运行时错误。
9、比较——可以使用关系运算符来比较两个指针的值,前提是两个指针具有相同的类型。
注意,这里有两种形式的减法,可以用一个指针减掉另一个指针得到一个整数,也可以从一个指针中减去一个整数得到一个指针。
特别注意:不能对未初始化的指针取值。例如下面的例子:
int *pt; //未初始化的指针
*pt=5; //一个可怕的错误
为什么这样的代码危害极大?这段程序的第二行表示把数值5存储到pt所指向的地址。但是由于pt没有被初始化,因此它的值是随机的,不知道5会被存储到什么位置。这个位置也许对系统危害不大,但也许会覆盖程序数据或者代码,甚至导致程序的崩溃。切记:当创建一个指针时,系统只分配了用来存储指针本身的内存空间,并不分配用来存储数据的内存空间。因此使用指针之前,必须给它赋予一个已分配的内存地址。
保护数组内容
int sum(const int ar[],int n);
这告知编译器:函数应当把ar所指向的数组作为包含常量数据的数组对待。这样使用const并不要求原始数组是固定不变的,只是说明函数在处理数组时,应把数组当作是固定不变的。使用const可以对数组提供保护,就像按值传递可以对基本数据类型提供保护一样。double rates[5]={88.99,100.12,59.45,183.11,340.5};
const double *pd=rates;
第二行代码把pd声明为指向const double的指针。这样,就不可以使用pd来修改它指向的数值。double rates[5]={88.99,100.12,59.45,183.11,340.5};
const double locked[4]={0.08,0.075,0.0725,0.07};
double * pnc=rates; //合法
pnc=locked; //非法
pnc=&rates[3]; //合法
因此,在函数参量定义中使用const,不仅可以保护数据,而且使函数可以使用声明为const的数组。double rates[5]={88.99,100.12,59.45,183.11,340.5};
double * const pc=rates; //pc指向数组开始处
pc = &rates[2]; //不允许
*pc= 92.99; //可以,更改rates[0]的值
这样的指针依然可以用于修改数据,但它只能指向最初赋给它的值。double rates[5]={88.99,100.12,59.45,183.11,340.5};
const double * const pc = rates;
pc=&rates[2]; //不允许
*pc=92.99; //不允许
指针和多维数组
int zippo[4][2]; //整数数组的数组
数组名zippo同时也是数组首元素的地址。在本例中,zippo的首元素(zippo[0])本身又是包含两个int的数组,因此zippo也是包含两个int的数组的地址。下面从指针属性进一步分析:指向多维数组的指针
int (* pz) [2]; //pz指向一个包含2个int值的数组
该语句表明pz是指向包含两个int值的数组的指针。为什么使用圆括号?因为表达式中[]的优先级高于*。因此如果这样声明:int * pax [2];
那么首先方括号和pax结合,表示pax是包含两个某种元素的数组。然后和*结合,表示pax是两个指针组成的数组。最后,用int来定义,表示pax是由两个指向int值的指针构成的数组。这种声明会创建两个指向单个int值的指针。但前面的版本通过圆括号使pz首先和*结合,从而创建一个指向包含两个int值的数组的指针。int * p1;
const int * p2;
const int ** pp2;
p1=p2; //非法,把const指针赋给非const指针
p2=p1; //合法,把非const指针赋给const指针
pp2=&p1; //非法,把非const指针赋给const指针,但有两层间接运算
函数和多维数组
void somefunction(int (*pt) [4]);
当且仅当pt是函数的形式参量时,也可以做如下这样声明:void somefunction(int pt[][4]);
注意到第一对方括号里是空的。这个空的方括号代表pt是一个指针。int sum4d(int ar[][12][20][30],int rows); int sum4d(int (*ar) [12][20][30], int rows); //等效的形式
这是因为首放括号代表这是一个指针,而其他方括号描述的是所指向对象的数据类型。P.S. 使用指向二维数组的指针时,无论一个确定的目标二维数组的行数(第一个方括号里的数值)是多少,使用的指针声明时形式都是一样的(例如int (*ar) [2])。这就好比无论一个一维int数组有多少个元素,使用指向它的指针时都是同样的形式int *pt一样。这也是为什么处理二位数组的函数需要一个额外的参数来传递行数信息,而列信息却被预置在函数内部。当出现在函数声明中时,(*ar)也可以写成ar[]以表明这是一个指针。
变长数组(VLA)
int quarters = 4;
int regions = 5;
double sales[regions][quarters]; //一个变长数组(VLA)
变长数组有一些限制。变长数组必须是自动存储类的,这意味着它们必须在函数内部或作为函数参量声明,而且声明时不可以进行初始化。下面的代码示范了如何声明一个带有二位变长数组参数的函数:
int sum2d(int rows, int cols, int ar[rows][cols]); int sum2d(int ar[rows][cols], int rows, int cols); //顺序不正确
注意前两个参量rows和cols用作数组参量ar的维数。因为ar的声明中使用了rows和cols,所以在参量列表中,它们两个的声明需要早于ar。C99标准规定,可以省略函数原型中的名称;但是如果省略名称,则需要用星号来代替省略的维数:
int sum2d(int, int, int ar[*][*]);
需要注意的一点是,函数定义参量列表中的变长数组声明实际上并没有创建数组。和老语法一样,变长数组名实际上是指针,也就是说具有变长数组参量的函数实际上直接使用原数组,因此它有能力修改作为参数传递进来的数组。复合文字
(int [2]) {10,20} //一个复合文字
其中(int [2])就是类型名。正如初始化一个命名数组时可以省略数组大小一样,初始化一个复合文字时也可以省略数组大小,编译器自动计算元素数目:(int []){50,20,90}; //有3个元素的复合文字
由于这些复合文字没有名称,不可能在一个语句中创建它们,然后在另一个语句中使用。而是必须在创建它们的同时通过某种方法来使用它们,一种方法是使用指针保存其位置。例如:int *pt1;
pt1=(int [2]){10,20};
这个文字常量被标识为一个int数组。与数组名相同,这个常量同时代表首元素的地址,因此可以用它给一个指向int的指针赋值。另外,复合文字也可以作为实际参数被传递给带有类型与之匹配的形式参量的函数。这样给函数传递信息就不必事先创建数组。