程序的结构和执行

在计算机中,信息都是二进制。这一节会尝试解答以下几个问题:

  1. 信息在计算机中是如何表示的?
  2. 计算机又是如何找到和存储这些信息的?
  3. 计算机对数据有哪些运算规则?
  4. 整数和浮点数在计算机中如何表示和运算?

信息的表示

计算机用二进制存储信息,为了兼顾人类能看懂,又方便表示,一般我们用十六进制来表达计算机中的数字。C语言用 0x 开头表示十六进制数字,如 0x8A 表示 10001010,即 138

hex

当一个数字 x 是 2 的非负整数 n 次幂时,即 x = 2^n,我们可以很快算出这个数的十六进制。

观察下面的进制转换:

  • 2^3 二进制为 1000
  • 2^4 二进制为 1 0000
  • 2^8 二进制为 1 0000 0000

不难发现, 2的几次方转化成二进制就是1后面有多少个0

所以我们很快能得出 2^111000 0000 0000, 改写成十六进制就是 0x800


字长

我们通常说的32位机器和64位机器,一般都是指他们的字长(word size),字长通俗地说就是一个字的长度。32位机器的字长为4个字节,64位机器则为8个字节。

字长决定虚拟地址的大小,32位字长的机器虚拟地址空间最大为 4GB ,而64位机器的则为 16EB。

当我们说32位程序还是64位程序时,说的是他们的编译方式,而不是运行的机器类型。64位的机器可以兼容32位程序。

1
linux> gcc -m32 hello.c

内存地址和顺序

内存地址

程序将内存视为一个非常大的字节数组,这个数组称为虚拟内存(virtual memory),虚拟内存中的每个字节,都可以由唯一的数字(数组下标)来标识,称为内存地址。所有可能地址的集合,就称为虚拟地址空间

寻址

当我们在程序中声明一个变量(对象)时,我们会好奇这个变量在内存中的位置(即地址),如果这个变量占用多个字节,它在内存中的排列顺序又是怎么样的?在内存中寻找某个“程序对象”的地址的过程,称为寻址

大端法和小端法

几乎所有的机器,都会把多字节对象存储在连续的地址中,例如一个4字节的int变量,地址为 0x100,那么它四个字节分别被存储在 0x1000x1010x1020x103 位置。

但是有一些机器,从最高有效字节到最低有效字节的顺序排列,称为大端法(big endian),另一些机器又是从最低到最高,称为小端法(little endian)。例如,一个 int 变量 0x01234567, 用大端法表示是 01 23 45 67, 而用小端法表示却是 67 45 23 01

big_little-endian

我们常用的 x86 机器(Intel兼容机),包括 Windows、Linux 和 Mac 操作系统,都是采用了小端法。而有一些 IBM 和 Oracle 机器采用了大端法。用于智能手机的ARM芯片,本身硬件支持大端或小端,但有趣的是,搭载了 Android 或 iOS 操作系统的这些智能手机,只能运行小端模式。

大端和小端并没有谁对谁错,谁好谁坏之分,甚至对于我们开发人员来说,字节顺序对我们是隐藏的。但是了解这些知识,可以避免我们在某些时候出错,例如通过网络发送二进制数据时,大端法的机器传送到小端法的机器,如果没有特殊处理,就会导致接收到的字节顺序反了。

字符串表示

C语言中,字符串的本质是字节数组,且以null(ASCII码为00)结尾。字符串 12345 的字节数组为 31 32 33 34 35 001的ASCII码是49,十六进制0x31)。

字符串的表示跟字节顺序和字大小规则无关。


运算

在C语言中,支持位运算、逻辑运算和移位运算。

布尔代数即数学中与、或、非、异或的概念。如下表:

符号 英文简写 中文含义
& AND
| OR
~ NOT
^ XOR 异或

C语言可以直接用这些符号进行位运算。例如: ~0x41~0100 0001) 的结果是 0xBE1011 1110

有时候我们会希望进行逻辑上的运算,比如某两个条件都成立时,或某两个条件之一成立时,这种场景则需要用逻辑运算。

符号 英文简写 中文含义
&& AND 逻辑与
|| OR 逻辑或
! NOT

逻辑运算只有两种结果, TRUEFALSE, 用 1 和 0 来表示。

例如,!0x41 的结果为 0x00,即 FALSE

移位运算,即把二进制数整体往左或往右移动。用 <<>> 符号来表示。需要注意的是,右移分为逻辑右移和算术右移,逻辑右移无脑填充0,算术右移会根据最高位符号位,来决定是填充0还是填充1。


整数

在C语言中,有无符号数和有符号数之分。无符号数只能表示非负数,用关键字 unsigned 声明。其内部是用无符号编码的,8位的数字取值范围为 0000 0000 ~ 1111 1111

但默认情况下,C语言的数字可以表示负数,这种称为有符号数。其内部用补码编码。二进制的第一位作为符号位,为0时表示非负数,为1时表示负数。

两个正数相加可能会得到一个负数,这是因为计算机的字长是有限的,当相加后的值超出所能表达的最大值,会出现溢出。


浮点数

在我们熟悉的十进制中,我们知道,一个数字乘以10,小数点就向右移动一位,除以10,小数点就向左移动一位。 例如 123.456,乘10为 1234.56,除以10为 12.3456

在二进制中也是类似,乘以2,向右移动一位,除以2,向左移动一位。例如 1101.1010,乘2为 11011.010,除以2为 110.11010。反过来说,移多少位,就是2的多少次方。

在计算机中,处理小数有两种表示方法 —— 定点浮点,定点就是小数点永远在固定的位置上,提前对齐。优点是简单,缺点是表示范围小,不能充分运用二进制的存储单元。而浮点相当于一个定点数加上一个阶码,阶码表示将这个定点数的小数点移动若干位,由于可以用阶码移动小数点,因此称为浮点数。

计算机中用三个部分组合的二进制位来表示浮点数。分别为:

  1. 数符:正数为0,负数为1
  2. 阶码:阶码的计算公式:阶数(左移多少位) + 偏移量
  3. 尾数:小数点后面的数

例如一个数字 178.125,换成二进制是 10110010.001,这个二进制数用浮点数怎么表示呢?

  1. 首先这个数字是正数,数符肯定为0
  2. 之后把小数点移动到整数位只有1(1.0110010001),发现需要左移7位,得到阶数为 111(3位的二进制数), 偏移量2^(e-1)-1 = 127(e是阶数的位数),即 01111111,计算阶码为111 + 01111111 = 10000110
  3. 尾数就是 1.0110010001 小数点后面的数 0110010001

最终得到 0 10000110 0110010001 0000000000000 (32位,后面补0)