二进制数、位和字节
通常都是基于数字10来书写数字。 例如2157的千位是2, 百位是1, 十位是5, 个位是7, 可以写成:
2×1000 + 1×100 + 5×10 + 7×1
1000是10的立方(即3次幂) , 100是10的平方(即2次幂) , 10是10的1次幂, 而且10(以及任意正数) 的0次幂是1。 因此, 2157也可以写成:
2×103+ 1×102+ 5×101+ 7×100
因为这种书写数字的方法是基于10的幂, 所以称以10为基底书写2157。
计算机适用基底为2的数制系统。 它用2的幂而不是10的幂。 以2为基底表示的数字被称为二进制数(binary number) 。 二进制中的2和十进制中的10作用相同。 例如, 二进制数1101可表示为:
1×23+ 1×22+ 0×21+ 1×20
以十进制数表示为:
1×8 + 1×4 + 0×2 + 1×1 = 13
用二进制系统可以把任意整数(如果有足够的位) 表示为0和1的组合。由于数字计算机通过关闭和打开状态的组合来表示信息, 这两种状态分别用0和1来表示, 所以使用这套数制系统非常方便。
二进制整数
1字节包含8位。 C语言用字节(byte) 表示储存系统字符集所需的大小, 所以C字节可能是8位、 9位、 16位或其他值。

128是2的7次幂, 以此类推。 该字节能表示的最大数字是把所有位都设置为1: 11111111。 这个二进制数的值是:
128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255
该字节最小的二进制数是00000000, 其值为0。 因此, 1字节可储存0~255范围内的数字, 总共256个值。 或者, 通过不同的方式解释位组合(bit pattern) , 程序可以用1字节储存-128~+127范围内的整数, 总共还是256个值。
有符号整数
表示有符号数最简单的方式是用1位(如, 高阶位) 储存符号, 只剩下7位表示数字本身(假设储存在1字节中) 。 用这种符号量(sign-magnitude) 表示法, 10000001表示−1, 00000001表示1。 因此, 其表示范围是−127~+127。
这种方法的缺点是有两个0: +0和-0。 这很容易混淆, 而且用两个位组合来表示一个值也有些浪费。
二进制补码(two’s-complement) 方法避免了这个问题二进制补码用1字节中的后7位表示0~127, 高阶位设置为0。 目前, 这种方法和符号量的方法相同。 另外, 如果高阶位是1, 表示的值为负。 这两种方法的区别在于如何确定负值。 从一个9位组合100000000(256的二进制形式) 减去一个负数的位组合, 结果是该负值的量。
要得到一个二进制补码数的相反数, 最简单的方法是反转每一位(即0变为1, 1变为0) , 然后加1。 因为1是00000001, 那么−1则是11111110+1,或11111111。
二进制反码(one’s-complement) 方法通过反转位组合中的每一位形成一个负数。
二进制浮点数
浮点数分两部分储存: 二进制小数和二进制指数。
二进制小数
1136一个普通的浮点数0.527, 表示如下:
5/10 + 2/100 + 7/1000
从左往右, 各分母都是10的递增次幂。 在二进制小数中, 使用2的幂作为分母, 所以二进制小数.101表示为:
1/2 + 0/4 + 1/8
用十进制表示法为:
0.50 + 0.00 + 0.125
即是0.625。
许多分数(如, 1/3) 不能用十进制表示法精确地表示。 与此类似, 许多分数也不能用二进制表示法准确地表示。 实际上, 二进制表示法只能精确地表示多个1/2的幂的和。 因此, 3/4和7/8可以精确地表示为二进制小数, 但是1/3和2/5却不能。
浮点数表示法
为了在计算机中表示一个浮点数, 要留出若干位(因系统而异) 储存二进制分数, 其他位储存指数。 一般而言, 数字的实际值是由二进制小数乘以2的指定次幂组成。
其他进制数
计算机界通常使用八进制记数系统和十六进制记数系统。 因为8和16都是2的幂, 这些系统比十进制系统更接近计算机的二进制系统。
八进制
八进制(octal) 是指八进制记数系统。 该系统基于8的幂, 用0~7表示数字(正如十进制用0~9表示数字一样) 。
每个八进制位对应3个二进制位。 表15.1列出了这种对应关系。 这种关系使得八进制与二进制之间的转换很容易。 例如, 八进制数0377的二进制形式是11111111。 即, 用111代替0377中的最后一个7, 再用111代替倒数第2个7, 最后用011代替3, 并舍去第1位的0。 这表明比0377大的八进制要用多个字节表示。 这是八进制唯一不方便的地方: 一个3位的八进制数可能要用9位二进制数来表示。 注意, 将八进制数转换为二进制形式时, 不能去掉中间的0。 例如, 八进制数0173的二进制形式是01111011, 不是0111111。

