好多人都说指针很难,其实指针并不难,你不是不懂指针,你是不懂内存管理,搞懂内存管理,指针就是纸老虎
内存物理上的实现我们不用关心,这是硬件工程师需要关心的问题,作为软件工程师,我们只需要了解内存的抽象逻辑即可
在学习内存管理之前,我们先来复习一下计算机的存储单位

<>计算机的存储单位

在计算机中,一个二进制位是最小的存储单位,只有0和1两种状态,我们称之为位(bit)

8个位组成一个字节(Byte),继续往上,使用1024为进制单位,即就是我们常说的1024Byte=1KB,后面还有1024KB=1MB,1024MB=1GB等等。严格来说,B才是他们的单位,而K、M、G不是单位,它们只代表数量,实际上,KB只是KByte的缩写。1KB严格说其含义是1K个Byte,也就是1024个Byte,其他同理。

这里提一句题外话,我们在办理宽带时,明明是100M的网线,为毛下载速度只有10M/S?严格说,宽带的100M指的是100Mbps,和上面的一样,Mbps不是单位,bps才是单位,bps的含义是每秒传输的二进制位数,而100Mbps指的是每秒传输100M个二进制位,也就是100x1024x1024个bit。而我们下载时显示的速度是以字节为单位的,是宽带的数据除以8之后的结果,这就是100M网线下载速度只有10M/S的原因。

<>内存的逻辑结构

先看张图


在逻辑上,我们把内存分成一个一个的小格子,每个小格子就是一个字节,而每个字节又有8个位。同时为每个小格子编上编号,而这个编号就是我们常说的内存地址。我们常说的计算机有4G内存指的就是计算机有4x1024x1024x1024个存储单元。

<>为什么32位系统只支持4G内存

关于CPU的体系结构不是本文的重点,不作详细说明,这里只是简单提一下。CPU在读取内存数据的时候,需要通过地址总线进行寻址,而32位的CPU其地址总线也是32位的,也就是说,CPU一次读取的地址最多是32位的,而一个32位的地址又能代表多大内存呢?这里的32位指的是有32个二进制位,而每个二进制位只有1和0两种状态,所以,32位的地址总线最多寻址范围是0到2的32次方减1,也就是2个32次方个内存单元。而2的32次方大家可以用计算器算一下,其结果的单位是字节,是B,除以3次1024就是4GB了。同理,我们可以算出16位的,8位的CPU的寻址范围。64位的略有不同,按照这个方法算出来的只是理论值,实际上目前人类还完全用不到这么大,所以目前关于64位寻址的上限不是由地址总线限制的。

<>数据在内存中的读写

如果想要在内存中正确读写数据,需要知道两个信息,第一,这个数据的首地址在哪,第二,这个数据的长度是多少。知道了首地址,就可以通过寻址操作定位到内存中的位置,通过数据长度,可以将数据进行正确的读写。

<>数据长度

首地址可以通过CPU寻址获得,那么长度怎么管理呢?数据长度可以分为自动管理和手动管理两种模式,自动管理就是根据数据的类型进行长度管理,比如我们常说的char型数据占一个字节,int型数据占四个字节等等。手动管理就是我们自己去管理读写数据的长度,最常见的就是memcpy函数。memcpy和两个int型数据进行赋值操作其本质上是相同的,不同的是,memcpy由我们手动去处理地址和长度,而int型数据进行赋值操作是由操作系统去处理地址和长度。

这里再多说一句,所谓的char型数据占一个字节,int型数据占四个字节,其实只是习惯上的一种说法。标准中并没有定义一个数据类型的具体长度,在纯C的标准中只是定义了一个大小关系和一个最小长度,即char≤short≤int≤long和每种类型的最小长度。理论上说,如果愿意,可以将char和int都定义为4个字节,也是符合标准的,只不过没人这么干而已。但如果说把char定义为8个字节,int定义为4个字节就是错了,不符合标准的(友情提示:这些知识面试的时候酌情使用,我曾经在一次面试中这么回答被面试官判定为错误回答,在他眼里32位系统下int就是4个字节,其他答案都是错的-_-
!)。所以在程序中需要用到变量长度的时候,切记不能直接写常数,一定要用sizeof。

