信息存储

在计算机中,都是使用8位的块或者字节来作为最小的可寻址内存空间,简单地说,就是不能只从内存中取一个比特,而是一下子取一个块或者一个字节。程序把内存看作一个巨大的数组,这个数组称为虚假内存,内存中每个字节都可以由唯一的数字区分,这个数字叫做内存地址,内存地址的集合称为虚假内存空间。

这一章是讲数据到底是怎么存储在计算机中的,更细致的,是指各种数字是怎么存的,会先从简单开始介绍。

十六进制表示法

十六进制就是以0x0X开头的数字,它是计算机数字表示的一种进制,可以让二进制位看得更简洁一些。

对于十进制的数字,用0-9表示;但对于十六进制的数字,因为它是逢十六进一,所以在9之后,紧接的是字母A-F,它们各自代表10-15。

十进制0...9101112131415
二进制0000...1001101010111100110111101111
十六进制0...9ABCDEF

在这里也很明显地发现,二进制转十六进制的时候,可以从低到高四位四位地转,剩下的若不足四位,则高位补0。

十六进制转二进制

下面展示十六进制的0x173A4C转换为二进制数的过程。简单地说就是一个十六进制顶四个二进制。

十六进制173A4C
二进制000101110011101001001100

2^n转十六进制

对于一些特殊点的情况,比如一些2^n的二进制数,转十六进制数则非常快,看看n由几个4组成,每有一个4,后面即跟一个0,剩下的余数则是正常转。

比如,对十进制的2^10,转成二进制的话,即1后面跟10个0,10000000000

因为10=2+4*2,即2^10=2^2*2^4*2^4

因此,转成十六进制为0x400

十六进制转十进制

其实和二进制转十进制一样,只是每一位权重不同。比如最低位是16^0,倒数第一位是16^1,倒数第二位是16^2,依此类推……

只要这一位有数,就乘就行。

十进制转十六进制

手动算的话,需要一步一步地除,下面举个例子,比如把十进制188转换成十六进制。

188 = 11 * 16 + 12

11 = 0 * 16 + 11

结果从下往上看,即为0xBC

我们再拿它转回去,即C+B*16=12+11*16=188,正确。

字数据相关

这部分了解得不深,难以分类,便简单地介绍一下。

现在想想,它们把这些写成书真牛逼,我连自己总结都憋不出来几个字。

一些介绍

每台计算机都有其特定的字长,通常为32位或64位。那对于一台字长为32位的机子来说,它的程序的虚拟内存空间最大能取2^32-1,程序最多能访问2^32个字节。这个计算是拿32位全1来看的,因为这个数是无符号的数。

书本上提到,32位机子内存空间是4GB,64位机子内存空间是4EB。(GB-->TB-->PB-->EB,非常大!)

它这里讲到一些不同字长的机子上运行同一个程序,它们要怎么兼容,在C中,在32位机子中,long为4字节,而64位机子中,它占8字节,就会很不同。

即,一段在32位机子上能正常运行的代码,到64位机子上却出异常了,这是为什么呢。。。就有这种疑惑。

image-20220510000157603

寻址和字节序

对于跨越多个字节的程序对象,要思考两个问题。

  1. 它的地址是什么
  2. 内存中如何排布这些字节

对于几乎所有的机器,这种对象的存储一般都是连续的内存空间,并且它的地址是使用字节的最低地址。假如一个整数x的地址是0x100,那么它就会从0X100开始,存储这个整数,总共占4个字节,即存放在0x100、0x101、0x102、0x103。

对于一个w位的整数,它的位可以表示为[x~w-1~,x~w-2~,x~w-3~...x~2~, x~1~,x~0~],其中,最高有效位是x~w-1~最低有效位是x~0~

  • 大端和小端

    在内存的低位存放的是最高有效位,这就是大端法
    在内存的高位存放的是最低有效位,这就是小端法。

    总觉得容易搞混,真是。

    那我们从人的角度来说,数据总是从低地址往高地址存放,在存放的过程中,如果是按我们正常的做法去放,那应该把整数正序地存放,即最高有效位在,最低有效位在后,因此,这就是内存的低地址存放最高有效位,也就是常说的大端法。

我发现我的机子好像使用的是小端,是通过练习题的结果知道的。

/**
 * @brief 结果是小端??
 * 结果:
 * 21
 * 21 43
 * 21 43 65
 */
