C_Primer_Plus15.bit_fiddling
位操作
- 要点
~, &, |, ^, <<, >>, &=, |=, ^=, >>=, <<=
二进制,十进制和十六进制计数法
处理一个值中的位的两个C工具:位运算符和位字段
关键字:_Alignas, _Alignof
C 在提供高级语言的便利的同时,还能在为汇编语言所保留的级别上工作,这使得其成为设备驱动程序和嵌入式代码的首选语言。
位运算与通常运算符计算速度:
CPU计算加减法的速度跟位运算(与、或、非、异或)相当,乘法的速度比加减法慢近10倍,除法的速度比加减法慢(近20倍——8位,近30倍——16位,40倍以上——32位)。算加减法,读取内存数据的比不读内存数据的慢,写内存的比读内存的慢。
二进制数、位和字节
有符号整数表示方法
有两种方法表示有符号整数,补码和反码表示法,补码是现在常用的方法,而反码存在 +0 和 -0 的问题。补码表示法能表示 -128~+127范围,而反码因为存在 +0和-0 的问题,只能表示 -127~+127范围。
二进制补码
用第一个位(最高位)表示符号,0表示整数,1表示负数。负数解码时,先取反,再加1,得到数值大小。
比如 10000000 表示 -128, 10000001 表示 -127,11111111 表示 -1
二进制反码
直接取反,得到数值大小。这种表示法存在 -0和+0 让人混淆的问题。比如 00000000 表示 +0,而 11111111 则表示 -0。所以这种方法只能表示 -127~+127 范围的整数
按位运算符
- ~ 取反
- & 按位与
- | 按位或
- ^ 按位异或
按位操作应用
- 常用于掩码
- 打开位/关闭
- 打开/关闭 某个特定位
- 切换位(异或)
- 打开关闭的位,关闭打开的位
flags ^= MASK;
- 检查位的值
if((flags & MASK) == MASK);
用或运算表示不同的颜色混合:
#define RED 1
#define GREEN 2
#define BLUE 4
#define YELLOW (RED | GREEN)
#define MAGENTA (RED | BLUE)
#define CYAN (GREEN | BLUE)
#define WHITE (RED | GREEN | BLUE)
移位运算符
产生一个新值,但不改变其运算对象
往左移位时一般用0填充低位;往右移位时有的编译器用0填充高位,而有的用1填充。
(10001010) << 2
(00101000) // 向左移位时用0填充低位
(10001010) >> 2
(00100010) // 有的系统用0填充高位
(10001010) >> 2
(11100010) // 另一些系统用1填充高位
移位运算符还可用于从较大单元中提取一些位。比如用 unsigned long 表示颜色值,低阶位的字节存储红色强度,下一个字节存储绿色强度,第3个字节存储蓝色强度。可以用移位和与运算方法提取不同的颜色:
#define MASK 0xff;
unsigned long color = 0x002a162f;
unsigned long red, green, blue;
red = color & MASK; // red
green = (color >> 8) & MASK; // green
blue = (color >> 16) & MASK; // blue
位字段
位字段介绍
bit field
位字段是一个 signed int 或 unsigned int 类型变量中的一组相邻的位(C99 和 C11 新增了 _Bool 类型的为字段)。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。
// 创建一个包含4个1位的字段:
struct {
unsigned int autfd : 1;
unsigned int bldfc : 1;
unsigned int undln : 1;
unsigned int itals : 1;
} prnt;
// 为成员赋值:
prnt.itals = 0;
prnt.undln = 1;
由于每个字段都是1位,所以只能赋值1或0. prnt 被存储在一个 int 大小的内存单元中,但它实际只使用了其中的4位,剩余的28个位被浪费掉。prnt 中的字段由于都是1位,只能当做开关用,若想要表示更多选择,可以用多位来表示:
struct {
unsigned int code1 : 2;
unsigned int code2 : 2;
unsigned int code3 : 8;
} prcode;
prcode.code1 = 0;
prcode.code1 = 3;
prcode.code1 = 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;
其中 field1 和 field2 之间存在一个2位的空洞,field3 被存储在下一个 unsigned int 中。stuff 总共占用了2个 int 的宽度,除去显式声明的空洞,总共浪费了 28+31 个位的空间。
字段存储在一个 int 中的顺序取决于机器。有些机器上,存储的顺序是从左往右,有的是从右往左。另外,不同机器中两个字段边界的位置也有区别。所以,位字段通常都不容易移植。
位字段的初始化与结构相同:
#define SOLID 0
#define DOTTED 1
#define DASHED 2
#define BLUE 4
#define GREEN 2
#define RED 1
#define BLACK 0
#define YELLOW (RED | GREEN)
#define MAGENTA (RED | BLUE)
#define CYAN (GREEN | BLUE)
#define WHITE (RED | GREEN | BLUE)
struct box{
bool opaque : 1;
unsigned int fill_color : 3;
unsigned int : 4;
bool show_border : 1;
unsigned int boarder_color : 3;
unsigned int border_style : 2;
unsigned int : 2;
};
struct box bb = {true, YELLOW, true, true, GREEN, DASHED};
位字段和按位运算符
在同类型的编程问题中,位字段和按位运算符是两种可替换的方法,用哪种方法都可以。但是一般而言,用按位运算符比较麻烦,不仅是需要保持清晰的头脑,还因为位字段和位的位置之间的相互对应因实现而异。
比如,大端法和小端法的高阶位和低阶位顺序是相反的,所以,结构表示法存储在前16位,而 unsigned int 表示法(按位操作方法)则存储在后16位。
大端法:高阶位存储在低位地址
小端法:高阶位存储在高位地址比如:
0x12345678 存储在地址范围为0x100到0x103字节的内存中
大端法:
0x100 0x101 0x102 0x103 12 34 56 78 小端法: 0x100 0x101 0x102 0x103 - - - - 78 56 34 12
对齐特性 (C11)
对齐指的是如何安排对象在内存中的位置。C11 的对齐特性比用位填充字节更自然,它们还代表了 C 在处理硬件相关问题上的能力。比如,为了效率最大化,系统可能要把一个 double 类型的值存储在4字节内存地址中,而 char 却存储在任意地址。某些情况下,程序会受益于对齐控制,比如把数据从一个硬件位置转移到另一个位置,或者调用指令同时操作多个数据项。
_Alignof 运算符给出了一个类型的对齐要求,在其后面的圆括号中写上类型名即可:
size_t d_align = _Alignof(float);
假设 d_align 的值是4,意思是 float 类型对象的对齐要求是4. 即,4是存储该类型值相邻地址的字节数。一般情况下,对齐值都应该是2的非负整数次幂。较大的对齐值叫做 stricter 或 stronger, 较小的叫做 weaker.
可以使用 _Alignas 指定一个变量或类型的对齐值,但不应要求该值小鱼基本对齐值。比如,如果 float 类型的对齐要求是4,不要请求其对齐值是1或2:
_Alignas(double) char c1;
_Alignas(8) char c1;
unsigned char _Alignas(long double) c_arr[sizeof(long double)];
stdalign.h 头文件中,使用了 alignas 和 alignof 分别作为 _Alignas 和 _Alignof 的别名。
C11 在 stdlib.h 库还添加了一个新的内存分配函数,用于对齐动态分配的内存:
void * aligned_alloc(size_t alignment, size_t size);
第一个参数代表指定的对齐,第二个是所需的字节数,其值应是第一个参数的倍数。与其他内存分配函数一样,要使用 free() 函数释放之前分配的内存。
小结
C 区别于许多高级语言的特性之一是访问整数中单独位的能力。该特性通常是与硬件设备和操作系统交互的关键。
C 有两种方法访问位。一种是通过按位运算符;另一种是在结构中创建位字段。
C11 新增了检查内存对齐要求的功能,而且可以指定比基本对齐值更大的对齐值。
大多数情况下,使用这些特性的程序限定于特定的硬件平台或操作系统,而且设计为不可移植的。

浙公网安备 33010602011771号