☀️光天化日学C语言☀️(18)- 位运算 << 的应用 | 左移的一些高端用法

举报
英雄哪里出来 发表于 2021/07/16 07:36:47 2021/07/16
【摘要】 左移的一些高端用法

一、前言

  本文作者是从 2007 年开始学 C语言 的,不久又接触了C++,基本就是 C/C++ 技术栈写了 14 年的样子,不算精通,但也算差强人意。著有《夜深人静写算法》系列,且承诺会持续更新,直到所有算法都学完。主要专攻 高中 OI 、大学 ACM、 职场 LeetCode 的全领域算法。由于文章中采用 C/C++ 的语法,于是就有不少读者朋友反馈语言层面就被劝退了,更何况是算法。
  于是,2021 年 06 月 12 日,《光天化日学C语言》 应运而生。这个系列文章主要服务于高中生、大学生以及职场上想入坑C语言的志同道合之人,希望能给祖国引入更多编程方面的人才,并且让自己的青春不留遗憾!
  这一章的主要内容是左移运算符的应用。

二、人物简介

  • 第一位登场的就是今后会一直教我们C语言的老师 —— 光天。
  • 第二位登场的则是今后会和大家一起学习C语言的没什么资质的小白程序猿 —— 化日。

三、左移运算符

1、左移的二进制形态

  • 左移运算符是一个二元的位运算符,也就是有两个操作数,表示为x << y。其中xy均为整数。
  • x << y念作:“将 x x 左移 y y 位”,这里的位当然就是二进制位了,那么它表示的意思也就是:先将 x x 用二进制表示,然后再左移 y y 位,并且在尾部添上 y y 个零。
  • 举个例子:对于二进制数 2 3 10 = ( 10111 ) 2 23_{10} = (10111)_2 左移 y y 位的结果就是:

( 10111 0...0 y ) 2 (10111\underbrace{0...0}_{\rm y})_2

2、左移的执行结果

  • x << y的执行结果等价于:
  • x × 2 y x \times 2^y

  • 如下代码:
#include <stdio.h>
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

最常用的就是当 x = 1 x = 1 时,1 << y代表的就是 2 y 2^y ,即 2 的幂。

3、负数左移的执行结果

  • 所谓负数左移,就是x << y中,当x为负数的情况,代码如下:
#include <stdio.h>
int main() {
    printf("%d\n", -1 << 1);
    return 0;
}
  • 它的输出如下:
-2
  • 我们发现同样是满足 x × 2 y x \times 2^y 的,这个可以用补码来解释,-1的补码为:
  • 11111111   11111111   11111111   11111111 11111111 \ 11111111 \ 11111111 \ 11111111

  • 左移一位后,最高位的 1 就没了,低位补上 0,得到:
  • 11111111   11111111   11111111   11111110 11111111 \ 11111111 \ 11111111 \ 11111110

  • 而这,正好是 -2的补码,同样,继续左移 1 位,得到:
  • 11111111   11111111   11111111   11111100 11111111 \ 11111111 \ 11111111 \ 11111100

  • 这是-4的补码,以此类推,所以负整数的左移结果同样也是 x × 2 y x \times 2^y

可以理解成 - (x << y)(-x) << y是等价的。

4、左移负数位是什么情况

  • 刚才我们讨论了 x < 0 x < 0 的情况,那么接下来,我们试下 y < 0 y < 0 的情况会是如何?
  • 是否同样满足: x × 2 y x \times 2^y 呢?
  • 如果还是满足,那么两个整数的左移就有可能产生小数了。
  • 看个例子:
#include <stdio.h>
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 的二进制表示为:最高位和最低位为1,其余为零。
#include <stdio.h>
int main() {
    int x = 0b10000000000000000000000000000001;
    printf("%d\n", x);                          // -2147483647
    return 0;
}
  • 输出结果为:
-2147483647
  • 那么,将它进行左移一位以后,得到的结果是什么呢?
#include <stdio.h>
int main() {
    int x = 0b10000000000000000000000000000001;
    printf("%d\n", x << 1);
    return 0;
}

  • 我们盲猜一下,最高位的 1 被移出去,最低位补上 0,结果应该是0b10
  • 实际输出的结果,的确是:
