位运算


C可以对变量的个别位进行操作,这在流行的其他高级语言中不太常见了,尤其是脚本语言很少有这样的能力了。位操作最常见的情景是设备控制的驱动程序,每一位都有特定的含义;数据的压缩与解压缩,数据加密解密等等。

基础:二进制数、位、字节

这部分是计算机理论基础,只提一个有意思的点:一个字节通常包括8个位,不过C使用术语字节(byte)表示用于存放系统字符集的空间大小,所以一个C字节可能为8,9,16位或者其他的值。然而描述存储器芯片和数据传输率时使用的字节就是指8位字节。

关于字符编码的原码,反码,补码的内容,不再赘述。

位运算符

C提供位的逻辑运算符移位运算符

位逻辑运算符

4个位运算符用于整型数据,位逻辑运算符与常规的逻辑运算符(&&, ||, !)不一样,使用时不要把它们混淆了。常规的逻辑运算符是对整个操作数进行的,而位逻辑运算是针对数的某一位操作,而不影响其他位。

  • 取二进制反码,或者按位取反: ~ 将一个数的二进制表示形式按位取反。注意,~是一个操作符, ~val 并不改变val的值,就像 3*val 不会改变val保存的值,该操作会返回一个操作的结果,用于赋值语句的右边。
  • 位与 & 两个操作数按位与产生一个新值。两个操作数对应位都为1时,结果的那一位也为1,否则为0.
  • 位或 | 和 &很像
  • 按位异或 ^ 对每一个位,如果操作数中的对应位有且只有一个为1,结果对应位也为1,都则结果对应位为0

这四个操作符和 数值运算(+, -, *, /)一样可以与赋值符号(=)结合成一个赋值运算符(&=, |=, ^=)。其中取反常写为 val = ~val;

掩码、打开位

掩码常用在如子网掩码的名称中,掩码是某些位设为1而某些位设为0的位组合。使用掩码与&操作符的目的是将特定位之外的所有位都设置为0.

与使用掩码相对的是打开某个为,使用 | 操作符与掩码将某些位保持不变而使某些位都设置位1。

其他还有关闭位( f &= ~mask),转置位(f ^= mask)都是很常用的。

查看某一位的值

用 & 实现查看变量某一个位的值.

int i = 123;
int mask = 0b01000000; // 01000000
if ( (i & mask ) == mask)
puts("bit 6 of i is 1");

这种方法中,为避免信息漏过边界,位掩码至少应该与其所屏蔽的值具有相同的宽度。

移位运算

移位运算包括左移和右移两种。

左移运算 << 将左侧操作数的值的每一位都向左移动,移动的位数由右侧操作数指定。空出的位用 0 填充,并且丢弃移出做操作数末端的未。移位操作返回一个新值而不改变其操作数。如 i << 2 返回i左移2位的结果,i的值不变。

右移运算 >> 将做操作数的每位向右移动。丢弃移出左侧操作数右端的位。对于无符号类型(unsigned),使用0填充左端空出的位,对于有符号数,结果依赖于机器,可能用0填充,可能用符号位填充(常用符号位填充)。

移位运算是高校的对2的幂的乘法和除法,在编译器的优化中常用移位运算和加法运算组合来优化乘法运算,比如 i*33 可以被优化为 i << 5 + i 这样子。这些优化取决于编译器实现,对程序员是透明的。

移位运算对于按位存储数据的变量很有用有用,比如颜色常保存为 0xff00ff00 这样子,使用移位与 & 操作符可以方便的取得 rgba 对应的数值。

位字段

位字段在之前的一片介绍链接的部分,ELF的结构体中使用过。这种技术在网络协议中也经常会使用。

位字段是一个singed int 或者 unsigned int 中一组相邻的位。位字段由一个结构声明建立,该结构声明为每一个字段提供标签,并决定字段的宽度

struct {
unsigned int autfd: 1;
unsigned int bldfc: 1;
unsigned int undln: 1;
unsigned int itals: 1;
} prnt

prnt.autfd = 1;
prnt.itals = 0;

该定义使prnt包含4个1位的字段,可以像使用普通结构成员运算符一样将值赋给单独的字段。变量prnt被存储在一个int大小的存储单元中,且只有4位被使用。

带有位字段的结构允许在单个单元中存储多项设置。但是要确保设置的值没有超出字段的容量。

如果声明的结构中总的字段数超过了一个unsigned int 大小,那么将会使用下一个unsigned int存储位置。不允许一个字段跨越两个存储位置的边界,如果有这种情况编译器将自动地移位字段定义,使字段按照unsigned int边界对齐。这样会在前一个存储位置留下一个未命名的洞。

可以使用未命名字段填充未命名的洞。使用一个宽度为0的未命名字段迫使下一个字段与下一个存储位置对齐。也就是说位字段结构成员的大小没有什么限制,只要注意边界对齐就可以了。

struct {
unsigned int field1 : 3;
unsigned int : 1; // leave a hole
unsigned int field2 : 3;
unsigned int : 0; // hole for align
unsigned int field : 5;
} staff;

一个可能的问题是字段在int中的顺序依赖于机器实现,有的机器上从左到右存储,在另一些机器则是从右向左,所以位字段往往难以移植,典型地,把他们用于不可移植的用途,例如按照某个特定硬件设备所使用的确切格式来存放数据。

位字段和位运算符是对于同类编程问题的两种可选择的方法,可以使用其中任何一种,通常来说位字段的方法比较好理解,使用也更加方便。

完?