重温C语言(9)之位运算

Feb 5, 2020


1 运算符

~,&,|,^,<<,>>,&=,|=,^=,>>=,<<=

2 二进制

以2为基底表示的数字为二进制。

二进制1111可表示为:

1*2^3 + 1*2^2 + 1*2^1 + 1*2^0

字节表示储存系统字符集所需的大小,1字节包含8位。 二进制无符号整数的范围是 0 ~ 255; 有符号整数的范围是 -127 ~ 127,因为八位的第一位储存符号(1负 0正),只剩下7位表示数字本身。也会产生 +0 和 -0 两个0.

原码,反码, 补码

计算机中存储的数据都是以二进制码存储的,程序对正数和负数都要进行计算,因此增加了符号位来区分正数和负数。计算机以二进制补码来表示有符号位。

计算机中的运算器只有加法运算器,所以计算机只能做加法,减一个数的运算也是通过加法实现相当于加上一个这个数的相反数。

  • 正数:原码=反码=补码
  • 负数:原码(符号位是1)=反码(除符号位,其他位取反)=补码(反码+1)
  • 0:原码=反码=补码=0

以8位二进制为例: 一个二进制和按位取反的结果相加,和必定是 11111111-1, 例如十进制2 00000010 反码是 11111101 它两相加(00000010 + 11111101 = 11111111)结果是 -1, 即 数+相反数=-1 得出 数+相反数+1=0 因而补码是 相反数+1 的结果。

2 - 2 举例: 即 2 + (-2),2在程序中的补码是00000010,-2 在程序中的原码是 10000010 反码是 11111101 补码(计算机中存储的数值)是 11111110 可以得出 00000010 + 11111110 = 00000000

6 - 8 举例: 即 6 + (-8),6在程序中的补码是00000110,-8 在程序中的原码是 10001000 反码是 11110111 补码(计算机中存储的数值)是 11111000 可以得出 00000110 + 11111000 = 1111111011111110-2 的补码。

以此类推,总的来说补码让计算机可以进行减法运算。

3 按位逻辑运算符

(1)二进制反码或按位取反: ~

-(10011010) 的结果是 (01100101), 取反就是0变1,1变0.

    unsigned char a = 2;  // 00000010
    a = ~a;
    printf("%d", a);   // 11111101 即输出 253
    char a = 2;  // 00000010
    printf("%d", ~a);   
    //输出 -3,因为~a取反是11111101第一个是符号位,计算机判断负数,取补码 10000011 即 -3

(2)按位与: &

(10010011)&(00111101) 结果是 00010001, 按位与就是两个对比位都是1才是1,否则是0.

    unsigned char a = 2;   // 00000010
    unsigned char b = 1;    // 00000001
    printf("%d", a & b);    // 00000000

使用掩码,就是按位与运算的过程。

(3)按位或:|

(10010011)|(00111101) 结果是 10111111, 按位与就是两个对比位只要有一位是1结果就是是1,否则是0.

(4)按位异或: ^

(10010011)^(00111101) 结果是 10101110, 按位与就是两个对比位只要不同结果就是是1,否则是0.

4 移位运算符

(1)左移:<<

(10010011) << 2 的结果是 01001100, 左移运算即把每一位向左移动指定位数,移出左端的值丢失,右端空出的位置补0. 一个数左移2位相当于该数乘以2的2次幂的结果。

(2)右移:>>

对无符号类型 (10010011) >> 2 的结果是 00100100; 对有符号位 (10010011) >> 2 的结果是 0010010011100100, 根据系统的不同补充位可能是0也可能是符号位的副本; 右移运算即把每一位向右移动指定位数,移出右端的值丢失,对有符号值左端空出的位置根据系统的不同补0或符号位的副本,对无符号值用0填充。

对于非负数,一个数右移2位相当于该数除以2的2次幂的结果。

编程:十进制转换为二进制

#include "limits.h"

// 这个方法将整数转换为二进制字符串
char * itobs(int n, char * ps){
    int i;
    const static int size = CHAR_BIT * sizeof(int);
    for (i = size - 1; i >= 0; i--, n >>= 1) {
        ps[i] = (01 & n) + '0';
    }
    ps[size] = '\0';
    return ps;
}
    printf("sizeof(int) = %ld\n", sizeof(int));
    char bit_str[CHAR_BIT * sizeof(int) + 1];
    int number = 11;
    itobs(number, bit_str);
    printf("%d 的二进制是:\n", number);
    for (int i = 0; bit_str[i]; ) {
        putchar(bit_str[i++]);
    }

输出:

sizeof(int) = 4
11 的二进制是:
00000000000000000000000000001011

-11 的二进制是:
11111111111111111111111111110101

5 位字段

struct {
    unsigned int a: 1;
    unsigned int b: 1;
    unsigned int c: 1;
    unsigned int d: 1;
} prnt;

prnt 包含 4 个 1位的字段,a,b,c,d是位字段,它们都确定了字段的宽度是1, 因为只有1位,所以只能赋值 0或1。 位字段是一个signedint或unsignedint类型变量中的一组相邻的位。可以通过位字段来规定字段的大小。

6 对齐特性

_Alignof运算符给出一个类型的对齐要求,在关键字_Alignof后面的圆括号中写上类型名即可:

size_t f_align = _Alignof(float);

如果 f_align 的值是4,就代表float 类型对象的对其要求是4,即存储float类型值需要相邻地址的字节数是4.

使用_Alignas说明符指定一个变量或类型的对齐值。但是,不应该要求该值小于基本对齐值。

_Alignas(float) char c1;
_Alignas(8) char c2;

C11在stdlib.h库还添加了一个新的内存分配函数,用于对齐动态分配的内存。该函数的原型如下:

void * aligned_alloc(size_t alignment, size_t size);

第1个参数代表指定的对齐,第2个参数是所需的字节数,其值应是第1个参数的倍数。与其他内存分配函数一样,要使用free()函数释放之前分配的内存。