计算机处理、存储的信息都是以二值符号表示的。这些二值数字,也就是位(bit),当独取一个出来,可能就没有什么意义,但是把位组合到一起,加上某种解释,就能够表示我们想要表示的信息了。这里的按位组合,某种解释,其实就是编码方式。我们先来看三种最重要的数字编码:
无符号(unsigned)编码,传统二进制表示法,表示大于或等于零的数字
二进制补码(two’s-complement)编码,表示有符号整数最常见的方式。
浮点数(floating-point)编码,表示实数的科学计数法以2为基数的版本。
计算机用有限的位来对一个数字编码,当运算的结果超出表示的范围时,运算就会导致溢出。
1.1.1.1. 基本概念:
字节:计算机中8位的块,最小的可寻址的存储器单位。
虚拟存储器:机器级程序把存储器视为一个非常大的字节数组。
地址:存储器的每个字节都有一个唯一的数字来标识。
虚拟地址空间:所有可能地址的集合。
字:每台计算机都有一个字长(word size),指明整数和指针数据的标称大小(norminal size)。一个字长为n位的机器,虚拟地址的范围为0~2n-1,程序最多能访问2n字节。
C中的指针,不论其指向什么,都是某个存储块的第一个字节的虚拟地址。C编译器把每个指针和类型信息联系起来,以根据指针类型,生成不同的机器级代码来访问存储在指针所指向位置的值。C编译器维护着这个类型信息,但它生成的机器级程序并没有关于数据类型的信息,它简单地把每个程序对象视为一个字节块,而将程序本身看做一个字节序列。
C语言中数字数据类型的大小(单位:字节)
C声明 |
典型32位机器 |
Compad Alpha机器 |
Char Short int Int Long int |
1 2 4 4 |
1 2 4 8 |
Char * |
4 |
8 |
Float Double |
4 8 |
4 8 |
1.1.1.2. 十六进制
一个字节的值域,用二进制表示为000000002~111111112。十六进制表示为0016~FF16。在C中,以0x或0X开头的数字被认为是16进制的值。
十进制数字x转十六进制:【x=q0*16 + r0】->【q0=q1*16 + r1】->……->【rn = 0*16 + r n】,则结果为【rn rn-1… r2 r1】,当然ri要写成相应的十六进制数字。
1.1.1.3. 寻址和字节顺序
对跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么?我们在存储器中如何对这些字节排序?
答:1.多字节对象被存储为连续的字节序列,对象的地址为所使用字节序列中最小的地址。
2.对表示一个对象的字节序列排序有两种规则,大端法和小端法。大端法:最高有效字节在最前面;小端法:最低有效字在最前面。如有一个十六进制数字0x01234567,其表示如下表所示:
大端法 |
|
0x100 |
0x101 |
0x102 |
0x103 |
|
… |
01 |
23 |
45 |
67 |
… |
|
小端法 |
… |
0x100 |
0x101 |
0x102 |
0x103 |
|
… |
67 |
45 |
23 |
01 |
… |
关于字节序在网络通信时就变得很重要。由于不同的计算机可能出现大端法和小端法的区别,所以在发送某些数据之前需要将数据从主机字节序转换为网络字节序,而在收到数据后,又需要将数据从网络字节序转换为主机字节序进行处理。
打印程序对象的字节表示:
#define SHOW_BYTE_H
#include < stdio.h >
typedef unsigned char * byte_pointer;
class CShowBytes
{
public:
void show_bytes(byte_pointer start, int len)
{
int i;
for(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 *));
}
void show_string(char start[], int len)
{
int i;
for(i=0; i<len; i++)
{
printf(" %.2x", start[i]);
}
printf("\n");
}
} ;
#endif
1.1.1.4. 字符串
C字符串为以NULL字符结尾的字符数组。为什么前面我提到在网络通信时,是需要对“某些”数据进行主机字节序和网络字节序之间的转换,而不是所有?是因为字符串具有平台独立性。为什么呢?
因为每个字符都由标准编码来表示,常用ASCII编码,如使用ASCII码的字符码在任何系统伤将得到同样的结果,与字节顺序和字大小无关。下图对比了数字和字符串在内存中的存储形式,由于一个字符恰好对应一个字节,所以与字节顺序没有关系,而数字的话涉及到位数先后顺序的问题就不同了。
1.1.1.5. 布尔代数和环,C的位、逻辑运算
二进制是计算机编码、存储和操作信息的核心。而围绕0和1的布尔运算和环结构就变得异常重要。布尔代数<{0, 1}, |, &, ~, 0, 1>和基本的算术运算有很多相似的特性,如交换性、结合性、同一性等等。红绿蓝三种基本色在{0,1}中取值,并进行不同的布尔运算,就产生出了丰富多彩的颜色。
C的位运算和逻辑运算就充分运用到了布尔代数的知识。记得在网上曾看到一道笔试题,就是交换两个变量x和y的值,但不要引入第三方的变量,如果布尔代数运用得好的话,可以很容易给出下面的解决方案。
{
*x = *x ^ *y;
*y = *x ^ *y;
*x = *x ^ *y;
}
其实这儿就运用到了 a ^ a = 0 和 a ^ 0 = a 的性质。
逻辑运算与位运算不同的是,如果第一个参数能确定表达式的结果,就不会对后续参数求值进行运算。如p&&*p++就不会间接引用空指针。具体原因请参看后面C的汇编表示中关于逻辑运算的汇编表示部分。
C的移位运算,左移比较简单,直接在右边的空位补0。而右移就不同了,分为逻辑右移和算术右移两种,逻辑右移也是直接在左边的空位补0,而算术右移则在左边的空位补上最高有效位的拷贝。几乎所有的编译器/机器组合都对有符号数使用算术右移。移位运算同样很重要,乘法在Cpu上的执行要比加减法多很多指令,为了提高效率,通常就可以用移位运算对乘法运算进行优化。