作者
编程指北
来源
编程指北(id:cs_dev)
这一篇的文章主题是「指针与内存模型」
说到指针,就不可能脱离开内存,学会指针的人分为两种,一种是不了解内存模型,另外一种则是了解。
不了解的对指针的理解就停留在“指针就是变量的地址”这句话,会比较害怕使用指针,特别是各种高级操作。
而了解内存模型的则可以把指针用得炉火纯青,各种byte随意操作,让人直呼。
这篇看完,相信你会对指针有一个新的认识,坐等打脸
内存本质
编程的本质其实就是操控数据,数据存放在内存中。
因此,如果能更好地理解内存的模型,以及C如何管理内存,就能对程序的工作原理洞若观火,从而使编程能力更上一层楼。
大家真的别认为这是空话,我大一整年都不敢用C写上千行的程序也很抗拒写C。
因为一旦上千行,经常出现各种莫名其妙的内存错误,一不小心就发生了coredump......而且还无从排查,分析不出原因。
相比之下,那时候最喜欢Java,在Java里随便怎么写都不会发生类似的异常,顶多偶尔来个NullPointerException,也是比较好排查的。
直到后来对内存和指针有了更加深刻的认识,才慢慢会用C写上千行的项目,也很少会再有内存问题了。
「指针存储的是变量的内存地址」这句话应该任何讲C语言的书都会提到吧。
所以,要想彻底理解指针,首先要理解C语言中变量的存储本质,也就是内存。
1.1内存编址
计算机的内存是一块用于存储数据的空间,由一系列连续的存储单元组成,就像下面这样:
每一个单元格都表示1个Bit,一个bit在EE专业的同学看来就是高低电位,而在CS同学看来就是0、1两种状态。
由于1个bit只能表示两个状态,所以大佬们规定8个bit为一组,命名为byte。
并且将byte作为内存寻址的最小单元,也就是给每个byte一个编号,这个编号就叫内存的地址。
这就相当于,我们给小区里的每个单元、每个住户都分配一个门牌号:、、、、......
在生活中,我们需要保证门牌号唯一,这样就能通过门牌号很精准的定位到一家人。
同样,在计算机中,我们也要保证给每一个byte的编号都是唯一的,这样才能够保证每个编号都能访问到唯一确定的byte。
1.2内存地址空间
上面我们说给内存中每个byte唯一的编号,那么这个编号的范围就决定了计算机可寻址内存的范围。
所有编号连起来就叫做内存的地址空间,这和大家平时常说的电脑是32位还是64位有关。
早期Intel、的CPU就是只支持16位地址空间,寄存器和地址总线都是16位,这意味着最多对2^16=64Kb的内存编号寻址。
这点内存空间显然不够用,后来,在的基础上将地址总线和地址寄存器扩展到了20位,也被叫做A20地址总线。
当时在写minios的时候,还需要通过BIOS中断去启动A20地址总线的开关。
但是,现在的计算机一般都是32位起步了,32位意味着可寻址的内存范围是2^32byte=4GB。
所以,如果你的电脑是32位的,那么你装超过4G的内存条也是无法充分利用起来的。
好了,这就是内存和内存编址。
1.3变量的本质
有了内存,接下来我们需要考虑,int、double这些变量是如何存储在0、1单元格的。
在C语言中我们会这样定义变量:
1inta=;2charc=c;
当你写下一个变量定义的时候,实际上是向内存申请了一块空间来存放你的变量。
我们都知道int类型占4个字节,并且在计算机中数字都是用补码(不了解补码的记得去百度)表示的。
换算成补码就是:
这里有4个byte,所以需要四个单元格来存储:
有没有注意到,我们把高位的字节放在了低地址的地方。
那能不能反过来呢?
当然,这就引出了大端和小端。
像上面这种将高位字节放在内存低地址的方式叫做大端
反之,将低位字节放在内存低地址的方式就叫做小端:
上面只说明了int型的变量如何存储在内存,而float、char等类型实际上也是一样的,都需要先转换为补码。
对于多字节的变量类型,还需要按照大端或者小端的格式,依次将字节写入到内存单元。
记住上面这两张图,这就是编程语言中所有变量的在内存中的样子,不管是int、char、指针、数组、结构体、对象...都是这样放在内存的。
指针是什么东西?
2.1变量放在哪?
上面我说,定义一个变量实际就是向计算机申请了一块内存来存放。
那如果我们要想知道变量到底放在哪了呢?
可以通过运算符来取得变量实际的地址,这个值就是变量所占内存块的起始地址。
(PS:实际上这个地址是虚拟地址,并不是真正物理内存上的地址
我们可以把这个地址打印出来:
1printf(%x,a);
大概会是像这样的一串数字:0x7ffcad3b8f3c
2.2指针本质
上面说,我们可以通过符号获取变量的内存地址,那获取之后如何来表示这是一个地址,而不是一个普通的值呢?
也就是在C语言中如何表示地址这个概念呢?
对,就是指针,你可以这样:
1int*pa=a;
pa中存储的就是变量a的地址,也叫做指向a的指针。
在这里我想谈几个看起来有点无聊的话题:
为什么我们需要指针?直接用变量名不行吗?
当然可以,但是变量名是有局限的。
变量名的本质是什么?
是变量地址的符号化,变量是为了让我们编程时更加方便,对人友好,可计算机可不认识什么变量a,它只知道地址和指令。
所以当你去查看C语言编译后的汇编代码,就会发现变量名消失了,取而代之的是一串串抽象的地址。
你可以认为,编译器会自动维护一个映射,将我们程序中的变量名转换为变量所对应的地址,然后再对这个地址去进行读写。
也就是有这样一个映射表存在,将变量名自动转化为