信息的表示和处理

比特及位级运算

现代计算机存储和处理信息以二进制信号表示,一个二进制数称为位。大多数的计算机使用8位,或者字节,作为最小可寻址单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能地址集合就称为虚拟地址空间。

因为计算机中只有0和1,对这些0和1的编码和解码的方式就显得尤为重要。这里要介绍3种重要的编码,字符的编码,整数的编码和浮点数的编码。在这之前,先看看MP2的头部编码方式。在MP2的标准中,有关于头部的定义:

header() {  
    syncword            12      bits    bslbf  
    ID                  1       bit     bslbf  
    layer               2       bits    bslbf  
    protection_bit      1       bit     bslbf  
    bitrate_index       4       bits    bslbf  
    sampling_frequency  2       bits    bslbf
    padding_bit         1       bit     bslbf  
    private_bit         1       bit     bslbf  
    mode                2       bits    bslbf  
    mode_extension      2       bits    bslbf  
    copyright           1       bit     bslbf  
    original/home       1       bit     bslbf  
    emphasis            2       bits    bslbf 
} 

可以看出,这是以比特位单位的,而不是以字节为单位的。如果要取出对应的数据,我们就需要位操作。C语言提供了以下的位操作:

符号 名称 运算规则
&(按位) 两个都为1,则为1,否则为0
| (按位) 两个都为0,则为0,否则为1
~(按位) 0变为1,1变为0
^(按位) 异或 两个相异为1,相同为0
&& 两个数都非0,则返回1,否则返回0
| | 两个数都为0,则返回0,否则返回1
! 如果非0,返回0,否则返回1
<< 左移 将数据的n比特左移,在右边补0
>> 右移 逻辑右移:将数据向右移动n比特,左边补0
算数右移:将数据向右移动n比特,左边补最高位

值得注意的是,移位的位数不能大于等于数据本身的位数,因为没有定义这种运算,取决于机器的实现。

通过位运算。我们可以取出对应的分量,但我们还需要每一个分量的解释,下面给出MP2(MP3的头部和MP2的格式是一样的,但是现在的MP3里面不仅有音频数据,还有艺术家名,封面等信息,解析要复杂一些,这里就以MP2为例)标准中关于sampling_frequency(采样率)的解释:

'00' 44.1 kHz  
'01' 48 kHz  
'10' 32 kHz  
'11' reserved 

下面的程序演示了如何从一个MP2文件中解析出一些简单的头部信息:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    unsigned char head[4];
    FILE *fp = fopen("test.mp2","rb");
    /* ignore the synchronization process */
    fread(head,1,4,fp);
    fclose(fp);
    int sampling_frequency = (head[2] >> 2) & 0x03;
    switch (sampling_frequency)
    {
        case 0:
            printf("sampling frequency = 44.1Khz\n");
            break;
        case 1:
            printf("sampling frequency = 48Khz\n");
            break;
        case 2:
            printf("sampling frequency = 32Khz\n");
            break;
        case 3:
            printf("reserved\n");
            break;
        default:
            break;
    }
    return 0;
}

对于其他数据的解析,也是类似的,就是先应用比特运算,取出对应的成员,然后根据协议,去解释各个成员。

字符的表示

对字符的表示,采取了一一映射的方式。通过查看ASCII码表我们可以知道,每一个字符都被映射到了一个整数。这里值得注意的是,对于字符串的表示,需要有一个特别的字符(‘\0’),来表示字符串的结束,不然无法确定字符串的结束。其他的编码方案,采取的策略也是差不多的,值得注意的是有的编码不是定长的,有的字符是1个字节,有的是2个字节,通常是为了和ASCII码兼容。汉字的编码需要两个字节,我们有时候会遇到这种情况,编辑器中删除一个汉字需要按两次删除,并且第一次删除后会出现一个乱码,这就是因为编码的方式不同导致的。

整数的表示和计算

先说说无符号数的表示,对向量\vec{x} =[x_{w-1} , x_{w-2} , ... , x_0]:
B2U_w(\vec{x}) = \sum^{w-1}_{i=0}x_i2^i
函数B2U_w (Binary to Unsigned的缩写,长度为w)来表示。最小值:0(64位的unsigned类型最小值为0x00000000),最大值:2^{w−1}(64位的unsigned类型最大值为0xFFFFFFFF)。

对于无符号数的加法和乘法,个人认为只需要记住一点,就是无符号数的位数有限,对于超出的部分,直接丢弃。这个结果也可以用另一种表述方式,即结果mod 2^ww是位数。

对于无符号数的减法,有一个比较有意思的结论,我们知道如果无符号数的最大值2^{w−1} + 1,那么会溢出,得到的结果将是0,于是有2^{w−1} + 1 = 0;于是有1 = -2^{w−1},也就是说,1是2^{w−1}的逆元。这样,无符号数的减法可以用加上对应的逆元实现。下面上一个具体的例子:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    printf("0xFFFFFFFF + 0x1 = %u.\n",0xFFFFFFFFu + 0x1u);
    int i;
    for(i=0;i<10;i++){
        unsigned int n1 = rand();
        unsigned int n2 = rand();
        unsigned int neg = ~n2 + 1; /* n2 + ~n2 = 0xFFFFFFFF, so n2 + ~n2 + 1 = 0 */
        printf("%u %u\n",n1 - n2, n1 + neg);  /* equal */
    }
    return 0;
}