十六进制
十六进制(hexadecimal或hex) 是指十六进制记数系统。 该系统基于16的幂, 用0~15表示数字。 但是, 由于没有单独的数(digit, 即0~9这样单独一位的数) 表示10~15, 所以用字母A~F来表示。
十六进制数A3F(在C中写作0xA3F) 表示为:
10×162+3×161+ 15×160= 2623(十进制)
由于A表示10, F表示15。 在C语言中, A~F既可用小写也可用大写。因此, 2623也可写作0xa3f。
每个十六进制位都对应一个4位的二进制数(即4个二进制位) , 那么两个十六进制位恰好对应一个8位字节。 第1个十六进制表示前4位, 第2个十六进制位表示后4位。 因此, 十六进制很适合表示字节值。
十六进制值0xC2可转换为11000010。 相反, 二进制值11010101可以看作是1101 0101, 可转换为0xD5。

C有两个操控位的工具。 第 1 个工具是一套(6 个) 作用于位的按位运算符。 第 2 个工具是字段(field) 数据形式, 用于访问 int中的位。
C按位运算符
C 提供按位逻辑运算符和移位运算符。
按位逻辑运算符
4个按位逻辑运算符都用于整型数据, 包括char。 之所以叫作按位(bitwise) 运算, 是因为这些操作都是针对每一个位进行, 不影响它左右两边的位。 不要把这些运算符与常规的逻辑运算符(&&、 ||和! ) 混淆, 常规的逻辑运算符操作的是整个值。
二进制反码或按位取反: ~
一元运算符~把1变为0, 把0变为1。 如下例子所示:
~(10011010) // 表达式
(01100101) // 结果值
假设val的类型是unsigned char, 已被赋值为2。 在二进制中, 00000010表示2。 那么, ~val的值是11111101, 即253。 注意, 该运算符不会改变val的值, 就像3 * val不会改变val的值一样, val仍然是2。 但是, 该运算符确实创建了一个可以使用或赋值的新值:
newval = ~val;
printf("%d", ~val);
把val的值改为~val:val = ~val;、
按位与: &
二元运算符&通过逐位比较两个运算对象, 生成一个值。 对于每个位, 只有两个运算对象中相应的位都为1时, 结果才为1(从真/假方面看,只有当两个位都为真时, 结果才为真) 。
C有一个按位与和赋值结合的运算符: &=。
按位或: |
二元运算符|, 通过逐位比较两个运算对象, 生成一个新值。 对于每个位, 如果两个运算对象中相应的位为1, 结果就为1(从真/假方面看, 如果两个运算对象中相应的一个位为真或两个位都为真, 那么结果为真)。
C有一个按位或和赋值结合的运算符: |=。
按位异或: ^
二元运算符^逐位比较两个运算对象。 对于每个位, 如果两个运算对象中相应的位一个为1(但不是两个为1) , 结果为1(从真/假方面看, 如果两个运算对象中相应的一个位为真且不是两个为同为1, 那么结果为真) 。
C有一个按位异或和赋值结合的运算符: ^=。
用法:掩码
按位与运算符常用于掩码(mask) 。 所谓掩码指的是一些设置为开(1) 或关(0) 的位组合。
flags = flags & MASK;
把flags中除1号位以外的所有位都设置为0, 因为使用按位与运算符(&) 任何位与0组合都得0。 1号位的值不变(如果1号位是1, 那么 1&1得1; 如果 1号位是0, 那么 0&1也得0) 。 这个过程叫作“使用掩码”, 因为掩码中的0隐藏了flags中相应的位。
把掩码中的0看作不透明, 1看作透明。 表达式flags &MASK相当于用掩码覆盖在flags的位组合上, 只有MASK为1的位才可见。


