目录
– 位运算
– 按位运算
– 按位取与
– 按位取或
– 按位取反
– 按位异或
– 与逻辑运算的区别
– 移位运算
– 左移
– 右移
– 误区
– 举例–输出一个数的二进制
– 实现思路
– 代码实现
– 位段
– 背景介绍
– 代码实现
– 利用结构体创造位段
– 定义一个结构体
– 为结构体赋值并查看结果
– 优缺点
位运算
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
+ 拼接两个数: 对于两个一个字节的数0xAB
和0xCD
, 计算0xAB00 | 0x00CD
可以得到拼接的结果0xABCD
按位取反
定义: 每个比特的值翻转
口诀: 一变零, 零变一
应用:
+ 让某一位为1
: 对于x
, 要使得其末位为1
, 则计算x | 0x01
+ 拼接两个数: 对于两个一个字节的数0xAB
和0xCD
, 计算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 << j
将i
中所有的位向左移动j
个位置, 右边填充0
所有小于int的类型, 移位的结果均为int
(可能会因为编译器而有不同)注意, 可能会把signed类型的符号位挤掉
x <<= n
等价于x *= (int)pow(2.0, n)
右移
定义: i >> j
将i
中所有的位向右移动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个模式”我们的目标是, 运用位运算, 将给定的一个二进制数中的某几位, 修改为
0
或1
题目: 有一个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种不同的选项
+ FLAG1
和FLAG2
可能代表两个开关
+ trailing
是尾巴的含义, 其作用是为没用到的比特初始化赋值
为什么
trailing
是27
比特呢?
因为总的比特数加起来是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指针的值
最终结果符合预期
优缺点
优点:
+ 可以直接通过位段的成员名称访问目标值
缺点:
+ 结构中位的排列方式是由编译器决定的(大端小端)
因此不具有可移植性