1、关于进制
十进制 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
八进制 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
十六进制 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
二进制 |
0000 |
0001 |
0010 |
0011 |
0100 |
0101 |
0110 |
0111 |
十进制 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
八进制 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
十六进制 |
8 |
9 |
A |
B |
C |
D |
E |
F |
二进制 |
1000 |
1001 |
1010 |
1011 |
1100 |
1101 |
1110 |
1111 |
2、字
每台计算机都有一个字长(word size),指明整数和指针数据的标称大小(nominal size)。因为虚拟地址是以这样的一个字来编码的,所以字长决定了系统中虚拟地址空间的大小。
3、数据大小
C语言中不同的数据类型分配的字节数依赖于机器和编译器。下表是32位和64位机器的典型值。
C声明 |
32位机器 |
64位机器 |
char |
1 |
1 |
short int |
2 |
2 |
int |
4 |
4 |
long int |
4 |
8 |
long long int |
8 |
8 |
char * |
4 |
8 |
float |
4 |
4 |
double |
8 |
8 |
long double |
|
|
4、寻址和字节顺序
对于跨越多字节的程序对象,需建立两规则:这个对象的地址是什么,如何在存储器中排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。排列表示一个对象的字节有两个通用的规则:最低有效字节在最前面的方式,称为小端法(little endian);最高有效字节在最前面的方式,称为大端法(big endian);
假设变量x类型为int,位于地址0x100处,它的十六进制表示为0x01234567。地址范围为0x100~0x103的字节,其排列顺序依赖于机器的类型。
小端法 |
| |||||
地址 |
... |
0x100 |
0x101 |
0x102 |
0x103 |
... |
数据 |
... |
67 |
45 |
23 |
01 |
... |
大端法 |
| |||||
地址 |
... |
0x100 |
0x101 |
0x102 |
0x103 |
... |
数据 |
... |
01 |
23 |
45 |
67 |
... |
typedef unsigned char *byte_pointer;
//=========================================
// 显示不同类型对象的字节表示
//=========================================
void show_bytes(byte_pointer start, int len)
{
for (int 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 *));
}
对指针的类型强制转换不会改变真实的指针,只是告诉编译器以新的数据类型来看待被指向的数据。
5、布尔代数
~ |
|
|
& |
0 |
1 |
|
| |
0 |
1 |
|
^ |
0 |
1 |
0 |
1 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
0 |
1 | |||
1 |
0 |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
布尔环(Boolen ring)
a ^ a = 0 ===> (a ^ b) ^ a = b在许多应用中可以使用该属性。
void inplace_swap(int *x, int *y)
{
*y = *x ^ *y;
*x = *x ^ *y;
*y = *x ^ *y;
}
6、整形的表示
6.1、无符号数的编码
假如一个整数类型的数据有w为,我们可以将位向量写成,表示整个向量,或者写成
,表示向量中的每一位。把
看做一个二进制表示的数,就获得了的无符号表示。用函数B2Uw(Binary to Unsigned)来表示:
无符号数的二进制表示有一个很重要的属性,就是每个介于0~2w-1之间的数都有唯一一个w位的值编码。函数B2Uw 是一个双射(bijection)。
6.2、补码编码
最常见的有符号数的计算机表示方式就是补码(two’s-complement)形式在这个定义中,将字的最高有效位解释为负权(negative weight)。用函数B2Tw(Binary to Two’s-complement)来表示:
最高有效位xw-1也称为符号位,它的权重为-2w-1。符号位设置为1时,表示值为负,而当设置为0时,值为非负。函数B2Tw是一个双射,每个介于-2w-1~2w-1-1之间的数都有唯一一个w位的值编码。补码的范围是不对称的:。
6.3、有符号数的其他表示方法
反码(Ones’ Complement):除了最高有效位的权是-(2w-1-1)而不是-2w-1,其它和补码一样的:
源码(Sign-Magnitude):最高有效位是符号位,用来确定剩下的位应该取负权还是正权:
这两种表示方法都有一个奇怪的属性,即对于数字0都有两种不同的编码方式。
6.4、有符号数和无符号数之间的转换
对于多数C语言的实现而言,处理同样字长的有符号数和无符号数之间的相互转换的一般规则是:数值可能会变,但是位模式不变。
有符号转换成无符号:
无符号转换成有符号:
6.5、扩展一个数字的位表示
无符号的数采用零扩展(zero extension),补码的数采用符号扩展(sign extension)。
补码(有符号的数)满足下列等式:
6.6、截断数字
无符号数的截断结果是:
补码数字的截断结果是:
7、整形运算
7.1、无符号的加法
无符号的运算可以被视为一种模运算形式。
定义参数x与y的运算如下:
判断无符号数x与y相加是否溢出的C函数:
int uadd_ok(unsigned int x, unsigned int y)
{
unsigned int sum = x + y;
return sum > x;
}
7.2、补码的加法
定义参数x与y的运算如下:
两个数的w位补码之和与无符号之和有完全相同的位级表示。
判断补码数x与y相加是否溢出的C函数:
int tadd_ok(int x, int y)
{
int sum = x + y;
int neg_over = x < 0 && y < 0 && sum >= 0;
int pos_over = x > 0 && y > 0 && sum > 0;
return !neg_over && !pos_over;
}
7.3、无符号的乘法
W位无符号乘法运算的结果为:
7.4、补码的乘法
w位补码乘法运算的结果为:
乘法运算的位级表示都是一样的。也就是,给定长度为w的位向量和
,无符号乘积的位级表示
补码乘积的位级表示
是相同的。
7.5、乘以常数与除以2的幂
由于整数乘法比移位和加法的代价要大得多,许多C编译器试图以移位、加法和减法的组合来消除很多整数乘以常数的情况。
整数的除法,当需要舍入时,采用向零取整的方法,而算法右移的结果,相当于向下取整。当和
,整数除法的结果是
(向上取整),与右移的
(向下取整)不同,因此在移位之前可以利用下面属性对x进行“偏置”(biasing)。
对于整数和任意
,有
对于使用算术右移的补码机器,C表达式:
(x < 0 ? (x + (1<<k)-1) : x) >> k
总结
计算机执行的“整数”运算实际上是一种模运算形式。表示数字的有限字长限制了可能的值的取值范围,结果运算可能溢出。补码表示提供了一种既能表示负数又能表示正数的灵活方法,同时使用了与执行无符号算术相同的位级实现,这些运算包括加法、减法、乘法、甚至除法,无论运算数是以无符号形式还是以补码形式表示,都有完全一样或者非常类似的位级行为。