对于程序员来说内存可以简化成这样一种东西:
<img src="https://pic1.zhimg.com/4d060c3f67c22cd4b07273db00f64708_b.jpg" data-rawwidth="543" data-rawheight="100" class="origin_image zh-lightbox-thumb" width="543" data-original="https://pic1.zhimg.com/4d060c3f67c22cd4b07273db00f64708_r.jpg">你可以把它想象成一条无限长的纸带。纸带上边有一个个的小格子,每个小格子正好是一字节,里边能够存放一个数字。计算机的工作就是对这些小格子里的数字做处理。虽然你在电脑上能够看视频、听音乐,但这些东西本质上都是存在内存这条纸带上的数字。你可以把它想象成一条无限长的纸带。纸带上边有一个个的小格子,每个小格子正好是一字节,里边能够存放一个数字。计算机的工作就是对这些小格子里的数字做处理。虽然你在电脑上能够看视频、听音乐,但这些东西本质上都是存在内存这条纸带上的数字。

对于纸带上的每个小格子来说能够采取的操作只有两种「读取」和「写入」。每次执行操作时都必须要指明对于哪一个格子进行操作,为了方便起见人们就给这些格子编了号。如上图所示,第一个就是0号,第二个是1号以此类推(前边的0x代表是16进制的意思)。而这些编号就是我们所说的指针的内容,指针变量里边所记载的就是这些编号。但为什么我们平时看到的指针值都是类似于0xa2cf23c3d这种呢?这是因为,内存非常之大,作为编号的数字也非常大。所以看是来就像是神秘代码一样。

举个例子:

unsigned char a = 1; // unsigned char 类型占一个字节
unsigned char* b = &a; // &a 代表的就是得到变量a所存储的那个小格子的编号。
// 赋值给变量b后,b格子里存的就是a格子的编号。
printf("%d", *b); //输出1,*b代表的是看看b格子里存储的编号所代表的格子里存的数是什么。

上边代码中的内容变成纸带就是:
<img src="https://pic1.zhimg.com/26702ad72a395403ce4c7d02ccd167e4_b.jpg" data-rawwidth="544" data-rawheight="213" class="origin_image zh-lightbox-thumb" width="544" data-original="https://pic1.zhimg.com/26702ad72a395403ce4c7d02ccd167e4_r.jpg">
ok,如果你觉得这个太简单,我们现在可以深入到一些更加复杂的概念——指针的指针。例如下边的代码:

unsigned char a = 1;
unsigned char* b = &a;
unsigned char** c = &b;

上文中我们提到指针就是格子的编号,那么指针的指针是什么呢?就是指针变量所在格子的编号。如图:
<img src="https://pic3.zhimg.com/0b6ca6a620fd381b4c2395e134137ae6_b.jpg" data-rawwidth="545" data-rawheight="188" class="origin_image zh-lightbox-thumb" width="545" data-original="https://pic3.zhimg.com/0b6ca6a620fd381b4c2395e134137ae6_r.jpg">至于什么指针的指针的指针,以此类推。有 时候人们会把这种关系视作是一种指向关系。所以当人们说一个指针指向某个变量时,他们指的是这个指针变量的值是某个变量的格子编号。至于什么指针的指针的指针,以此类推。有 时候人们会把这种关系视作是一种指向关系。所以当人们说一个指针指向某个变量时,他们指的是这个指针变量的值是某个变量的格子编号。

但是这里有个问题,前文说到 ,每个格子只有一个字节。有些类型的变量需要多个字节来存储,比如int型就需要4个字节(不同平台长度不一致,这里假定是4个)。如果我有一个int型的变量a存在0x01号地址,我必须有方法告诉程序后边的三个格子也是这个变量值的一部分。

为了解决这个问题人们引入了指针类型的概念,对于int型的指针他表示的是从当前格子开始共4个格子都是该变量的值,而对于unsigned char型则只表示当前的格子。例如:

unsigned int c = 257;
unsigned int* a = &c;
unsigned char* b = (unsigned char*)&c; //必须强制转换一下 printf("%d", *a); // 输出 257
printf("%d", *b); // 输出 1

如图:
<img src="https://pic3.zhimg.com/f18c1c1a62294bc5c45682ea69776e7e_b.jpg" data-rawwidth="545" data-rawheight="188" class="origin_image zh-lightbox-thumb" width="545" data-original="https://pic3.zhimg.com/f18c1c1a62294bc5c45682ea69776e7e_r.jpg">
有的同学可能会奇怪为什么*b的值是1。要解决这个问题我们先要了解一个概念,大小端。
计算机使用二进制来表示数字。对于所有unsigned的整数类型变量来说,二进制的值本身就代表了他自己的数值,257转换成二进制是多少?「100000001」。8位是一个字节,int型就是4个字节32位。补齐前边的0后257可以写作「00000000 00000000 00000001 00000001」。8个位8个位的转换成10进制,上边的数字就是「0 0 1 1」。
OK,现在给你编号为1-4的四个格子和四个数字,让你按顺序存进去,请问有几种方法?

