当前位置:C++技术网 > 资讯 > 数组的深入理解以及数组与指针的关系的深入分析

数组的深入理解以及数组与指针的关系的深入分析

更新时间:2015-06-23 12:21:14浏览次数:1+次

    在开篇之前,提醒一下,如果此刻的你对指针还是很模糊,如果是因为没有看《带你深入理解指针,轻松掌握指针》这篇文章,请赶紧去看看,至少从感性上把指针弄明白,最好多看几遍,最大化吸收和理解,如果你是看过了,请不要走马观花,不要只是觉得你好像认为是这么回事就可以了,你要是觉得还是模糊的话,还是先去看一遍,如果你能从那篇文章中看到可以形成自己的思维认识,或者提出质疑,那就差不多了,不然,后续的文章,你还是迷迷糊糊的,要脚踏实地,一步一步的吸收,才能走得更远,不然基础的没弄明白,后面的就步步维艰。当然一下子你不能够全部理解,但是多看几遍绝对是有益的,特别是,一些概念和想法你没听说过,那么你一定要先混个脸熟,也是为以后的深入打基础,不能在接触一个新的东西时就想弄透,那有点过了,至少初学者没基础不应该这样想。对于一些句子所涉及的知识你不清楚,可以尝试读出来,慢慢断好句子,也是能够促进理解的,快速扫描式看的话,很多重要的知识点都会漏掉的。
    好了,前言说完,自己对号入座,看到这里,说明你不存在指针理解问题,那我们进入正题。
    说起数组,一维数组倒是简单,很好理解,但是到了二维,难度就急剧增加,到了三维数组,心理就有点承受不住了。这是我起初自学这个部分的感觉。因此我决定暂停,然后去加深指针和其他基础,不然就往下走不下去了。加深了基础,再继续,难度就大大降低了,这也是我为什么开篇就提醒的原因。
    要从深入的角度分析,那就必须深入到内存。数组在内存的形式以及其他方面的。这里不会教大家怎么去使用数组,这些都很容易在教科书找到,而且还罗列的一条一条的,但是你却没有真的弄懂它,所以,我所要做的,就是将它的内在本质解释一下,让读者看清数组是什么东西,这样你就能够成竹在胸,运用自如了。
    在《带你深入理解指针,轻松掌握指针》一文中,我已经提到过,内存的结构是线性排列的,当然这指的是虚拟内存,也就是逻辑内存,不是我们看得见的那些内存条。内存条的物理结构比较复杂,是有平面排列的,通常是矩阵式的分布。这里不讨论具体的内存相关的,只是告诉你,在编程涉及到的内存,是线性结构,即内存地址是按照从0到最大地址一次排列的。线性排列就导致很多问题,比如,非线性的结构无法直接表示和处理,必须进行转换进行表示。一维数组,是线性的,对其操作都是很直观的,数组的线性排列和内存排列一直,所以我们就直接将一维数组和内存进行对应。但是二维数组,就不是线性的了,二维的是一个平面,由x和y两个轴向,而一维的就只有x轴向,三维的就有x,y和z三个轴向。因为计算机的所有数据包括指令都是经过内存到CPU的,也就是说,所有的数据都要在内存存放,但是二维数组如何存放在一维的内存结构(线性的布局)呢?这就是一个问题,那三维的问题就更大。还有树结构、图结构等等,这些都是问题,凡是生活中出现的结构,除了线性能够表示的,都不能直接用内存结构直接表示出来,因为内存是线性的,只能表示线性的,所以就有问题。这也就是数据结构这门学科出现的原因。我们要将非线性的表示转换成线性的,我们就要下工夫去研究了。当然这里不是介绍数据结构,因此就不多讲数据结构的知识。因为二维以及以上维熟的数组就是一种非线性的,我们就要知道它们是怎么表示到内存的,知道了这个,我们就能够很深入的把握数组,那多少维数,本质都一样,至于更多维数表示的逻辑意义,就不探讨,比如二维的逻辑意义就是平面结构,三维就是立体结构,四维五维我就不清楚了。这就是学习数组以及数据结构的真正难处,你要明白各种数据结构在内存的分布规则,那学习就轻而易举了。
    既然多少维本质都是一样的,我们就拿最常用的二维来说明问题。
    二维数组,表示表格形式,你在使用时就把它当做表格使用,行列和表格的对应就是了。但是真的要灵活的使用,那我们来看看内部表示。在内存中,要确定一个数组,都是在一个连续的内存进行指定的。正因为是连续的内存,也就是说相邻的元素都是紧挨着的,数组在内存也是紧挨着排序的,这样就可以使用数组的索引轻松访问,只要有正确的索引,就能够访问那个索引对应的数组元素,所以这种特性叫做随机存取特性。一维数组是这样的,二维也是这样的。那么二维数组如何表示,我们如何正确的在线性排列中表示二维数组呢?先看看一维的表示,不要以为一维简单就匆匆忽略了,往往我们能从简单的加以扩展就能够把复杂的类推出来。在声明一个数组后,数组名就是数组的起始地址,对于这个地址,就是内存的一个序号。现在请认真看待这个序号,也就是这个数组名,也就是这个数组起始地址。一个完整的内存地址是很大的,而我们的数组是非常微小的,你想过数组最后落到内存何处的问题吗?数组在内存究竟是什么样?通常我们学习数组都是在逻辑意义学习的,总以为数组的就是一个独立的一段区域。其实不是这样的。这点的理解很重要,这会导致对于栈结构、队列结构、堆结构等的理解。很多人都以为,一个数组,一个栈都是像我们学习的逻辑表示时的那样的一个独立的区块。对于数组可能不明显,但是对于栈就明显了。提起栈我们就想起先入后出,我们知道栈只能从栈顶读写,栈底不能直接操作。如果一直停留在这种逻辑的理解上,在后续学习数据结构会有很大的困难,在实现堆、栈上更是不知所措。因为我们的思维陷入了逻辑的理解中,我们不知道这些结构在内存中到底是什么样的,所以我们以为栈底不能直接读写,其实这是错的。那只是一个逻辑的概念,是我们对这种结构的限定,保证了这种特性,才实现了这种逻辑结构而已。事实上我们可以去操作栈底的。要理解这些,我们先把数组在内存的样子搞清楚。把内存想象成一个很长的铁轨,我们的数组只是放在铁轨上的其中的一小段车厢而已。数组可以移动,可以进行一系列操作。当然,一旦系统给我们分配了数组,就给我们在内存中某一段指定了数组的起点和终止点,也就是数组的地址和结束的地址。而3元素int组的起始位置可能是在内存地址为10-12这三个单元中,10之前的后12之后的都是其他数据,当然这个地址只是假设的,助于理解。
    我们的数组就是这样的三个单元构成的,其实我们的数组是在一个开放的地址中的,我们可以向前走一位,或向后走一位,结果就超出了数组的范围,越界了。至于数组那个是头那个是尾,看系统怎么决定,如果系统要以低地址为开头也行,以高地址为起始地址也行,所以我们不要局限自己的思维,至于它是从哪个起始地址的我们不用细究,但是我们要把思维放开。当然数组还可以在内存地址移动的,不过这个移动一般不是由我们来移动的,系统可以,比如内存压缩时候就会将内存的数据块移动,把数据都移动到一整块,把数据块时间的小片的内存就腾出来拼成大块的了,这些小块的就是内存碎片,因为太小,不能满足程序分派的最小需求就浪费了。
    到这里,我想你对于数组在内存是哪样的有了个感性的认识了。至于堆栈的表示相关的,以后再说。不管是一维还是二维数组都是这样子在内存放置的。这样的表示我们就清楚了,数组的起始地址也知道了。我们来看看二维如何表示成一维的。二维数组的声明如int data2[3][4];像这样的数组,你肯定知道是表示12个元素的,一共有三行,有四列。那么系统如何知道一个线性存放的数据是一维数组还是二维数组呢?比如 int data1[12];这个一维数组,放在内存也是12个元素,类型也一样,系统如何知道呢?
    如何知道就是一个逻辑的概念,你可以规定一个约定,使之能够表示出逻辑的结构,既然都是12个元素,从元素本身是无法辨别一维还是二维的,辨别的方法就和数据类型相似。数据类型在内存就是以所占内存的大小来定义的。在内存,12个整型占了12x4字节,以4个字节表示一个整型,整型指针可以4个字节一个跨度来读取整型值,就能够正确读取整型的值,data1[1]就相当于一个整型所占的4字节的内存单元,data1就是数组的其实地址,索引的递增可以正确的找到元素,其实就是根据数组的元素的类型作为跨度了读取数据的值的,这就是为什么使用一种的类型的指针去读取另一种类型的数据导致数据出错的原因,因为跨度不一样,也就是数据类型需要内存大小不一样。因为数组名就是一个地址而已,不能对数组名进行赋值递增等操作,只有变量的递增才能把结构再存入变量所对应的内存中,而数组名就是一个名字而已,没有地方存放递增后的值的,所以我们使用数组名加上索引只是沿着这个数组的起始地址往后找来确定数组元素而已。
    那么二维数组,因为也是线性表示,我们无法直接区分,就通过约定,使用int data[3][4];这种方式逻辑的表示一个二维数组,告诉系统,每一行只有4个元素,一共有3行。数组的12的元素还是依次排列好,至于数组的排列是按行排列还是按列排列就和具体的实现有关了。我们默认的都是按行排列的。我们就以这种方式讨论。为了说明方便,我把二维数组声明成int data2[2][3];int data[6];这样减少数目便于罗列。加入二维的数组的元素在内存是1,2,3,4,5,6,一维的也是这样的。在内存中都是按照线性排开的,那看上去就是一样的。但是我们的二维数组在生成时就告诉了系统,这个是2行3列的。所以系统就先读取3个整数作为3列,表示为第一行,再读取3个元素就是第二行,更多行列数都是依次类推。但是一维数组就是有6个元素,因此就一次性读取6个元素就表示了这个一维数组。这样就区分开了不同维数的数组了。这样区分开是我们认为规定的,因此就是逻辑结构,在计算机中,其实这两个没有任何区别。存储表示一模一样。
    而data2[0],data2[1],类似于一维数组的数组名,表示的是这一行的起始地址,只是个名字而已,不能进行赋值运算操作。你可以把一维数组理解为一行,二维数组就是两个一维数组组成的。其实在内存的表示就是这样的。两行就是紧挨着的存放的,读取时只是在第一行后面的元素就停止了,表示前面读取的几个元素就是一行,后面点元素就是其他行。data[1][1]就是第二行的一个元素的地址,就相当于一维数组的data[4]。这两种在内存中地址意义是一样的,只是表示的逻辑意义不一样罢了。你了解内部的意义,你就能很轻松的理解数组了。二维数组名就是整个线性表示的二维数组的起始位置,那么第一行的第一个元素是第一行的其实位置,以及第一行的第一个元素的地址,你会发现,这三个地址就是一样的。但是你别高兴,data2 、data2[0]和data2[0][0],虽然地址是一样的,但是逻辑意义却完全不一样,就如同数据类型一样,有一个内存大小的问题。data2 表示的是整个数组的起始地址,而data2[0]表示的是一行的起始地址,data2[0][0],表示的是一个元素的其实地址,data2不讨论大小问题,因为你对它进行+1索引没有意义,超界了。而对data2[0]+1可以索引到第二行的地址,data2[0][0]地址索引就找到了下一个元素,正是因为他们的逻辑大小意义不一样,所以才有区别,在物理层面上都是一个相同的地址而已。
    这里要说指针的意思,就是很多经常会把数组地址和指针混在一起了。指针是一个变量,存储内存地址的。而数组名则是一个内存地址而已,你不能对它进行赋值等操作,因为它只是一个代号而已,指针是变量,可以存放地址,可以存放地址运算后的结果。只不过我们可以通过指针的值找到响应的内存地址,比如我们可以找到数组起始地址,然后进行操作,这就是指针的厉害之处,找到你就可以操作你,当然,我通过数组起始地址再找也能找到数组中的元素,再操作它,这两种方式的区别就在这,估计很多开始学的都不理解这两种行为。也就是因为这两种行为而把数组名、数组行起始地址和指针混在了一起。指针是根据地址找元素的,所以指针本身存储地址,数组名就是起始地址,所以就晕了。因为不懂内在的内存表示,所以始终都分不清楚。这样的深入的看清了数组,你怎么可能还会分不清了。
    说实话,指针和数组没多大关系,就地址来说吧,什么变量都有地址,包括指针变量自己也是有内存地址,你要说因为指针存储的是地址而把数组和指针混了就有点说不过去了。像这样的仔细解释了,我想大概是不会在晕乎了。
    不过,这样是把内存掏出来看,看的很清楚,但是初学者接受了很多固定的思维,一开始还是不明白,但是,请你学完课程的数组之后,反复琢磨这篇文章,多读几遍,我相信你之前的疑问一定会烟消云散,豁然开朗。
    如果有任何问题,可以在文章后面提问和讨论,如果有不对的地方,请指正,虚心求教,相互学习。
    可能这文章有点深度,但是却不难理解,讲的还算通俗。所以慢慢看,慢慢体会就会懂得,因为这又是与固有的思维作斗争,纠正过来是需要付出努力的。看到底层,你就能够自由的运用了,不会将自己的思维束缚了。
    好好加油,这些还是基础,慢慢一步一步的学好基础,为走得更远打下坚实的基础,不浮躁才能学好。带你深入理解指针,轻松掌握指针