C语言基础19(位运算)

文章目录

    • 位运算
      • 位运算常用的运算符
      • 位运算符的运算规则
        • **~**:按位取反
        • **&**:按位与
        • **|**:按位或
        • **^**:按位异或
        • **<<**:左移,按位往左偏移
        • **>>**:右移,按位往右偏移
      • 位运算赋值符
      • 不同长度数据进行位运算
      • 位运算符的应用场景
      • 位段**/**位域
        • 概念
        • 说明
        • 注意

位运算

位运算常用的运算符

符号 说明 符号 说明
& 按位与 ~ 按位取反
| 按位或 << 按位左移
^ 按位异或 >> 按位右移

注意:参与位运算的运算量只能是整型或者字符型,不能是实型

位运算符的运算规则

~:按位取反

说明:单目运算符,数据的每一个bit位取反,也就是二进制数位上的0变1,1变0

举例:

unsigned char ret = ~0x05; // 0000 0101 --> 1111 1010
printf("%d\n",~5);// -6 // %d:有符号十进制int,%u:无符号十进制int
&:按位与

运算规则:对与左右操作数,只有相应二进制位数据都为1时,结果数据对应位数据为1,简单来说:

你是1,我是1,结果就是

举例:

5 & 6 = 4 // 0000 0101 & 0000 0110 = 0000 0100

作用(四阶段理解):

① 获取某二进制位的数据

② 将指定二进制数据清零

③ 保留指定位

|:按位或

运算规则:对于左右操作数据,只要相应二进制位数据有一个为1,结果数据对应位数据为1,简单来说:

你是1 或 我是1 结果就是1

举例:

5 | 6 = 7 // 0000 0101 | 0000 0110 = 0000 0111

作用:

① 置位某二进制位数据

^:按位异或

运算规则:对与左右操作数据,只要相应二进制位数据相同,结果数据对应位数据为0,不同为1,简单来说:

你我相同,结果为0;你我不同,结果为1

作用:

① 反转

② 值交换

面试题:
C语言基础19(位运算)_第1张图片

<<:左移,按位往左偏移

运算规则:原操作数所有的二进制位数向左移动指定位;

无符号左移:

  • 语法: 操作数 << 移动位数
unsigned int a = 3 << 3; // 计算规则:3 * 2 ^ 3
unsigned int b = 5 << 4; // 计算规则:5 * 2 ^ 4
printf("%d\n",a); // 24

有符号左移:

  • 语法: 操作数 << 移动位数
int a = -3 << 3; // -3 * 2 ^ 3
printf("%d\n",a); // -24
  • 注意:
    • 如果移出的高位都是0,我们可以这样理解:a << n ,可以看做是:a * 2^n
    • 如果移出的高位都是1,我们是不能使用上面的计算公式的。
>>:右移,按位往右偏移

运算规则:原操作数所有的二进制位数据向右移动指定位,移出的数据舍弃。

如果操作数是无符号数:左边用0补齐

如果操作数是有符号数:左边用什么去补全,取决于计算机系统:

  • 逻辑右移:用0补全

  • 算术右移:用1补全

大部分情况下,系统是遵循“算术右移”的

无符号右移:

  • 语法: 操作数 >> 移动位数
unsigned char a = 3 >> 3;
printf("%d\n",a);// 0

有符号右移:

语法: 操作数 >> 移动位数

char a = -3 >> 3;
printf("%d\n",a);// -1

位运算赋值符

  • 运算符:&=, |=, >>=, <<=, ∧=

  • 举例:

a &= b 相当于 a = a & b 相同bit位,都为1,结果为1
a <<=2 相当于 a = a << 2
a >>=2 相当于 a = a >> 2
a |= b 相当于 a = a | b 相同bit位,只要有一个为1,结果为1
a ^= b 相当于 a = a ^ b 相同bit位,相同为0,不同为1
~a 对a取反 每一个bit位,01,10

不同长度数据进行位运算

​ 如果两个数据长度不同(例如long型和short型),进行位运算时(如a & b,而a为long型,b为short型),系统会将二者按右端对齐。

  • 如果b为正数,则左侧16位补满0;

  • 如果b为负数,左端应补满1;

  • 如果b为无符号整数型,则左侧添满0。

位运算符的应用场景

注意:以下所有场景的n都是从右侧开始计数,从0开始

  1. 将某个数据从右侧(低位)指定的二进制位(n1,n2,n3…)清零

    • 公式:
    a &= ~(1<<(n1-1) | 1<<(n2-1) | 1<<(n3-1) | ...); // 如果是指定第几,n就代表第n个,从1开始
    a &= ~(1<<n1 | 1<<n2 | 1<<n3) | ...); // 如果是指定索引,n代表索引,从0开始
    
    • 解析

      其中, n1, n2, n3, ... 是我们想清零的位的索引,从右侧开始计数(从0开始)。

      通过一个例子来详细解释这个公式:

      假设我们有一个 char 类型的变量 a ,其二进制表示是 1011 0110 (即十进制的94)。我

      们想要将它的第1位、第3位和第5位清零(注意:从右侧开始计数,从0开始)。

      1. 确定位的位置
      • 第1位:从右侧开始第1位(索引0)

      • 第3位:从右侧开始第3位(索引2)

      • 第5位:从右侧开始第5位(索引4)

      1. 生成位掩码
      • 我们需要生成一个掩码,其中这些位是1,其他位是0。然后取反(使用 ~ 运算符),使得这些位变成0,其他位变成1。

      • 1<<(1-1)1<<0 结果是 1011 0110 -- 0000 0001

      • 1<<(3-1)1<<2 结果是 1011 0110 -- 0000 0100

      • 1<<(5-1)1<<4 结果是 1011 0110 -- 0001 0000

      • 将这些位掩码进行按位或运算: 0000 0001 | 0000 0100 | 0001 0000 结果是 0001

      • 取反: ~(0001 0101) 结果是 1110 1010

      1. 应用掩码

      使用按位与运算将 a 与掩码 1110 1010 进行运算: 1011 0110 & 1110 1010 结果是 1010 0010

  • 案例:
#include 
#include 
int main(int argc, char *argv[])
{
#include 
// unsigned int a = 0b10110110;
unsigned int a = 0xB6; // 初始值:1011 0110(二进
制),182(十进制)
a &= ~(1 << (1 - 1) | 1 << (3 - 1) | 1 << (5 - 1)); // 清零第1(索引0)、3(索引
2)、5(索引4)位
// 打印结果
printf("Result: %u,%x\n", a,a); // 十进制:162,十六进制:A2
printf("Binary: ");
for (int i = 7; i >= 0; i--)
{
printf("%d", (a >> i) & 1); // 二进制:1010 0010
}
printf("\n");
return 0;
}
  1. 获取某个数据指定的二进制位(n)上的数据是0还是1 10010010
  • 公式:
(a & (1 << n)) >> n
  • 解析:
  1. 1< :将数字 1 左移 n 位,生成一个只在第 n 位(从右侧开始,从 0 计数)上有1,其他位上都是 0 的掩码。

  2. a & (1< :使用按位与运算将 a 与这个掩码进行运算。结果将是一个只在第 n 位上有可能是 1(如果 a 的第 n 位是 1 的话),其他位上都是 0 的数。

  3. (...) >> n :将上一步的结果右移 n 位。由于结果中只有在第 n 位上可能是 1,右移 n 位后,这个 1 就会移动到最低位(索引 0)上。如果 a 的第 n 位原来是 0,那么结果就会是 0。

这样,我们就可以通过检查最低位是 0 还是 1 来确定 a 的第 n 位是 0 还是 1。

  • 案例:
#include 
#include 
int main(int argc, char *argv[])
{
#include 
unsigned int a = 0b10110110; // 二进制 1011 0110 ,十进制 182 初始值
int n = 3;
int bit = (a & (1<<n)) >> n;
printf("这个二进制数据%d位上的数据是%d\n", n, bit); // 3,0
return 0;
}
  1. 将某个数据从右侧指定的二进制位(n1,n2,n3)设置为1

    • 公式:
    a |= (1 << n1 | 1 << n2 | 1 << n3 |...);
    
    • 解析:

    • 1 << n :将数字1左移n位,得到一个仅在n位上为1的数。

    • 1 << n1 | 1 << n2 | 1 << n3 :将多个这样的数进行按位或运算,得到一个在n1, n2,n3位上为1的数。

    • a |= (1 << n1 | 1 << n2 | 1 << n3) :将a与上述得到的数进行按位或运算,从而将a在n1, n2, n3位上设置为1。

  • 案例:
#include 
#include 
int main(int argc, char *argv[])
{
#include 
unsigned int a = 0b10101010; // 初始值,二进制表示为10101010
int n1 = 2, n2 = 4, n3 = 6; // 要设置为1的位
// 将a在第n1, n2, n3位上设置为1
a |= (1 << n1) | (1 << n2) | (1 << n3);
// 打印结果
printf("Result: %u\n", a); // 输出结果,二进制表示为11111110
printf("Binary: ");
for (int i = 7; i >= 0; i--)
{
printf("%d", (a >> i) & 1);
}
printf("\n");
    return 0;
}
  1. 将某个数据指定的二进制位(n)反转

    • 公式:
    a ^= (1 << n)
    
    • 解析:

      • 1 << n :将数字1左移n位,得到一个仅在n位上为1的数
      • a ^= (1 << n) :将a与上述得到的数进行按位异或运算,从而反转a在n位上的值(0变为1,1变为0)

      案例:

      #include 
      int main() {
      unsigned int a = 0b10101010; // 初始值,二进制表示为10101010
      int n = 2; // 要反转的位
      // 将a在第n位上反转
      a ^= (1 << n);
      // 打印结果
      printf("Result: %u\n", a); // 输出结果,十进制表示为174
      printf("Binary: ");
      for (int i = 7; i >= 0; i--) {
      printf("%d", (a >> i) & 1);// 输出结果,二进制表示为10101110
      }
      printf("\n");
          return 0;
      }
      

      位段**/**位域

      概念

      在结构体中,以位(bit)为单位的成员,称为位段或位域 位段本质是结构体的成员,可以通过数字指明它所占内存空间的大小(以bit为单位)

      说明
      // eg: STM32中电源控制寄存器(PWR_CR)
      struct PWR
      {
      unisgned int ldps : 1; -- 1bit
      unsigned int pdds : 1; -- 1bit
      unsigned int cwuf : 1; -- 1bit
      unsigned int csbf : 1; -- 1bit
      unsigned int pvde : 1; -- 1bit
      unsigned int pls : 3; -- 3bit
      unsigned int pvde : 1; -- 1bit
      unsigned short d ; -- 16bit
      };
      
      
      struct Stu stu = {1,0,1,1,1,7,1};
      // 应用:
      // 方便给寄存器中的某些数据位设置数据
      
      注意
      • 位段不能取地址
      struct S
      {
      char a:2; // 2bit
      unsigned char b:4;// 4bit
      };
      struct S s;
      printf("%p\n",&s); // s是结构体变量,可以取地址
      printf("%p\n",&(s.a)); // s.a是结构体成员位段a,不能对它取地址
      
      • 给位段成员赋值时,不能超出成员所占内存
      struct S
      {
      char a : 2; // 2bit
      unsigned char b : 4; // 4bit
      unsigned char c : 5; // 5bit
      };
      struct S s = {1, 3};
      // 赋值操作
      s.a = 1; // a占2个bit位,并且是有符号数,它能被赋值的范围:-2,-1,0,1除此之外的都会报错
      s.b = 10; // b占4个bit位,并且是无符号数,它的取值范围为:[0,15],如果取值是[-8,15],编译正常,只
      是在-8输出时值为15
      s.c = 20; // c占5个bit位,并且是无符号数,它的取值范围为:[0,31],如果取值是[-16,31],编译正常,
      只是在-16输出时值为31
      
      • 一个位段必须存放在一个内存单元中,不能跨两个单元
      struct A
      {
      char a:7; //a在一个内存单元中,剩余一个bit位
      char b:7; //b得从下一个内存单元开始,因为上一个内存单元剩余一个bit,放不下7个bit
      char c:3; //c得从下一个内存单元开始,因为上一个内存单元剩余一个bit, 放不下3个bit
      char d:3; //d和c可以在同一个内存单元中,因为c只占了3个还剩余5个bit,能够放下d的3个bit不用跨
      内存单元
      };
      
      • 位段的长度不能大于存储单元的长度
      struct A
      {
      char a:1;
      char b:8; //一个char占一个字节,最大分配8个bit位
      char c:9; //一个char占一个字节,最大分配8个bit位,如果分配9个就是错误的,无法通过编译
      };
      
      • 如果一个位段要从另一个存储单元开始,可以在它前面定义一个匿名成员占0个字节
      struct A
      {
      char a:1;
      char b:2; //a,b总共占3个字节,在一个内存单元中就可以存放,所以不用跨内存单元
      char :0; //匿名成员,占0个字节
      char c:2; //c不和a,b在一个字节(内存单元)中,而是在另外一个字节(内存单元)中
      }
      

      说明:如果成员c上面没有匿名成员,c就和a,b在同一个字节(同一个内存单元中)

你可能感兴趣的:(c语言,学习,笔记)