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 新增了检查内存对齐要求的功能,而且可以指定比基本对齐值更大的对齐值。

大多数情况下,使用这些特性的程序限定于特定的硬件平台或操作系统,而且设计为不可移植的。

posted @ 2020-08-26 22:12  keep-minding  阅读(97)  评论(0)    收藏  举报