一、前言
本文作者是从 2007 年开始学 C语言 的,不久又接触了C++,基本就是 C/C++ 技术栈写了 14 年的样子,不算精通,但也算差强人意。著有《夜深人静写算法》系列,且承诺会持续更新,直到所有算法都学完。主要专攻 高中 OI 、大学 ACM、 职场 LeetCode 的全领域算法。由于文章中采用 C/C++ 的语法,于是就有不少读者朋友反馈语言层面就被劝退了,更何况是算法。 于是,2021 年 06 月 12 日,《光天化日学C语言》 应运而生。这个系列文章主要服务于高中生、大学生以及职场上想入坑C语言的志同道合之人,希望能给祖国引入更多编程方面的人才,并且让自己的青春不留遗憾! 这一章的主要内容是左移运算符的应用。
二、人物简介
第一位登场的就是今后会一直教我们C语言的老师 —— 光天。
第二位登场的则是今后会和大家一起学习C语言的没什么资质的小白程序猿 —— 化日。
三、左移运算符
1、左移的二进制形态
左移运算符是一个二元的位运算符,也就是有两个操作数,表示为x << y。其中x和y均为整数。
x << y念作:“将
x
x
x 左移
y
y
y 位”,这里的位当然就是二进制位了,那么它表示的意思也就是:先将
x
x
x 用二进制表示,然后再左移
y
y
y 位,并且在尾部添上
y
y
y 个零。
举个例子:对于二进制数
2
3
10
=
(
10111
)
2
23_{10} = (10111)_2
2310=(10111)2 左移
y
y
y 位的结果就是:
(
10111
0...0
⏟
y
)
2
(10111\underbrace{0...0}_{\rm y})_2
(10111y
0...0)2
2、左移的执行结果
x << y的执行结果等价于:
x
×
2
y
x \times 2^y
x×2y
如下代码:
#include
int main() {
int x = 3;
int y = 5;
printf("%d\n", x << y);
return 0;
}
输出结果为:
96
正好符合这个左移运算符的实际含义:
96
=
3
×
2
5
96 = 3 \times 2^5
96=3×25
最常用的就是当
x
=
1
x = 1
x=1 时,1 << y代表的就是
2
y
2^y
2y,即 2 的幂。
3、负数左移的执行结果
所谓负数左移,就是x << y中,当x为负数的情况,代码如下:
#include
int main() {
printf("%d\n", -1 << 1);
return 0;
}
它的输出如下:
-2
我们发现同样是满足
x
×
2
y
x \times 2^y
x×2y 的,这个可以用补码来解释,-1的补码为:
11111111
11111111
11111111
11111111
11111111 \ 11111111 \ 11111111 \ 11111111
11111111 11111111 11111111 11111111
左移一位后,最高位的 1 就没了,低位补上 0,得到:
11111111
11111111
11111111
11111110
11111111 \ 11111111 \ 11111111 \ 11111110
11111111 11111111 11111111 11111110
而这,正好是 -2的补码,同样,继续左移 1 位,得到:
11111111
11111111
11111111
11111100
11111111 \ 11111111 \ 11111111 \ 11111100
11111111 11111111 11111111 11111100
这是-4的补码,以此类推,所以负整数的左移结果同样也是
x
×
2
y
x \times 2^y
x×2y。
可以理解成 - (x << y)和(-x) << y是等价的。
4、左移负数位是什么情况
刚才我们讨论了
x
<
0
x < 0
x<0 的情况,那么接下来,我们试下
y
<
0
y < 0
y<0 的情况会是如何?
是否同样满足:
x
×
2
y
x \times 2^y
x×2y 呢?
如果还是满足,那么两个整数的左移就有可能产生小数了。
看个例子:
#include
int main() {
printf("%d\n", 32 << -1); // 16
printf("%d\n", 32 << -2); // 8
printf("%d\n", 32 << -3); // 4
printf("%d\n", 32 << -4); // 2
printf("%d\n", 32 << -5); // 1
printf("%d\n", 32 << -6); // 0
printf("%d\n", 32 << -7); // 0
return 0;
}
虽然能够正常运行,但是结果好像不是我们期望的,而且会报警告如下:
[Warning] left shift count is negative [-Wshift-count-negative]
实际上,编辑器告诉我们尽量不用左移的时候用负数,但是它的执行结果不能算错误,起码例子里面对了,结果不会出现小数,而是取整了。
左移负数位其实效果和右移对应正数数值位一致,右移相关的内容,我们会在 光天化日学C语言(19)- 位运算 >> 的应用 中讲到。
5、左移时溢出会如何
我们知道,int类型的数都是 32 位的,最高位代表符号位,那么假设最高位为 1,次高位为 0,左移以后,符号位会变成 0,会产生什么问题呢?
举个例子,对于
−
2
31
+
1
-2^{31}+1
−231+1 的二进制表示为:最高位和最低位为1,其余为零。
#include
int main() {
int x = 0b10000000000000000000000000000001;
printf("%d\n", x); // -2147483647
return 0;
}
输出结果为:
-2147483647
那么,将它进行左移一位以后,得到的结果是什么呢?
#include
int main() {
int x = 0b10000000000000000000000000000001;
printf("%d\n", x << 1);
return 0;
}
我们盲猜一下,最高位的 1 被移出去,最低位补上 0,结果应该是0b10。
实际输出的结果,的确是:
2
但是如果按照
x
×
2
y
x \times 2^y
x×2y 答案应该是 $$(-2^{31}+1) \times 2 = -2^{32}+2$$
这里又回到了补码的问题上,事实上,在计算机中,int整型其实是一个环,溢出以后又会回来,而环的长度正好是
2
32
2^{32}
232,所以
−
2
32
+
2
=
2
-2^{32}+2 = 2
−232+2=2,这个就有点像同余的概念,这两个数是模
2
32
2^{32}
232 同余的。更多关于同余的知识,可以参考我的算法系列文章:夜深人静写算法(三)- 初等数论入门(学生党记得找我开试读)。
四、左移运算符的应用
1、取模转化成位运算
对于
x
x
x 模上一个 2 的次幂的数
y
y
y,我们可以转换成位与上
2
y
−
1
2^y-1
2y−1。
即在数学上的:
x
m
o
d
2
y
x \ mod \ 2^y
x mod 2y
在计算机中就可以用一行代码表示:x & ((1 << y) - 1)。
2、生成标记码
我们可以用左移运算符来实现标记码,即1 << k作为第
k
k
k 个标记位的标记码,这样就可以通过一句话,实现对标记位置 0、置 1、取反等操作。
1)标记位置1
【例题1】对于
x
x
x 这个数,我们希望对它二进制位的第
k
k
k 位(从0开始,从低到高数)置为 1。
置 1 操作,让我们联想到了 位或 运算。
它的特点是:位或上 1,结果为 1;位或上0,结果不变。
所以我们对标记码的要求是:第
k
k
k 位为 1,其它位为 0,正好是(1 << k),那么将 第
k
k
k 位 置为 1 的语句可以写成:x | (1 << k)。
有关位或运算的更多内容,可以参考:光天化日学C语言(15)- 位运算 | 的应用。
2)标记位置0
【例题2】对于
x
x
x 这个数,我们希望对它二进制位的第
k
k
k 位(从0开始,从低到高数)置为 0。
置 0 操作,让我们联想到了 位与 运算。
它的特点是:位与上 0,结果为 0;位与上 1,结果不变。
所以在我们对标记码的要求是:第
k
k
k 位为 0,其它位为 1,我们需要的是(~(1 << k)),那么将 第
k
k
k 位 置为 0 的语句可以写成:x & (~(1 << k))。
有关位与运算的更多内容,可以参考:光天化日学C语言(14)- 位运算 & 的应用。
有关 按位取反 运算的更多内容,可以参考:光天化日学C语言(17)- 位运算 ~ 的应用。
3)标记位取反
【例题3】对于
x
x
x 这个数,我们希望对它二进制位的第
k
k
k 位(从0开始,从低到高数)取反。
取反操作,联想到的是 异或 运算。
它的特点是:异或上 1,结果取反;异或上 0,结果不变。
所以我们对标记码的要求是:第
k
k
k 位为1,其余位为 0,其值为(1 << k)。那么将 第
k
k
k 位 取反的语句可以写成:x ^ (1 << k)。
有关 异或 运算的更多内容,可以参考:光天化日学C语言(16)- 位运算 ^ 的应用。
3、生成掩码
同样,我们可以用左移来生成一个掩码,完成对某个数的二进制末
k
k
k 位执行一些操作。
对于(1 << k)的二进制表示为:1 加上 k 个 0,那么 (1 << k) - 1的二进制则代表
k
k
k 个 1。
把末尾的
k
k
k 位都变成 1,可以写成:x | ((1 << k) - 1)。
把末尾的
k
k
k 为都变成 0,可以写成:x & ~((1 << k) - 1)。
把末尾的
k
k
k 位都取反,可以写成:x ^ ((1 << k) - 1)。
通过这一章,我们学会了: 1)位运算 << 的用法; 2)用 << 来生成标记位; 3)用 << 来生成掩码;
希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!
课后习题
【第43题】左移的应用 | 一句话判断一个数是否是 2 的幂
📢博客主页:https://blog.csdn.net/WhereIsHeroFrom📢欢迎各位 👍点赞 ⭐收藏 📝评论,如有错误请留言指正,非常感谢!📢本文由 英雄哪里出来 原创,转载请注明出处,首发于 🙉 CSDN 🙉作者的专栏: 👉C语言基础专栏《光天化日学C语言》 👉C语言基础配套试题详解《C语言入门100例》 👉算法进阶专栏《夜深人静写算法》