详解计算机内存及基于内存理解的几种数据结构

tech2022-07-06  234

详解计算机内存

前言

计算机是进行数据处理的设备,而程序表示的就是处理顺序和数据结构。。由于处理对象数据是存储在内存和磁盘上的,因此程序必须能自由地使用内存和磁盘。本文详解内存的物理结构,逻辑结构以及利用内存构建各种各样数据结构的应用。

一、内存的物理结构

内存实际上是一种名为内存IC的电子元件。内存 IC 中有电源、地址信号、数据信号、控制信号等用于 输入输出的大量引脚(IC的引脚),通过为其指定地址(address),来进行数据的读写。内存 IC 的引脚配置如下图

将电源连接到VCC和GND后, 就可以给其他引脚传递比如0或者1这样的信号。大多数情况下,+ 5V 的直流电压表示1,0V表示0。那么上图内存IC可以存储多少数据呢?一个内存IC能够存储多少数据要看地址信号的个数,因为数据在计算机中存储,每个数据必须对应一个地址,这样这个数据才是有意义的数据。也就是说这个内存IC有多少地址就可以存储多少数据。上面的内存IC有A0~A9 共十个地址信 号引脚,表示可以指定 0000000000~1111111111 共 1024个地址,因此上面的内存IC可以存储1024个数据。那么一个数据是多大呢?它是由数据信号引脚所确定的,上图内存IC中有D0~D7 共八个数据信号引脚,表示一次可以输入输出8位(= 1字节)的数据。因此,该内存IC一共可以存储1024个1字节的数据。因为1024 = 1K(计算机领域K代表2的10次幂),所以该内存IC的容量就是1KB。但现在大家使用的计算机至少有512M的内存,也就是512000个1KB大小的内存IC,当然,一台计算机中不太 可能放入如此多的内存IC。通常情况下,计算机使用的内存IC中会有 更多的地址信号引脚,这样就能在一个内存IC中存储数十兆字节的数据。因此,只用数个内存IC,就可以达到512MB的容量。内存IC写入数据的过程是,首先接通电源(+5V或0V)指定数据,然后地址信号指定存储场所,再把数据的值输入给数据信号存储在指定位置。读取数据时,只需通过A0~A9的地址信号指定数据的存储场所,指定地址中存储的数据就会被输出到 D0~D7 的数据信号引脚 。控制信号是用以控制目前是读操作还是写操作。当写入数据时WD设为1,读取数据时RD设为1。总体来讲,内存IC内部有大量可以存储8位数据的地方,通过地址指定这些场所, 之后即可进行数据的读写。

二、内存的逻辑模型

虽然内存的实体是内存IC,不过从程序员的角度来看,也可以把 它假想成每层都存储着数据的楼房,并不需要过多地关注内存IC的电 源和控制信号等。如内存为1KB 时,表示的是下图所示的有1024 层的楼房(这里地址的值是从上往下逐渐变大,不过也有与此相反的情况)。 不过,程序员眼里的内存模型中,还包含着物理内存中不存在的概念,那就是数据类型。编程语言中的数据类型表示存储的是何种类型的数据,它从内存来看,就是占用的内存大小(占有的楼层数)的意思。比如C语言中数据类型char代表占用一字节内存大小,short代表占用2字节内存大小,long代表占用4字节内存大小。因此不同的数据类型虽然存放同样的数据,但是他们占用内存的大小是不一样的。 仔细思考一下就会发现,根据程序中所指定的变量的数据类型的 不同,读写的物理内存大小也会随之发生变化,这其实是非常方便的。 大家不妨想一想,假如程序中只能逐个字节地对内存进行读写,那该 多么不便啊。在处理超过1个字节的数据时,还必须要编写分割处理 程序。此外,在不同的编程语言中,变量可以指定的数据类型的最大 长度也不相同。C语言中,8字节(= 64位)的double类型是最大的。

三、理解指针变量

有了上面内存的理解,我想指针就非常容易理解了。因为数据都是存放在计算机相应的地址中,那么,如果有一种变量专门用来存储地址,是不是就可以通过这个地址自由的读写这个地址中的数据了呢?这个变量就是指针变量。指针也是一种变量,它所表示的不是数据的值,而是存储着数据的内存的地址。通过使用指针,就可以对任意指定地址的数据进行读写。如下代码示例了几种数据类型指针的定义: 上面代码中,我们知道d,e,f分别是用来存储内存地址的变量,那么为什么还需要char,short,long这些数据类型呢?实际上,这些数据类型表示的是从指针存储的地址中一次能够读写的数据字节数。比如d一次能够从地址开始位置读取1个字节的数据,而short可以读取2个字节的数据,long可以读取4个字节的数据。