<>声明一个变量都做了什么

当我们在程序中写下一句“int
a;”时,程序执行这一段代码,操作系统就会在内存中分配一个四个字节长度的空间,并且由操作系统去维护其首地址和数据长度。当我们在使用变量a时,操作系统会自动定位到该块内存进行数据读写。

这里再多说一句吧,操作系统在管理内存的时候,把内存分成三个区域:堆内存、栈内存和静态存储区。其中栈内存和静态存储区由操作系统管理,堆内存由程序去管理。局部变量定义在栈内存上,全局变量和静态变量定义在静态存储区上,程序中malloc或new的内存定义在堆上,用完后需要free或者delete释放掉。

<>关于指针,你需要知道的

喜大普奔,终于到指针了
首先你需要知道的就是,指针没什么特殊的,就是一个普通变量,和上面的“int
a”没有任何本质上的区别,如果非要说区别,就是对数据的解释上吧,变量a中的数据我们可以解释成年龄,身高等,指针变量的数据必须解释成地址。
其次是指针的组成部分,指针由数据和类型两个部分组成,其中数据就是我们之前说的首地址,类型就是之前说的数据长度。
再次是指针可以计算偏移,也就是指针的加减法。指针加减一个整数,就是以该指针指向的地址为基地址向前后偏移 该整数 个数 个
指针类型长度的长度(这里比较拗口,为方便阅读,加上空格断句),偏移经常在数组中使用。

最后,不管什么类型的指针,其变量本身的长度永远等于地址总线的位数除以8。为什么除以8?因为地址总线位数的单位是位,而数据类型的长度的单位是字节,一个字节等于8位。

OK,如果你懂得了内存管理,那么关于指针,你只需要知道这么多就够了。至于编程语言中的各种蛋疼的指针语法,那就是一个熟练度的问题,网上有好多总结的文章,这里就不多说了。

<>举个栗子

看上面内存逻辑的那张图,在那张图中,画了一个范围从0到6的7个字节的内存空间。我们假设该内存被插入一台16位CPU的计算机中,而16位的地址总线,一次能读取2个字节,所以其指针变量的长度是2个字节。同时,我们定义该平台的int类型的长度也是2个字节。
当我们在代码中写下第一句“int
a;”时,操作系统为其分配了一块长度为两个字节的内存,假设就是0x01和0x02这两块内存。由于我们没有对a进行任何赋值操作,所以a中的数据是不确定的,是一个随机值。
接下来我们又写了一句“int *p =
&a;”,这句话是什么意思呢?首先定义一个变量,名字是p,类型是个int指针,然后用变量a的地址为其赋值。那么执行完这句内存中有什么变化呢?首先操作系统会分配一个指针长度的内存,本例中是2个字节,然后读取变量a的地址,再将这个地址写入到变量p中,也就是是途中的0x03和0x04两块内存中存放一个值为0x01的数据。
如果程序执行一句“int b =
*p;”又是什么情况呢?*p就是从指针p中读取数据,怎么读呢?前面在内存数据读写小节中说过,从内存中读取数据需要知道首地址和数据长度。变量p的首地址和数据长度是由操作系统维护的,操作系统知道p的首地址,类型是个int型的指针,那么就会从变量p中读取p的值,并将该值解释为是一个存储int型数据的变量的地址,然后根据指针的类型确定数据的长度。

最后是定义了一个二级指针,二级指针和一级指针同样是没有任何区别的,同样的分配内存,同样的将变量p的地址数据写到内存,同理,可以上推到三级指针,四级指针。在你机器允许的情况下,而你又闲的蛋疼,可以搞出来100级指针,10000级指针,只要你愿意,多少级都可以,和一级指针没有任何区别,只不过没什么实际的应用意义而已。

<>使用指针时需要注意的

使用指针,一定要尽量去避免三个问题:野指针、数组越界和内存泄露

<>野指针

野指针通常会由于两种情况产生,第一是指针只定义但没有初始化,第二是一块堆内存释放掉之后指针没有置0。

