操作符的解析
首先我们先了解下数字存放在计算机中的形式--------补码
一般数据的类型分为: 有符号型 / 无符号型
- - ( s i g n e d ) (signed) (signed) ( u n s i g n e d ) (unsigned) (unsigned) 又称 size_t
1.有符号数 ----- (正数/负数)
原码:是计算机中对数字的二进制表示方法, 就是平时正常使用二进制的方法.
反码:正数的反码补码与原码相同; 负数的反码是对其原码逐位取反, 符号位不变.
补码:正数的反码补码与原码相同; 负数的补码是反码+1.
在 32 32 32 位机器中正数的符号位是 0 0 0, 负数的符号位为 1 1 1, 并且符号位不参与运算.
数字存在计算机中有趣的运算
引言:此处引用举例的数字表达形式均为8位.(32位数太过冗长)
我们都知道, 在一个二进制位上能够出现的数字有两种可能: ( 0 / 1 0 / 1 0/1 ) , 那么8位上一共存在 $ 2^8$ 种可能
那么就是说 8 8 8 位有符号数拢共有 256 256 256 种可能吗?
答案当然是否定的: ( a b s o l u t e l y (absolutely (absolutely $ not)$
二进制表格来解析
数值 | 原码 | 反码 | 补码 |
---|---|---|---|
127 127 127 | 0111 0111 0111 1111 1111 1111 | 0111 0111 0111 1111 1111 1111 | 0111 0111 0111 1111 1111 1111 |
126 126 126 | 0111 0111 0111 1110 1110 1110 | 0111 0111 0111 1110 1110 1110 | 0111 0111 0111 1110 1110 1110 |
125 125 125 | 0111 0111 0111 1101 1101 1101 | 0111 0111 0111 1101 1101 1101 | 0111 0111 0111 1101 1101 1101 |
⋯ \cdots ⋯ ⋯ \cdots ⋯ | ⋯ \cdots ⋯ ⋯ \cdots ⋯ | ⋯ \cdots ⋯ ⋯ \cdots ⋯ | ⋯ \cdots ⋯ ⋯ \cdots ⋯ |
+ 0 +0 +0 | 0000 0000 0000 0000 0000 0000 | 0000 0000 0000 0000 0000 0000 | 0000 0000 0000 0000 0000 0000 |
− 0 -0 −0 | 1000 1000 1000 0000 0000 0000 | 1111 1111 1111 1111 1111 1111 | 0000 0000 0000 0000 0000 0000 |
− 1 -1 −1 | 1000 1000 1000 0001 0001 0001 | 1111 1111 1111 1110 1110 1110 | 1111 1111 1111 1111 1111 1111 |
− 2 -2 −2 | 1000 1000 1000 0010 0010 0010 | 1111 1111 1111 1101 1101 1101 | 1111 1111 1111 1110 1110 1110 |
⋯ \cdots ⋯ ⋯ \cdots ⋯ | ⋯ \cdots ⋯ ⋯ \cdots ⋯ | ⋯ \cdots ⋯ ⋯ \cdots ⋯ | ⋯ \cdots ⋯ ⋯ \cdots ⋯ |
− 127 -127 −127 | 1111 1111 1111 1111 1111 1111 | 1000 1000 1000 0000 0000 0000 | 1000 1000 1000 0001 0001 0001 |
通过以上表格我们能大致看出因为最高位被符号位控制,实际表达数字位只有 7 7 7 位, 故最大数 ( 7 位全为 1 ) (7位全为1) (7位全为1) 位127,反知最小数 -127
那么有符号数的取值范围是 − 127 -127 −127 ~ 127 127 127 吗? 并不是因为存在 ± \pm ± 0 所以范围应当是 − 128 -128 −128 ~ 127 127 127, // ERR warning
虽然原码反码无法表示 -128, 但**-127**补码 - 1就是 -128
计算机中8个二进制位表示为1个字节,4 个 二进制位代表一个16进制数 (方便观察).(0 ~ 15 不正好是16个数吗?)
所以4 个 bit表示1个16进制数, 1个 字节表示2个16进制数.
但实际上计算机显示 16 进制 , 储存运算还是使用 2 进制的
(16)二进制: 00000000 00000000 00000000 00010000 |
---|
**(16)十六进制: ** 0 x 00 00 00 10(大端) / 0 x 10 00 00 00(小端) |
2.无符号数 (正数)
8位无符号数的取值范围是 0 ~ 255( 2 8 − 1 2^8 - 1 28−1)
但我们不能因为简单觉得, 它是正数就对他掉以轻心!
-
我们来看两个例子
-------例1------- #include <stdio.h> unsigned char i = 0; int main() { for(i = 0;i<=255;i++) { printf("hello world\n"); } return 0; } -------例2------- unsigned int i; for(i = 9; i >= 0; i--) { printf("%u\n",i); }
long double double float unsigned long int long int unsigned int int
移位操作符
< < << <<(左移) | > > >> >>(右移) |
---|
疑惑: 计算机已经能完成基本运算为什么要有这两个操作符呢?
思考: 我们都知道计算机是模仿人的简单格式指令搭建的, 那么可以操作二进制位就像手握手术刀般精准操作.
左移⬅️⬅️⬅️
// 左移(<<) 规则:向左移多少位就丢弃多少位,右边补0.
int main()
{
int num = 10; 00000000 00000000 00000000 00001010 //左移前 10
int c = num << 1; 0|0000000 00000000 00000000 00001010(0) //左移后 20
printf("%d", c); /* 移位后num自身并未改变 */
}
右移➡️➡️➡️
//右移(>>) 规则:看编译器为,但大部分都是像右移多少位就丢弃多少位,左边补符号数.
int main()
{
int num = 10; 00000000 00000000 00000000 00001010 //右移前 10
int c = num >> 1; (0)00000000 00000000 00000000 0000101|0 //右移后 5
printf("%d", c); /* 移位后num自身并未改变 */
}
位操作符
| & (按位与) |
∣
|
∣ (按位或) | ^ (按位异或) |
| :------------: | :--------------: | :--------------: |
位操作符的使用对象是 二进制数 , 故想了解如何操作, 先得求 补码 .
位操作符的使用
int main()
{ /*important 正数原反补相同! */
int a = 3; //-5的原码 10000000 00000000 00000000 00000101
int b = -5; //-5的反码 11111111 11111111 11111111 11111010
printf("%d\n", a & b);
printf("%d\n", a | b); //-5的补码 11111111 11111111 11111111 11111011
printf("%d\n", a ^ b); // 3的补码 00000000 00000000 00000000 00000011
// 3 & -5 00000000 00000000 00000000 00000011 //补码为正数->反码原码都相同故打印 3
// 3 | -5 11111111 11111111 11111111 11111011 //补码为负数->1000000000000000101 -5
// 3 ^ -5 11111111 11111111 11111111 11111000 //补码为负数->1000000000000001000 -8
//因为%d打印的是有符号数,故补码 -> -1 -> 符号位不变其他位取反
}
趁热打铁 – 练习
已经了解了上述两类操作符,我们来几道练习题.
母题:数(四声)二进制数中 1 的个数.
母题:数(四声)二进制数中 1 的个数
// 缺陷版本 必须循环32次
int num_of_1(unsigned int c)// 为什么输入负数 不陷入死循环?
{
int count = 0;
int i = 0;
for (i = 0; i <= 31; i++)// 不知道那些位有 1 就循环32次遍历(32次判断)
{
if (c & 1)
count++;
i = c >> 1;
}
return count;
}
------------------------------------------------------------------------------------------------------------
//缺陷版本 不强制转换会进入死循环.原因:负数右移一直补1
int num_of_1(unsigned int c)
{
int count = 0;
while (c) //总体上来说while是for的优化
{
if (c % 2)
count++;
c = c >> 1;
}
return count;
}
//在初中物理我们就学过参考系不同但是运动最后产生在两个物体之间的宏观效果是一样的(相对位移,力....)
//上述方法中我们都使用的是右移 num 来判断二进制位的最左端是否为 1 正数的判断还是很轻松的.
//然而当你输入负数的时候你会发现怎么陷入死循环了捏 ? 这里又回到了右移操作符 -- 右移时左边是补充符号位的.
//正数在右移的时候 左端补的是 0 所以可以遍历完整个二进制数, 但负数左端补的是 1 也就是说移动一位就补 1 出现死循环.
------------------------------------------------------------------------------------------------------------
//优化版本 换个角度
//逆向思维 避免死循环
int main()
{
int num = 0;
scanf("%d", &num);//
int flag = 1;
int count = 0;
while (flag)
{
if (flag & num) //-5 10000000 00000000 00000000 00000101
count++; // 11111111 11111111 11111111 11111010
flag = flag << 1; // 11111111 11111111 11111111 11111011 所求 1 的个数为补码中的
} //循环停止是当 flag 为 -0 时(遍历)
printf("%d\n", count);
return 0;
}
------------------------------------------------------------------------------------------------------------
//high quality version (最终版本 在下坂本)
int main()
{
int num = 0;
scanf("%d", &num);
int count = 0;
while (num) //二进制位有 1 才进循环
{
count++; // 1000011000000000 -> 1000010 (取下一个 1 统计完就丢掉)
num = num & (num - 1); // 1000010111111111 -> 根本没用到左右移动操作符
// 完成了类似其的操作不会有副作用.
}
printf("%d", count);
return 0;
}
变式:比较两个数二进制不同的个数.
衍生(判断数字是2 ^ n):
int check(int a)
{
int count = 0;
int flag = 1;
if ((a & (a - 1)) == 0 && a > 0)
{
while (flag != a)
{
count++;
flag <<= 1;
}
return count;
}
else
return -1;
}
-----------------------------------------------------------------------------------------
以上代码只能判断当(N >= 0)正整数的情况
小数&&fu's 情况看进阶来.
lexp表示左值表达式,rexp表示右值表达式。//翻翻 6 月1 日之前的code