C语言 位运算操作符及应用(骚操作)

C语言长盛不衰霸榜长久的一部分原因,在于它对于计算机底层的操作。位运算,作为实现底层操作的一部分功能值得我们关注。
当然从功利的角度而言,位运算在以后的面试、笔试过程中对我们有着极大的便利。

前言:
首先需要了解整数储存的机制;
原码、反码、补码;
三种码都是32位的二进制数
输出靠原码,内存存补码;
1、正整数原码、反码、补码相同,直接进行二进制转换就可;(2^32-1个正整数)
2、负整数32位的首位为符号位(1代表负数);(2^31-1个符数)
原码将剩余的31位通过负整数的绝对值进行二进制转化
反码是符号位不变,其余位数取反
补码是反码+1
3、0是一个特殊的数,由于符号位存在,分为+0,-0;
+0 为00000000000000000000000000000000
-0 为10000000000000000000000000000000

Part 1:位运算操作符
(1)& 按位与 :如果两个相应的二进制位都为1,则该位结果为1,否则为0。

#include<stdio.h>
int main(){
    
    
    int a = 15;   //00000000000000000000000000001111
    int b = 19;   //00000000000000000000000000010011
    int c = a & b;//00000000000000000000000000000011
    printf("%d", c);//输出结果为3
    return 0;
}

(2)| 按位或:两个对应的二进制位只要有一个为1,则该位结果为1,否则为0;

int main(){
    
    
    int a = 15;   //00000000000000000000000000001111
    int b = 19;   //00000000000000000000000000010011
    int c = a | b;//00000000000000000000000000011111
    printf("%d", c);//输出结果为31
    return 0;
}

(3)^按位异或:两个对应的二进制位不同时则该位结果为1,否则为0;

int main(){
    
    
    int a = 15;   //00000000000000000000000000001111
    int b = 19;   //00000000000000000000000000010011
    int c = a ^ b;//00000000000000000000000000011100
    printf("%d", c);//输出结果为28
    return 0;
}

(4)~取反:对该数的二进制位,若该位为1则结果为0,若改为为0则结果为1;

int main(){
    
    
    int a = 19;//00000000000000000000000000010011
    int b = ~a;//11111111111111111111111111101100(内存中的补码)
               //11111111111111111111111111101011(反码,即补码-1)
               //10000000000000000000000000010100(原码,即反码符号位不变,其余位数取反)
    printf("%d", b);//输出结果-28
    return 0;
}

(5)<<左移操作符:将一个数的各二进制位向左平移i个单位(<<为双目操作符,通常写为a<<i),高位溢出的删去,地位补0;

int main(){
    
    
    int a = 19;  //00000000000000000000000000010011
    int b = a<<1;//00000000000000000000000000100110
    printf("%d", b);//输出结果38
    return 0;
}

(6)>>右移操作符;将一个数的各二进制位向右平移i个单位(用法同左移操作符),根据编译器不同分为两种不同的右移
1、逻辑右移:右边溢出位数丢弃,左边高位补0
2、算术右移:右边溢出位数丢弃,左边根据符号位补位

int main(){
    
    
    int a = -19; //10000000000000000000000000010011(原码)
                 //11111111111111111111111111101100(反码)
                 //11111111111111111111111111101101(补码)
    int b = a>>1;//11111111111111111111111111110110(补码)
                 //11111111111111111111111111110101(反码)
                 //10000000000000000000000000001010(原码
    printf("%d", b);//输出结果-10
    return 0;
}

Part 2 一些重要的注意点:
1、所有的位操作符,均只能对整型使用;
2、位操作符操作的都是该数储存在内存中的补码,而printf输出的都是原码。所以针对负整数,我们需要先转化成补码,进行操作后,再转换成原码输出。
3、>> <<中移的位数i 为正整数,使用负整数为UB(Undefined Behavior)

Part3 位运算操作符的一些应用:
1、scanf函数输入错位返回值为-1
而~-1==0 利用这一特性我们可以实现循环多行输入

int a=0;
while(~scanf("%d",&a)){
    
    
;
}

