fp指针类型不兼容_微课 | 学指针,只需要yi点点智慧

点击蓝字

bfd32eb9aed366e3aa69cea3bc1c0a03.png

关注我们

班 

这周的课上,难点很多,苏格捞底老师都表示捞不动了。在学委和各位同学的支持下,本次微课将带大家一起学习指针内容

d6c6d01dbc98beb0715f6c109cecaaf3.gif

前言:复杂类型说明

说到指针,就不可能脱离开内存,学会指针的人分为两种,一种是不了解内存模型,另外一种则是了解。不了解的对指针的理解就停留在“指针就是变量的地址”这句话,会比较害怕使用指针,特别是各种高级操作。而了解内存模型的则可以把指针用得炉火纯青,各种 byte 随意操作,让人直呼 666。

d6c6d01dbc98beb0715f6c109cecaaf3.gif

一、内存

3c9fa95a2f03f81370137dcd379e4b0b.png

编程的本质其实就是操控数据,数据存放在内存中。因此,如果能更好地理解内存的模型,以及 C 如何管理内存,就能对程序的工作原理洞若观火,从而使编程能力更上一层楼。

大家真的别认为这是空话,因为一旦上千行,经常出现各种莫名其妙的内存错误,一不小心就发生了 coredump...... 而且还无从排查,分析不出原因。「指针存储的是变量的内存地址」这句话应该任何讲 C语言的书都会提到吧。所以,要想彻底理解指针,首先要理解 C 语言中变量的存储本质,也就是内存。

计算机的内存是一块用于存储数据的空间,由一系列连续的存储单元组成,由于 1 个 bit 只能表示两个状态,所以大佬们规定 8个 bit 为一组,命名为 byte。并且将 byte 作为内存寻址的最小单元,也就是给每个 byte 一个编号,这个编号就叫内存的地址。

在计算机中,我们也要保证给每一个 byte 的编号都是唯一的,这样才能够保证每个编号都能访问到唯一确定的 byte。

9cbd81f06d71d597579a4c58e2411713.png

上面我们说给内存中每个 byte 唯一的编号,那么这个编号的范围就决定了计算机可寻址内存的范围。

所有编号连起来就叫做内存的地址空间,这和大家平时常说的电脑是 32 位还是 64 位有关。

150db6d1-c442-eb11-8da9-e4434bdf6706.svg

拓展阅读

  早期 Intel 8086、8088 的 CPU 就是只支持 16 位地址空间,寄存器和地址总线都是 16 位,这意味着最多对 2^16 = 64 Kb 的内存编号寻址。这点内存空间显然不够用,后来,80286 在 8086 的基础上将地址总线和地址寄存器扩展到了20 位,也被叫做 A20 地址总线。当时在写 mini os 的时候,还需要通过 BIOS 中断去启动 A20 地址总线的开关。但是,现在的计算机一般都是 32 位起步了,32 位意味着可寻址的内存范围是 2^32 byte = 4GB。所以,如果你的电脑是 32 位的,那么你装超过 4G 的内存条也是无法充分利用起来的。

1.3 变量的本质

有了内存,接下来我们需要考虑,int、double 这些变量是如何存储在 0、1 单元格的。

在 C 语言中我们会这样定义变量:

654d725acd068d6da25f7404fef4584c.png

我们都知道 int 类型占 4 个字节,并且在计算机中数字都是用补码(不了解补码的记得去百度)表示的。

999 换算成补码就是:0000 0011 1110 0111这里有 4 个byte,所以需要四个单元格来存储:

a95b2ef7ecfcf6db793a6277497a0a53.png

有没有注意到,我们把高位的字节放在了低地址的地方。那能不能反过来呢?当然,这就引出了大端和小端。像上面这种将高位字节放在内存低地址的方式叫做大端反之,将低位字节放在内存低地址的方式就叫做小端。

159d95a2f95e132dd9071569162aac60.png

上面只说明了 int 型的变量如何存储在内存,而 float、char 等类型实际上也是一样的,都需要先转换为补码。

对于多字节的变量类型,还需要按照大端或者小端的格式,依次将字节写入到内存单元。

记住上面这两张图,这就是编程语言中所有变量的在内存中的样子,不管是 int、char、指针、数组、结构体、对象... 都是这样放在内存的。

d6c6d01dbc98beb0715f6c109cecaaf3.gif

二、指针

定义一个变量实际就是向计算机申请了一块内存来存放。那如果我们要想知道变量到底放在哪了呢?可以通过运算符&来取得变量实际的地址,这个值就是变量所占内存块的起始地址。

我们可以把这个地址打印出来,大概会是像这样的一串数字:0x7ffcad3b8f3c

既然指针的本质都是变量的内存首地址,即一个 int 类型的整数。那为什么还要有各种类型呢?比如 int 指针,float 指针,这个类型影响了指针本身存储的信息吗?这个类型会在什么时候发挥作用?

上面的问题,就是为了引出指针解引用的。

pa中存储的是a变量的内存地址,那如何通过地址去获取a的值呢?

这个操作就叫做解引用,在 C 语言中通过运算符 *就可以拿到一个指针所指地址的内容了。

比如*pa就能获得a的值。我们说指针存储的是变量内存的首地址,那编译器怎么知道该从首地址开始取多少个字节呢?

这就是指针类型发挥作用的时候,编译器会根据指针的所指元素的类型去判断应该取多少个字节。

