Published on

内存里有没有数据类型

Authors
  • avatar
    Name
    KuibotLabs
    Twitter

程序里面的数据类型在内存里到底是怎么表示的呢?lk 和 ok 最近对 C 语言的指针孜孜不倦地追求,但是“纸上得来终觉浅,绝知此事要躬行。”(出自南宋·陆游《冬夜读书示子聿》,原著:古人学问无遗力,少壮工夫老始成。纸上得来终觉浅,绝知此事要躬行)

内存是由操作系统内核(Kernel)进行初始化,内核就像一个工人,每次电脑一开机,内核就开始对一块叫做内存的区域(集成电路 + 半导体)进行初始化,并给它们进行编号。一旦电脑关机,整个“内存大厦”就土崩瓦解,所有的编号都消失了,成了一堆没有用的电子电路而已,如果再次开机,内核又会重建整个“内存大厦”。小 K 问,那操作系统内核又是怎么初始化的呢?这个问题就比较复杂了,超过了目前这篇文章关注的主题。

那内存里面究竟是什么样的呢?我们可以把内存看成一串又一串的小格子,就像下面这样。

20230222-1_a

每个格子叫一个位(bit),有很多很多的位连接在一起,每个位上就只能写 0 或者 1。操作系统内核一般会把这些位按照 8 个一组的方式进行初始化,而 8 个bit就组成了一个字节(Byte),为了方便看我们就按下面这样来进行图示。

20230222-1_b

接下来操作系统内核开工了,他会为每个字节编号(动词),发给每个字节的编号(名词)就是这个字节的内存地址,如下图所示。

20230222-1_c

0X0000、0X0001 和 0X0002 就是内存地址,现在用一段 C 语言代码来做验证。

#include <stdio.h>

int main(void)
{
    int a = 20000;
    int* a_ptr = &a;
    
    printf("%p\n", a_ptr);
    
    return 0;
}

这段代码输出了整型指针a_ptr指向的内存地址,我这里的输出为:000000000065FE0C(程序下次运行可能会变的),接着我们再用计算器的程序员模式算出 20000 的二进制表示为:0100111000100000。

20230222-1_d

因为变量a是整型,所以a需要使用 4 个字节(在我的操作系统上),按照从末尾 8 位一个字节的方式来解析这个二进制,那么可以得到 4 个字节,分别是:00000000、00000000、01001110、00100000,前面补两组 8 个 0 的字节,是因为 20000 这个二进制并不能填满 4 个字节,所以需要在前面补 0。

接下来用代码验证一下逻辑,将这个整型指针转换为字符型指针,因为字符型的长度是一个字节,所以字符型指针也是按一个字节进行内存地址的“寻址”。

#include <stdio.h>

int main(void)
{
    int a = 20000;
    int* a_ptr = &a;
    
    printf("%p->%d\n", a_ptr, *a_ptr);
    
    char* c_ptr = a_ptr; // 强制把一个字符型指针指向原来整形指针的位置
    
    printf("%p->%c\n", c_ptr, *c_ptr);
    
    c_ptr++;
    printf("%p->%c\n", c_ptr, *c_ptr);
    
    c_ptr++;
    printf("%p->%c\n", c_ptr, *c_ptr);
    
    c_ptr++;
    printf("%p->%c\n", c_ptr, *c_ptr);
    
    return 0;
}

输出的结果如下图。

20230222-1_e

可以看到整形变量a原来的地址为 000000000065FE0C,这个地址按照整型指针寻址后,值是 20000。现在我们把一个新的字符型指针c_ptr指向和a_ptr一样的地址,则改变了内存中这段二进制数字的读取方式。因为a原来为整型,而整型变量占用 4 个字节,所以我们将c_ptr指针可控地自增 3 次(超过 3 次概不负责哦),并且每次打印出内存地址以及这个地址指向的这个字节里面存的值(使用*c_ptr)。

先来研究下 4 个字节按道理都应该是些什么值。前面已经说过 4 个字节里面的二进制分别是:00000000、00000000、01001110、00100000,因为现在我们访问内存时用的是字符型指针,所以一个字节就会形成一个字符,而字符又和十进制通过 ASCII 码表进行对应,那么可以用程序员计算器算出这 4 个字节的整型值分别是:0(00000000)、0(00000000)、78(01001110)、32(00100000),然后去 ASCII 码表中查找对应的字符。

十进制数ASCII码控制字符
0NUT(不输出)
78N(字符)
32(space)(不输出)

观察程序的输出,里面刚好出现了“N”这个字符,但是我们发现程序好像不是按照0、0、78、32的顺序输出的,而是反着输出的(其实这和操作系统如何存储数据有关,计算机操作系统存储数据有大端模式和小端模式,不同的操作系统可以采用不同的方式),根据我们刚才的实验结果,在 ok 的 Windows 电脑上可以做一个指针寻址的猜想(当然这不一定适合小 K 的树莓派操作系统),我猜想类型指针寻址是按照如下模式进行的。

20230222-1_f

整型指针指向“字节0”,因为是整型指针,所以需要再找 3 个字节到“字节3”,但是要完整构建这个整形数,需要从下往上排列这些二进制位,然后转换为相应的整型数。

为了验证这个的猜想,假设刚好有一个整型数通过上面的程序执行后会输出K、U、I、B四个char类型的字符,那先用我们的大脑来算一算这个整型数应该是多少。

ASCII码控制字符十进制数二进制数
K7501001011
U8501010101
I7301001001
B6601000010

按照刚才的猜想需要把这些二进制数按照从下往上的顺序排列出来:01000010010010010101010101001011,这个二进制数对应的整型数用计算器算出来是:1112102219,如下图所示。

20230222-1_g

好了,现在只需要把代码中的整型数 20000 改成 1112102219,如果输出分别为:K、U、I、B,那这个实验就成功了。

20230222-1_h

完美!

这个实验说明,内存中的数据根本没有什么数据类型的说法,内存中的数据如何解释完全是由我们的程序来定义的,因为我们想怎么玩就怎么玩,想让内存中的数据被解释成什么它就是什么!