逻辑移位和算术移位

作者:jicanmeng

时间:2016年06月27日


逻辑左移和算术左移相同,都是在右侧补零。

逻辑右移时,在左侧补零。算术右移时,在左侧补符号位。

stackoverflow上面有如下的描述:

Note that <<< is not an operator, because it would be redundant. Also note that C and C++ do not distingiush between the right shift operators. They provide only the >> operator, and the shifting behavior is implementation defined.


2017.07.31补充

示例程序如下:

#include <stdio.h>

typedef unsigned char *byte_pointer;

void show_bytes(byte_pointer start, int len)
{
    for (int i = 0; i < len; i++) {
        printf("%.2x ", start[i]);
    }
    printf("\n");
}

int main(void){
    /* 1. left shift */
    char ch_a = 0x35;
    char ch_b = ch_a << 2;

    show_bytes((byte_pointer)&ch_b, 1);

    /* 2. right shift */
    char ch_c = 0xf3;
    unsigned char ch_d = 0xf3;

    char ch_e = ch_c >> 2;
    unsigned char ch_f = ch_d >> 2;

    show_bytes((byte_pointer)&ch_e, 1);
    show_bytes((byte_pointer)&ch_f, 1);

    return 0;
}

运行结果如下:

[jicanmeng@andy tmp]$ ./a.out
                d4
                fc
                3c
                [jicanmeng@andy tmp]$ 

有符号char型变量ch_c和无符号char型变量ch_d都左移2位,为什么结果不一样呢?很显然,ch_c使用了算术右移,ch_d使用了逻辑右移。

其实,上面这个简单的程序,涉及到了另外三个重要的知识点:

  1. 零扩展和符号扩展
  2. 类型转换
  3. 赋值运算

首先看第一个知识点:零扩展和符号扩展。将一个占字节较少的数据类型的变量转换为一个占字节较多的数据类型时,就会发生扩展。如果变量是无符号数,就会发生零扩展。如果变量是有符号数,就会发生符号扩展。

看下面的例子:

#include <stdio.h>

int main(void){
	/* 1. char ->int, unsigned char->unsigned int */
	char ch_a = 0x80;
	unsigned char ch_b = 0x80;
	int i_a = ch_a;
	unsigned int ui_b = ch_b;
	printf("i_a is 0x%x, i_b is 0x%x\n", i_a, ui_b);

	/* 2. char->unsigned int */
	ui_b = ch_a;
	printf("i_b is 0x%x\n", ui_b);

	return 0;
}

运行结果如下:

[jicanmeng@andy tmp]$ ./a.out
                i_a is 0xffffff80, i_b is 0x80
                i_b is 0xffffff80
                [jicanmeng@andy tmp]$ 

这个程序比较有意思的是最后一个结果。将char型变量ch_a转换为unsigned int类型的变量,顺序是怎么样的呢?实际上,(unsigned int)ch_a等价于(unsigned int)(int)ch_a, 而不是(unsigned int)(unsigned char)ch_a。在《深入理解计算机系统》第2章也提到这样的例子,里面提到,这个规则是C语言标准要求的。

再看第二个知识点:类型转换。其实上面提到的零扩展和符号扩展就是一种特殊的类型变换。c语言中二元操作符执行运算时,都会进行隐式的类型转换,例如加、减、乘、除、移位等运算。规则如下:

  1. If an operand is an integer that is narrower than an int, it undergoes integral promotion to int.
  2. If the operands still do not match, then the compiler finds the highest priority operand and implicitly converts the other operand to match.

举例说明如下:

#include <stdio.h>
#include <typeinfo>
#include <iostream>

int main(void){
    /* 1. 加、减、乘、除、移位等运算时,进行隐式的类型转换, 左右两边转换为相同的类型  */
    short a(4);
    short b(5);
    std::cout << typeid(a + b).name() << " " << a + b << std::endl;

    float d(4.0);
    short s(2);
    std::cout << typeid(d + s).name() << " " << d + s << std::endl;

    std::cout << 5u - 10 << std::endl;

    char z_high = 0xff;
    printf("typeid of z_high is %s\n", typeid(z_high).name());
    printf("typeid of z_high << 8 is %s\n", typeid(z_high << 8).name());

    unsigned char z_high2 = 0xff;
    printf("typeid of z_high is %s\n", typeid(z_high2).name());
    printf("typeid of z_high << 8 is %s\n\n", typeid(z_high2 << 8).name());

    /* 2. 赋值时,进行隐式的类型转换,参数列表中的数据转换为要求的类型 */
    unsigned int vcc = 0xf2;
    printf("cc is 0x%x\n", vcc);
    char vdd = 0xf2;
    vcc = vdd;
    printf("cc is 0x%x\n", vcc);

    return 0;
}

运行结果如下:

[jicanmeng@andy tmp]$ ./a.out
                int 9
                float 6
                4294967291
                typeid of z_high is char
                typeid of z_high << 8 is int
                typeid of z_high is unsigned char
                typeid of z_high << 8 is int

                cc is 0xf2
                cc is 0xfffffff2
                [jicanmeng@andy tmp]$ 

最后看第三个知识点:赋值运算。对于整数之间的运算,其实就是内存拷贝。将浮点数(单双精度)转换为整数时,将舍弃浮点数的小数部分,只保留整数部分。将整型值赋给浮点型变量,会检查这个整数位于哪两个浮点数之间,离哪个浮点数近,那么浮点型变量就保存哪个浮点型变量的值(浮点数在浮点数的表示方法一文中有详细讲解)。

举例说明如下:

#include <stdio.h>
#include <limits.h>

int main(void){
    /* 1. 整型和整型赋值 */
    unsigned int ui_a = UINT_MAX;
    int i_a = ui_a;
    printf("i_a is %d\n", i_a);

    /* 2. 整型和浮点型赋值 */
    int i_b = 8388608.754;
    printf("i_b is %d\n", i_b);

    float f_a = 8388608.7;
    printf("f_a is %f\n", f_a);

    return 0;
}

运行结果如下:

[jicanmeng@andy tmp]$ ./a.out
                    i_a is -1
                    i_b is 8388608
                    f_a is 8388609.000000
                [jicanmeng@andy tmp]$ 

这三个知识点都明白后,那么最开始的例子的运行结果肯定也能理解了。char ch_e = ch_c >> 2;这一行代码中,>>是一个二元操作符,所以首先将ch_c进行符号扩展,成为int类型,成为0xfffffff3, 这是一个有符号数,右移两位后成为0xfffffffc,然后赋值给左侧的char类型的变量ch_e,直接进行内存截取,所以ch_e的值为0xfc。

类似地,char ch_f = ch_d >> 2;这一行代码中,>>是一个二元操作符,所以首先将ch_d进行零扩展,成为int类型,成为0x000000f3, 这是一个有符号数,右移两位后成为0x0000003c,然后赋值给左侧的unsigned char类型的变量ch_f,直接进行内存截取,所以ch_f的值为0x3c。

参考资料

  1. What are bitwise shift (bit-shift) operators and how do they work?
    http://stackoverflow.com/questions/141525/what-are-bitwise-shift-bit-shift-operators-and-how-do-they-work