2、除号,乘号可以利用>>,<<代替;
事实上,c语言编译器在运行的时候会将你的/2优化为>>1,将你的*2优化为<<1,利用位操作符其实可以优化程序运行速度;从另一个角度,在笔试面试中将简单的乘除法替换成位运算符,能显示出更高水平(雾

#include<math.h>
int main(){
    
    
    int a = 120;
    int i = 0;
    int temp = 1;
    for (i = 1; i <= 3;i++){
    
    
        temp *= 2;
        printf("%d\n", a >> i);//右移相当于➗(2^i),左移相当于×(2^i)
        printf("%d\n", a / temp);
    }
    return 0;
}

3、交换两个变量的值,且同时不创造新的临时变量;

int main(){
    
    
    int a = 10, b = 5;
    printf("a=%d b=%d\n", a, b);
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    printf("a=%d b=%d", a, b);
    return 0;
}

在这里插入图片描述
4、判断整型数二进制转化后1的个数
(可以结合应用2理解)

int main(){
    
    
    int a = 55,count=0;//55-->00000000000000000000000000110111
                       //1 -->00000000000000000000000000000001
                       //a&1->00000000000000000000000000000001
                       //a/2->00000000000000000000000000011011
    while(a>0){
    
    
        if(a&1==1){
    
    
            count++;
        }
        a = a / 2;
    }
    printf("%d", count);
}

5、关于n&(n-1)的一些骚应用
(1)判断一个数是否为2的次方项。首先我们观察到2的二进制表示为10,4为100,8为1000…以此类推,2的次方项的二进制表达有一定的规律
2的二进制10-------1的二进制 01----- 2&1得 0
4的二进制100-----3的二进制011-----4&3得 0
8的二进制1000----7的二进制0111—8&7得 0
于是我们发现n&(n-1)==0的时候n为2的次方项,背后的原理其实很简单,类似十进制的退位原理,2的次方项转化成二进制,就类似于十进制中的10的n次方,此时-1后,它的各位一定与原来的数均不相同。

int is_two_pow(int n){
    
    
    if((n&(n-1))==0){
    
    
        return 1;
    }else{
    
    
        return 0;
    }
}

(2)统计一个数的二进制转化后有多少个1(同上文4)
过程类比于判断2的次方,对转化后的每一个1进行类似于上文的比较,count每一次加一后的n=n&(n-1)都会使得n二进制内的一个1消失

int count_one(int n){
    
    
    int count = 0;
    while(n>0){
    
    
        count++;
        n = n & (n - 1);
    }
    return count;
}
int main(){
    
    
    int a = 55;//55-->   00000000000000000000000000110111
               //54-->   00000000000000000000000000110110
               //55&54-->00000000000000000000000000110110-->54
               //53-->   00000000000000000000000000110101
               //54&53-->00000000000000000000000000110100-->52
               //51-->   00000000000000000000000000110011
               //52&51-->00000000000000000000000000110000--48
               //47-->   00000000000000000000000000101111
               //47&46-->00000000000000000000000000100000--32
               //31-->   00000000000000000000000000011111
               //32&31-->00000000000000000000000000000000--0 循环结束
    printf("%d", count_one(a));
    return 0;
}

总结:n&(n-1)非常巧妙的利用了二进制的运算规律,其实本质并不复杂,难点只是在于,长时间进行了十进制运算后,大脑短暂难以迅速理解二进制,n=n&(n-1)的本质是将n二进制转化后的最后一个1变成0
6、实现转化后二进制的逆序

int reverse(int n){
    
    
    int result = 0;
    for (int i = 0; i < 32;i++){
    
    
        result = result << 1;//左移1位留出空格
        result += (n&1);//n&1得出n二进制的最后一位
        n >>1;//n右移一位获得下一位
    }
    return result;
}

7、实现正负数转化
正数取反+1得到负数得补码,-1后再取反(此时符号位不变)得到负数的原码
负数补码取反+1,得到符号位变为0的其原码

int pos_neg(int n){
    
    
     return ~n+1;
}

8、实现取绝对值操作

int my_abs(int n){
    
    
   int i=n>>31;//获取符号位
   return i==0? n:~n+1;
}

Part 4 总结:
以上只是一些浅层次的理解,挖坑日后继续补充。
位运算操作,更符合c语言编译器的需求,毕竟计算机的本质是二进制运算,可以优化程序效率。当然在另一层面,更体现了编写者对语言的理解,从而在面试、笔试中留下更好的印象。

猜你喜欢

转载自blog.csdn.net/weixin_54225634/article/details/113389913