四、数组是高效使用内存的基础

数组是指多个同样数据类型的数据在内存中连续排列的形式。作为数组元素的各个数据会通过连续的编号被区分开来,这个编号称为索引(index)。指定索引后,就可以对该索引所对应地址的内存进行读写操作。而索引和内存地址的变换工作则是由编译器自动实现的。之所以说数组是内存的使用方法的基础,是因为数组和内存的物理构造是一样的。特别是1字节类型的数组,它和内存的物理构造完全一致。

五、栈、队列以及环形缓冲区

虽然是通过指定索引来使用数组,但这和内存的物理读 写并没有特别大的区别。有没有更人性化的方法来操作计算机的物理内存呢?其中,栈和队列,都可以不通过指定地址和索引来对数组的元素进行读写。那么,栈和队列是如何实现不用指定地址和索引呢?其实很简单,如果我们在内存中预留出栈和队列所需要的空间,并确定好写入和读出的顺序, 就不用再指定地址和索引了。那么,栈的写入和读出顺序按照“后进先出”的方式(LIFO,Last Input First Out);而相对应队列的写入和读出顺序按照“先进先出”的方式(FIFO,First Input First Out)。同时,如果要在程序中实现栈和队列,就需要以适当的元素数来定义一 个用来存储数据的数组,以及对该数组进行读写的函数对(栈的函数对是Push和Pop;队列的函数对是EnQueue和DeQueue)。总之,这些数据结构都是为了更方便的使用内存。通过使用这些函数,可以很方便的将数据临时保存(写入), 然后再在需要时候把这些数据读出来。为了充分利用内存空间,队列一般是以环状缓冲区(ring buffer)的方式来实现的。让出队数据的位置可以循环给其它要入队的数据使用。

六、链表使元素的追加和删除更容易

不知道大家有没有发现,队列和栈这种数据结构有一个很大的缺陷,那就是对于数据的读写必须按照顺序进行读写。如果想要在数据中间硬夹一个数据,或者从中间删除一个数据,这都需要耗费极大的动作去移动整个数据结构。那么有没有一种更高效且方便的数据结构可以不用考虑索引的顺序就可以对数组元素进行读写操作呢?这个时候链表就出现了。在数组的各个元素中,除了数据的值之外,通过为其附带下一 个元素的索引,即可实现链表。数据的值和下一个元素的索引组合在一起,就构成了数组的一个元素。由于链表末尾的元素没有后续的数据,因此就需要用别的 值(在这里是-1)来填充。 在需要追加或删除数据的情况下,使用链表是很高效的。只需要将数据删除或者追加,然后修改上一个元素中的下一个元素索引即可。如果不使用链表数组,那么中途删除或追加元素时,其后的元素就必须要全部移动。

七、二叉查找树使数据搜索更有效

二叉查找树是指在链表的基础上往数组中追加元素时,考虑到数据的大小关系,将其分成左右两个方向的表现形式。例如,假设我们 事先把50这个值保存到了数组中。那么,如果接下来的值比先前保存 的数值大的话,就要将其放到右边,反之如果小的话就放在左边。 那么如何实现二叉查找树呢?其实数组的每个元素 中只要有数据的值和两个索引信息(左右值的索引信息)就可以了。如下图实现上图二叉查找树的逻辑 使用二叉查找树的便利之处在于可以使数据的搜索等更有效率。 在使用一般的数组时,必须从数组的开头按照索引顺序来查找目标数 据。而使用二叉查找树时,当目标数据比现在读出来的数据小时就可 以转到左侧,反之目标数据较大时即可转到链表的右侧,这样就加快 了找到目标数据的速度。

总结

其实上面无论是栈,队列,链表还是二叉查找树他们都是数组的改造,而数据是使用内存的基础,因为数组的结构和内存的物理结构是“一样”的。因此,各种数据结构都是为了更好的使用物理内存,同时根据各种需求对内存的改造而且,而在程序中对内存操作的基础就是数组。

参考:[图灵程序设计丛书].程序是怎样跑起来的
最新回复(0)