位操作...

位操作

​ 在C语言中,可以单独操控变量中的位。读者可能好奇,竟然有人想这样做。有时必须单独操控位,而且非常有用。例如,通常向硬件设备发送一两个字节来控制这些设备,其中每个位(bit)都有特定的含义。

1. 二进制数、位和字节

​ 从某种意义上看,计算机的位只有2根手指,因为它只能被设置为0或1,关闭或打开。
​ 因此,计算机适用基底为2的数制系统。以2为基底表示的数字被称为二进制数(binary number)。

1.1 二进制整数

​ 1字节包含8位。C语言用字节(byte)表示储存系统字符集所需的大小,所以C字节可能是8位、9位、16位或其他值。

​ 可以从左往右给这8位分别编号为7~0。在1字节中,编号是7的位被称为高阶位(high-order bit),编号是0的位被称为低阶位(low-order bit)。

在这里插入图片描述

1.2 有符号整数

​ 如何表示有符号整数取决于硬件,而不是C语言。也许表示有符号数最简单的方式是用1位(如,高阶位)储存符号,只剩下7位表示数字本身(假设储存在1字节中)。用这种符号量(sign-magnitude)表示法,10000001表示−1,00000001表示1。因此,其表示范围是−127~+127。

​ 这种方法的缺点是有两个0:+0和-0。这很容易混淆,而且用两个位组合来表示一个值也有些浪费。

1.3 二进制浮点数

​ 浮点数分两部分储存:二进制小数和二进制指数。

1.二进制小数

​ 一个普通的浮点数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的幂的和。

2.浮点数表示法

​ 为了在计算机中表示一个浮点数,要留出若干位(因系统而异)储存二进制分数,其他位储存指数。一般而言,数字的实际值是由二进制小数乘以2的指定次幂组成。例如,一个浮点数乘以4,那么二进制小数不变,其指数乘以2,二进制分数不变。

2. 其他进制数

​ 计算机界通常使用八进制记数系统和十六进制记数系统。因为8和16都是2的幂,这些系统比十进制系统更接近计算机的二进制系统。

2.1 八进制

与八进制位等价的二进制位
八进制位等价的二进制位八进制位等价的二进制位
00004100
10015101
20106110
30117111

2.2 十六进制

十进制十六进制等价二进制十进制十六进制等价二进制
000000881000
110001991001
22001010A1010
33001111B1011
44010012C1100
55010113D1101
66011014E1110
77011115F1111

3. C按位运算符

​ C 提供按位逻辑运算符和移位运算符。在下面的例子中,为了方便读者了解位的操作,我们用二进制记数法写出值。

​ 在实际的程序中不必这样,用一般形式的整型变量或常量即可。例如,在程序中用25或031或0x19,而不是00011001。

3.1 按位逻辑运算符

1.二进制反码或按位取反:~
​ 一元运算符~把1变为0,把0变为1。

~(10011010) // 表达式
(01100101)  // 结果值
/*	
 *	假设val的类型是unsigned char,已被赋值为2。
 *  在二进制中,00000010表示2。那么,~val的值是11111101,即253。
 *	注意,该运算符不会改变val的值,就像3 * val不会改变val的值一样, val仍然是2。
 *	但是,该运算符确实创建了一个可以使用或赋值的新值:
 *	newval = ~val;
 */

2.按位与:&
​ 二元运算符&通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位都为1时,结果才为1(从真/假方面看,只有当两个位都为真时,结果才为真)。

(10010011) & (00111101)  // 表达式
(00010001)  			 // 结果值
//	C有一个按位与和赋值结合的运算符:&=。下面两条语句产生的最终结果相同:
val &= 0377;
val = val & 0377;

3.按位或:|
​ 二元运算符|,通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位为1,结果就为1(从真/假方面看,如果两个运算对象中相应的一个位为真或两个位都为真,那么结果为真)。

(10010011) | (00111101) 	// 表达式
(10111111) 					// 结果值
//	C有一个按位或和赋值结合的运算符:|=。下面两条语句产生的最终作用相同:
val |= 0377;
val = val | 0377;

4.按位异或:^
​ 二元运算符^逐位比较两个运算对象。对于每个位,如果两个运算对象中相应的位一个为1(但不是两个为1),结果为1(从真/假方面看,如果两个运算对象中相应的一个位为真且不是两个为同为1,那么结果为真)。

(10010011) ^ (00111101) 	// 表达式
(10101110)  			   // 结果值
//	C有一个按位异或和赋值结合的运算符:^=。下面两条语句产生的最终作用相同:
val ^= 0377;
val = val ^ 0377;

3.2 掩码

