快乐的春饼

浮点数和端序

float 和 double

计算机中通常用浮点数来表示小数。在 IEEE 754 标准中,规定了两种常用的浮点类型:float和double,分别为单精度和双精度浮点类型。float采用4个字节存储,double采用8个字节存储。浮点类型类似于科学计数法的结构。float的结构是,从高位向低位,依次是1比特符号、8比特阶码、23比特尾数。double的结构是,从高位向低位,依次是1比特符号、11比特阶码、52比特尾数。比如-123.25的float表示是

 1   10000101   11101101000000000000000
 -   --------   -----------------------
符号   阶码               尾数

符号位为0表示正数,为1表示负数。阶码部分采用偏移量形式,即实际指数加上0b01111111(127)。二进制尾数(除了0)的第一个比特必然是1,因此省略这一位以节约空间。double类似,不过偏移量是0b01111111111(1023)。

有几个特殊的浮点数:

浮点数符号阶码尾数意义
0.00或1(正0.0等于负0.0)全为0全为0浮点数0.0,或者根据阶码的意义认为是无穷小
正无穷0全为1全为0浮点数正无穷
负无穷1全为1全为0浮点数负无穷
NaN0或1全为1非全0Not a Number,非数字,比如C语言里0.0/0.0

端序

计算机内存可以看作是一维线性空间(虚拟地址),内存地址用一个无符号整数就能表示。把数据存入内存时,会遇到一个问题,究竟是把数据的高位放在内存的高地址,低位放在低地址,还是把数据的高位放在内存的低地址,低位放在高地址。前者叫小端序(Little-Endian),后者叫大端序(Big-Endian)。争论哪种方式更好确实是个无聊的问题(出自《格列佛游记》,小人国为应该从小端打蛋还是从大端打蛋争论不休)。端序指的仅仅是字节间的关系,字节内部的顺序叫字节序,和现在讨论的内容关系不大。

存储基本数据类型(整型、浮点型等)时,因为CPU中有相应计算单元,所以此时端序和CPU架构有关,如x86架构采用小端序。当数据结构较为复杂时,端序规则则不太统一,如网络传输中用大端序,JPEG用大端序,BMP用小端序。

判断端序

我们可以逐比特打印数据来判断端序,并按照人类阅读习惯,数据高位在前,低位在后。C代码如下

void print_little_endian_binary(void* ptr_small, size_t size) {
    // 数据以小端序存储时,打印正确结果
    unsigned char* ptr = (unsigned char*)ptr_small + size - 1;
    for (; size > 0; size -= 1, ptr -= 1) {
        for (int i = 7; i >= 0; i -= 1) {
            putchar(((1 << i) & *ptr) ? "1" : "0");
        }
        putchar(" ");
    }
    putchar("\n");
}

void print_big_endian_binary(void* ptr_small, size_t size) {
    // 数据以大端序存储时,打印正确结果
    unsigned char* ptr = (unsigned char*)ptr_small;
    for (; size > 0; size -= 1, ptr += 1) {
        for (int i = 7; i >= 0; i -= 1) {
            putchar(((1 << i) & *ptr) ? "1" : "0");
        }
        putchar(" ");
    }
    putchar("\n");
}

文章开头数据可以通过在x86计算机上执行以下代码得到

float v = -123.25;
print_little_endian_binary(&v, sizeof(v));

my_float

有意思的是,根据C语言标准,结构体内各字段的存储方式总是先定义的字段在低地址,后定义的字段在高地址,而且结构体还支持位字段,每个基本数据类型字段的端序仍和CPU架构相关。所以我们可以在x86计算机上以如下方式定义一个my_float:

struct my_float {
    unsigned int tail : 23;
    unsigned int expo : 8;
    unsigned int sign : 1;
};

float build_float(unsigned int sign, int exponent, unsigned int tail) {
    unsigned int expo = exponent + 127;
    struct my_float v = {tail, expo, sign};
    return *((float*)&v); // 也可使用共用体
}

所以build_float(1, 6, 0b11101101000000000000000)可得float类型的-123.25。