答案是2种,一种是1号放0,2号放0, 3号放1, 4号放1。另外一种是,4号放0,3号放0, 2号放1, 1号放1。我们把第一种方式叫小端,第二种叫大端。由于没有什么人来规定怎么在内存里存多个字节的数字,所以人们就随便来了。碰巧在我的机器上,内存里是大端存储的。所以就变成了上图中所示的样子。

此时计算机看到*a,于是找到了编号0x00的格子,因为是int型的指针,于是顺手把后边3个也读了出来,得到「1 1 0 0」。因为我的机器是大端,所以调整成人类的书写格式(低位在最右边)就是「0 0 1 1」,然后「00000000 00000000 00000001 00000001」,再然后「257」。

之后计算机又看了 *b,因为是unsigned char型的指针所以只拿0x00格子里的内容。于是就是1了。

ok,如果你还有兴趣看下去的话,我们来讨论一下指针运算的问题。
假设有一个指针a,那么a+1代表什么呢?就存储a的格子编号,加上a的类型所占的字节数所得到的的新地址。如图:
<img src="https://pic3.zhimg.com/04789934874fc4f8c8d8d742d8fa447e_b.jpg" data-rawwidth="630" data-rawheight="191" class="origin_image zh-lightbox-thumb" width="630" data-original="https://pic3.zhimg.com/04789934874fc4f8c8d8d742d8fa447e_r.jpg">
减法也是一个意思。两个指针相减就是他们之间差了几个格子。比如0x04-0x00 ==>4之类的。课本上一般会说两个指针相加没有意义,或者指针不能相加。现在知道为什么了吧,因为0x00+0x04之类的运算是没有实际含义的,算倒是可以算。

你知道为什么C语言中的数组下表从0开始吗?因为arr[n]的形式事实上等价于*(arr+n)。数组名其实就是数组的首地址,也就是数组的第一个格子的地址。作为数组中的第一个元素当然得加0啦。

关于数组我多说两句,数组就是一段连续的小格子。数组名代表的就是第一个小格所在编号。因此数组名就是指针,只不过这个指针变量的值是无法改变的。对于二维数组int [3][4] a来说,唯一的区别就是指针+1后移动过的格子数不同。感兴趣的同学可以自己查查为什么。

至于什么指针数组、数组的指针之类的等等概念,都是基于上述简单概念的组合,以此类推就可以了。

ok,理解到这一步其实基本上就算是可以了。不过为了稍微严谨一点我再多说几句。

实际中的计算机可没有这么简单。这个纸带不一定是一条,有可能一条是内存,一条是硬盘上的虚拟内存。纸带还会被分成多个区域,像什么堆栈之类的,并不是每个地址都可以访问,纸带也不一定是连续的。但是你在写程序的时候操作系统帮你把内存抽象成了一条连续的纸带。很多时候我们并不关心具体的值是什么。此外有的时候这种格子编号,不一定都指的是内存中的小格子。也有可能是某个硬件设备之类的。不过这就是另外一个话题了。

PS:上文中格子编号一律可以替换为内存地址。

作者:冯昱尧
链接:https://www.zhihu.com/question/24466000/answer/27893272
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最新文章

  1. 操作系统篇-hello world(免系统运行程序)
  2. 海思h264解码库
  3. 向 Git 服务器添加 SSH 公钥
  4. MVVM架构~knockoutjs系列之级联select
  5. 路由器换大Flash
  6. 黄聪:wordpress如何开启文章格式post format
  7. boost环境搭建
  8. ios 缓存相关信息收集
  9. Drupal 7.31 SQL注射分析POC
  10. 半透命opacity:(0-1),对于IE6版本不支持需要用filter:alpha(opacity=0-100)
  11. 分享一次Oracle数据导入导出经历
  12. Codeforces Round #402 (Div. 1)
  13. 【转载】Sqlserver阻止保存要求重新创建表的更改
  14. 【python】*与**
  15. es 基于match_phrase的模糊匹配原理及使用
  16. 如何在命令长度受限的情况下成功get到webshell(函数参数受限突破、mysql的骚操作)
  17. SC1243sensor噪点问题调试
  18. Pandas删除数据的几种情况
  19. Struts标签判断当前用户是否存在
  20. php发送 与接收流文件

热门文章

  1. Linux环境变量(小马哥推荐)
  2. [POJ] #1007# DNA Sorting : 桶排序
  3. Android通过tcpdump抓包
  4. systemd详解
  5. Apache Spark 架构
  6. UVALive 7077 - Song Jiang's rank list(模拟)
  7. poj 3020 Antenna Placement(最小路径覆盖 + 构图)
  8. 【原创】用Pwnage + Redsnow 制作完美越狱固件
  9. jquery的clone方法bug的修复
  10. Win8制作和使用恢复盘