​ 按位与运算符常用于掩码(mask)。所谓掩码指的是一些设置为开(1)或关(0)的位组合。

​ 假设定义符号常量MASK为2 (即,二进制形式为00000010),只有1号位是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的位才可见。

在这里插入图片描述

3.3 打开位(设置位)

​ 需要打开一个值中的特定位,同时保持其他位不变。这种情况可以使用按位或运算符(|)。

flags = flags | MASK;
//	假设flags是00001111,MASK是10110110。下面的表达式:
flags | MASK
//	即是:
(00001111) | (10110110)  // 表达式
//	其结果为:
(10111111)        // 结果值
//	MASK中为1的位,flags与其对应的位也为1。MASK中为0的位,flags与其对应的位不变
//  用|=运算符可以简化上面的代码,如下所示:
flags |= MASK;

3.4 关闭位(清空位)

​ 有时也需要在不影响其他位的情况下关闭指定的位。

//	假设要关闭变量flags中的1号位。同样,MASK只有1号位为1(即,打开)。可以这样做:
flags = flags & ~MASK;
//	例如,假设flags是00001111,MASK是10110110。下面的表达式:
flags & ~MASK
//	即是:
(00001111) & ~(10110110) // 表达式
//	其结果为:
(00001001)        // 结果值
//	MASK中为1的位在结果中都被设置(清空)为0。
//  flags中与MASK为0的位相应的位在结果中都未改变。可以使用下面的简化形式:
flags &= ~MASK;

3.5 切换位

​ 切换位指的是打开已关闭的位,或关闭已打开的位。可以使用按位异或运算符(^)切换位。

//	假设b是一个位(1或0),如果b为1,则1^b为0;如果b为0,则1^b为1。
//	值与MASK为0的位相对应的位不变。要切换flags中的1号位,可以使用下面两种方法:
flags = flags ^ MASK;
flags ^= MASK;
//	例如,假设flags是00001111,MASK是10110110。表达式:
flags ^ MASK
即是:
(00001111) ^ (10110110)  // 表达式
其结果为:
(10111001)        // 结果值

3.6 检查位的值

​ 有时,需要检查某位的值。

//	例如,flags中1号位是否被设置为1?不能这样直接比较flags和MASK:
if (flags == MASK)
puts("Wow!"); /* 不能正常工作 */
//	这样做即使flags的1号位为1,其他位的值会导致比较结果为假。
//	因此,必须覆盖flags中的其他位,只用1号位和MASK比较:
if ((flags & MASK) == MASK)
puts("Wow!");
//	由于按位运算符的优先级比==低,所以必须在flags & MASK周围加上圆括号。
//	为了避免信息漏过边界,掩码至少要与其覆盖的值宽度相同。

3.7 移位运算符

​ 移位运算符向左或向右移动位。

1.左移:<<
​ 左移运算符(<<)将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。下面的例子中,每一位都向左移动两个位置:

(10001010) << 2 // 表达式
(00101000)    // 结果值

//	该操作产生了一个新的位值,但是不改变其运算对象。
//	例如,假设stonk为1,那么 stonk<<2为4,但是stonk本身不变,仍为1。
    
//	可以使用左移赋值运算符(<<=)来更改变量的值。
//	该运算符将变量中的位向左移动其右侧运算对象给定值的位数。如下例:
int stonk = 1;
int onkoo;
onkoo = stonk << 2;   /* 把4赋给onkoo */
stonk <<= 2;    	/* 把stonk的值改为4 */

2.右移:>>
​ 右移运算符(>>)将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出右末端位的值丢。对于无符号类型,用0 填充空出的位置;对于有符号类型,其结果取决于机器。空出的位置可用0填充,或者用符号位(即,最左端的位)的副本填充:

(10001010) >> 2    // 表达式,有符号值
(00100010)      // 在某些系统中的结果值
(10001010) >> 2    // 表达式,有符号值
(11100010)      // 在另一些系统上的结果值
//	下面是无符号值的例子:
(10001010) >> 2    // 表达式,无符号值
(00100010)      // 所有系统都得到该结果值
//	每个位向右移动两个位置,空出的位用0填充。

3.用法:移位运算符
​ 移位运算符针对2的幂提供快速有效的乘法和除法:
​ number << n   number乘以2的n次幂
​ number >> n   如果number为非负,则用number除以2的n次幂
​ 这些移位运算符类似于在十进制中移动小数点来乘以或除以10.

4.位字段

​ 操控位的第2种方法是位字段(bit field)。位字段是一个 signed int 或 unsigned int 类型变量中的一组相邻的位。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。