有符号数的表示,对向量\vec{x}=[x_{w−1} , x_{w−2} , ... , x_0 ]:
B2T_w (\vec{x})=−x_{w−1} 2^{w−1}+\sum^{w−2}_{t=0}x_i 2^i
函数B2T_w(Binary to Two's-complement的缩写,长度为w)来表示。最小值:-2^{w-1}(64位的int类型最小值为0x80000000),最大值:2^{w−1}−1(64位的int类型最大值为0x7FFFFFFF)。

从无符号数的减法我们可以看出,在溢出的情况下,每个数都有逆元。现在我们将这些逆元直接就表示为负数,这样会使得正数减少一部分。通过观察可以发现,补码和无符号数,如果表示相同,那么相加之和的表示也是相同的。所以,加法的法则是一样的,这一点书上有论证,这里就说明结论就可以了,乘法也有类似的结论。对于加法和乘法,有符号数如果有溢出,仍然需要截断,也就是mod2^w。下面是一个具体的例子:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    int x = 0x7FFFFFFF * 2;
    unsigned int y = 0x7FFFFFFFu * 2u; 
    printf("0x7FFFFFFF * 2 = %d, 0x7FFFFFFFu * 2u = %x.\n",x,y); /* 0xFFFFFFFE = -2 */
    printf("0x7FFFFFFF + 0x1 = %d.\n",0x7FFFFFFF + 0x1); /* positive overflow */
    printf("0x80000000 + 0x80000000 = %d.\n",0x80000000 + 0x80000000);/* negtive overflow */
    return 0;
}

无符号整数和有符号整数的转化

对于大多数C语言的实现,处理同样的字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,但是位模式不变。但是还有一点是重要的,就是当执行一个运算的时候,如果一个数是有符号的,而另一个数是无符号的,C语言会隐式地将有符号转化为无符号,这会导致一些细微的错误。

#include <stdio.h>
#include <string.h>

int strlonger(char *s, char *t){
    return strlen(s) - strlen(t) > 0; /*  strlen()return unsigned, if t is longer than s, this will return true*/
}

int main(int argc, char const *argv[])
{
    if(-1 < 0U){
        printf("-1 < 0U\n");
    }else{
        printf("-1 >= 0U\n"); /* this will execute */
    }
    return 0;
}

寻址和字节顺序

在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。这里,就有两种方式进行排序了,可以把最低有效字节放在低位,称为小端法;也可以把最高的有效字节放在前面,称为大端法。其实这两种差异可以看成是数据协议的不同,字节序的转化主要用在了网络编程。因为网络是大端序,而主机可能是大端,也有可能是小端。如果不统一字节序,那么路由器这些网络设备就不能正确的读取数据(如目的IP地址,端口号等)。下面这个程序显示了网络字节序和主机字节序。

#include<stdio.h>
#include <arpa/inet.h>

int main(){
    unsigned int x = 0x12345678;
    unsigned char *p =(unsigned char *)&x;
    printf("%x %x %x %x\n",p[0],p[1],p[2],p[3]);
    unsigned int y = htonl(x);
    p = (unsigned char *)&y;
    printf("%x %x %x %x\n",p[0],p[1],p[2],p[3]);
    return 0;
}

在x86平台下运行结果如下:

78 56 34 12
12 34 56 78

IEEE浮点表示

IEEE浮点标准用V=(−1)^s \times M \times 2^E的形式来表示一个数:

  • 符号(sign)s决定这个数是负数(s=1)还是正数(s=0),而对于数值0的符号位解释作为特殊情况处理。
  • 尾数(significand)M是一个二进制小数,它的范围1 -2,或者是0-1
  • 阶码(exponent)E的作用对浮点数加权,这个权重是2的E次幂。
  • 一个单独的符号s直接编码s,k位的阶码字段exp编码阶码E,n位小数字段frac编码尾数M,顺序是s,exp,frac。
  • 64位机器上,单精度s,exp,frac字段分别为1位,8位,和23位;双精度s,exp,frac字段分别为1位,11位,52位。

对于一个具体的浮点数,要得到它的具体值,还要对M和E的编码进行解析,有以下的三种情况:

  • 规格化的值:exp不全为0,也不全为1。
    E的阶码的值E=exp-Bias,M=1+f,其中Bias等于2^{k-1}−1
  • 非规格化的值:exp全为0.
    E=1-Bias,M=f,其中Bias等于2^{k-1}−1
  • 特殊值:exp全为1.
    当frac=0,s=0时,表示+\infty;当frac=0,s=1时,表示-\infty。当frac非0时,表示不是一个数。

其实从这里可以看出,这个格式的解析仍然是取出对应的成分,然后进行解释。下面是一个例子:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    unsigned int x;
    unsigned int y;
    unsigned int z;   

    float f1 = 123.456f, f2 =  123.456f;
    unsigned int *neg_f = (unsigned int *)&f2;
    *neg_f = *neg_f ^ 0x80000000;
    printf("f = %f,neg_f = %f.\n",f1,*((float *)neg_f));

    x = 0x00000000;
    y = 0x80000000;
    float *positive_zero = &x;
    float *negtive_zero = &y;
    printf("positive zero = %f, negtive zero = %f.\n",positive_zero,negtive_zero);

    x = 0x7F800000;
    y = 0xFF800000;
    z = 0x7F800001;
    float *positive_infinity = (float *)&x;
    float *negtive_infinity = (float *)&y;
    float *nan = (float *)&z;
    printf("positive infinity = %f,negtive infinity  = %f, nan = %f.\n",*positive_infinity,*negtive_infinity,*nan);
    return 0;
}

输出为:

f = 123.456001,neg_f = -123.456001.
positive zero = 0.000000, negtive zero = 0.000000.
positive infinity = 1.#INF00,negtive infinity  = -1.#INF00, nan = 1.#QNAN0.

这个程序展示通过符号位来对一个小数取反,0的两种表示方式,以及特殊值的表示方式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容