相信各位在大一开始学习C语言的时候,第一次遇到指针的时候都是懵懵懂懂,等到后来大概搞清楚指针怎么使用的时候,又在某个风和日丽的下午在学到malloc函数时遇到这么一段代码:
int *num = (int *) malloc(sizeof(int) * 4);
What? 指针强转?这是什么东西?笔者当时就有这样的疑问(可能也是我太菜了嘿嘿)
数据之间的强转相信大家都不陌生,但是这指针的强转是个什么玩意??,当时笔者在学习时被告知这就是固定写法,这么写就完事了,所以笔者当时也是懵懵懂懂的就这么稀里糊涂学过去了,直到后来学习了单片机,在深入了解二进制之后突然恍然大悟
那么,什么是指针的强转呢?
其实总结下来无非三句话,指针强转之后改变如下:
1. 改变了指针进行++或--移动的字节数
2. 改变了编译器要解析多少个字节
3. 改变了编译器解析数据的方式
让我们举一些例子来看一看到底是在说什么
首先我们通过下面的代码来看一看第一条规则
#include <stdio.h>
int main() {
int arr[] = {15, -3, 131084};
int *p = arr;
short *h = (short*) p;
h += 4;
printf("short指针的解引用为:%hd", *h);
return 0;
}
这段代码的运行结果是:
short指针的解引用为:12
12?哪来的12?别急,计算机中所有存储的本质都是二进制,不妨我们来看看其作为在int数组中三个数字的二进制存储
那么12是怎么来的呢?我们一步一步往下看,首先,在short *h = (short *) p 执行完毕后会发生什么呢?
没错!什么都不会发生,只是*h这个指针指向了数组的首地址而已,就和int *p = arr是一样的
奥秘在于接下来的 h += 4 的操作,由于short类型在C语言中占两个字节,所以对于short类型的指针而言,每次对指针 ++ 或者 -- 的操作其实是移动了两个字节,那么在 h += 4 执行完毕后,h指针所在位置就在:
然后他会解析当前指针开始的后两个字节的数据,也就是00000000 00000010,也就是十进制的2,2?无良主播!不是说运行结果是12吗?怎么又成了2了?耍我?兄弟们干他!!
咳咳
回归正题,那么为什么计算的结果是12而不是我们分析的2呢?这里就涉及到一个知识,计算机的存储模式:大端模式和小端模式
那么什么大端模式是什么?小端模式又是什么?
通俗来讲大端模式就是数据的高位存储在低地址,低位存储在高地址,而小端模式则相反,小端模式数据的高位存储在高地址,低位存储在低地址
那么什么叫数据的高位?什么叫数据的低位呢?我们二进制在存储数字的时候,如果数字不是无符号类型的话,那么二进制的最前面那一位就是用来存储正负的符号位,那么越靠近符号位就是高位,越远离符号位就是低位
OK
接下来让我们通过int类型的数字15在内存中的存储来看看大端模式直观上是怎么样的:
这是int类型的数字15在内存中长这样,那么此时的存储方式就是大端存储,因为符号位也就是数据的高位是存放在低地址处的
接下来我们看看小端模式下的存储长什么样
此时可以看到,数据的低位存储在低地址的地方,高位存储在高地址的地方,那么此时就是小端存储
OK
那么介绍完大小端存储,接下来我们看一看真正的数据应该是如何存储的
因为数据是小端存储的缘故,所以上图才是数据在内存中的真正存储方式,那么我们看一下当前指针指向的这个位置,从这个位置开始拿两个字节 将 00001100 00000000 解析后的数据是.........
3072?
NONONO,孩子们,不要忘记了,我们高地址存储的是什么?符号位,所以真正的二进制数据应该是 00000000 00001100才对,那么解析出来的数据就是12啦!
所以从上述的例子中我们可以看出指针的强转确实会改变 ++ 和 -- 操作移动的字节数量,以及会影响拿多少个字节出来进行解析
但是第三条是什么意思?什么叫编译器改变了数据的解析方式?
OK,我们看接下来这段代码:
#include <stdio.h>
int main() {
int arr[] = {1061158912, -3, 131084};
void *p = arr;
int *t = arr;
float *q = (float*)t;
printf("float 指针解引用第一个元素值为:%.5f", *q);
return 0;
}
看到这段代码第一眼,卧槽,好多指针
学过malloc点开过函数的小伙伴应该知道,malloc返回的指针类型就是void *,那么为什么要返回void *呢,我们继续往下看
首先我们可以看一下当指针q指向该数组第一个元素,然后解引用之后的结果是什么
float 指针解引用第一个元素值为:0.75000
0.75?哪来的0.75?这0.75是谁变换过来的呢?没错,就是数组的第一个元素1061158912这位大汉(说起来这么大一个数被float指一下就萎掉了啊喂!)
为什么呢?老规矩,我们看一下内存的二进制分布
此时p,t,q,三个指针的值都是一样的,他们都指向该数组的第一个元素
然后我们对float类型的指针q进行解引用的操作,得到了0.75这个数,可以这个内存中的二进制怎么看都不像是一个这么小的值才对啊?别忘了,我们上一个例子中是对short类型的指针解引用,所以自然是拿出两个字节按照short类型的方式进行的解析,但是此时我们是float类型的指针,因此我们会从当前地址拿四个字节出来,并且按照float方式解引用
那么float类型数据是如何存储的呢?
这就不得不提到C语言的幽默小笑话
请看代码!
#include <stdio.h>
int main() {
float num = 0.1f;
if (num == 0.1) {
printf("相等");
} else {
printf("不相等");
}
return 0;
}
没错!这段代码运行的结果是不相等!但是如果你将num的值和判断的值都换成0.25,你会惊奇的发现相等了!what fa?我被C语言做局了,怎么都是小数,为什么他相等我不能相等!!!
这就不得不提到计算机是如何存储浮点数的了(感觉越说越多了怎么办)
没错!正是:国际标准IEEE(电⽓和电⼦⼯程协会)规定的浮点数存储规则!
下面所有蓝色的插图借用《程序是怎么跑起来》一书中的插图给大家展示
那么浮点数在计算机中的存储方式就如下图所示:
感兴趣的各位如果想深入了解float的存储方式可以自行查阅,笔者这里就不作过多赘述,只会简单讲解一下11.1875是如何转化成二进制的
首先,11.1875就会被变成上图所示的样子,这里可能就有人有疑问了,这转的什么玩意这是?
其实就是将小数点前的数字和小数点后的数字分别转成二进制而已,那么小数点前的数字转二进制大家都知道怎么个事,那么小数点后是怎么转的呢?
大家可以参考下面这张位权图:
所以11.1875后面的.1875自然就是0011啦,因为代表了次方加上
,那么结果自然就是0.1875啦
所以这就解释了为什么刚才我们那个C语言小笑话,为什么float变量出来的0.1不等于0.1
在十进制中我们会遇到这样一种情况,等于0.333333.......,无限循环了对吧,在二进制中也是一样的,因为没有哪个2的负多少次方能够表示0.1所以他只能不断的往小里找,然后不断地进行累加(跟微积分一样,只能靠近不能相等),但是我们float最多只能存储23个尾数部分,所以只能够进行截断,我们可以打印float变量的0.1的后面几位看看:
#include <stdio.h>
int main() {
float num = 0.1f;
if (num == 0.1) {
printf("相等\n");
} else {
printf("不相等\n");
}
printf("float的值为:%.30f\n", num);
return 0;
}
不相等
float的值为:0.100000001490116120000000000000
所以这就是为什么二者不相等的原因啦,那为什么0.25可以?因为次方刚好就是0.25,所以当然没问题!
OK,那我们继续
光转化为二进制还不够,我们还需要对他进行移位,IEEE使用的是将小数点前面的值固定为1的正则表达式
很好,光看文字描述完全不知道在说什么,没关系,让我们借用书中的插图来说明这件事
诺,就是这样,只是做了简单的移动小数点而已,够简单吧
那么上图中我们最后得到的二进制数值其实就是要存在尾数部分的值,注意!尾数部分存的只是小数点后的二进制值,小数点前的值不需要进行存储(等会解释为什么不需要存储)。
现在,我们得到了符号部分,尾数部分,那这个指数部分是个什么玩意?
还记得我们刚才移动小数点吗?没错,在十进制的世界中,移动小数点相当于对这个数字进行×10或者÷10的操作,而在二进制的世界中移动小数点相当于对这个数字进行×2或者÷2的操作,我们刚才将小数点前移了三位,因此指数部分应当存一个3次幂,也就是存个3
但是IEEE规定了一套规则,不能够直接存3,而是应当以一个数值作为基准线,小于这个基准线的视为负数,大于的视为正数,就好比我规定基准线是10,你想存-1,那么实际写入的值就是9,你想存3,那么实际写入的值就是13
我们将这套规则称作为EXCESS,具体的可以看下图
上图表格中所谓的EXCESS的系统表现其实就是实际的指数值
那么我们希望存入一个3次幂,所以实际写入的值就是130
OK
完成上面种种步骤,我们终于可以将其存在计算机中了,所以最终我们存储在计算机中的11.1875的二进制就是:0-10000010-01100110000000000000000
(终于完活了)
让我们回到正题,那么为什么那么大一个数字在float的解引用下就成了0.75呢?
我们将其二进制写出来看一下:00111111 01000000 00000000 00000000 (记得我们是符号位存储在高地址位哦)
转化为IEEE的二进制看看:0-01111110-10000000000000000000000
那么要如何转化为十进制呢?
首先符号位为正,然后我们看指数部分,转化为十进制就是:126,我们刚才EXCESS系统的基准值是多少呢?127,所以实际存储的指数就是(126 - 127),就是-1次方,最后,我们来看小数部分,很明显的次方,也就是0.5
那么齐活之后我们就来进行还原,首先,拿出我们的小数部分10000000000000000000000,接着别忘了我们刚才提到的正则表达式规则,也就是小数点前只有一个1,所以我们需要将这个1补上,所以值就是1.10000000000000000000000,将其转化为十进制就是 +
,也就是1 + 0.5,结果就是1.5
嗯...嗯?1.5?不是0.75么?
别忘了,我们还有个指数部分呢,指数部分是-1,所以还需要让1.5乘以,也就是1.5 * 0.5,最终的结果就是0.75!
所以这就是float和double的转化规则,这也就是为什么float指针指向那个数值解引用之后值会变得这么小的原因了,因为他并不是按照二进制转十进制来的,而是按照IEEE规则进行解析,所以得到了0.75这个数值
至此,就结束了,另外值得一说的是,void*类型的指针不允许解引用,会直接报错,我想malloc之所以要返回void*可能就是怕谁忘记转化直接用了吧
所以我们让不同的指针指向相同的值,在解引用会得到不同的结果就是因为大家解引用的字节数,方式都不一样导致的
有啥用?
好问题!
大家可能会遇到这样一种情况,你需要传输一个float类型的数据,但是有个问题,发送方只能一字节一字节慢吞吞给你发,那么你就可以使用一个char数组,将一字节一字节的数据放进去,最后使用float*指针指向并解引用就可以获得对应的float的数据啦!
其实所有的数据本质上都是二进制,而C语言给我们提供了指针强转这样的强力帮手,我们可以将二进制像捏橡皮泥一样捏成我们所想要的样子,我想这也就是C语言魅力所在吧!
/* 小彩蛋 */
#include <stdio.h>
int main() {
int arr[] = {1819043176, 1870078063, 6581362};
char *c = (char*)arr;
while (*c != '\0') {
printf("%c", *c++);
}
}
这段代码大家可以猜猜看输出结果是什么?
没错!就是我们每个程序员的第一句话:
hello world
我们总在类型转换中寻找世界的真相,就像在咖啡因与月色交织的凌晨三点,指针的每次解引用都是打开潘多拉魔盒的咒语。当强制转换的星尘洒落在内存的荒原,那些精心设计的字节终于呢喃出最古老的情话——那是所有程序员与计算机的初吻,是硅基世界与碳基灵魂的量子纠缠。
愿你们永远保持指针般敏锐的直觉,拥有内存管理般清醒的克制,在编译通过的绿色光芒里,看见自己最初敲下"hello world"时,那份让二极管颤栗的温柔。