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
总结
计算机执行的“整数”运算实际上是一种模运算形式。表示数字的有限字长限制了可能的值的取值范围,结果运算可能溢出。补码表示提供了一种既能表示负数又能表示正数的灵活方法,同时使用了与执行无符号算术相同的位级实现,这些运算包括加法、减法、乘法、甚至除法,无论运算数是以无符号形式还是以补码形式表示,都有完全一样或者非常类似的位级行为。