oxff的二进制形式是11111111, 八进制形式是0377。 这个掩码保持ch中最后8位不变, 其他位都设置为0。 无论ch原来是8位、 16位或是其他更多位, 最终的值都被修改为1个8位字节。
用法:打开位(设置位)
需要打开一个值中的特定位, 同时保持其他位不变。
以上一节的flags和MASK(只有1号位为1) 为例。 下面的语句:
flags = flags | MASK;
把flags的1号位设置为1, 且其他位不变。 因为使用|运算符, 任何位与0组合, 结果都为本身; 任何位与1组合, 结果都为1。
假设flags是00001111, MASK是10110110。 下面的表达式:
flags | MASK
即是:
(00001111) | (10110110) // 表达式
其结果为:
(10111111) // 结果值
MASK中为1的位, flags与其对应的位也为1。 MASK中为0的位, flags与其对应的位不变。
用|=运算符可以简化上面的代码, 如下所示:
flags |= MASK;
同样, 这种方法根据MASK中为1的位, 把flags中对应的位设置为1, 其他位不变。
用法:关闭位(清空位)
和打开特定的位类似, 有时也需要在不影响其他位的情况下关闭指定的位。 假设要关闭变量flags中的1号位。 同样, MASK只有1号位为1(即, 打开) 。
flags = flags & ~MASK;
由于MASK除1号位为1以外, 其他位全为0, 所以~MASK除1号位为0以外, 其他位全为1。 使用&, 任何位与1组合都得本身, 所以这条语句保持1号位不变, 改变其他各位。 另外, 使用&, 任何位与0组合都的0。 所以无论1号位的初始值是什么, 都将其设置为0。
用法:切换位
切换位指的是打开已关闭的位, 或关闭已打开的位。 可以使用按位异或运算符(^) 切换位。 也就是说, 假设b是一个位(1或0) , 如果b为1, 则1^b为0; 如果b为0, 则1^b为1。 另外, 无论b为1还是0, 0^b均为b。
要切换flags中的1号位, 可以使用两种方法:
flags = flags ^ MASK;
flags ^= MASK;
例如, 假设flags是00001111, MASK是10110110。 表达式:
flags ^ MASK
即是:
(00001111) ^ (10110110) // 表达式
其结果为:
(10111001) // 结果值
flags中与MASK为1的位相对应的位都被切换了, MASK为0的位相对应的位不变。
用法:检查位的值
如何改变位的值。 有时, 需要检查某位的值。 例如, flags中1号位是否被设置为1? 不能这样直接比较flags和MASK:
if (flags == MASK)
puts("Wow!"); /* 不能正常工作 */
这样做即使flags的1号位为1, 其他位的值会导致比较结果为假。 因此,必须覆盖flags中的其他位, 只用1号位和MASK比较:
if ((flags & MASK) == MASK)
puts("Wow!");
由于按位运算符的优先级比==低, 所以必须在flags & MASK周围加上圆括号。
移位运算符
C的移位运算符。 移位运算符向左或向右移动位。
左移: <<
左移运算符(<<) 将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。 左侧运算对象移出左末端位的值丢失, 用0填充空出的位置。可以使用左移赋值运算符(<<=) 来更改变量的值。
右移: >>
右移运算符(>>) 将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。 左侧运算对象移出右末端位的值丢。 对于无符号类型, 用0 填充空出的位置; 对于有符号类型, 其结果取决于机器。
每个位向右移动两个位置, 空出的位用0填充。右移赋值运算符(>>=) 将其左侧的变量向右移动指定数量的位数。
用法: 移位运算符
移位运算符针对2的幂提供快速有效的乘法和除法,
number << n number乘以2的n次幂number >> n 如果number为非负, 则用number除以2的n次幂
这些移位运算符类似于在十进制中移动小数点来乘以或除以10。移位运算符还可用于从较大单元中提取一些位。
编程示例

