注册 登录
  • 推荐使用最新版火狐浏览器和Chrome浏览器访问本网站

为什么要有指针?

C语言 admin 来源:Tim Shen 68次浏览 0个评论

<0>史前

    早期的CPU(也许并没有真正的实现)并不如今天的强大,内存读写的指令可能只有“从*常数*0x1234地址处读入1字节到寄存器a”,或者“把寄存器b的值写入*常数*地址0x5678这个地方”。

那个时候没有变量这一说,所有的内存读写都得指定好常数,也就是得把具体的数字(也称为字面量,literal)写死在程序里。

换言之,你如果想清空100字节的内存,而每条指令只能对内存中某一个字节进行写入,那就得写100条指令。随着要处理的数据的膨胀,程序也得跟着膨胀,而这是不可接受的。

<1>流程跳转

为了解决这个问题,人们发明了控制流程的指令,比如“如果寄存器c的值为0,则略过下一条指令”和“无条件跳转到首地址为0x9012”的地方,从那个地方继续运行”。注意,指令本身也是编码成字节存在内存里的,这就是冯诺伊曼机器最优雅的地方。这时候我们可以写出循环了(这里不展开,感觉没必要)。

<2>自修改程序

问题并没有解决,因为就算有了循环,整个程序还是只能写常量,所以如果想要对不同的内存块清零,只能写不同的指令。

但是人是很聪明的。

利用“指令本身也是编码成字节存在内存里”这个特征,我们可以通过修改内存来修改指令,继而修改行为。举例来说,在0x3454处存了一条指令,这条指令叫做“把0x5678开始的一字节清零”。比方说这条指令占用了三个字节,第一个字节告诉CPU这是一条清零指令,后两个字节(0x3455和0x3456)存了一个表示地址的整数,告诉CPU到底要把哪个字节清零。显然,这个整数会和0x5678有关,而且大多情况就是0x5678。

现在,要是CPU在某处执行了一句“把起始地址为0x3455的那个2字节数自增1”,那实际上0x3454处的指令就变成了“把0x5679开始的一字节清零”!

看到希望了么,我们可以写一个指令,然后不断修改这个指令,再加上流程控制的指令产生一个循环,就能用一段固定长度(注意,代码内容随着执行并不固定)的代码清零任意指定的一段内存块!

这叫做“自修改程序”。

<3>间接寻址

后来的事情就很简单了。编写这种自修改程序极容易出错,因为稍微改错一个地方指令就全改乱了(比如在上例中如果把0x3454中的数字给改了,就完全变成了另一条指令),所以人们再次发明新的工具,叫间接寻址。所以有了这样的指令“把寄存器a存的数字当成地址,取出该地址处的字节放到寄存器b里”和“把寄存器a存的数字当成地址,把寄存器b的字节写入到该地址处”。

回到自修改程序的那个例子里,我们可以把“把0x5678开始的一字节清零”换成:
“把寄存器b设为0”
“把0x5678这个数字写到0x1234处”
“读入0x1234的数字到寄存器a”
“把寄存器a存的数字(此处即0x5678)当成地址,把寄存器b(此处为0)的字节写入到该地址处”

这样实现虽然看起来费事,但是总算得到了等价的功能。另外,我们以后只需要读写0x1234这个内存就能达到改变行为的目的,而不要冒险去修改指令本身了。换言之,指令本身被固化,其行为更加稳定。今天,代码虽然也在内存里,也编码成了一个个字节,但是一般不和数据放在一起,而且一般执行的时候是只读的。

假设程序员无限聪明(当然这种好事从来就没有发生过:),写的代码从来不出错,那么间接寻址是没有必要的,因为直接写自修改程序就行了。间接寻址没有增加任何新的功能,这点不像跳转指令。

<4>指针

哦,现在解释指针就很简单了,指针就是间接寻址例子里面那个0x1234的内存块。它存了一片地址,而指针解引用(比如*p)就对应的是间接寻址读写的指令了。

所以说到最后,“为什么要有指针的”就可以化成“为什么要有间接寻址”,问题基本等价于“为什么不直接使用自修改程序”了。


个人博客 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权 , 转载请注明为什么要有指针?
喜欢 (0)
admin
关于作者:
这个好学的男人很懒,很少会有东西想说
发表我的评论
取消评论

*

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址