C语言中指针与数组的区别与联系

好久不写东西了,从毕业以来,整个人都懒散了很多。今天终于鼓起勇气,来写一点儿东西……

指针与数组对于C语言程序员来说肯定不会陌生,一说起这个话题,我就想起了曾经被内存、地址、地址里的内容这些概念狂虐时的情形。经过三年的学习,加上最近又看了一些这方面的书籍,现在自我感觉对这方面已经有了一个比较全面的理解,分享出来,和大家共勉。
1.指针与数组的爱恨情仇

为什么这一块儿内容很绕呢?我想主要有两个方面的原因,一是指针本身就是C语言中最为难掌握的内容。二是指针与数组之间的暧昧关系让人头疼。

针对第一个方面,我想没有什么特殊的好办法,只有你把教科书上指针这一部分吃透,并且要经历过一段时间的使用经验,甚至是几个令人抓狂的bug,才会对指针有所了解。这里给大家两个经验,一是要把内存的概念和指针同时来学习,因为指针本来就是放的内存地址,没有内存的概念,指针是很难学好的。二是如果要想深入理解指针,最好去读一些有关汇编语言的书籍,看看指针在汇编层面上的表示,相信你对指针的理解会上升一个档次。这里推荐大家看《深入理解计算机系统》一书,这本书是难得一见的好书,应该是每一个程序员必看的经典之一。

针对第二个方面,也就是指针与数组的关系,我们可以从下面这个方面入手。在C语言创建之初,很大一部分C语言的使用者是编译器的设计者,所以为了迎合这些人的口味儿,C语言做了很多特定的规则,这些规则后来在标准化的过程中被委员会采纳,成了语言标准的一部分。具体到指针与数组这一块儿,我们同样从两个方面来看,一是作为一个语言,数组是必须要支持的一种数组类型,原因很简单,数组是线性表的直接体现。而从编译器设计者的角度来看,如果为数组专门设计一套实现标准会非常繁杂(事实上,后来C++完成了这一任务,它就是标准库中的vector容器)。这一对矛盾最后以双方的相互妥协得以解决,而解决方法就是利用现有的指针来间接实现数组。然而,数组与指针终究不是一个东西,所以,语言在设计过程中就必须在二者之间和稀泥。到最后,就形成了这样一对令人头大的冤家。了解了这一背景,你就不会为他俩扑朔迷离的关系而感到惊讶了。

2.数组的引用被转化为指针加偏移量的引用

从上面我们知道,编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转化为指针加偏移量的引用。这么说大家可能不理解,首先什么是引用呢?引用其实就是使用,从编译器的角度来看,就是从一个符号找到对应内存并进行读写的过程。为了理解这一个过程,我们先来看看对于一个普通变量,编译器是怎么做的。

比如我们用C语言写了这样的语句

int a;
a = 3;


编译器为了完成这两句代码,首先在编译过程中要创建一个符号表,样子大概如下图:

然后在运行过程中,编译器发现a=3这句代码时,会在符号表里找a对应的地址,然后把3放入对应的地址,即这里的0x1000。那如果是一个指针呢?即如果是*p=3会怎么做呢?首先,符号表变成了这个样子

扫描二维码关注公众号,回复: 4271373 查看本文章

在运行过程中,编译器遇到*p=3时,首先要从符号表中找到p的地址0x1004,然后取出0x1004中的内容,这里假设为0x2000,最后把3放到0x2000内存地址中,即*(0x1000)=3。

相信大家已经看明白了,想比于利用普通变量,利用指针存取数据的过程中多了一部取地址的的过程。这也就是指针变量于普通变量最大的不同。

下面再来看一下指针加偏移量的引用方式,还以上面的指针p为例,让我们来看一下*(p+2)=3的实现过程。首先,编译器从符号表中找到p然后,取出里面的内容0x2000,再根据其类型(int*),做一个运算,0x2000+2×sizeof(int)=0x2008。所以编译器会把3放入0x2008这个内存地址。整个过程可表示为*(*(0x1004)+2)=3。从这里也可以看出为什么指针必须有类型,因为在引用过程中要用到指针所指类型的长度。

最后来看一下,数组元素的引用是如何实现的,假设我们定义了一个数组,并对其元素进行了引用

int b[10];
b[4] = 3;

对应的符号表变成了这个样子

那b[4]=3如何完成呢?首先,找到符号b,然后发现其类型为int[](假想表达方式,C语言中不支持这样写),所以计算式变成了0x1008+4×sizeof(int)=0x1018,然后把3放入0x1018就可以了。用一个式子表达就是*(0x1008+4)=3。

从上面的寻址式子可以看出,普通变量、指针、数组三者对于编译器的区别。具体到数组,它即具有普通变量的直接性,即不用取两次地址里的内容而是取一次,同时又具有和指针相同的偏移量引用方式,即下标的实现实际是由指针加偏移量实现的。

为了表明上述事实(或者是为了提高C语言入门门槛),C语言对指针与数组的引用方式做了可以“交叉”使用的语法规定。就上面的例子来说,如果p指针指向数组b时,b[i]、*(b+i)、p[i]、*(p+i)都是对数组第i个元素的正确引用方式,这也给很多C语言学习者制造了“指针和数组一样”的错觉。

3.在函数形参中的表现

在向函数传递参数时,如果实参是一个一维数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组。这么做的原因主要是效率,这是无可争议的,但为了使用上的简便与通用,C语言接受两种形参的写法,即下面两种写法是相同的

int foo(int *a, int n);
int foo(int a[], int n);

甚至是

int foo(int a[20], int n);

在底层都是完全一样的形式。这简化了C语言的使用,但同时也增加了对其进一步理解的难度。正因为很多程序稀里糊涂就通过了,所以程序员就会一直稀里糊涂下去。至于哪种写法好的争论,只要你理解了,用哪种写法到是不必强求。指针形式表明了传参过程的实质,而数组形式表明了这个指针对应一个数组实参,甚至后面的数字可以提供数组长度的参考。
4.如何区分二者

数组和指针的关系如此微妙,那如何区分呢?

首先说为什么要区分,主要由两个方面的原因,一是C语言的主要战场还是偏向于底层的,如果使用者对这块儿一直模糊不清,那对其他知识的理解就会有困难,比如编译器,硬件系统等相关知识。二是在特定的情况下,二者的表现的确不同,最常见的就是sizeof关键字的作用结果,另外还有取址操作符&的结果等情况。

至于如何区分,个人认为凡是要区分两个相关的概念,一定要将二者的作用区分清楚,即为什么要有这个东西。就比如sizeof与strlen的区分,只要明白了二者各自的作用,就会发现二者其实根本没有一点儿关系,完全是为了实现不同功能而设置的。数组和指针也是一样,指针是一类特殊的变量,主要用途是函数间的传址,用这种方式来改变实参内容。而数组是用来实现线性表的结构,用于把同类对象集中在一起放置。所以,如果下次有人问你数组和指针的区别,你首先第一句要说的就是二者根本没有联系,这么说并这么理解,对于弄清这两个家伙的关系是很有裨益的。
 

 

猜你喜欢

转载自blog.csdn.net/alidada_blog/article/details/83014565