还记得当初来西邮Linux兴趣小组来面试的时候,第一轮面试,学长们问我的问题就把我问的晕头转向。来到小组前,还自以为把课本看得挺透,没想到却是“只知其表“。那次面试回去之后我反思了好多,学会了、懂得了好多之前没弄明白,或是根本没有勇气弄明白的问题,亦或是自己根本没有想过、意识到的问题。其中就有这样一个问题:
“你知道指针和数组名有什么区别吗?”
听到这个面试题的时候,自己着实倒吸了一口冷气。只记得自己吱吱唔唔了半天,也没有给出学长们一个能让自己满意的答案。所以,回去之后,开始研究这个问题,终于弄懂了其中的玄机。
上个月期末复习的时候,也有位别的学校的朋友来问我些C语言的问题,她表示对数组和指针方面很懵懂,为了给她讲的明白清楚些,自己又上网查询、整理了好多资料,自己对指针与数组这两个看似没什么关系的概念有了一个更深的理解。现把自己对这个问题的理解与体会整理出来和大家分享,第一次写技术博客,难免有缺漏、不足之处,欢迎大家指出错误,提出意见,共同学习共同进步!
数组,一种基本的构造数据类型。在教材上和老师的口中,我们没少见过、听过这样一句话:数组名就是数组的首地址。指针,一个无符号整数(unsigned int),其中存放了其所指向的变量的地址。至此,一个共同之处显而易见:地址。二者都跟地址有着关联。那么两者到底有着怎样的联系与区别?
一、指针和数组名不同?
#include <stdio.h>
#include <string.h>
int main(void)
{
char str1[20] = "My first blog";
char str2[20];
strcpy(str2,str1);
puts(str1);
puts(str2);
return 0;
}
输出:
My first blog
My first blog
标准的strcpy函数声明原型为:
char* strcpy(char *dest, const char *src);
其功能为把从src地址开始的字符串(包括'\0')复制到从dest开始的地址空间。而且从函数原型也可以看出,strcpy函数可以接纳的两个参数都是char型指针。而在这个程序中我们给strcpy函数的两个参数均为数组名。
标准的puts函数原型为:
int puts(const char *s);
同上,puts函数也是接受一个char型指针,我们给puts函数的参数也是数组名,程序也可以正常输入数组str1和str2中的内容。
由此可见,指针和数组名在某些场合的功能和作用是相同的。
二、指针和数组名相同?
我们借助sizeof这一运算符来加以验证。
#include <stdio.h>
#include <string.h>
int main(void)
{
char str[10];
char *p = str;
printf("%d %d\n",sizeof(str),sizeof(p));
return 0;
}
输出:
10 4
倘若数组名和指针相同,那经 sizeof 的“考验”,两者输出结果应该一致才是。而从程序输出来看, sizeof(str) 的结果还是数组的大小, sizeof(p) 的结果就是指针大小( 32 位)。由此可见,数组名不是指针,两者之间还不可以圆满地画上等号。
那么,说了这么多,数组名到底是什么,为什么它在有些场合有着与指针一样的表现,有些场合却“原形毕露”,显示出数组的本质?
“对于一个数组,我们只够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其它有关数组的操作,哪怕它们乍看上去是以数组下标进行运算的,实际上都是通过指针进行的……”
——《C陷阱与缺陷》P34
关于这一段话,可以用一个例子来很好地进行验证。
记得在这学期,学校里举行的一次新老生经济交流会上,一位即将去RedHat工作的学长,面对“怎样学好数组和指针”这个问题时,曾给我们提到过这样一个例子:
“a[i]= *(a+i),如果你能真正地去深刻理解这个式子,你就对数组和指针有了一个比较深的了解了。”
当时听完后却不以为意,认为只是一个可进行替换的恒等式罢了,没太当做回事。直到有一天,偶然去听了一节一个叫杨和平老师讲的C++竞赛辅导课,我才对这个式子“肃然起敬”。他讲了这样一个让我大跌眼镜的例子:
大家都知道,a[i]= *(a+i),那么好,根据加法交换律,a+i= i+a,代入a[i]= *(a+i),那么是不是也意味着,a[i]= i[a]呢?
#include <stdio.h>
int main(void)
{
int a[3] = {1,2,3};
printf("%d %d\n",a[1],1[a]);
return 0;
}
输出:
2 2
就是这么一个 2 ,让我大惊失色。 1[a] 这样一个奇葩的表达式竟然也可以正确表达,看来 a[i]= *(a+i) 这个式子,果然被我小看了。
查阅得知,“[]”这个俗称方括号的东西实际上有着自己专门的名称:变址运算符。其功能就是将a[i]按a+i计算地址,然后取出此地址单元中的值。这也正好验证了上面的结论,数组名在某些方面跟指针有着相同的功能,在程序中加上一句int*p = a;则p和a的指向都是数组0号元素的地址,那么a[1]= *(a+1) = *(p+1)也就不难理解了,先将指针p向后移动了1,再用*取出其中地址单元中的元素,效果自然和a[1]相同。
在网上查到一份对于数组名和指针的比较深刻的定义:
1、数组名的内涵在于数组名是一种实体数据结构——数组。
2、数组名的外延在于它可以转换为指向其实体的指针,而且是一个指针常量。
对于这两条定义的解释:
根据
char str[10];
printf(“%d\n”,sizeof(str));
输出结果为10,就可以证明数组名指代一种数据结构。
#include <stdio.h>
#include <string.h>
int main(void)
{
int str[10];
str++;
return 0;
}
这段程序不会被编译通过,原因就在于第二条定义。虽然,数组名可以转换为指向其实体的指针,但仅仅是一个指针常量,并非一个指针变量。而一个常量的值是不可以被修改的。故str++这句不能被编译通过,编译时会提示:“错误:自增操作数必须是左值”。
还有一种特殊情况:
#include <stdio.h>
void func(char str[])
{
printf("%d \n",sizeof(str));
}
int main(void)
{
char str[10];
printf("%d \n",sizeof(str));
func(str);
return 0;
}
输出:
10
4
为什么会是这个结果呢,同样的一个大小为10的str数组,在主函数main中和函数func中,sizeof测得的大小却不相同。原来,在主函数中,str还是一个数组名,它还具有着数据结构的特性,用sizeof测得的大小还是它所指代的数据结构——数组的大小。而当数组名做了形参后,它的实质则会“沦落”为一个普通的指针,不再具有了数据结构的内涵,故在函数func中,用sizeof测得的大小就是一个32位下指针的大小:4。
“当数组名做为实参时,传递给参数的实际上是一个指向数组起始位置的指针,也就是数组在内存中的地址。正因为实际传递的是一个指针而不是一份数组的拷贝,才使数组名作为参数时具备了传址的语义。“
——《C和指针》 P12
对上面的程序中的func函数中再增加一条str++语句:
#include <stdio.h>
void func(char str[])
{
printf("%d \n",sizeof(str));
str++;
}
int main(void)
{
char str[10];
printf("%d \n",sizeof(str));
func(str);
return 0;
}
程序竟然可以正常编译通过!由此可见,当数组名做了形参后,失去的不仅仅是其指代数组这个数据结构的内涵,同时失去的还有指针常量的特性——已经可以进行自增自减操作了。
“这个事实解释了为什么数组形参可以与任何长度的数组匹配——它实际上传递的只是指向数组第1个元素的指针。另一方面,这种实现方法使函数无法知道数组的长度。如果函数需要知道数组的长度,它必须作为一个显示的参数传递给函数。”
——《C和指针》P152
参考文章:http://soft.yesky.com/242/2082242.shtml