文章目录
前言
本片文章将会细致深入地讲解 整数的三种二进制表示形式:原码、反码、补码之间的关系、存在的意义;移位操作符:<< 、>> ; 位操作符: & 、 | 、^ 、~ 的注意事项和规则 ;文章结尾有三个实际使用移位操作符与位操作符的例子
注:全文详细,非常适合新手阅读
铁铁们,记得收藏,不迷路~~【星星眼】
一、数据在内存中的存储
思考:在开始移位操作符之前,我们先来了解一下数据在内存中的存储方式是怎么样的。众所周知,计算机存储数据是以二进制的形式存储的。什么是二进制呢?二进制是数据的一种表现形式,常见的进制有二进制、八进制、十进制、十六进制。
int a = 7; 这里的 7 是用十进制来呈现的,但是如果将a的数据存放到内存空间中,就需要用到二进制,而一个 0 或者 1 的二进制位的大小为 1 Bit。变量a的类型为int,int类型占内存空间 4 Byte,即为32 Bit (PS. 1 Byte = 8 Bit)。
所以想要存放变量a的数据到内存空间中,就需要32位的二进制位,而进制仅仅是数据的表示形式,十进制的7换算为32位的二进制:00000000 00000000 00000000 00000111
进制仅仅只是数据的表示形式,当然,任何数都可以用二进制来表示,但是整数的二进制表示形式有三种:原码、反码、补码。对于一个数, 计算机要使用一定的编码方式进行存储,原码, 反码, 补码就是机器存储一个具体数字的编码方式
注:正整数的原码、反码、补码相同;负整数的原码、反码、补码需要计算才能得到
原码:最高位(左边第一位)为符号位,0代表正数,1代表负数。原码表示这个数据真正的数值
例如:(按 8bit 举例) 十进制的11 对应的二进制为1011,因为正整数的原码、反码、补码相同,所以原=反=补:0000 1011
十进制的 -7 对应的二进制的原码(按 8bit ): 1000 0111
反码:
正整数的原码、反码、补码相同;负整数的反码求解:在原码基础上,符号位不变,其他位按位取反。
例如:(按 8bit 举例)-7--> 二进制的原码 1000 0111 --> 反码:
1111 1000
11 -- > 二进制的原码 0000 1011--> 反码:
0000 1011
补码:正整数的原码、反码、补码相同;负整数的补码求解:在反码的基础上 +1
例如:(按 8bit 举例)-7反码:
1111 1000 --> 补码:1111 1001
11反码:
0000 1011 --> 补码: 0000 1011
存在即合理,既然存在原码、反码、补码那么就一定有它们的特殊意义。可以简单思考一下,有了符号位,就可以让计算机的减法变成加法,从而简便运算,提高计算效率
但是如果用原码进行减法的运算:(按 8bit 举例)十进制中 : 1-1=0
1-1=1+(-1)=[1] + [-1] = [0000 0001]原 + [1000 0001]原=[1000 0010]原 = -2
显然,用原码来计算减法是行不通的,于是乎就出现了反码
1-1=1+(-1)=[1] + [-1] = [0000 0001]原 + [1000 0001]原=[0000 0001]反 + [1111 1110]反=[1111 1111]反=[1000 0000]原 = -0
用反码来计算有点差强人意-->用补码试一试
1-1=1+(-1)=[1] + [-1] = [0000 0001]原 + [1000 0001]原=[0000 0001]反 + [1111 1110]反=[0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原 = 0
总之,补码实现了计算中减法也可以用加法来进行,让计算更加便捷,这也是整数在内存中存储的形式是二进制序列的补码的原因(详情见:http://t.csdnimg.cn/FeH5q)
注:在整数在内存中存储的是二进制序列的补码
二、移位操作符
<< 左移操作符
>> 右移操作符
Tips: 移位操作符的操作数只能是整数,且移位移动的是二进制位
1.<< 左移操作符
移位规则:左边丢弃,右边补0
例子1:(对正整数进行操作)
#include<stdio.h>
int main()
{
int a = 7;
int b = a << 1;
printf("a=%d\n", a);
printf("b=%d\n", b);
return 0;
}
详细的分析过程:
思考:int b = a << 1; ---> 左移操作符,移动的是二进制位,移动的是什么二进制位呢?由上可知,整数在内存中存储的是二进制序列的补码--> 计算的数据是从内存中拿取,所以此操作符的操作对象是变量a在内存中存储的二进制序列的补码
注:正整数的原码、反码、补码相同;负整数的原码、反码、补码需要计算才能得到
再啰嗦地分析一下由来:变量a 在内存中存储数据的形式是二进制,十进制的7--> 二进制 111,由于变量a的类型为int ,int 类型所占内存空间大小为 4 Byte,即 32 Bit; 而一个0或者1的二进制位的大小为 1 Bit,所以存储变量a的值需要32位的二进制位,即
00000000 00000000 00000000 00000111由于
正整数的原码、反码、补码相同,所以 7 的补码为00000000 00000000 00000000 00000111
注:<< 左移操作符的移位规则:左边丢弃,右边补0
由于正整数的原码、反码、补码相同,所以b 的原码为00000000 00000000 00000000 00001110
(反码、补码的存在是为了满足计算,原码才是
这个数值的大小表示,进制仅仅只是一种表示形式)
即 int b = 14;
例子2:(对负整数进行操作)
#include<stdio.h>
int main()
{
int a = -7;
int b = a << 1;
printf("a=%d\n", a);
printf("b=%d\n", b);
return 0;
}
代码运行结果:
int a = -7; 操作符 << 的操作对象是变量a 存放在内存空间中的二进制序列——补码
负整数的原码、反码、补码需要计算才能得到
-7 的原码:10000000 00000000 00000000 00000111
反码:(在原码的基础上,符号位不变,其它位按位取反)
11111111 11111111 11111111 11111000
补码:(在反码的基础上加1)
11111111 11111111 11111111 11111001
注:<< 左移操作符的移位规则:左边丢弃,右边补0
b 的补码:11111111 11111111 11111111 11110010
反码:(在补码的基础上-1)
11111111 11111111 11111111 11110001
原码:(在反码的基础上,符号位不变,其他位按位取反)
10000000 00000000 00000000 00001110
(注:反码、补码的存在是为了满足计算,原码才是
这个数值的大小表示,进制仅仅只是一种表示形式)
即 int b = -14 ;
2、>> 右移操作符
右移操作符的移位规则有两种,一是逻辑右移,二是算术右移
逻辑右移:左边补0,右边丢弃
算术右移:左边由原符号位填充,右边丢弃
注:VS编译器用的是算术右移(主要是看编译器,不同编译器采取的右移规则不同)
例子3:(对正整数进行操作)
#include<stdio.h>
int main()
{
int a = 7;
int b = a >> 1;
printf("a=%d\n", a);
printf("b=%d\n", b);
return 0;
}
代码运行结果:
分析过程:int b = a >> 1; 操作符 >> 的操作对象是变量a 存储在内存中的二进制序列——补码
正整数的原码、反码、补码相同。所以 a 的补码为:00000000 00000000 00000000 00000111
注:
VS编译器用的是算术右移,故而此处用算术右移演示
所以 int b = 3;
例子4:(对负整数进行操作)
#include<stdio.h>
int main()
{
int a = -7;
int b = a >> 1;
printf("a=%d\n", a);
printf("b=%d\n", b);
return 0;
}
代码运行结果:
分析:
int b = a >> 1; int a = -7; 操作符 << 的操作对象是变量a 存放在内存空间中的二进制序列——补码
负整数的原码、反码、补码需要计算才能得到
a 的原码:10000000 00000000 00000000 00000111
反码:(在原码的基础上,符号位不变,其它位按位取反)
11111111 11111111 11111111 11111000
补码:(在反码的基础上加1)
11111111 11111111 11111111 11111001
注:
VS编译器用的是算术右移,故而此处用算术右移演示
(注:反码、补码的存在是为了满足计算,原码才是
这个数值的大小表示,进制仅仅只是一种表示形式)
b 的补码:11111111 11111111 11111111 11111100
反码:(在补码的基础上-1)
11111111 11111111 11111111 11111011
原码:(在反码的基础上,符号位不变,其他位按位取反)
10000000 00000000 00000000 00000100
故 int b = -4;
思考:综上可知移位操作符-- << 左移操作符 、>> 右移操作符 的执行规则,那么移位操作符在代码的实际编写中究竟有什么用呢?移位操作符的操作数是该值存放在内存中的二进制序列——补码,那么是不是可以通过改变它在内存中存储的二进制序列来更根本地更改这个数。在此之前,我们先来来了解一下位操作符,想要实现通过二进制序列移位地方式让我们某一个二进制来到我们想要的位置上,就得用到移位操作符和位操作符。
三、位操作符
位操作符:& 、| 、^ 、~
1.& //按位与
注:按位与中的 ’位‘ 指的是二进制位,意思为按照二进制位来与。
规则:eg. a & b ; --> &的操作对象是a、b 存储在内存中的二进制序列——补码
这两个补码对应的二进制位,有0,按位与的结果便为0,两个同时为1,按位与的结果才为1
注:可以联想逻辑与(&&)是并且的意思,只要有假(0)便为假,全真(1)才为真。
举例1:
#include<stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a & b;
printf("c=%d\n", c);
return 0;
}
代码运行结果:
分析:正整数的原码、反码、补码相同;负整数的原码、反码、补码需要计算才能得到
所以 a 的补码:00000000 00000000 00000000 00000011
b 的原码:10000000 00000000 00000000 00000101
反码:(在原码的基础上,符号位不变,其他位按位取反)
11111111 11111111 11111111 11111010
补码:(在补码的基础上+1)
11111111 11111111 11111111 11111011
注:c的补码的符号位(最高位)为0,所以c 为正整数,而正整数的原码、反码、补码相同
所以 int c = 3;
2、| //按位或
注:按位或中的 ’位‘ 指的是二进制位,意思为按照二进制位来或。
规则:eg. a | b ; --> | 的操作对象是a、b 存储在内存中的二进制序列——补码
这两个补码对应的二进制位,有1,按位或的结果便为1,两个同时为0,按位或的结果才为0
注:可以联想逻辑或( || )或者的意思,只要有真(1)便为真,全假(0)才为假
举例2:
#include<stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a | b;
printf("c=%d\n", c);
return 0;
}
代码运行结果:
分析:正整数的原码、反码、补码相同;负整数的原码、反码、补码需要计算才能得到
a 的补码:00000000 00000000 00000000 00000011
b的补码: 11111111 11111111 11111111 11111011
(注:反码、补码的存在是为了满足计算,原码才是
这个数值的大小表示,进制仅仅只是一种表示形式.得到c的补码的符号位为1,意味着c是一个负数)
c 的补码:11111111 11111111 11111111 11111011
反码:(在补码的基础上-1)
11111111 11111111 11111111 11111010
原码:(在反码的基础上,符号位不变,其他位按位取反)
10000000 00000000 00000000 00000101
所以 int c = 5;
3、^ //按位异或
注:按位异或中的 ’位‘ 指的是二进制位,意思为按照二进制位来异或。
规则:eg. a ^ b ; --> ^ 的操作对象是a、b 存储在内存中的二进制序列——补码
这两个补码对应的二进制位,相同为0,相异为1
注:方便理解,从命名入手,按位异或,“异或”--> 相异就或,而'或',就联想到逻辑或,也就是说有1为1,全0才为0 --> 也就是说可以将异或这样理解:当这两个补码对应的二进制位不相同时为1.
举例3:
#include<stdio.h>
int main()
{
int a = 3;
int b = -5;
int c = a ^ b;
printf("c=%d\n", c);
return 0;
}
代码运行结果:
分析:
a 的补码:00000000 00000000 00000000 00000011
b的补码: 11111111 11111111 11111111 11111011
(注:反码、补码的存在是为了满足计算,原码才是
这个数值的大小表示,进制仅仅只是一种表示形式.得到c的补码的符号位为1,意味着c是一个负数)
c 的补码:11111111 11111111 11111111 11111000
反码:(在补码的基础上-1)
11111111 11111111 11111111 11110111
原码:(在反码的基础上,符号位不变,其他位按位取反)
10000000 00000000 00000000 00001000
所以 int c = -8;
4、~ //按位取反
注:按位取反中的 ’位‘ 指的是二进制位,意思为按照二进制位取反。
规则:将这个二进制序列位上的0变为1,1变为0
举例4:
#include<stdio.h>
int main()
{
int a = 0;
printf("%d\n", ~a);
return 0;
}
代码运行结果:
注:0 不是负的
分析:正整数的原码、反码、补码相同
a 的补码:00000000 00000000 00000000 00000000
~a : 11111111 11111111 11111111 11111111 --> 得到的是补码,符号位为负,故想得到原码需计算
反码:(在补码的基础上-1)
11111111 11111111 11111111 11111110
原码:(在反码的基础上,符号位不变,其他位按位取反)
10000000 00000000 00000000 00000001
所以输出为 -1
四、移位操作符与位操作符的实际应用
看到这里,你大概就能理解到移位操作符和位操作符的用法了,那么怎么在实际的编程中利用它们实现更改存储在内存中的二进制序列的某一位呢?
1、将二进制序列的第n位改为1(也就是说0-->1)
思考:想让0变为1,那么这个二进制位与对应的二进制位按位或( | )便可以实现
注:按位或( | ),联想到逻辑或记忆:有真(1)为真,全假(0)才为假
代码如下:
#include<stdio.h>
int main()
{
int a = 13;//a 的二进制序列:00000000 00000000 00000000 00001101
int n = 0;
scanf("%d", &n);
int b = 1;
b <<= (n - 1); //b = b <<(n-1); 将1移动到想要改变的二进制位上
a |= b; //a = a | b; 利用按位或有1为1
printf("%d\n", a);
return 0;
}
代码简化:
#include<stdio.h>
int main()
{
int a = 13;//a 的二进制序列:00000000 00000000 00000000 00001101
int n = 0;
scanf("%d", &n);
a |= (1 << (n - 1));
printf("%d\n", a);
return 0;
}
代码运行结果:
2、将二进制序列的第n位改为0(也就是说1-->0)
思考1:将1移动到想要改变的二进制位上,然后再利用 按位异或--> 相同为0,这样就可以将二进制序列的1更改为0
本质就是利用1与1相对应变为0,然后其他位没有0被影响;或者就是0与1相对应,将1变为了0,恰好按位与(&)就有这个功能,有0便为0,那么将1移位的操作不变,利用按位取反(~)
代码如下:
方法一:利用 ^
#include<stdio.h>
int main()
{
int a = 13; //a 的二进制序列:00000000 00000000 00000000 00001101
int n = 0;
scanf("%d", &n);
int b = 1;
b <<= (n - 1); // b = b<< (n-1); 将1移动到想要改变的二进制位上
a ^= b; //a = a ^ b ; 利用按位异或,相同为0,巧妙地将1更改为0
printf("%d", a);
return 0;
}
简化代码:
#include<stdio.h>
int main()
{
int a = 13; //a 的二进制序列:00000000 00000000 00000000 00001101
int n = 0;
scanf("%d", &n);
a ^= 1 << (n - 1);
printf("%d", a);
return 0;
}
代码运行结果:
方法二:利用 & 和 ~
#include<stdio.h>
int main()
{
int a = 13; //a 的二进制序列:00000000 00000000 00000000 00001101
int n = 0;
scanf("%d", &n);
int b = 1;
b <<= (n - 1); // b = b<< (n-1); 将1移动到想要改变的二进制位上
b = ~b; // 将1变为0,将0变为1
a &= b;//a = a & b; 想要更改的那一位对应的是1、0,利用&:有0便为0,全1才为1
printf("%d", a);
return 0;
}
简化代码:
#include<stdio.h>
int main()
{
int a = 13; //a 的二进制序列:00000000 00000000 00000000 00001101
int n = 0;
scanf("%d", &n);
a &= (~(1 << (n - 1)));
printf("%d", a);
return 0;
}
代码运行结果:
3、练习:求一个整数存储在内存中的二进制序列中1的个数
思考:整数在内存中存储的是补码,因正整数的原码、反码、补码相等;而负整数的原码、反码、补码需要计算才可以得到;所以,如果单纯地想要地通过这个整数数值来计算得到它存储在内存中的二进制序列中1的个数显然是行不通的。换个思路呢,在知道有多少位的二进制序列的情况下,如果我们就让这个序列向右移动32次,每次都与1的二进制序列比较判断,加上计数器,是1则count++,直达32次循环结束
代码如下:
#include<stdio.h>
int main()
{
int a = 0;
scanf("%d", &a);
int i = 0;
int count = 0;
for (i = 0; i < 31; i++)
{
if (a & 1 == 1) //巧妙在于,& :有0为0,全为1才为1--> 0就不会影响判断
{
count++;
}
a >>= 1;
}
printf("%d", count);
return 0;
}
运行结果如下:
7 的补码:00000000 00000000 00000000 00000111 ---> 有3个1
总结
1、整数的二进制序列表达形式有三种:原码、反码、补码
2、正整数的原码、反码、补码相同;负整数的原码、反码、补码需计算才能得到
3、负整数的原码、反码、补码的计算
原码-->反码--> 补码
原码--->反码:在原码基础上,符号位不变,其他位按位取反
反码-->补码 :在反码基础上+1
补码-->反码-->原码
补码-->反码:在补码基础上-1
反码--> 原码 : 在反码基础上,符号位不变,其他位按位取反
4、移位操作符:<<、>>
<< //左移操作符 :左边丢弃,右边补0
>> //右移操作符 :逻辑右移:左边补0,右边丢弃
算术右移:左边用符号位填充,右边丢弃
5、位操作符:& 、| 、^ 、~
& //按位与 :有0为0,全为1才为1
| //按位或 :有1为1,全为0才为0
^ //按位异或 :相同为0,相异为1
~ //按位取反 :0变为1,1变为0