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
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<
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
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;
}
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语言编译器的需求,毕竟计算机的本质是二进制运算,可以优化程序效率。当然在另一层面,更体现了编写者对语言的理解,从而在面试、笔试中留下更好的印象。