1.了解二进制
其实二进制;八进制;十进制和十六进制都是数值的不同表示形式而已
- 二进制:基数为2,由0和1两个数字组成,逢2进1。
- 八进制:基数为8,由0~7八个数字组成,逢8进1。
- 十进制:基数为10,由0~9十个数字组成,逢10进1。
- 十六进制:基数为16,由0~9十个数字和A/a(10);B/b(11);C/c(12);D/d(13);E/e(14);F/f(15)六个数字共同组成,逢16进1。
(1)二进制转十进制
位权:以十进制为例
百位 | 十位 | 个位 | |
---|---|---|---|
十进制的位 | 1 | 2 | 3 |
权 | 10^2 | 10^1 | 10^0 |
按权展开 | 100 | 10 | 1 |
求值 | 1*100+ | 2*10+ | 3*1 |
注意:
- 基数的多少次幂,这样的数被称为位权
- 任何数的1次方都等于其本身;任何数的0次方都等于1
因此,二进制转十进制的做法如下:以二进制的111为例
二进制的位 | 1 | 1 | 1 |
---|---|---|---|
权 | 2^2 | 2^1 | 2^0 |
按权展开 | 4 | 2 | 1 |
求值 | 1*4+ | 1*2+ | 1*1 |
(2)十进制转二进制
口诀:转2除2,倒取余,从下往上依次排序
如:将十进制的10转为二进制
注意: 当里面的数小于外面的基数时,也就是除不开的时候,里面的数就是最后的余数
(3)二进制转八进制
方法:将3个二进制数划为一组,按421这个上标的顺序标上标(分组是从后往前分,标上标是从前往后标),不足3位的在最前边补0(0标上标后依旧是0,不参与运算)其余的每组上标相加得出结果,最后每个组的结果合并就是转换完成的八进制数
如:将二进制数1101111转换为八进制的数
二进制数: | 1101111 |
---|---|
分组后 | 001 101 111 |
上标 | 421 421 421 |
八进制数结果 | 1 + 5 + 7 = 157 |
注意: 以0开头的数字会被当做八进制
(4)二进制转十六进制
方法:将4个二进制数划为一组,按8421这个上标的顺序标上标(分组是从后往前分,标上标是从前往后标),不足4位的在最前边补0(0标上标后依旧是0,不参与运算)其余的每组上标相加得出结果,最后每个组的结果合并就是转换完成的十六进制数
如:将二进制数110110111转换为十六进制的数
二进制数: | 0001 1011 0111 |
---|---|
分组后 | 0001 1011 0111 |
上标 | 8421 8421 8421 |
十六进制数结果 | 1 + b + 7 = 0x1b7 |
注意:
- 转换成十六进制时,10~15这些数字必须用字母表示
- 表示十六进制的时候前面要加
0x
2.原码、反码和补码
整数的二进制表示方法有3种:原码、反码和补码
这三种表示方法均有符号位和数值位两部分,符号位都是用0表示正、用1表示负;而数值位最高位的一位是被当做符号位,剩余的都是数值位。
正整数的原码、反码和补码都相同,但负整数的三种表示方法各不相同:
- 原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码
- 反码:将原码的符号位(从左往右数第一位就是符号位)不变,数值位(除去符号位的其它位)依次按位取反就可以得到反码
- 补码:反码+1就得到补码
如:
int
类型 5 的原码;反码和补码
如:
int
类型 -5 的原码;反码和补码
为什么表示5的是32位二进制数?
因为1字节等于8比特,而1个int类型占4个字节,也就是32比特位,所以用32个二进制数来表示
对于整型来说,数据存放内存中其实存放的是补码。 因为在计算机系统中,数值一律用补码来表示和存储。原因在于使用补码可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器);此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
3.移位操作符
<<
左移操作符>>
右移操作符
(1)左移操作符
移位规则:左边丢弃,右边补上0
代码示例:将
num
左移一位
#include <stdio.h>
int main()
{
int num = 10;
// num的二进制表示:0000000000 0000000000 0000001010
int ret = num << 1;
// num << 1的移位结果:000000000 0000000000 00000010100
printf("num = %d\n",num);
printf("ret = %d\n",ret);
return 0;
}
注意:
- 整个过程
num
本身的值是不会发生改变的,只有将移位过程赋值给num
本身的时候值才会发生改变 - 无论是正数还是负数,在内存中移动运算的都是补码
(2)右移操作符
- 逻辑右移:左边用0填充,右边丢弃
- 算术右移:左边用原该值的符号位填充,右边丢弃
注意:
右移所采用的是逻辑右移,还是算术右移是不确定的,取决于编译器。但是大部分的编译器是采用算术右移的
代码示例:将
num
右移一位(逻辑右移)
#include <stdio.h>
int main()
{
int num = -1;
// num的二进制表示:1111111111 1111111111 1111111111
int ret = num >> 1;
// num >> 1逻辑右移的移位结果:01111111111 1111111111 111111111
printf("num = %d\n", num);
printf("ret = %d\n", ret);
return 0;
}
代码示例:将
num
右移一位(算术右移)
#include <stdio.h>
int main()
{
int num = -1;
// num的二进制表示:1111111111 1111111111 1111111111
int ret = num >> 1;
// num >> 1算术右移的移位结果:1111111111 1111111111 1111111111
printf("num = %d\n", num);
printf("ret = %d\n", ret);
return 0;
}
注意:
- 移位操作符的操作数只能是整数,不能是浮点数
- 对于移位操作符,不要移动负数位,这个标准是未定义的
//如:
int num = 10;
num >> -1; //移动负数位是错误的
4.位操作符
位操作符有:
& // 按位与
| // 按位或
^ // 按位异或
注意:
- 以上位操作符的操作数只能是整数
- 位指的是二进制位
多说无益,代码示例:
(1)按位与的运算规则:对应的二进制位上,有0则为0,两个同时为1才为1
#include <stdio.h>
int main()
{
// &
int a = 5;
int b = -6;
int c = a & b; // 按位与
// a的二进制表示:0000000000 0000000000 0000000101
// b的二进制表示:1111111111 1111111111 1111111010
printf("%d\n",c);
// a & b:0000000000 0000000000 0000000000
return 0;
}
(2)按位或的运算规则:对应的二进制位上,有1则为1,两个同时为0才为0
#include <stdio.h>
int main()
{
// |
int a = 5;
int b = -6;
int c = a | b; // 按位或
// a的二进制表示:0000000000 0000000000 0000000101
// b的二进制表示:1111111111 1111111111 1111111010
printf("%d\n",c);
// a | b:1111111111 1111111111 1111111111
return 0;
}
(3)按位异或的运算规则:对应的二进制位上,相同则为0,相异则为1
#include <stdio.h>
int main()
{
// ^
int a = 5;
int b = -6;
int c = a ^ b; // 按位异或
// a的二进制表示:0000000000 0000000000 0000000101
// b的二进制表示:1111111111 1111111111 1111111010
printf("%d\n",c);
// a ^ b:1111111111 1111111111 1111111111
return 0;
}
练习1:实现两个数的交换
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
int tmp = 0;
printf("交换前:a = %d b = %d\n",a,b);
tmp = a;
a = b;
b = tmp;
printf("交换后:a = %d b = %d\n",a,b);
return 0;
}
练习2:不能创建临时变量(第三个变量),实现两个数的交换
// 第一种方法
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("交换前:a = %d b = %d\n",a,b);
a = a + b;
b = a - b;
a = a - b;
printf("交换后:a = %d b = %d\n",a,b);
return 0;
}
注意: 第一种的这个方法有缺陷,当a和b的数特别大的时候会导致范围越界
// 第二种方法
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("交换前:a = %d b = %d\n",a,b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("交换后:a = %d b = %d\n",a,b);
return 0;
}
1. 异或运算符的特点:
- a ^ a = 0
- 0 ^ a = a
- a ^ a ^ b = b
- a ^ b ^ a = b
通过以上的特点分析得出一个结论:异或是支持交换律的
2. 异或运算的局限性:
- 异或只能用于整数交换
- 代码的可读性较差
- 代码的执行效率也是低于使用第3变量的方法的
练习3:求一个整数存储在内存中的二进制中1的个数
// 第一种方法
#include <stdio.h>
int main()
{
int a = 15;
int count = 0;
while(a)
{
if(a % 2 == 1)
{
count++;
}
a = a / 2;
}
printf("count = %d\n",count);
return 0;
}
// 第二种方法
#include <stdio.h>
int main()
{
int a = 0;
scanf("%d",&a);
int i = 0;
int count = 0;
for(i<0; i<32; i++)
{
if(((a>>i) & 1) == 1)
{
count++;
}
}
printf("count = %d\n",count);
return 0;
}
// 第三种方法
#include <stdio.h>
int main()
{
int n = 0;
int count = 0;
scanf("%d",&n);
while(n)
{
n = n & (n-1);
count++;
}
printf("count = %d\n",count);
return 0;
}
练习4:判断一个数是否是2^n次方
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d",&n);
if(n & (n - 1) == 0)
{
printf("yes\n");
}
else
{
printf("no\n");
}
return 0;
}