<>初始化导致的

前面说过,指针就是一个普通变量,没有任何特殊的。而一个变量在定义的时候会被分配内存,但内存中的数据却是随机的。这个特性对于非指针变量来说,大不了就是读个错误数据,至少不会造成程序运行的问题。但如果对于指针变量来说,就很可能会影响程序运行,因为指针变量中的数据都是地址,而一个未初始化的指针变量就是指向一个随机的地址,这个地址可以是内存中的任何位置,这个时候读写该指针就会向一个未知的内存位置读写数据,很容易导致程序运行错误。解决办法很简单,就是在定义的时候就进行初始化。

<>堆内存释放导致的

使用malloc或者new申请一块内存,会返回一个该块内存的首地址。此时操作系统会记录该块内存已经被使用,再次进行内存分配时就不会再使用这块内存了。

随着程序的运行,我们已经使用完该块内存需要使用free或delete释放掉。这个时候操作系统会将该块内存标记为未使用,再次进行内存分配时可以再次使用该块内存。

但是,操作系统只是释放掉了该块内存,但程序中标记该块内存的指针还是指向该块内存的。如果该块内存再次被分配,已经和程序中的指针没有任何关系了,但该指针还是指向这块内存,一旦对该内存进行读写,同样会产生错误。解决办法就是在释放掉内存之后,紧接着就将指针指向0地址。

<>数组越界

虽然说是数组,其实对于任何类型的内存都是适用的,就是读写数据的时候,不要超过数据的长度。比如有两个变量char a和int b,我们可以这样玩“char
*p = &b”,但不能这样玩“int *p =
&a”。因为char长度小于int长度,第一种是四个字节的长度,但我只读写第一个字节,没有越界行为,所以可以。第二种是一个字节长度,但我却按四个字节读写,我读写的长度超过了数据本身的长度,这明显是错误的,这就是数组越界。
这里顺便提一个我以前在程序里玩的一个骚操作,首先定义一个整型变量“int
i;”,后来这个整型变量用完了,我还需要两个char型的变量,我没有再次定义两个字符类型“char c1, c2;”,而的定义了两个char型的指针“char
*c1 = &i, *c2 = c1 + 2;”,这样我通过两个char指针将一个int拆分成四个char,并使用了其中的第一个和第三个,第二个和第四个没有用。
所以这就是我为什么没有介绍具体的指针语法,当你理解了本质,可以自行创造各种骚操作。

<>内存泄露

前面野指针提过一些堆内存的事,如果堆内存释放掉但没有将指针置0就会产生野指针。那如果我在内存释放之前就将指针指向其他内存呢?或者,在堆内存释放之前,指针变量的生命周期结束了,指针变量被操作系统自动释放掉(注意:是指针变量被自动释放,不是指针变量指向的堆内存被自动释放掉,这里强调一下,方便理解指针变量和指向内存的关系)呢?那么很遗憾,除非你还有其他的指针指向该块内存,否则你就永远失去了她。而一旦失去她,你就不知道该内存的首地址,不知道首地址就无法释放,不释放操作系统就会认为该内存正在被使用,也就是该块内存再也无法被使用了,这就是内存泄露。如果泄露的内存很多,超过了系统本身的内存,轻则程序挂掉,重则系统重启。

内存泄露一直是C程序的一个老大难问题,很难完全避免,所以我觉得Java等语言抛弃指针更多的可能是因为内存泄露问题吧,关于内存泄露的话题可以延伸很广,这里就不多说了,就是简单介绍一下什么是内存泄露。

<>最后

至此,关于指针本质的东西介绍的差不多了,简单总结成一句话:指针就是内存首地址和数据长度。具体语法上的东西网上有很多总结文章,这里就不多说了。

技术
下载桌面版
GitHub
Gitee
SourceForge
百度网盘(提取码:draw)
云服务器优惠
华为云优惠券
腾讯云优惠券
阿里云优惠券
Vultr优惠券
站点信息
问题反馈
邮箱:[email protected]
吐槽一下
QQ群:766591547
关注微信