struct {
	unsigned int autfd : 1;
	unsigned int bldfc : 1;
	unsigned int undln : 1;
	unsigned int itals : 1;
} prnt;
//	根据该声明,prnt包含4个1位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值

prnt.itals = 0;
prnt.undln = 1;

//	有时,某些设置也有多个选择,因此需要多位来表示。这没问题,字段不限制 1 位大小。可以使用如下的代码:
struct {
	unsigned int code1 : 2;
	unsigned int code2 : 2;
	unsigned int code3 : 8;
} prcode;
//	以上代码创建了两个2位的字段和一个8位的字段。可以这样赋值:
prcode.code1 = 0;
prcode.code2 = 3;
prcode.code3 = 102;
//	但是,要确保所赋的值不超出字段可容纳的范围。
//	如果声明的总位数超过了一个unsigned int类型的大小会怎样?
//	会用到下一个unsigned int类型的存储位置。
//	一个字段不允许跨越两个unsigned int之间的边界。
//	编译器会自动移动跨界的字段,保持unsigned int的边界对齐。
//	一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”

可以用未命名的字段宽度“填充”未命名的“洞”。
使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐:
struct {
	unsigned int field1  : 1 ;
	unsigned int       : 2 ;
	unsigned int field2  : 1 ;
	unsigned int       : 0 ;
	unsigned int field3  : 1 ;
} stuff

4.1 位字段示例

​ 通常,把位字段作为一种更紧凑储存数据的方式。例如,假设要在屏幕上表示一个方框的属性。

假设方框具有如下属性:
方框是透明的或不透明的;
方框的填充色选自以下调色板:黑色、红色、绿色、黄色、蓝色、紫色、青色或白色;
边框可见或隐藏;
边框颜色与填充色使用相同的调色板;
边框可以使用实线、点线或虚线样式。

可以使用单独的变量或全长(full-sized)结构成员来表示每个属性,但是这样做有些浪费位。例如,只需1位即可表示方框是透明还是不透明;只需1位即可表示边框是显示还是隐藏。8种颜色可以用3位单元的8个可能的值来表示,而3种边框样式也只需2位单元即可表示。总共10位就足够表示方框的5个属性设置。

//	一种方案是:一个字节储存方框内部(透明和填充色)的属性,一个字节储存方框边框的属性,每个字节中的空隙用未命名字段填充。
//	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 ;
};
//	加上未命名的字段,该结构共占用 16 位。

5.对齐特性

​ _Alignof运算符给出一个类型的对齐要求,在关键字_Alignof后面的圆括号中写上类型名即可:
size_t d_align = _Alignof(float);

6.本章小结

​ 计算硬件与二进制记数系统密不可分,因为二进制数的1和0可用于表示计算机内存和寄存器中位的开闭状态。

​ C 提供多种按位运算符,之所以称为按位是因为它们单独操作一个值中的每个位。

​ ~运算符将其运算对象的每一位取反,将1转为0,0转为1。

​ 按位与运算符(&)通过两个运算对象形成一个值。如果两运算对象中相同号位都为1,那么该值中对应的位为1;否则,该位为0。

​ 按位或运算符(|)同样通过两个运算对象形成一个值。如果两运算对象中相同号位有一个为1或都为1,那么该值中对应的位为1;否则,该位为0。

​ 按位异或运算符(^)也有类似的操作,只有两运算对象中相同号位有一个为1时,结果值中对应的位才为1。
​ C还有左移(<<)和右移(>>)运算符。这两个运算符使位组合中的所有位都向左或向右移动指定数量的位,以形成一个新值。对于左移运算符,空出的位置设为 0。对于右移运算符,如果是无符号类型的值,空出的位设为0;如果是有符号类型的值,右移运算符的行为取决于实现。

​ 可以在结构中使用位字段操控一个值中的单独位或多组位。

​ 可以使用_Alignas强制执行数据存储区上的对齐要求。
值中对应的位为1;否则,该位为0。

​ 按位或运算符(|)同样通过两个运算对象形成一个值。如果两运算对象中相同号位有一个为1或都为1,那么该值中对应的位为1;否则,该位为0。

​ 按位异或运算符(^)也有类似的操作,只有两运算对象中相同号位有一个为1时,结果值中对应的位才为1。
​ C还有左移(<<)和右移(>>)运算符。这两个运算符使位组合中的所有位都向左或向右移动指定数量的位,以形成一个新值。对于左移运算符,空出的位置设为 0。对于右移运算符,如果是无符号类型的值,空出的位设为0;如果是有符号类型的值,右移运算符的行为取决于实现。

​ 可以在结构中使用位字段操控一个值中的单独位或多组位。

​ 可以使用_Alignas强制执行数据存储区上的对齐要求。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值