深入理解C++中的指针和多维数组

指针篇
“指针是什么?”“指针就是一种数据类型。”“你确定?”“那数据类型是什么?额,这个???类型就是类型,还能怎么解释嘛。”“指针有多少种?”“指针有好多种,比如整型指针,字符指针等等。”“指针是怎么确定分类的?”“就是声明时在变量类型后加个*声明指针呀。”等等。这是我们对指针对基本的认识,可是指针到时是什么,什么是指针类型,指针的值和指针本身怎么区别,几乎初学者都凌乱了。给个简单的指针使用,倒还可以看明白,问点复杂的就懵了,到头来很委屈的回一句,“老师没教呀!”。
似乎初学者都有这样那样的无助,都寄希望与老师,可是自己没有基础,老师说了自己也听不懂,问了也白问,心里瘆的慌。问同学吧,也都和自己差不多,就算懂的也说不出个所以然,真是“茶胡子煮饺子”。
作为初学者到深入的我来说,没有老师的直接灌输,完全自己摸索成长,对于初学者的心理是完全明白,也知道为什么会有这么多的困惑。应试教育基本都是灌输,学生没有自主思考的机会,导致只能被动的接受,从而不能够深入。那么我将以自学者的角度深入分析指针,抛开书本中的条条框框,让你真正自由的学习指针的精髓。如果没有一个自由的心,将无法捕捉自由的指针的灵魂。
扯多了,进入正题。看完本文,相信第一次可能还不完全明白,但是一定要形成一种印象,然后多看几遍,在敲代码的过程中彻底理解,光靠看书是不可能完全理解的,一定要敲代码中思考才能掌握,但是文章会给你一个正确的方向,助你快速理解指针。
指针是一种数据类型,占4个字节。为什么指针的类型需要和具体的类型相关呢?比如,你不能说指针就是一种类型,而只能附带一种指向的类型来确定指针的类型。初学者很不理解这个意思,为什么指针还要依赖其他类型来确定类型呢?那么,如果你也这样问了,恭喜你,你慢慢上道了,不过,更可喜的是,本文可以加速你的思考,助你成功。现在,我们展开深入的分析了。下面请认真的阅读哦。
在计算机中,变量存储,指针,数据类型等等,都是反映在内存中的,当然这个内存指的是虚拟内存。虚拟内存,简单来说,就是我们程序使用的内存,不是真正的内存条内存,不过程序运行后,虚拟内存会映射到实际的内存条内存即物理内存。了解虚拟内存,请阅读计算机操作系统内存章节。我们理解的程序中的内存都是虚拟内存,你当做是内存就是了。这个不清楚对我们程序理解没多少影响,有个概念就行了,继续往下看。
32位计算机只支持4GB内存大小,多出的是无效的,即使你装了8GB内存条,也只能使用4GB,为什么最多只支持4GB呢,这就跟指针有关系了。计算机使用一个计算机字,32位的计算机就是32位,也就是4字节,因此这也就是为什么我们所知道的指针所占的内存是4字节的原因。因为我们的计算机普遍是32位的,也就是支持的最大内存是4GB,使用32位即4字节的指针就能够找到4GB的所有内存位置。如果将来4GB内存不够用,普及了64位的计算机,支持的内存大小就是2的64次方byte,远远超过了4GB,那么32位指针就找不到超出4GB以外的内存了,所以就不够了。因为32位指针所能表示的最大内存地址为2的32次方减一byte。所谓的不支持就是因为指针所能表示的范围没有内存的地址范围大,找不到全部的地址。
在目前的内存体系中,内存地址排序是线性的,也就是,32位地址(4GB内存)从地址0x00000000到0xFFFFFFFF 。因为是线性的体系结构,因此,内存地址都是按照序号递增的,如0,1,2,3,…,这样就表示了内存地址,当然,因为是用十六进制带前导0表示的。因此地址就是我们常见的0x00000000,0x00000001,0x00000002,0x00000003,…。正是因为是线性的,因此,根据这个序号就能够轻松的找到相应的内存地址,对内存进行读写。那么内存地址其实就是一个编号,从0开始的,指针所占内存的位数就决定了能表示的数字的大小,这个表示的数字就是内存地址。32位即4字节指针只能表示4GB内存,这也是我们现在知道的。所以,对于指针的认识,指针所占的大小就是这个意思,请反复揣摩。




数组篇
说起数组,一维数组倒是简单,很好理解,但是到了二维,难度就急剧增加,到了三维数组,心理就有点承受不住了。这是我起初自学这个部分的感觉。因此我决定暂停,然后去加深指针和其他基础,不然就往下走不下去了。加深了基础,再继续,难度就大大降低了,这也是我为什么开篇就提醒的原因。
要从深入的角度分析,那就必须深入到内存。数组在内存的形式以及其他方面的。这里不会教大家怎么去使用数组,这些都很容易在教科书找到,而且还罗列的一条一条的,但是你却没有真的弄懂它,所以,我所要做的,就是将它的内在本质解释一下,让读者看清数组是什么东西,这样你就能够成竹在胸,运用自如了。
我已经提到过,内存的结构是线性排列的,当然这指的是虚拟内存,也就是逻辑内存,不是我们看得见的那些内存条。内存条的物理结构比较复杂,是有平面排列的,通常是矩阵式的分布。这里不讨论具体的内存相关的,只是告诉你,在编程涉及到的内存,是线性结构,即内存地址是按照从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]地址索引就找到了下一个元素,正是因为他们的逻辑大小意义不一样,所以才有区别,在物理层面上都是一个相同的地址而已。
这里要说指针的意思,就是很多经常会把数组地址和指针混在一起了。指针是一个变量,存储内存地址的。而数组名则是一个内存地址而已,你不能对它进行赋值等操作,因为它只是一个代号而已,指针是变量,可以存放地址,可以存放地址运算后的结果。只不过我们可以通过指针的值找到响应的内存地址,比如我们可以找到数组起始地址,然后进行操作,这就是指针的厉害之处,找到你就可以操作你,当然,我通过数组起始地址再找也能找到数组中的元素,再操作它,这两种方式的区别就在这,估计很多开始学的都不理解这两种行为。也就是因为这两种行为而把数组名、数组行起始地址和指针混在了一起。指针是根据地址找元素的,所以指针本身存储地址,数组名就是起始地址,所以就晕了。因为不懂内在的内存表示,所以始终都分不清楚。这样的深入的看清了数组,你怎么可能还会分不清了。
说实话,指针和数组没多大关系,就地址来说吧,什么变量都有地址,包括指针变量自己也是有内存地址,你要说因为指针存储的是地址而把数组和指针混了就有点说不过去了。像这样的仔细解释了,我想大概是不会在晕乎了。
不过,这样是把内存掏出来看,看的很清楚,但是初学者接受了很多固定的思维,一开始还是不明白,但是,请你学完课程的数组之后,反复琢磨这篇文章,多读几遍,我相信你之前的疑问一定会烟消云散,豁然开朗。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值