C语言:数组和指针
数组和指针
const 指针 数组
数组
初始化
int powers[8] = {1,2,4,6,8,16,32,64}; /* 从ANSI C开始支持这种初始化 */
使用const声明数组
有时需要把数组设置为只读。这样,程序只能从数组中检索值,不能把 新值写入数组。要创建只读数组,应该用const声明和初始化数组。因此,初始化数组应改成:
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
这样修改后,程序在运行过程中就不能修改该数组中的内容。和普通变 量一样,应该使用声明来初始化 const 数据,因为一旦声明为 const,便不能 再给它赋值。明确了这一点,就可以在后面的例子中使用const了。
当初始化列表中的值少于数组元素个数 时,编译器会把剩余的元素都初始化为0。也就是说,如果不初始化数组, 数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;但是,如果 部分初始化数组,剩余的元素就会被初始化为0。
可以省略 方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数
const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31 };
/*如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的 项数来确定数组的大小。
*/
指定初始化器(C99)
利用 该特性可以初始化指定的数组元素。例如,只初始化数组中的最后一个元 素。对于传统的C初始化语法,必须初始化最后一个元素之前的所有元素, 才能初始化它:
int arr[6] = {0,0,0,0,0,212}; // 传统的语法
int arr[6] = {[5] = 212}; // 把arr[5]初始化为212
/*对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置 为0。*/
如果未指定元素大小会怎样?
int stuff[] = {1, [6] = 23}; //会发生什么?
int staff[] = {1, [6] = 4, 9, 10}; //会发生什么?
/*编译器会把数组的大小设置为足够装得下初始化的值。所以,stuff数组 有7个元素,编号为0~6;而staff数组的元素比stuff数组多两个(即有9个元 素)。
*/
指定数组的大小
在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所 谓整型常量表达式,是由整型常量构成的表达式。sizeof表达式被视为整型 常量,但是(与C++不同)const值不是。另外,表达式的值必须大于0:
多维数组
初始化
初始化二维数组是建立在初始化一维数组的基础上
const float rain[YEARS][MONTHS] =
{
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4},
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2},
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2}
};
指针和数组
指针提供一种以符号形式使用地址的方法。因为计 算机的硬件指令非常依赖地址,指针在某种程度上把程序员想要传达的指令 以更接近机器的方式表达。因此,使用指针的程序更有效率。尤其是,指针 能有效地处理数组。数组表示法其实是在变相地使用指 针。
//变相使用指针的例子:数组名是数组首元素的地址。
flizny == &flizny[0]; // 数组名是该数组首元素的地址
我们的系统中,地址按字节编址,short类型占用2字节,double类型占 用8字节。在C中,指针加1指的是增加一个存储单元。对数组而言,这意味 着把加1后的地址是下一个元素的地址,而不是下一个字节的地址(见图 10.3)。这是为什么必须声明指针所指向对象类型的原因之一。只知道地址 不够,因为计算机要知道储存对象需要多少字节(即使指针指向的是标量变 量,也要知道变量的类型,否则*pt 就无法正确地取回地址上的值)。
函数、数组和指针
既然能使用指针表示数组名,也可以用数组名表示指针。
int sum(int * ar) // 相应的函数定义
关于函数的形参,还有一点要注意。只有在函数原型或函数定义头中, 才可以用int ar[]代替int * ar:
int *ar形式和int ar[]形式都表示ar是一个指向int的指针。但是,int ar[]只 能用于声明形式参数。第2种形式(int ar[])提醒读者指针ar指向的不仅仅 一个int类型值,还是一个int类型数组的元素。
指针操作
#include <stdio.h>
int main(void)
{
int urn[5] = { 100, 200, 300, 400, 500 };
int * ptr1, *ptr2, *ptr3;
ptr1 = urn; // 把一个地址赋给指针
ptr2 = &urn[2]; // 把一个地址赋给指针
// 解引用指针,以及获得指针的地址
printf("pointer value, dereferenced pointer, pointer address:\n");
printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
// 指针加法
ptr3 = ptr1 + 4;
printf("\nadding an int to a pointer:\n");
printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
ptr1++; // 递增指针
printf("\nvalues after ptr1++:\n");
printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
ptr2--; // 递减指针
printf("\nvalues after --ptr2:\n");
printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
--ptr1; // 恢复为初始值
++ptr2; // 恢复为初始值
printf("\nPointers reset to original values:\n");
printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
// 一个指针减去另一个指针
printf("\nsubtracting one pointer from another:\n");
printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);
// 一个指针减去一个整数
printf("\nsubtracting an int from a pointer:\n");
printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
return 0;
}
下面分别描述了指针变量的基本操作。
赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的 变量名、另一个指针进行赋值。在该例中,把urn数组的首地址赋给了ptr1, 该地址的编号恰好是0x7fff5fbff8d0。变量ptr2获得数组urn的第3个元素 (urn[2])的地址。注意,地址应该和指针类型兼容。也就是说,不能把 double类型的地址赋给指向int的指针,至少要避免不明智的类型转换。 C99/C11已经强制不允许这样做。
解引用:运算符给出指针指向地址上储存的值。因此,ptr1的初值是 100,该值储存在编号为0x7fff5fbff8d0的地址上
取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言, &运算符给出指针本身的地址。本例中,ptr1 储存在内存编号为 0x7fff5fbff8c8 的地址上,该存储单元储存的内容是0x7fff5fbff8d0,即urn的地 址。因此&ptr1是指向ptr1的指针,而ptr1是指向utn[0]的指针。
指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针 相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位) 相乘,然后把结果与初始地址相加。因此ptr1 +4与&urn[4]等价。如果相加 的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好 超过数组末尾第一个位置,C保证该指针有效
递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个 元素。因此,ptr1++相当于把ptr1的值加上4(我们的系统中int为4字节), ptr1指向urn[1](见图10.4,该图中使用了简化的地址)。现在ptr1的值是 0x7fff5fbff8d4(数组的下一个元素的地址),*ptr的值为200(即urn[1]的 值)。注意,ptr1本身的地址仍是 0x7fff5fbff8c8。毕竟,变量不会因为值发 生变化就移动位置。
指针减去一个整数:可以使用-运算符从一个指针中减去一个整数。指 针必须是第1个运算对象,整数是第 2 个运算对象。该整数将乘以指针指向 类型的大小(以字节为单位),然后用初始地址减去乘积。所以ptr3 - 2与 &urn[2]等价,因为ptr3指向的是&arn[4]。如果相减的结果超出了初始指针所 指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位 置,C保证该指针有效。
递减指针:当然,除了递增指针还可以递减指针。在本例中,递减ptr3 使其指向数组的第2个元素而不是第3个元素。前缀或后缀的递增和递减运算 符都可以使用。注意,在重置ptr1和ptr2前,它们都指向相同的元素urn[1]。
指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向 同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数 组类型的单位相同。例如,程序清单10.13的输出中,ptr2 - ptr1得2,意思是 这两个指针所指向的两个元素相隔两个int,而不是2字节。只要两个指针都 指向相同的数组(或者其中一个指针指向数组后面的第 1 个地址),C 都能 保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出 一个值,或者导致运行时错误。
比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象
保护数组中的数据
如果函数使用的是原始数据的副本,就不会意外修改原始 数据。但是,处理数组的函数通常都需要使用原始数据,因此这样的函数可 以修改原数组。
对形式参数使用const
ANSI C提供 了一种预防手段。如果函数的意图不是修改数组中的数据内容,那么在函数 原型和函数定义中声明形式参数时应使用关键字const
int sum(const int ar[], int n); /* 函数原型 */
int sum(const int ar[], int n) /* 函数定义 */
{
int i;
int total = 0;
for( i = 0; i < n; i++)
total += ar[i];
return total;
}
以上代码中的const告诉编译器,该函数不能修改ar指向的数组中的内 容。如果在函数中不小心使用类似ar[i]++的表达式,编译器会捕获这个错 误,并生成一条错误信息。
这里一定要理解,这样使用const并不是要求原数组是常量,而是该函 数在处理数组时将其视为常量,不可更改。这样使用const可以保护数组的 数据不被修改,就像按值传递可以保护基本数据类型的原始值不被改变一 样。一般而言,如果编写的函数需要修改数组,在声明数组形参时则不使用 const;如果编写的函数不用修改数组,那么在声明数组形参时最好使用 const。
const其他内容
用#define指令可以创建类似功能的符号常量,但是const的用法更加 灵活。可以创建const数组、const指针和指向const的指针。
关于指针赋值和const需要注意一些规则。首先,把const数据或非const 数据的地址初始化为指向const的指针或为其赋值是合法的
void show_array(const double *ar, int n)
{
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};
const double * pc = rates; // 有效
pc = locked; //有效
pc = &rates[3]; //有效
}
/*然而,只能把非const数据的地址赋给普通指针:
*/
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数组中的数据。
C标准规定,使用非const标识符(如,mult_arry()的形参ar)修改const 数据(如,locked)导致的结果是未定义的。
可以声明并初始化一个不能指向别处的 指针,关键是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]的值
指向const的指针不能用于改变值。考虑下面的代码:
double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
const double * pd = rates; // pd指向数组的首元素
*pd = 29.89; // 不允许
pd[2] = 222.22; //不允许
rates[0] = 99.99; // 允许,因为rates未被const限定
/*无论是使用指针表示法还是数组表示法,都不允许使用pd修改它所指向 数据的值
另外,可以让pd指向别处:
*/
pd++; /* 让pd指向rates[1] -- 没问题 */
在创建指针时还可以使用const两次,该指针既不能更改它所指 向的地址,也不能修改指向地址上的值:
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]; /* 内含int数组的数组 */
数组名zippo是该数组首元素的地址。在本例中,zippo的首元素是 一个内含两个int值的数组,所以zippo是这个内含两个int值的数组的地址。 下面,我们从指针的属性进一步分析。
因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]的值相同。 而zippo[0]本身是一个内含两个整数的数组,所以zippo[0]的值和它首元素 (一个整数)的地址(即&zippo[0][0]的值)相同。简而言之,zippo[0]是一 个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地 址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippo 和zippo[0]的值相同
给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippo 和zippo[0]不同,因为zippo指向的对象占用了两个int大小,而zippo[0]指向的 对象只占用一个int大小。因此, zippo + 1和zippo[0] + 1的值不同。
指针的兼容性
指针之间的赋值比数值类型之间的赋值要严格。例如,不用类型转换就 可以把 int 类型的值赋给double类型的变量,但是两个类型的指针不能这样做。
把const指针赋给非const指针不安全,因为这样可以使用 新的指针改变const指针指向的数据。编译器在编译代码时,可能会给出警 告,执行这样的代码是未定义的。但是把非const指针赋给const指针没问 题,前提是只进行一级解引用:
C const和C++ const
C和C++中const的用法很相似,但是并不完全相同。区别之一是, C++允许在声明数组大小时使用const整数,而C却不允许。区别之二是, C++的指针赋值检查更严格:
const int y;
const int * p2 = &y;
int * p1;
p1 = p2; // C++中不允许这样做,但是C可能只给出警告
C++不允许把const指针赋给非const指针。而C则允许这样做,但是如果 通过p1更改y,其行为是未定义的。
函数和多维数组
如果要编写处理二维数组的函数,首先要能正确地理解指针才能写出声 明函数的形参。在函数体中,通常使用数组表示法进行相关操作。
void somefunction( int (* pt)[4] );
void somefunction( int pt[][4] );
int sum2(int ar[][], int rows); // 错误的声明
int sum2(int ar[][4], int rows); // 有效声明
int sum2(int ar[3][4], int rows); // 有效声明,但是3将被忽略
一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中 的值:
int sum4d(int ar[][12][20][30], int rows);
因为第1对方括号只用于表明这是一个指针,而其他的方括号则用于描 述指针所指向数据对象的类型。下面的声明与该声明等价:
int sum4d(int (*ar)[12][20][30], int rows); // ar是一个指针
变长数组
,C99新增了变长数组(variable-length array,VLA),这意味着变长数组的大小延迟到程序运行时才确定。
允许使用 变量表示数组的维度。如下所示:
int quarters = 4;
int regions = 5;
double sales[regions][quarters]; // 一个变长数组(VLA)
变长数组必须是自动存储类别,这 意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern 存储类别说明符而且,不能在声明中初始化它们。最终, C11把变长数组作为一个可选特性,而不是必须强制实现的特性。
变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用 变量指定数组的维度
要声明一个带二维变长数组参数的函数,如下所示:
int sum2d(int rows, int cols, int ar[rows][cols]); // ar是一个变长数组 (VLA)
注意前两个形参(rows和cols)用作第3个形参二维数组ar的两个维度。 因为ar的声明要使用rows和cols,所以在形参列表中必须在声明ar之前先声 明这两个形参。因此,下面的原型是错误的:
int sum2d(int ar[rows][cols], int rows, int cols); // 无效的顺序
C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必 须用星号来代替省略的维度:
int sum2d(int, int, int ar[*][*]); // ar是一个变长数组(VLA),省略了维度 形参名
//其次,该函数的定义如下:
int sum2d(int rows, int cols, int ar[rows][cols])
{
int r;
int c;
int tot = 0;
for (r = 0; r < rows; r++)
for (c = 0; c < cols; c++)
tot += ar[r][c];
return tot;
}
以变长数组作为形参的函数既可处理传统C数组,也 可处理变长数组。
C99/C11 标准允许在声明变长数组时使用 const 变量。所以该数组的定 义必须是声明在块中的自动存储类别数组。
变长数组还允许动态内存分配,这说明可以在程序运行时指定数组的大 小。普通 C数组都是静态内存分配,即在编译时确定数组的大小。由于数组 大小是常量,所以编译器在编译时就知道了
复合字面量
在C99 标准以前,对于带数组形参的函数,情 况不同,可以传递数组,但是没有等价的数组常量。C99新增了复合字面量 (compound literal)。发布C99标准的委员会认为,如果有代表数组和结构内容的复 合字面量,在编程时会更方便
对于数组,复合字面量类似数组初始化列表,前面是用括号括起来的类 型名。例如,下面是一个普通的数组声明:
int diva[2] = {10, 20};
下面的复合字面量创建了一个和diva数组相同的匿名数组,也有两个int 类型的值:
(int [2]){10, 20} // 复合字面量
//初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大 小,编译器会自动计算数组当前的元素个数:
(int []){50, 20, 90} // 内含3个元素的复合字面量
因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建 的同时使用它。使用指针记录地址就是一种用法。也就是说,可以这样用:
int * pt1;
pt1 = (int [2]) {10, 20};
把信息传入函数前不必先创建 数组,这是复合字面量的典型用法。
int sum(const int ar[], int n);
...
int total3;
total3 = sum((int []){4,4,4,5,5,5}, 6);
复合字面量的定义在最 内层的花括号中。一旦离开定义复合字面量的块,程序将无法保证该字面量是否存在