zhyDaDa的个人站点

目录
位运算
按位运算
按位取与
按位取或
按位取反
按位异或
与逻辑运算的区别
移位运算
左移
右移
误区
举例–输出一个数的二进制
实现思路
代码实现
位段
背景介绍
代码实现
利用结构体创造位段
定义一个结构体
为结构体赋值并查看结果
优缺点


位运算

C语言中有如下位运算的运算符
| 运算符 | 含义 |
| :—-: | :——–: |
| & | 按位的与 |
| \| | 按位的或 |
| ~ | 按位取反 |
| ^ | 按位的异或 |
| << | 左移 |
| >> | 右移 |

按位运算

按位取与

定义: 对于第i位, 如果两数都为1, 则与的结果为1, 否则为0
口诀: 同真则真, 一假则假
应用:
+ 让某一位为0: 对于x, 要使得其末位为0, 则计算x & 0xFE
+ 截取一个数的某一段: 对于一个int x, ta有4个字节, 如果只要最后一个字节的内容, 则计算x & 0xFF

按位取或

定义: 对于第i位, 如果两数都为0, 则与的结果为0, 否则为1
口诀: 一真则真, 同假则假
应用:
+ 让某一位为1: 对于x, 要使得其末位为1, 则计算x | 0x01
+ 拼接两个数: 对于两个一个字节的数0xAB0xCD, 计算0xAB00 | 0x00CD可以得到拼接的结果0xABCD

按位取反

定义: 每个比特的值翻转
口诀: 一变零, 零变一
应用:
+ 让某一位为1: 对于x, 要使得其末位为1, 则计算x | 0x01
+ 拼接两个数: 对于两个一个字节的数0xAB0xCD, 计算0xAB00 | 0x00CD可以得到拼接的结果0xABCD

补码取反不同!
补码是在原码取反后+1
补码可以看做原码的负数

按位异或

定义: 两个位相等则结果为0, 不相等则为1
口诀: 同则零, 异则一
应用:
+ 加密: 对一个数用同一个值异或运算两次, 仍得到原来的数
x ^ y ^ y -> x

与逻辑运算的区别

计算机底层只有按位运算
所谓逻辑运算其实就是将任何非0的数看做1, 然后做按位运算
| 按位运算 | 逻辑运算 |
| :————-: | :—————: |
| 5 & 4 -> 4 | 5 && 4 -> 1 |
| 5 \| 4 -> 5 | 5 \|\| 4 -> 1 |
| ~4 -> 3 | !4 -> 0 |

移位运算

左移

定义: i << ji中所有的位向左移动j个位置, 右边填充0

所有小于int的类型, 移位的结果均为int
(可能会因为编译器而有不同)

注意, 可能会把signed类型的符号位挤掉

x <<= n等价于x *= (int)pow(2.0, n)

右移

定义: i >> ji中所有的位向右移动j个位置
– 对于unsigned类型, 左边填0
– 对于signed类型, 左边填原来的最高位(即表示符号的那一位)

所有小于int的类型, 移位的结果均为int
(可能会因为编译器而有不同)

x >>= n等价于x /= (int)pow(2.0, n)

误区

不要以为x << -2等价于x >> 2!!!
这个行为未被定义, 会报错

举例–输出一个数的二进制

实现思路

用一个unsigned的mask去查看这个数字每一位是0还是1
mask只有一个位上有1, 其余位皆为0
那么和要算的数字运算按位取与
如果那一位是0, 那么结果是0, 否则就是1

代码实现

void printBin(int number){
    unsigned mask = 1u<<31;
    for (; mask;mask>>=1){
        printf("%d", (number & mask) ? 1 : 0);
    }
}

位段

背景介绍

在单片机中, 需要给芯片做一些设置
这些设置是通过一个二进制数读入的
为了容易理解, 以下将这个二进制数类比为一连串手势
手势的含义在单片机的手册中有明确规定
比如, 规定, 这个二进制数的第3位如果是1, 其含义就是”开启某功能”
这个二进制数的第3位如果是0, 其含义就是”关闭某功能”
当然, 还有可能出现”第4和第5这两个位置上0/1组合出的4种情况对应某个功能的4个模式”

我们的目标是, 运用位运算, 将给定的一个二进制数中的某几位, 修改为01

题目: 有一个unsigned二进制数, 要求将从右往左数的第2和第3位比特变成1, 第4位比特变成0

代码实现

const unsigned int origin = 0b111000;
unsigned int result = 0b111000;
const unsigned int flag2 = 1u << 1; //注解1
const unsigned int flag3 = 1u << 2;
const unsigned int flag4 = 1u << 3;

result |= flag2 | flag3; //注解2
result &= ~flag4; //注解3

printBin(origin);
printf("\n");
printBin(result);

注解1:
为什么要用const?

因为这个既然是开关, 那么在后续的程序中修改还会用到
方面后面的使用

另外还要注意, 1u所指就是第一位比特
所以为了对应第二位比特, 只需左移一位

注解2:
这里其实先运算等号右侧的表达式, 其结果是0b110
也就是两个开关并在一起

然后|=是一个简写, 就是按位取或之后再赋值
由于只有指定的两个比特是1, 对于其他比特没有变动
而对于指定的两位, 一定变1, 达到效果

注解3:
和上面一样, 不过这一次是先要按位取反
这样得到的结果除了指定的位置是0其余都是1
运算按位取与, 和1取与结果就是不变
而和0取与必定是0, 同样达到效果

利用结构体创造位段

考虑到结构体中的成员就是直接排列在一起的
用结构体可以更轻松容易的实现上述效果

定义一个结构体

struct U0
{
    unsigned int leading : 3;
    unsigned int FLAG1 : 1;
    unsigned int FLAG2 : 1;
    unsigned int trailing : 27;
};

这里冒号:后的数字代表这个成员所占的比特数
其中:
+ leading代表前三个比特, 能表示8种不同的选项
+ FLAG1FLAG2可能代表两个开关
+ trailing是尾巴的含义, 其作用是为没用到的比特初始化赋值

为什么trailing27比特呢?
因为总的比特数加起来是32, 刚好是4个字节, 即一个int的大小
一般来说就是一个位段的单元

为结构体赋值并查看结果

struct U0 uu;
uu.leading = 5;
uu.FLAG1 = 0;
uu.FLAG2 = 1;
uu.trailing = 0;
printBin(*(int *)&uu);

这里leading部分可以直接赋十进制的数
因为在内存中都是二进制

注意输出时的操作: *(int *)&uu
uu是一个结构变量, 无法直接取出内存中的二进制储存
这里是典型做法:
1. 先取其地址
2. 强制类型转化为int类型的指针
3. 取这个int指针的值

最终结果符合预期

优缺点

优点:
+ 可以直接通过位段的成员名称访问目标值

缺点:
+ 结构中位的排列方式是由编译器决定的(大端小端)
因此不具有可移植性

Avatar photo
我是 zhyDaDa

前端/UI/交互/独立游戏/JPOP/电吉他/游戏配乐/网球/纸牌魔术

发表回复