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

计算机的存储单位

在计算机中,一个二进制位是最小的存储单位,只有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等语言抛弃指针更多的可能是因为内存泄露问题吧,关于内存泄露的话题可以延伸很广,这里就不多说了,就是简单介绍一下什么是内存泄露。

最后

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

深入理解指针:一文让你彻底理解指针相关推荐

  1. _一文让你透彻理解Linux的SOCKET编程(含实例解析)

    1. 网络中进程之间如何通信 进 程通信的概念最初来源于单机系统.由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进 程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如 U ...

  2. 收藏 | 一文带你深入理解深度学习最新进展

    点上方蓝字计算机视觉联盟获取更多干货 在右上方 ··· 设为星标 ★,与你不见不散 仅作学术分享,不代表本公众号立场,侵权联系删除 本文整合自机器之心.网络资源 AI博士笔记系列推荐 周志华<机 ...

  3. C++ 智能指针std::shared_ptr简单使用和理解

    参考:https://blog.csdn.net/u011068702/article/details/83692838 1  智能指针std::shared_ptr相关知识和如何使用 我们这里先说下 ...

  4. 一文带你深入理解JVM内存模型

    一文带你深入理解JVM内存模型 一.JAVA的并发模型 共享内存模型 在共享内存的并发模型里面,线程之间共享程序的公共状态,线程之间通过读写内存中公共状态来进行隐式通信 该内存指的是主内存,实际上是物 ...

  5. 带参函数_更好的理解Python第五弹函数预处理与指针

    编译预处理 预处理概念:在编译之前进行处理 在C语言中,预处理行为宏定义,文件包含,条件编译 指令 用处 # 无 #define 定义一个宏 #undef  取消定义一个已经定义的宏 #include ...

  6. [css] 举例说明你对指针事件(pointer-events)的理解

    [css] 举例说明你对指针事件(pointer-events)的理解 pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target. 当 ...

  7. 一文搞定C语言指针问题

    是的,这一篇的文章主题是「指针与内存模型」 说到指针,就不可能脱离开内存,学会指针的人分为两种,一种是不了解内存模型,另外一种则是了解. 不了解的对指针的理解就停留在"指针就是变量的地址&q ...

  8. python用字典统计单词出现次数_python - 如何使用字典理解来计算文档中每个单词的出现次数...

    我有一个用python编写的列表,其中充满了文本.就像每个文档中的固定单词.所以对于每个文档,我都有一个列表,然后在列表中列出所有文档. 所有列表只包含唯一的单词.我的目的是计算完整文档中每个单词的出 ...

  9. 文档智能理解:通用文档预训练模型与数据集

    向AI转型的程序员都关注了这个号???????????? 机器学习AI算法工程   公众号:datayx 预训练模型到底是什么,它是如何被应用在产品里,未来又有哪些机会和挑战? 预训练模型把迁移学习很 ...

最新文章

  1. 【SLAM建图和导航仿真实例】(三)- 使用RTAB-MAP进行SLAM建图和导航
  2. 2017-2018-1 20155328 《信息安全系统设计基础》第十四周学习总结
  3. 【Linux基础】查看硬件信息-内存和硬盘
  4. 云服务优缺点_什么是云服务器,云服务器的优缺点
  5. HTML5画布(CANVAS)速查简表
  6. Android中常用的编码和解码(加密和解密)的问题
  7. 《LED调光-DMX512灯光协义接收控制》转
  8. 前端学习(1350):用户的增删改查操作7增删改查
  9. 微型计算机和接口技术考题,微型计算机接口技术以及应用考题
  10. 用计算机打cf,CF能用的特殊符号有什么 CF特殊符号怎么打
  11. 谷歌 Provisional headers are shown 和360急速模式 网络连接错误
  12. c语言中fprintf的作用,C语言中的printf(),sprintf()和fprintf()
  13. Python已知经纬度求两点距离
  14. YUI 3 Cookbook 中文版
  15. LinkLab 链接
  16. 计算机二级题百度云,题库吧百度_计算机二级 office 题库 百度云 谢谢_淘题吧
  17. 常见的SREng操作
  18. uniApp微信小程序获取当前用户手机号码(前端)
  19. java script是什么_Java Script的工作原理是什么?怎样用它来生成简单的
  20. brctl 配置网桥

热门文章

  1. 周杰伦:他们只顾嘲讽,却不知眼前是神的降生
  2. Linux文件操作及管理
  3. 关闭计算机的几种方法
  4. mysql升序降序关键字(DESC降序,ASC升序)
  5. Python windows高效截屏
  6. Cobalt Strike基本使用
  7. (转)VC++之系统控制之设置显示系统当前时间
  8. drawboard pdf拆分文件_请收藏!这是一份最全的PDF问题解决方案。
  9. android webview问题汇总
  10. unity image sprite 赋值不显示