指针操作
可以对指针进行哪些操作? C提供了些基本的指针操作,下面的程序示例中演示了8种不同的操作。为了显示每种操作的结果,该程序打印了指针的值(该指针指向的地址)、储存在指针指向地址上的值,以及指针自己的地址。如果编译器不支持%p转换说明,可以用%u或%lu代替%p:如果编译器不支持用%td转换说明打印地址的差值,可以用%d 或号%ld来代替。
以下程序演示了指针变量的8种基本操作。除了这些操作,还可以使用关系运算符来比较指针。
#include<stdio.h>
#include<stdlib.h>
int main()
{
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("\n adding an int to pointer : \n");
printf("ptr1 + 4 = %p ,*(ptr1+4) = %d \n", ptr1+4, *(ptr1+4));
ptr1++;
printf("\n values after ptr1++ :\n");
printf("ptr1 = %p ,*ptr1 = %d ,&ptr1 = %p \n", ptr1, *ptr1, &ptr1);
ptr2--;
printf("\n values after --ptr2 :\n");
printf("ptr2 = %p ,*ptr2 = %d ,&ptr2 = %p \n", ptr2, *ptr2, &ptr2);
--ptr1;
++ptr2;
printf("\n pointers reset to original values:\n");
printf("ptr1 = %p , ptr2 = %p", ptr1, ptr2);
printf("\n subtracting one pointer from another : \n");
printf("ptr1 = %p , ptr1 = %p , ptr2 - ptr1 = %td \n", ptr2, ptr1, ptr2 - ptr1);
printf("\n subtracting an int from a pointer : \n");
printf("ptr3 = %p , ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
return 0;
}
- 赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。注意,地址应该和指针类型兼容。也就是说,不能把double类型的地址赋给指向int的指针,至少要避免不明智的类型转换。
- 解引用:运算符给出指针指向地址上储存的值。因此,*ptr1 的初值是100,该值储存在编号007BFE6C的地址上。
- 取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。
- 指针与整数相加:可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
- 递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。因此,ptr1++相当于把ptr1的值加上4(我们的系统中int为4字节),ptr1 指向urn[1]。注意ptr1的地址仍是007BFE60,毕竟,变量不会因为值发生变化就移动位置。
- 指针减去一个整数:可以使用-运算符从一个指针中减去一个整数。指针必须是第一个运算对象,整数是第二个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
- 递减指针:当然,除了递增指针还可以递减指针。前缀或后缀的递增和递减运算符都可以使用。
- 指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误。
- 比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
在递增或递减指针时还要注意一些问题。 编译器不会检查指针是否仍指向数组元素。C只能保证指向数组任意元素的指针和指向数组后面第一个位置的指针有效。但是,如果递增或递减一个指针后超出了这个范围,则是未定义的。另外,可以解引用指向数组任意元素的指针。但是, 即使指针指向数组后面一个位置是有效的,也能解引用这样的越界指针。
指针和多维数组
指针和多维数组有什么关系?为什么要了解它们的关系?处理多维数组的函数要用到指针,所以在使用这种函数之前,先要更深入地学习指针。至于第1个问题,我们通过几个示例来回答。 为简化讨论。我们使用较小的数组。假设有下面的声明:
int zippo[4][2];
然后数组名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的值不同。
- 解引用一个指针 (在指针前使用运算符)或在数组名后使用带下标的[]运算符,得到引用对象代表的值。因为zippo[0]是该数组首元素(zippo[0][0])的地址,所以(zippo[0])表示储存在zippo[0][0]上的值(即个int类型的值)。与此类似,zippo代表该数组首元素(zippo[0])的值,但是zippo[0]本身是个int类型值的地址。该值的地址是&zippo[0][0],所以zippo就是&zippo[0][0]。对两个表达式应用解引用运算符表明,zippo与&zippo[0][0]等价,这相当于zippo[0][0],即一个int类型的值。简而言之,zippo是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针是就是双重间接(double indiretion)*的例子。
指向多维数组的指针
如何声明一个指针变量pz 指向一个二维数组(如, zippo) ?在编写处理类似zippo这样的二维组时会用到这样的指针。把指针声明为指向int的类型还不够。因为指向int只能与zippo[0]的类型匹配,说明该指针指向个int类型的值。 但是zippo 是它首元素的地址, 该元素是一个内含两个 int,型值的一维数组。 因此,pz必须指向一个内含两个int类型值的数组,而不是指向一个int类型值。声明如下:
int (*pz)[2];
以上代码把pz声明为指向一个 数组的指针,该数组内含两个int 类型值。为什么要在声明中使用圆括号?因为[]的优先级高于*。考虑下面的声明:
int *pax[2];
由于[]优先级高,先与pax结合,所以pax成为个内含两个元素的数组。 然后*表示pax数组内含两个指针,最后int表示pax数组中的指针都指向int类型的值。因此,这行代码声明了两个指向int的指针,而前面有圆括号的版本,*先与pz结合,因此声明的是一个指向数组(内含两个int类型的值)的指针。
函数和多维数组
如果要编写处理二维数组的函数,首先要能正确地理解指针才能写出声明函数的形参。在函数体中,通常使用数组表示法进行相关操作。
下面,我们编写个处理二维数组的函数。一种方法是,利用for循环把处理维数组的函数应用二维数组的每行。如下所示:
int junk[3][4] = {{2,4,5,8},{3,5,6,9},{12,10,8,6}};
int i ,j;
int total = 0:
for (i= 0;i<3;i++)
total +=sum(junk[i], 4);
记住,如果junk是二维数组,junk[i]就是一维数组, 可将其视为一维数组的一行。这里,sum()函数计算二维数组的每行的总和,然后for循环再把每行的总和加起来。
然而,这种方法无法记录行和列的信息。用这种方法计算总和,行和列的信息并不重要。但如果每行代表年,每列代表一个月, 就还需要个函数计算某列的总和。该函数要知道行和列的信息,可以通过声明正确类型的形参变量来完成,以便函数能正确地传递数组。在这种情况下,数组junk是一个内含3个数组元素的数组,每个元素是内含4个int类型值的数组(即junk是个3行4列的二维数组)。通过前面的讨论可知,这表明junk是个指向数组 (内含4个int类型值)的指针。可以这样声明函数的形参:
void somefunction( int (* pt) [4) ) ;
另外,如果当且仅当pt是一个函数的形式参数时,可以这样声明:
void somefunction( int pt[][4] );
注意,第1个方括号是空的。空的方括号表明pt是一个指针。这样的变量稍后可以用作相同方法作为junk。
变长数组(VLA)
以之前的sum()函数为例,sum()函数之所以能处理这些数组,是因为这些数组的列数固定为4,而行数被传递给形参rows,rows是一个变量。但是如果要计算6X5的数组(即6行5列),就不能使用这个函数,必须重新创建一个cols为5的函数。因为C规定,数组的维数必须是常量,不能用变量来代替cols。
要创建一个能处理任意大小维数组的函数,比较繁琐(必须把数组作为一维数组传递, 然后让函数计算每行的开始处)。而且,这种方法不好处理FORTRAN的子例程,这些子例程都允许在函数调用中指定两个维度。虽然FORTRAN是比较老的编程语言,但是在过去的几十年里,数值计算领域的专家已经用FORTRAN开发出许多有用的计算库。C正逐渐替代FORTRAN,如果能直接转换现有的FORTRAN库就好了。
鉴于此,C99 新增了实长教组*(voriable length array, VLA)*, 允许使用变量表示数组的维度, 如:下所示:
int quarters = 4;
int regions = 5;
double sales[regions][quarters];
前面提到过,变长数组有一些限制。 变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern存储类别说明符。而且,不能还是在声明中初始化它们。最终,C11 把变长数组作为一个可选特性,而不是必须强制实现的特性。
- 注意 变长数组不能改变大小
变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度。
由于变长数组是C语言的新特性,目前完全支持这特性的编译器不多。 下面我们来看一个简单的例子:如何编写一个函数, 计算int的二维数组所有元素之和。 首先,要声明一个带二维变长数组参数的函数,如下所示:
int sum(int rows, int cols, int ar[rows][co1s]);
注意前两个形参(rows和cols)用作第3个形参二维数组ar的两个维度。因为ar的声明要使用rows和cols,所以在形参列表中必须在声明ar之前先声明这两个形参。因此,下 面的原型是错误的:
int sum(int ar[rows][cols],int rows, int cols);
C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度:
int sum(int, int, int ar[*][*])
其次,该函数的定义如下:
int sum(int rows, int cols, int arr[rows][cols])
{
int r;
int c;
int total = 0;
for (r = 0; r < rows; r++)
for(c = 0; c < cols; c++)
total += ar[r][c];
return total;
}
该函数除函数头与传统的C函数不同外,还把符号常量COLS替换成变量cols.
这是因为在函数头中使用了变长数组。由于用变量代表行数和列数,所以新的sum()现在可以处理任意大小的二维int数组。但是,该程序要求编译器支持变长数组特性。
需要注意的是,在函数定义的形参列表中声明的变长数组并未实际创建数组。和传统的语法类似,变长数组名实际上是个指针, 这说明带变长数组形参的函数实际上是在原始的数组中处理数组,因此可以改变传入的数组。下面的代码段指出指针和实际数组是何时声明的:
int thing[10][6];
twoset(10,6,thing);
……
}
void twoset(int n, int m, int ar[n][m])
{
int temp[n][m];
temp[0][0] = 2;
ar[0][0] = 2;
}
如上代码所示调用twoset()时,ar成为指向thing[0]的指针,temp被创建为10x6 的数组,因为ar和thing都是指向thing[0]的指针,ar[0][0]与thing [0] [0]访问的数据位置相同。
const和数组大小 是否可以在声明数组时使用const变量?
const int SZ = 80:
double ar[SZ]:
C90标准不允许(也可能允许),数组的大小必须是给定的整型常量表达式,可以是整型常,如20,sizeof 表达式或其他不是const的内容。由于C实现可以扩大整型常量表达式的范围,所以可能会允许使用const,但是这种代码可能无法移植。 C99/C11标准允许在声明变长数组时使用const变量。所以该数组的定义必须是声明在块中的自动存储类别数组。
柔性数组
在标准C和C++中0长数组如cha rArray[0]是不允许使用的,因为这从语义逻辑上看,是完全没有意义的。但是,GUN中却允许使用,而且,很多时候,应用在了变长结构体中,如:
struct Packet
{
int state;
int len;
Char cData[0];
};
长度为0的数组的主要用途是为了满足需要变长度的结构体。在一个结构体的最后 ,声明一个长度为0的数组,就可以使得这个结构体是可变长的。对于编译器来说,此时长度为0的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量, 数组名这个符号本身代 表了一个不可修改的地址常量 ,但对于这个数组的大小,我们可以进行动态分配。
柔性数组的特点
- 结构体中的柔性数组成员前面必须至少含有一个别的类型的成员;
- sizeof返回的结构体的大小不包含柔性数组的大小;
- 包含柔性数组成员的结构体的大小用malloc函数动态分配内存,并且分配的内存大小应该大于结构体的大小,以适应柔性数组的预期大小。
|