2
  • 但是如果按照 x × 2 y x \times 2^y 答案应该是 $$(-2^{31}+1) \times 2 = -2^{32}+2$$
  • 这里又回到了补码的问题上,事实上,在计算机中,int整型其实是一个环,溢出以后又会回来,而环的长度正好是 2 32 2^{32} ,所以 2 32 + 2 = 2 -2^{32}+2 = 2 ,这个就有点像同余的概念,这两个数是模 2 32 2^{32} 同余的。更多关于同余的知识,可以参考我的算法系列文章:夜深人静写算法(三)- 初等数论入门(学生党记得找我开试读)。

四、左移运算符的应用

1、取模转化成位运算

  • 对于 x x 模上一个 2 的次幂的数 y y ,我们可以转换成位与上 2 y 1 2^y-1
  • 即在数学上的:
  • x   m o d   2 y x \ mod \ 2^y

  • 在计算机中就可以用一行代码表示:x & ((1 << y) - 1)

2、生成标记码

  我们可以用左移运算符来实现标记码,即1 << k作为第 k k 个标记位的标记码,这样就可以通过一句话,实现对标记位置 0、置 1、取反等操作。

1)标记位置1

【例题1】对于 x x 这个数,我们希望对它二进制位的第 k k 位(从0开始,从低到高数)置为 1。

  • 置 1 操作,让我们联想到了 位或 运算。
  • 它的特点是:位或上 1,结果为 1;位或上0,结果不变。
  • 所以我们对标记码的要求是:第 k k 位为 1,其它位为 0,正好是(1 << k),那么将 第 k k 位 置为 1 的语句可以写成:x | (1 << k)
  • 有关位或运算的更多内容,可以参考:光天化日学C语言(15)- 位运算 | 的应用

2)标记位置0

【例题2】对于 x x 这个数,我们希望对它二进制位的第 k k 位(从0开始,从低到高数)置为 0。

  • 置 0 操作,让我们联想到了 位与 运算。
  • 它的特点是:位与上 0,结果为 0;位与上 1,结果不变。
  • 所以在我们对标记码的要求是:第 k k 位为 0,其它位为 1,我们需要的是(~(1 << k)),那么将 第 k k 位 置为 0 的语句可以写成:x & (~(1 << k))
  • 有关位与运算的更多内容,可以参考:光天化日学C语言(14)- 位运算 & 的应用
  • 有关 按位取反 运算的更多内容,可以参考:光天化日学C语言(17)- 位运算 ~ 的应用

3)标记位取反

【例题3】对于 x x 这个数,我们希望对它二进制位的第 k k 位(从0开始,从低到高数)取反。

  • 取反操作,联想到的是 异或 运算。
  • 它的特点是:异或上 1,结果取反;异或上 0,结果不变。
  • 所以我们对标记码的要求是:第 k k 位为1,其余位为 0,其值为(1 << k)。那么将 第 k k 位 取反的语句可以写成:x ^ (1 << k)
  • 有关 异或 运算的更多内容,可以参考:光天化日学C语言(16)- 位运算 ^ 的应用

3、生成掩码

  • 同样,我们可以用左移来生成一个掩码,完成对某个数的二进制末 k k 位执行一些操作。
  • 对于(1 << k)的二进制表示为:1 加上 k 个 0,那么 (1 << k) - 1的二进制则代表 k k 个 1。
  • 把末尾的 k k 位都变成 1,可以写成:x | ((1 << k) - 1)
  • 把末尾的 k k 为都变成 0,可以写成:x & ~((1 << k) - 1)
  • 把末尾的 k k 位都取反,可以写成:x ^ ((1 << k) - 1)

通过这一章,我们学会了:
  1)位运算 << 的用法;
  2)用 << 来生成标记位;
  3)用 << 来生成掩码;

  • 希望对你有帮助哦 ~ 祝大家早日成为 C 语言大神!

课后习题


📢博客主页:https://blog.csdn.net/WhereIsHeroFrom
📢欢迎各位 👍点赞 ⭐收藏 📝评论,如有错误请留言指正,非常感谢!
📢本文由 英雄哪里出来 原创,转载请注明出处,首发于 🙉 CSDN 🙉
作者的专栏:
  👉C语言基础专栏《光天化日学C语言》
  👉C语言基础配套试题详解《C语言入门100例》
  👉算法进阶专栏《夜深人静写算法》

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。