使用limits.h中的CHAR_BIT宏, 该宏表示char中的位数。sizeof运算符返回char的大小, 所以表达式CHAE_BIT * sizeof(int)表示int类型的位数。 bin_str数组的元素个数是CHAE_BIT * sizeof(int) + 1, 留出一个位置给末尾的空字符。
itobs()函数返回的地址与传入的地址相同, 可以把该函数作为printf()的参数。 在该函数中, 首次执行for循环时, 对01 & n求值。 01是一个八进制形式的掩码, 该掩码除0号位是1之外, 其他所有位都为0。 因此, 01 & n就是n最后一位的值。 该值为0或1。 但是对数组而言, 需要的是字符'0'或字符'1'。该值加上'0'即可完成这种转换(假设按顺序编码的数字, 如 ASCII) 。 其结果存放在数组中倒数第2个元素中(最后一个元素用来存放空字符) 。
循环执行i--和n >>= 1。 i--移动到数组的前一个元素, n >>= 1使n中的所有位向右移动一个位置。 进入下一轮迭代时, 循环中处理的是n中新的最右端的值。 然后, 把该值储存在倒数第3个元素中, 以此类推。 itobs()函数用这种方式从右往左填充数组。

另一个例子
~运算符切换一个字节的所有位, 而不是选定的少数位。 但是, ^运算符(按位异或) 可用于切换单个位。



位字段
操控位的第2种方法是位字段(bit field) 。 位字段是一个signed int或unsigned int类型变量中的一组相邻的位(C99和C11新增了_Bool类型的位字段) 。 位字段通过一个结构声明来建立, 该结构声明为每个字段提供标签,并确定该字段的宽度。
带有位字段的结构提供一种记录设置的方便途径。 许多设置(如, 字体的粗体或斜体) 就是简单的二选一。
有时, 某些设置也有多个选择, 因此需要多位来表示。
如果声明的总位数超过了一个unsigned int类型的大小会怎样?会用到下一个unsigned int类型的存储位置。 一个字段不允许跨越两个unsigned int之间的边界。 编译器会自动移动跨界的字段, 保持unsigned int的边界对齐。
字段储存在一个int中的顺序取决于机器。 在有些机器上, 存储的顺序是从左往右, 而在另一些机器上, 是从右往左。 另外, 不同的机器中两个字段边界的位置也有区别。 由于这些原因, 位字段通常都不容易移植。 尽管如此, 有些情况却要用到这种不可移植的特性。
位字段示例
通常, 把位字段作为一种更紧凑储存数据的方式。
方框具有如下属性:
方框是透明的或不透明的;
方框的填充色选自以下调色板: 黑色、 红色、 绿色、 黄色、 蓝色、 紫
色、 青色或白色;
边框可见或隐藏;
边框颜色与填充色使用相同的调色板;
边框可以使用实线、 点线或虚线样式。
可以使用单独的变量或全长(full-sized) 结构成员来表示每个属性, 但是这样做有些浪费位。
一种方案是: 一个字节储存方框内部(透明和填充色) 的属性, 一个字节储存方框边框的属性, 每个字节中的空隙用未命名字段填充。
struct box_props声明如下:
struct box_props {
bool opaque : 1 ;
unsigned int fill_color : 3 ;
unsigned int : 4 ;
bool show_border : 1 ;
unsigned int border_color : 3 ;
unsigned int border_style : 2 ;
unsigned int : 2 ;
};
C 以unsigned int作为位字段结构的基本布局单元。因此, 即使一个结构唯一的成员是1位字段, 该结构的大小也是一个unsigned int类型的大小, unsigned int在我们的系统中是32位。
对于opaque成员, 1表示方框不透明, 0表示透明。 show_border成员也用类似的方法。 对于颜色, 可以用简单的RGB(即red-green-blue的缩写) 表示。 这些颜色都是三原色的混合。 显示器通过混合红、 绿、 蓝像素来产生不同的颜色。 在早期的计算机色彩中, 每个像素都可以打开或关闭, 所以可以使用用 1 位来表示三原色中每个二进制颜色的亮度。 常用的顺序是, 左侧位表示蓝色亮度、 中间位表示绿色亮度、 右侧位表示红色亮度。



该程序要注意几个要点。 首先, 初始化位字段结构与初始化普通结构的语法相同
struct box_props box = {YES, YELLOW , YES, GREEN, DASHED};
类似地, 也可以给位字段成员赋值:
box.fill_color = WHITE;
另外, switch语句中也可以使用位字段成员, 甚至还可以把位字段成员用作数组的下标:
printf("The fill color is %s.\n", colors[pb->fill_color]);
根据 colors 数组的定义, 每个索引对应一个表示颜色的字符串,而每种颜色都把索引值作为该颜色的数值。