void think()
{
    int val = 0x87654321;
    byte_pointer valp = (byte_pointer)&val;
    // 这个函数是打印,按char输出(即两个字节,8位)
    show_bytes(valp, 1);
    show_bytes(valp, 2);
    show_bytes(valp, 3);
}

这个大端小端,似乎与操作系统有关系,这种没有好坏之分,只要协商好规则即可。

字节序可能引起的问题

  1. 不同类型的计算机传输数据时。

    若是不同的字节序,就会收到反序的数据,没有得到正确的结果。

  2. 阅读表示整型数据的字节序列时。

    这里展示的在需要阅读的机器级程序的场景,代码是一个反汇编的语句,里面有一行说,需要把一个字长的数据怎样地添加,但是,这行数据的存储方式是大端或小端会影响最终的结果,因为会加错,因此需要注意。

  3. 编写规避正常的类型系统的程序时。

    就是上面那段代码了,再加一段。

    typedef unsigned char *byte_pointer;
    
    void show_bytes(byte_pointer start, size_t len)
    {
        size_t i;
        for (i = 0; i < len; i++)
        {
            printf(" %.2x", start[i]);
        }
        printf("\n");
    }
    void show_int(int x)
    {
        show_bytes((byte_pointer)&x, sizeof(int));
    }
    
    void show_float(float x)
    {
        show_bytes((byte_pointer)&x, sizeof(float));
    }
    
    void show_pointer(void *x)
    {
        show_bytes((byte_pointer)&x, sizeof(void *));
    }
    

    把数据转成char*指针,然后打印。

    打印的结果,你会发现,跟大端/小端有关,如果是小端,打印出来的结果是数字的低位;如果是大端,打印出来的结果就是我们正常阅读的位数。


    这块实在太高深,总结不出来什么,不过可以记录一下这段话:

    指令编码是不同的,不同的机器类型使用不同的且不兼容的指令和编码方式,即使完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的,二进制代码很少能够在不同机器和操作系统组合之间移植。

    计算机系统的一个基本概念就是,从机器的角度来看,程序仅仅只是字节序列。机器没有关于原始源程序的任何信息,除了可能有些用来帮助调度的辅助表以外。

布尔代数

把逻辑值true和false编码为二进制的1和0,能够创建一种新的代数。

  • AND:&

    同为1才是1,否则为0

    &01
    000
    101
  • OR:|

    只要有1就是1

    01
    001
    111
  • NOT:~

    取反

    ~
    01
    10
  • EOR:^

    异或,相异为1,相同为0

    ^01
    001
    110

C语言中的位级运算

同布尔代数的符号,&、|、~、^。

这些位运算,非常精妙,就是对于整数,它是一位一位地算的,没什么技巧。

下面展示一些精妙的东西,比如这个交换元素,真是炫技:

void swap(int *x, int *y)
{
    *y = *x ^ *y;
    *x = *x ^ *y;
    *y = *x ^ *y;
}

但是配上这个,就要翻车了。

void reverse_array(int a[], int cnt)
{
    int first, last;
    for (first = 0, last = cnt - 1;
         first <= last;
         first++, last--)
    {
        swap(&a[first], &a[last]);
    }
}

在偶数个元素时,最后一次会交换中间的元素。swap很正常,但是它传过去的是同一个地址,要swap的第3行时,就直接异或成0了,因此中间的元素总是会为0。

解决方法是,把=去掉。

C语言中的逻辑运算

逻辑运算,就是结果只有1或0的运算,而不是像位运算一样,都有结果。

运算的符号有:&&、||、!

而且,这个操作还短路,即它的左边判断出来了,右边就不会继续执行!


  • 做一道题,总结一下位运行和逻辑运行

    对于x=0x66 01100110,y=0x39 00111001

    x=0x6601100110y=0x3900111001
    x & y0x20x && y0x01
    x | y0x7fx || y0x01
    ~x | ~y0xdf!x || !y0x00
    x & !y0x00x && ~y0x01

C语言中的位移运算

左移:<<

左移x位,抛弃溢出的最高位,右端补0。

逻辑右移:>>

同左移,右移x位,抛弃溢出的最低位,左端补0 。

算术右移:>>

几乎所有的编译器和处理器对于>>都是采用算术右移,即右移x位,抛弃溢出的最低位,左端补符号位

但是对于Java这种,它有>>>区分。

下面展示一个例子

xx << 3x >> 2 (逻辑右移)x >> 2 (算术右移)
11000011000110000011000011110000