如果是 int 型的指针,那么编译器就会产生提取四个字节的指令,char 则只提取一个字节,以此类推。

下面是指针内存示意图:

d4902f826aa1be9f07b46226d56b4963.png

pa 指针首先是一个变量,它本身也占据一块内存,这块内存里存放的就是 a 变量的首地址。当解引用的时候,就会从这个首地址连续划出 4 个 byte,然后按照 int 类型的编码方式解释。

2.4 活学活用

别看这个地方很简单,但却是深刻理解指针的关键。举两个例子来详细说明:比如:

4833f03385723a2e46981e15caeff9ed.png

如图:

917ef5c22aad228ce9e041b137303e75.png

具体过程和上述一样,但上面肯定不会报错,这里却不一定。

为什么?

(float*)&c会让我们从c  的首地址开始取四个字节,然后按照 float 的编码方式去解释。

但是c是 short 类型只占两个字节,那肯定会访问到相邻后面两个字节,这时候就发生了内存访问越界。

当然,如果只是读,大概率是没问题的。

但是,有时候需要向这个区域写入新的值,比如:

95bbc1a6ea79ca2baae2a7eff6d31b7a.png

那么就可能发生 coredump,也就是访存失败。

另外,就算是不会 coredump,这种也会破坏这块内存原有的值,因为很可能这是是其它变量的内存空间,而我们去覆盖了人家的内容,肯定会导致隐藏的 bug。

如果你理解了上面这些内容,那么使用指针一定会更加的自如。

讲到这里,我们来看一个问题

f0617c46e81f63cb207cc3550f530cc5.png

这是他写的代码:

8aa1776d53dcc7cd4e6f7dd6187eb50d.png

他把 double 写进文件再读出来,然后发现打印的值对不上。

而关键的地方就在于这里:

fba3a58a79190985e64d16601c7fabef.png

他可能认为 buffer 是一个指针(准确说是数组),对指针解引用就该拿到里面的值,而里面的值他认为是从文件读出来的 4 个byte,也就是之前的 float 变量。

注意,这一切都是他认为的,实际上编译器会认为:“哦,buffer 是 char类型的指针,那我取第一个字节出来就好了”。然后把第一个字节的值传递给了 printf 函数,printf 函数会发现,%f 要求接收的是一个 float 浮点数,那就会自动把第一个字节的值转换为一个浮点数打印出来。

错误关键就是,这个同学误认为,任何指针解引用都是拿到里面“我们认为的那个值”,实际上编译器并不知道,编译器只会傻傻的按照指针的类型去解释。

所以这里改成:

5d0466721c9f94e7e3c0a6d746bf7da9.png

相当于明确的告诉编译器:“buffer指向的这个地方,我放的是一个 float,你给我按照 float 去解释”

d6c6d01dbc98beb0715f6c109cecaaf3.gif

三、结构体和指针

结构体内包含多个成员,这些成员之间在内存中是如何存放的呢?

比如:

e7705d3175af8b7ef6153261fd68656a.png

这是一个定点小数结构体,它在内存占 8 个字节(这里不考虑内存对齐),两个成员域是这样存储的:

52e919d95e06745a4b2a68f1e27f102e.png

我们把 10 放在了结构体中基地址偏移为 0 的域,2 放在了偏移为 4 的域。

接下来我们做一个正常人永远不会做的操作:

624673625737cd1bd37ca42195255c18.png

上面这个究竟会输出多少呢?自己先思考下噢~

接下来我分析下这个过程发生了什么:

e6d6b04d5da535fc10e6a82e8ce06b5a.png

首先,&fp.denom表示取结构体 fp 中 denom 域的首地址,然后以这个地址为起始地址取 8 个字节,并且将它们看做一个 fraction 结构体。

在这个新结构体中,最上面四个字节变成了 denom 域,而 fp 的 denom 域相当于新结构体的 num 域。

因此:

((fraction*)(&fp.denom))->num = 5

实际上改变的是 fp.denom,而

((fraction*)(&fp.denom))->denom = 12

则是将最上面四个字节赋值为 12。

当然,往那四字节内存写入值,结果是无法预测的,可能会造成程序崩溃,

因为也许那里恰好存储着函数调用栈帧的关键信息,也可能那里没有写入权限。

大家初学 C 语言的很多 coredump 错误都是类似原因造成的。

所以最后输出的是 5。

为什么要讲这种看起来莫名其妙的代码?

就是为了说明结构体的本质其实就是一堆的变量打包放在一起,而访问结构体中的域,就是通过结构体的起始地址,也叫基地址,然后加上域的偏移。

其实,C++、Java 中的对象也是这样存储的,无非是他们为了实现某些面向对象的特性,会在数据成员以外,添加一些 Head 信息,比如C++ 的虚函数表。

实际上,我们是完全可以用 C 语言去模仿的。

这就是为什么一直说 C 语言是基础,你真正懂了 C 指针和内存,对于其它语言你也会很快的理解其对象模型以及内存布局。

好啦~今天的微课到此结束,

相信看到这里的你,也有了些许收获。

你可能仍然好奇,指针更高阶的玩法

在这里小八我要卖个关子

我们明天再见

83a434daa9f456ec9d8552bffe56a703.gif

学习使我快乐

435ee96e67f3009e41631c3f6141eccd.png

扫码关注我们

图源 | 仇慧芳 甘一伟

撰稿 | 李梓楷

期待你的

分享

点赞

在看

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值