转]C语言灵魂——指针
原作者:wuliming
指针与数组1(c缺陷与陷阱3.1节)
c语言中的数组值得注意的地方有以下两点:
1、c语言中只有一维数组,而且数组的大小必须在编译期间就作为一个常数确定下来(C99标准允许变长数组,GCC编译器中实现了变长数组)。然而,c语言中数组的元素可以是任何类型的对象,当然也可以是另外一个数组。这样,要仿真出一个多维数组就不是一件难事。
2、对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其他有关数组的操作,哪怕它们乍看上去是以数组下标进行运算的,实际上都是通过指针进行的。换句话说,任何一个数组下标运算都等同于一个对应的指针运算,因此我们完全可以依据指针行为定义数组下标的行为。
现在考虑下面的例子:
int i;
int *p;
int calendar[12][31];
上面声明的calendar是一个数组,该数组拥有12个数组类型的元素,其中的每个元素都是一个拥有31个整型元素的数组。因此,sizeof(calendar)的值是:31×12×sizeof(int)。
考虑一下,calendar[4]的含义是什么?因为calender是一个有着12个数组类型元素的数组,它的每个数组类型元素又是一个有着31个整型元素的数组,所以calendar[4]是calendar数组的第5个元素,是calendar数组中12个有着31个整型元素的数组之一。因此,calendar[4]的行为也表现为一个有着31个整型元素的数组的行为。例如,sizeof(calendar[4])的结果是:31×sizeof(int)。
又如,p = calendar[4];这个语句使指针p指向了数组calendar[4]中下标为0的元素。因为calendar[4]是一个数组,我们可以通过下标的形式来指定这个数组中的元素:i = calendar[4][7],这个语句也可以写成下面这样而表达的意思保持不变:i = *( calendar[4] + 7 ),还可以进一步写成:i = *( *( calendar + 4 ) + 7 )。
下面我们再看:p = calendar; 这个语句是非法的,因为calendar是一个二维数组,即“数组的数组”,在此处的上下文中使用calendar名称会将其转换为一个指向数组的指针。而p是一个指向整型变量的指针,两个指针的类型不一样,所以是非法的。显然,我们需要一种声明指向数组的指针的方法。
int calendar[12][31];
int (*monthp)[31];
monthp = calendar;
int (*monthp)[31] 语句声明的 *monthp 是一个拥有31个整型元素的数组,因此,monthp就是一个指向这样的数组的指针。monthp指向数组calendar的第一个元素。
指针与数组2(c和指针.P141.)
·1、数组的名的值是一个指针常量,不能试图将一个地址赋值给数组名;
·2、当数组名作为sizeof操作符的操作数时,sizeof(arrayname)返回的是整个数组的长度,而不是指向数组的指针的长度;
·3、当数组名作为单目操作符&的操作数,取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量值的指针。
·4、指针和数组并不总是相等的。为了说明这个概念,请考虑下面这两个声明:
int a[5];
int *b;
a和b能够互换吗?它们都具有指针值,它们都可以进行间接访问和下标操作。但是,它们还是有很大的区别的:声明一个数组时,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置。声明一个指针变量时,编译器只为指针本身保留内存空间,它并不为任何整型值分配内存空间。而且,指针变量并未被初始化为指向任何现有的内存空间,如果它是一个自动变量,它甚至根本不会被初始化。把这两个声明用图的方法表示,可以发现它们之间存在显著的不同:
| | | | |
? |
#include<stdio.h> int main() { } |
指针和数组的相同与不同(c专家编程.P199.)
在实际应用中,数组和指针可以互换的情形要比两者不可互换的情形更为常见。让我们分别考虑“声明”和“使用”这两种情况。声明本身还可以进一步分为3种情况:
·外部数组的声明;
·数组的定义(定义是声明的一种特殊情况,它分配内存空间,并可能提供一个初始值);
·函数参数的声明;
也既是:作为函数参数时、在语句或表达式中使用数组时,我们可以采用数组或者指针的任何一种形式,除此之外的其他情况下,指针和数组不要互换。下面就数组和指针相同的情况做详细的说明:
·规则1、表达式中的数组名被编译器当作一个指向该数组第一个元素的指针。
假如我们声明:
int a[10];
int *p = a;
就可以通过一下任何一种方式来访问a[i]:
p[i]
事实上,可以采用的方法很多。对数组的引用如a[i] 在编译时总是被编译器改写成*(a+i)的形式,C语言标准要求编译器必须具备这个概念性的行为。
编译器自动把下标值的步长调整到数组元素的大小。如果整型数的长度是4个字节,那么a[i+1]和a[i]在内存中的距离就是4。对起始地址执行加法操作之前,编译器会负责计算每次增加的步长。这就是为什么指针总是有类型限制,每个指针只能指向一种类型的原因所在,因为编译器需要知道对指针进行解除引用操作时应该取几个字节,以及每个下标的步长应取几个字节。
·规则2、下标总是和指针的偏移量相同。
把数组下标作为指针加偏移量是c语言从BCPL(C语言的祖先)继承过来的技巧。在人们的常规思维中,在运行时增加对c语言下标的范围检查是不切实际的。因为取下标操作只是表示将要访问该数组,但并不保证一定要访问。而且程序员完全可以使用指针来访问数组,从而绕过下标操作符。在这种情况下,数组下标范围检测并不能检测所有对数组的访问的情况。事实上,下标范围检测被认为不值得加入到c语言当中。
还有一个说法是,在编写数组算法时,使用指针比使用数组更有效率。这个颇为人们所接收的说法在通常情况下是错误的。使用现代的产品质量优化的编译器,一维数组和指针引用所产生的代码并不具有显著的差别。不管怎样,数组下标是定义在指针的基础上,所以优化器常常可以把它转化为更有效率的指针表达式,并生成相同的机器指令。
·规则3、在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针。
my_function( int *turnip ) {···}
my_function( int turnip[] ) {···}
my_function( int turnip[200] ) {···}
array_name和&array_name的异同
前者是指向数组中第一个元素的指针,后者是指向整个数组的指针。
char a[MAX];
char *p = a;
char *pa = &a;
char (*pb)[MAX] = &a;
#include<stdio.h> void main() { char a[5] = {'a','b','c','d','\0'}; char *p = a; //运行下面这句后, vc6.0 提示的错误为:cannot convert from 'char (*)[5]' to 'char *',&a的类型应该是指向一个数组的指针 //char *pa = &a; //所以,应该定义一个指向相同类型和大小的数组的指针来获得“&a”的值 char (*point_to_str)[5]; } 运行结果为: 1245044 1245040 abcd abcd
|