指针和数组是不相同的,但“很多时候”我们总认为指针和数组等价的。不可否认,这两者在某种情况下是可以相互替换的,但并不能就因此而认为在所有情况下都适合。《指针和数组不是一回事儿》系列文章将逐步深入分析指针和数组的不同之处,并解释什么时候指数组等价于指针。本文属于《指针和数组不是一回事儿》系列文章之二。
前文从内存结构的角度说明了指针和数组的不同,本文将以访问方式的角度再次说明指针和数组的不同。先看下面的代码:
char str[] = "edsionte"; char *p = "edsionte";
当编译完程序后,程序中的标示符都有一个地址,所有标示符的地址形成一个符号表。
数组的访问方式
以数组str为例,如果要访问str[1],即数组str的第二个元素,则它的访问步骤如下:
1.从编译器的符号表中得到str的地址,比如0x0000FE00。这个地址即为数组str首元素的首地址;
2.在这个地址上加一个偏移量得到新的地址0x0000FE01;
3.从这个新地址中读取数据;
通过下图可以加深对数组访问方式的理解:
指针的访问方式
以上述代码中的指针p为例,如果要访问*(p+1),即指针p所指向的匿名字符串的第二个字符,则它的访问步骤如下:
1.从编译器符号表中得到指针p的地址,比如0x0000EE20。这个地址即为&p,也就是指向指针p的指针;
2.从地址0x0000EE20中读取它内容即为指针p,比如0x00F0A000;
3.在0x00F0A000的基础上加一个偏移量,得到新地址0x00F0A001;
4.读取0x00F0A001中的内容,即为指针p所指的数据;
通过下图可以近一步理解指针的访问方式:
通过分析得知,在符号表中得到的是指针的地址而不是我们所要访问的指针;而在符号表中可以直接得到数组首元素的首地址。因此,访问指针时必须先通过符号表中指针的地址得到所要访问的指针,再接着进行指针所指内容的访问;而数组则直接可以通过符号表中的地址进行元素访问。也就是说指针的访问比数组的访问多了一次对内存地址的读取。
一不小心就引发的错误
现在看下面的两段代码:
/* 代码段1 */ file1: char str[] = "edsionte"; file2: extern char *str; /* 代码段2 */ file1: char *p == "edsionte"; file2: extern char str[];
对于代码段1,在文件1中定义了数组str,而在文件2中将str声明为指针;对于代码段2,在文件1中定义了指针p,而在文件2中将p声明为数组。这里的声明指的是外部引用型声明,定义指的是定义型声明。
不管是上述那种情形,编译的时候都会出现错误。从上述对指针和数组访问方式的分析中可以得知,一个标示符被声明成指针还是数组对其访问方式影响巨大。下面我们对这两种错误作详细分析。
定义为数组,声明为指针
在文件2中,既然str被声明成指针,那么就应当按照指针的方式进行访问。首先从符号表中得到指针str的地址;从该地址中读取4个字节的数据即为指针str;接下来根据指针str访问其所指向的数据。这个过程好像很顺利,不过对于文件1中的数组str,其访问过程又是怎样的?文件1和文件2的访问结果是否一致?下图可帮助你理解。
从上图可以看到,str在文件2中被声明成指针,那么就符号表中str的地址0x0000FE00会被当作指针str的地址。根据指针的访问方式,必须从这个地址中取出指针p。虽然以0x0000FE00为首的四个字节中存储的是“edsi”,但是它们一律会被当成地址,按十六进制表示即为0x65647379。即便可以访问到这个地址,但是从这个地址中按照char型取出的数据并不是我们想要的。
由于编译器对每个文件进行单独编译,文件2并不知道str在文件1中被定义成什么类型。str在文件1中被定义成数组,那么就应该按照数组的方式访问数据;str在文件2中被声明成指针,那么就应该按照数组的方式访问数据。因此,在两个不同的文件中分别将str定义成数组而声明成指针会出现对str访问不一致的现象,所以编译器会产生错误。
定义为指针,声明为数组
此时,对于这种情况的理解也就简单多了。由于在文件而中p被声明成数组,因此就应该按照数组的方式对其进行访问。编译器会将原本指针str的地址当作str数组首元素的首地址,再对其加相应偏移量进行访问。这显然也是不合理的,因此编译器产生错误。具体可参见下图:
对上述的错误进行分析后,我们应该清楚将一个标示符声明成数组,编译器就会按照数组的访问方式去访问它;指针也是如此。因此,应该在多个文件中保持声明和定义相匹配。
指针和数组的其他区别
指针和数组除了在内存构造和访问方式上不同外,还有一些其他的区别。
1.指针通常用于指向一个动态的数据结构,而数组则用于存储固定大小和数据类型相同的数据;
2.指针所指向的数据通过malloc()分配,并且需要free()释放;而数组本身的内存空间则是隐士分配和释放,也就是在定义数组的时候进行;
3.指针所指向的数据通常是匿名的,而数组名则是数组所占内存空间的名字;
在本系列的最后一篇文章中,我们将分析指针和数组易被混淆的根源——也可将其称为指针和数组的可交换性。
参考:
《C专家编程》 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;
《C语言深度解剖》北京航空航天大学出版社;陈正冲 著;