2 信息的表示和处理
2.1 信息存储
- 字节(
byte):计算机使用8位的块,作为最小的可寻址内存单位() - 虚拟内存(
virtual memory):程序将内存视为非常大的字节数组 - 地址(
address):内存的每个字节由唯一的数字标识 - 虚拟地址空间(
virtual address space):所有地址的集合
2.1.1 十六进制表示法
为了方便表示一个字节,通常用16进制表示。可以很容易地互相转换二进制和十六进制数(逐位转换):
【例】16进制数字0x173A4C,转为二进制:0001 0111 0011 1010 0100 1100
| 十六进制 | 1 | 7 | 3 | A | 4 | C |
|---|---|---|---|---|---|---|
| 二进制 | 0001 | 0111 | 0011 | 1010 | 0100 | 1100 |
2.1.2 字数据大小
- 字长(
word size):指明虚拟地址空间的范围。如:计算机字长为64位,则表示虚拟地址范围是,也就是程序最多访问个字节 - C语言中,不同数据类型在32位机器与64位机器所占字节数的大小不同
| 有符号类型 | 无符号类型 | 32位字节数 | 64位字节数 |
|---|---|---|---|
| signed char | unsigned char | 1 | 1 |
| short | unsigned short | 2 | 2 |
| int | unsigned int | 4 | 4 |
| long | unsigned long | 4 | 8 |
| int32_t | uint32_t | 4 | 4 |
| int64_t | uint64_t | 8 | 8 |
| char | 4 | 8 | |
| float | 4 | 4 | |
| double | 8 | 8 |
2.1.3 寻址和字节顺序
多字节对象的存储方式分为两种:
- 小端法(little endian):最低有效字节在最前面(大多数Intel的机器只使用小端)
- 大端法(big endian):最高有效字节在最前面(大多数IBM和Sun的机器采用大端)
【例】数字0x01234567,在内存中存放的方式
【例】打印字节序(Linux64位机器)
#include <stdio.h>
typedef unsigned char* byte_pointer;
void show_bytes(byte_pointer start, size_t len)
{
size_t i;
for (i = 0; i < len; i++) {
printf("%.2x ", start[i]);
}
printf("\n");
}
int main(int argc, char* argv[])
{
int num = 0x01234567;
show_bytes((byte_pointer) &num, sizeof(int));
return 0;
}
【输出】小端序
67 45 23 01
2.1.4 表示字符串
C语言字符串:以'0x0'字符结尾的字符串数组
【例】"abcd"在内存中记录为:61 62 63 64 00
2.1.5 表示代码
从机器角度看,程序仅仅是字节序列。不同的操作系统上将程序编译为不兼容的机器代码:
2.1.6 布尔代数、位运算
- 逻辑值:真(TRUE)、假(FALSE)
- 运算:非、与、或、异或
2.1.7 C语言中的位级运算
使用|代表OR;&代表AND;~代表NOT;^代表XOR
【例】两数交换
#include <stdio.h>
void swap(int *x, int *y)
{
*y = *x ^ *y;
*x = *x ^ *y;
*y = *x ^ *y;
}
int main(int argc, char* argv[])
{
int x = 2, y = 3;
swap(&x, &y);
printf("x=%d, y=%d\n", x, y);
return 0;
}
2.1.8 C语言中的逻辑运算
逻辑运算符:或(||)、与(&&)、非(!),结果返回TRUE或FALSE
【注】C语言逻辑运算有短路效应:
/* 这里a=0时,不会执行1/a */
if (a && 1/a) printf("%d\n", a);
2.1.9 C语言中的移位运算
移位运算符:<<和>>。其中左移都是末尾补0。右移注意区分逻辑右移和算数右移:
- 逻辑右移:首位补0
- 算数右移:如果首位是0补0;如果首位是1补1
【注】C语言中,有符号数是算数右移,无符号数是逻辑右移
#include <stdio.h>
int main(int argc, char* argv[])
{
unsigned int x = 2, y = -2;
printf("x=%d, y=%d\n", x>>1, y>>1);
return 0;
}
【输出】
x=1, y=2147483647
【注】Java中使用>>代表算数右移,>>>代表逻辑右移
2.2 整数表示
2.2.1 整型数据类型
- 32位机器上整型的表示:
- 64位机器上整型的表示:
2.2.2 无符号数的编码
无符号数范围:。这里书中解释的严谨但晦涩,用例子说明:
【例】以字长w=4为例,无符号数11,编码为1011:
2.2.3 有符号数的编码
有符号数范围:,几乎所有现代机器使用补码方式编码。
- 补码:编码最高位为1时,表示。比如当字长为4,编码最高位为1,代表-8
【例】以字长w=4为例,有符号数5,编码为0101;有符号数-5,编码为1011
【补充】有符号数的其它表示
- 反码:最高有效位的权是。好处是比较直观:;缺点:数值0有两种表示方法
- ;
- 原码:最高有效位是符号位,剩下的位确认数值大小。优缺点类似反码
- ;
2.2.4 有符号数和无符号数之间的转换
C语言中可以将无符号数和有符号数互相转换,转换规则:位不变,解释方式改变
- 有符号数转无符号数
- 无符号数转有符号数
【例】有符号数-12345转换为无符号数53191
,即:
2.2.5 C语言中的有符号数和无符号数
C语言中,默认使用有符号数。创建无符号数时,需加上后缀'U'
有符号数和无符号数在以下情况发生转换:
- 显示强制转换:
int tx, ty;
unsigned ux, uy;
tx = (int) ux;
uy = (unsigned) ty;
- printf输出,隐式转换
int x = -1;
printf("x = %u\n", x);
- 如果是 有符号数 <运算> 无符号数,则会将有符号数隐式转换为无符号数
- 有符号+无符号无符号
- 关系运算符
【注】由于C存在无符号到有符号的隐式转换,容易出错,需特别小心。有些语言如Java语言没有无符号数,规避了这个问题
2.2.6 扩展数据类型
- 较小的数据类型较大的数据类型
- 无符号数:补零即可(也称零扩展,zero extension)
- 有符号数:如果首位是0补0;如果首位是1补1
【例】有符号数的扩展
2.2.7 截断数据类型
- 较大的数据类型=>较小的数据类型
- 无符号数:截断为k位的数字,相当于取模:
- 有符号数:无符号数截断,然后将最高位转换为符号位:
【例】有符号数的截断
unsigned int x = 53191;
printf("x=%d\n", (short)x);
【输出】-12345
【说明】short范围:-32768~32767,所以必丢精度
2.3 整数的运算
2.3.1 无符号加法
- 原理:正常相加,溢出时取截断,丢弃最高位
【例】
2.3.2 有符号加法
- 原理:类似无符号加法,包含正溢出和负溢出。溢出时取截断
【例】
2.3.3 有符号减法和无符号减法
-
减法:x-y等价于x+y',其中y'是y的加法逆元,即y+y'=0
-
无符号数的加法逆元:
【例】
- 有符号数的加法逆元:
【例】有符号数的加法逆元
【例】有符号数的减法:
2.3.4 无符号乘法
- 有溢出情况,截断处理。原理:
【例】无符号乘法:12345*12345
unsigned short x = 12345, y = 12345;
printf("x=%d\n", (unsigned short)(x*y)); //结果:x=27825
【解释】
2.3.5 有符号乘法(补码乘法)
- 有溢出情况,截断处理。无符号乘法和有符号乘法的乘积的位级表示都相同(证明略)
2.3.6 乘以常数
- 乘以:转化为左移k位。
- 乘以常数:转化为左移和加法。
2.3.7 除以2的幂
- 无符号数除以:使用逻辑右移实现
- 有符号数除以:使用算数右移实现。如果除不尽,向零取整。
- 为了实现向零取整,当x<0时,有符号数需要加上偏量(biasing)
【例】书中例子,。为了保证向零取整,需要加上偏量
- 不加偏量的情况,可以看到,结果不正确。实际我们要的是
- 加上偏量,此时
【注意】上述方法不能推广到除以任意常数
2.4 浮点数
2.4.1 二进制小数
二进制小数:即将二进制整数扩展到小数位:
【例】
2.4.2 IEEE浮点表示
IEEE标准:。类似科学计数法
- 符号s(sign):s=0表示正数;s=1表示负数
- 小数字段M(significand):表示二进制小数的值
- 阶码E(exponent):对浮点数加权,权重是
C语言的浮点数:分单精度浮点(float)和双精度浮点(double):
- 单精度(32位):s、exp、frac分别为1位、8位、23位
- 双精度(64位):s、exp、frac分别为1位、11位、52位
float格式编码类型:根据exp的值,编码类型分为三大类:
- 规格化值(exp位不全为0且不全为1):最普遍的情况
- 阶码:单精度是、双精度是
- 小数字段M:看做是 的数字。由于都是以1开头,不需要显示表示
- 非规格化值(exp位全为0):表示数值0、表示非常接近0.0的数
- 阶码:单精度是、双精度是
- 小数字段M:看做是 的数字
- 数值0:有两种表示方法:+0.0和-0.0
- 特殊值(exp位全为1)
- :s=0、exp全1、frac全0
- :s=1、exp全1、frac全0
- :exp全1、frac不全为0
2.4.3 数字示例
- :假定阶码字段是一个无符号整数所表示的值
- :偏置之后的阶码值
- :阶码的权重
- :小数值
- :尾数的值
- :该数(未规约的)小数值
- :该数规约后的小数值(约分后的值)
- 十进制:该数的十进制表示
【例】整数12345和小数12345.0的关系
-
整数12345具有二进制表示。
-
,因此:
- (丢弃第一个1,后面补0)
- 得到:
-
有意思的是:12345和12345.0两者有部分数字是重叠的:
2.4.4 舍入
IEEE定义了四种舍入方式
向偶数舍入的理解:可防止结果的统计偏差,一半情况向上舍入,一半情况向下舍入
二进制数向偶数舍入:1看做奇数,0看做偶数
2.4.5 浮点运算
由于浮点数有溢出的存在,有些运算带来影响(需特别小心!)
- 加法交换律:具备
- 加法结合律:不具备。例:
- 乘法交换律:具备
- 乘法结合律:不具备。例:
- 乘法分配律:不具备。例:
2.4.6 C语言中的浮点数
GCC在库中定义了一些特殊值:如INFINITY(代表)、NAN(代表)
#ifndef _MATH_H
#define _MATH_H 1
......
#ifdef __USE_ISOC99
/* IEEE positive infinity. */
# if __GNUC_PREREQ (3, 3)
# define INFINITY (__builtin_inff ())
# else
# define INFINITY HUGE_VALF
# endif
/* IEEE Not A Number. */
# if __GNUC_PREREQ (3, 3)
# define NAN (__builtin_nanf (""))
# else
/* This will raise an "invalid" exception outside static initializers,
but is the best that can be done in ISO C while remaining a
constant expression. */
# define NAN (0.0f / 0.0f)
# endif
#endif /* __USE_ISOC99 */
C语言定义了int、float、double三种类型。其强制类型转换规则:
- int转换成float:数字不会溢出,但是可能舍入
- int、float转换成double:保留精确值
- double转换成float:可能溢出、舍入
- float、double转换成int:向零舍入、有可能溢出
#include <stdio.h>
int main(int argc, char *argv[])
{
float num = 1e10;
printf("%d\n", (int)num);
return 0;
}
【输出】-2147483648