比特及位级运算
现代计算机存储和处理信息以二进制信号表示,一个二进制数称为位。大多数的计算机使用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码兼容。汉字的编码需要两个字节,我们有时候会遇到这种情况,编辑器中删除一个汉字需要按两次删除,并且第一次删除后会出现一个乱码,这就是因为编码的方式不同导致的。
整数的表示和计算
先说说无符号数的表示,对向量
函数 (Binary to Unsigned的缩写,长度为w)来表示。最小值:(64位的unsigned类型最小值为0x00000000),最大值:(64位的unsigned类型最大值为0xFFFFFFFF)。
对于无符号数的加法和乘法,个人认为只需要记住一点,就是无符号数的位数有限,对于超出的部分,直接丢弃。这个结果也可以用另一种表述方式,即结果,是位数。
对于无符号数的减法,有一个比较有意思的结论,我们知道如果无符号数的最大值,那么会溢出,得到的结果将是0,于是有;于是有,也就是说,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;
}
有符号数的表示,对向量
函数(Binary to Two's-complement的缩写,长度为w)来表示。最小值:(64位的int类型最小值为0x80000000),最大值:(64位的int类型最大值为0x7FFFFFFF)。
从无符号数的减法我们可以看出,在溢出的情况下,每个数都有逆元。现在我们将这些逆元直接就表示为负数,这样会使得正数减少一部分。通过观察可以发现,补码和无符号数,如果表示相同,那么相加之和的表示也是相同的。所以,加法的法则是一样的,这一点书上有论证,这里就说明结论就可以了,乘法也有类似的结论。对于加法和乘法,有符号数如果有溢出,仍然需要截断,也就是。下面是一个具体的例子:
#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浮点标准用的形式来表示一个数:
- 符号(sign)s决定这个数是负数(s=1)还是正数(s=0),而对于数值0的符号位解释作为特殊情况处理。
- 尾数(significand)M是一个二进制小数,它的范围,或者是。
- 阶码(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等于。 - 非规格化的值:exp全为0.
E=1-Bias,M=f,其中Bias等于。 - 特殊值:exp全为1.
当frac=0,s=0时,表示;当frac=0,s=1时,表示。当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的两种表示方式,以及特殊值的表示方式。