第15章 位操作

发布于 2021-10-31  48 次阅读


在C语言中,可以单独操控变量中的位。
例如:通常向硬件设备发送一两个字节来控制这些设备,其中每个位(bit)都有特定的含义。
另外,与文件相关的操作系统信息经常被储存,通过使用特定位表明特定项,许多压缩和加密操作都是直接处理单独的位,高级语言一般不会处理这个级别的细节,C在提供高级语言的便利的同时,还能再位汇编语言所保留的级别上工作,这使其成为编写设备驱动程序和嵌入式代码的首选语言。

15.1 二进制数、位和字节

通常都是基于数字10来书写数字。
计算机的位只有2位,0或1,打开或关闭,计算机适用于基底为二进制(binary number)系统。
用二进制系统可以把任意整数(如果有足够的位)表示为0和1的组合。

15.1.1 二进制整数

file

15.1.2 有符号整数

补码,反码

15.1.3 二进制浮点数

浮点数分两部分储存:二进制小数和二进制指数。

1.二进制小数

一个普通的浮点数0.257,表示如下:
5/10+2/100+1/1000
从左往右,各分母都是10的递增次幂。在二进制小数中,使用2的幂作为分母,所以二进制小数,101表示为:
1/2+0/4+1/8
用十进制表示法为:
0.5+0+0.125
即为0.625
许多分数(1/3)不能用十进制表示法精确地表示。与次类似的,许多分数也不能用二进制表示法准确的表示。实际上,二进制表示法只能精确地表示多个1/2的幂的和。因此,。3/4和7/8可以紧缺的表示为二进制小数,但是1/3和2/5却不能

2.浮点数表示法

为了在计算机中表示一个浮点数,要留出若干位(因系统而异)储存二进制分数,其他位存储指数,一般而言,数字的实际值是由二进制小数乘2的指定次幂组成。例如,一个浮点数乘4,那么二进制小数不变,其指数乘以2,二进制分数不变,如果一份浮点数乘以一个不是2的幂的数,会改变二进制小数部分,如果有必要,也会改变指数部分。

15.2 其他进制数

八进制和十六进制

15.3 C按位运算符

C提供按位逻辑运算符和移位运算符。
在程序中一般用25和031或者0x19而不是00011001.

15.3.1 按位逻辑运算符

4个按位逻辑运算符都用于整型数据,包括char。之所以叫做按位(bitwise)运算,是因为这些操作都是针对每一个位进行,不影响它左右两边的位。
不要把这些运算符与常规的逻辑运算符(&&,||和!)混淆,常规的逻辑运算符操作的是整个值。

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

一元运算符~把1变成0,把0变成1.
~(10011010)     //表达式
(01100101)      //结果值
假设val的类型是unsigned char,已被赋值为2,在二进制中,00000010表示为2.那么,~val的值是11111101,即为253,注意,该运算符不会改变val的值,就像3*val不会改变val的值一样,val仍然是2,但是,该运算符确实创建了一个可以使用或赋值的新值:
newvak = ~val;
printf("%d",~val);
如果要把val的值改为~val,使用下面这条语句:
val = ~val;

2.按位与:&

二元运算符&通过逐位比较两个运算对象。生成一个新值。对于每个位,只有两个运算对象中相应的位都为1时,结果才为1:
(10010011) & (00111101):
(00010001)  //结果
C有一个按位与和赋值结合的运算符:
val &= 0377;
val = val & 0377;

3.按位或:|

二元运算符|,通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位为1,结果就为1.
(10010011) | (00111101):
(10111111)
C有一个按位或和赋值运算符:|=
val |= 0377;
val = val |0377;

4.按位异或:^

file

(10010011) ^ (00111101):
(10101110)
C有一个按位异或和赋值结合的运算符: ^=
val ^= 0377;
val = val ^ 0377;

15.3.2 用法:掩码

按位与运算符常用于验码(mask).所谓验码指的是一些设置位开(1)或关(0)的位组合。要明白成为验码的原因,先来看通过&把一个量与验码结合后发生什么情况。
例如:假设定义符号常量位MASK位2(00000010)。只有1号位是1,其他位都是0.
flags = flags & MASK;
把flags中除1号位以外的所有位都设置为0,因为使用按位与运算符(&)任何位与0组合都得0。1号位的值不变。这个过程叫做使用掩码。因为验码中的0隐藏了flags中相应的位。
可以这样例比:把验码中的0看作不透明,1看作透明。表达式flags & MASK相当于用掩码覆盖在flags的位组合上,只有MASK为1的位才可见。

file

用&=运算符可以简化前面的代码:
flags &= MASK;
下面这条语句是按位与的一种常见用法:
ch &= 0xff;
这个掩码保持ch中最后8位不变。其他位置都是指位0,无论ch原来是8位、16位或者是其他更多位,最终值都被修改位1个8位字节。在该例中,掩码的宽度为8位

15.3.3 用法:打开位(设置位)

有时,需要一个值中的特定位,同时保持其他位不变。例如,一台IBM PC通过向端口发送值来控制硬件。例如,为了打开内置扬声器,必须打开1号位,同时保持其他位不变。这种情况可以用按位或运算符(|).
flags = flags | MASK;
把flags的1号位设置位1,其他位不变。因为使用|运算符,任何与0组合,结果都为本身:任何位与1组合,结果都为1.
例如:假设flags是00001111,MASK是10110110.下面的表达式:
flags | MASK:
(10111111)      //结果值
同样,这种方法根据MASK 中为1的位,把flags中对应的位设置为1,其他位不变。

15.3.4 用法:关闭位(清空位)

和打开特定的位类似,有时也需要在不影响其他位的情况下关闭指定的位,假设要关闭变量flags中的1号位。同样,MASK只有1号位位1.可以这样做。
flags = flags & ~MASK;

15.3.5 用法:切换位

切换位指的是打开已关闭的位,或关闭已经打开的位。可以使用按位异或运算切换位。也就是说,假设b是一个位,如果b位1,则1^b=0如果b为0,1^b=1.另外,无论b为1还是0,0^b均为b,因此,如果使用^组合一个值和一个掩码,将切换该值与MASK为1的位相对应的位,该值与MASK为0的为相对不变,要切换flags中的1号位,可以使用下面两种方法:
flags = flags ^MASK;
flags ^= MASK;
例如:
假设flags是00001111,MASK是10110110表达式:
flags ^ MASK:
(10111001)
flags中与MASK为1的位相对应的位都被切换了,MASK为0的相对应的位不变。

15.3.6 用法:检查位的值

前面介绍了如何改变位的值。有时,需要检查某位的值。例如,flags中1号位是否被设置为1?不能这样直接比较flags和MASK:
if(flags==MASK)
    puts("Wow!");     //不能正常工作
这样做即使flags 的1号位为1,其他位的值会导致比较结果为假。因此,必须覆盖flags中的其他位,只用1号位和MASK比较:
if((flags & MASK) == MASK)
    puts("Wow!");
由于按位运算符的优先级比==低,所以必须在flags & MASK 周围加上圆括号。
为了避免信息漏过边界,掩码至少要与其覆盖的值宽度相同。

15.3.7 移位运算符

1. 左移(<<)

左移运算符(<<)将其左侧运算对象每一位的值向左移动其右侧对象指定的位数。左侧运算符对象移出左末端位的值丢失,用0填充空出来的位置。
(10001010) << 2 :
(00101000)      //结果
该操作产生了一个新的值的位值,但是不改变其运算对象。例如:假设stonk位1,那么stonk <<2为4,但是stonk本身不变,仍然为1.可以使用左移赋值运算符(<<=)更改变量的值。该运算符将变量中的位向左移动其右侧运算对象给定值的位数。如:
int stonk = 1;
int onkoo;
onkoo = stonk <<2;            //onkoo = 4;
stonk <<= 2;

2.右移:>>

右移运算符(>>)将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出末尾的值丢失,对于无符号类型,用0填充空出的位置:对于有符号类型,其结果取决于机器,空出的位置可用0填充,或者用符号位的副本填充。
(10001010) >> 2       //表达式,有符号值
(00100010)          //在某些系统中的结果
(10001010) >> 2       //表达式,有符号数
(11100010)          //在另外一些系统上的结果值
下面是无符号值的例子:
(10001010) >> 2       //表达式,无符号值
(00100010)          //所有系统都得到该结果
每个位向右移动两个位置,空出的位用0填充。
右移赋值运算符(>>=)将其左侧的变量向右移动指定数量的位数,如下所示:
int sweet = 16;
int ooosw;

ooosw = sweet >> 3;       //ooosw =2,sweet的值仍然位16
sweet >>= 3;          //sweet的值位2

3.用法:移位运算符

移位运算符针对2的幂提供快速有效的乘法和除法。
number << n           number 乘以2的n次幂
number >> n           如果number为非负,则用number除以2的n次幂
这些运算符类似于在十进制中移动小数电来乘以或除以10.
移位运算符还可以用于从较大单元中提取一些位。例如,假设用一个unsigned long类型的值表示颜色值,低阶位字节储存红色的强度,下一个字节储存绿色的强度,第3个字节储存蓝色的强度,随后你希望把每种颜色强度分别储存在3个不同的unsigned char类型的变量中。那么,可以使用下面的语句:
#define BYTE_MASK 0xff
unsigned long color = 0x002a162f;
unsigned char blue,green,red;
red = color & BYTE_MASK;
green = (color >> 8) & BYTE_MASK;
bule = (color >> 16) & BYTE_MASK;
以上代码中,使用右移运算符将8位颜色值移动至低字节,然后使用掩码技术把低阶字符赋给指定变量。

15.3.8 编程示例

//读取用户从键盘输入的整数,将该整数和一个字符串地址传递给itobs()函数(itobs表示interger to binary string,即整数转换乘二进制字符串)。然后,该函数使用移位运算符计算出正确的1和0的组合,并将其放入字符串中。
//binbit.c  --使用位操作显示二进制
//binbit.c
#include<stdio.h>
#include<limits.h>            //提供CHAR_BIT的定义,CHAR_BIT表示每字节的位数
char *itobs(int,char *);
void show_bstr(const char *);

int main(void)
{
    char bin_str[CHAR_BIT*sizeof(int)+1];
    int number;

    puts("Enter integers and see them binary.");
    puts("Non-numeric input terminates program.");
    while(scanf("%d",&number) == 1)
    {
        itobs(number,bin_str);
        printf("%d is ",number);
        show_bstr(bin_str);
        putchar('\n');
    }   
    puts("Bye!");

    return 0;
} 
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;
}

//4位一组显示二进制字符串
void show_bstr(const char *str)
{
    int i = 0;

    while(str[i])       //不是一个空字符
    {
        putchar(str[i]);
        if(++i%4==0 && str[i])
            putchar(' '); 
    }    
 } 

file

程序清单使用limits.h中的CHAR_BIT宏,该宏表示char中的位数。sizeof运算符返回char的大小,所以表达式CHAR_BIT*sizeof(int)表示int类型的位数。bin_str数组的元素个数是CHAR_BIT * sizeof(int) +1,留出一个位置给末尾的空字符。
itobs()函数返回的地址与传入的地址相同,可以把该函数作为printf()的参数。在该函数中,首次执行for循环时,对01&n求值。01是一个8进制形式的掩码,该掩码除0号位是1之外,其他所有位都为0,因此,01 & n就是n最后一位的值。该值为0或1.但是对数组而言,需要的是字符'0'或者字符'1'.该值加上'0'即可完成这种转换。其结果存放在数组中倒数第2个元素中。(最后一个元素用来存放空字符)
顺带一提,用1&n或者01&n都可以。我们用八进制1而不是十进制1,只是为了更接近计算机的表达方式。
然后,循环执行i--和n>>=1.i--移动到数组的前一个元素,n >>=1使n中的所有位向右移动一个位置。进入下一轮迭代时,循环中处理的是n中新的最右端的值。然后,把该值储存在倒数第3个元素中,以此类推。itobs()函数用这种方式从右往左填充数组。
可以使用printf()或puts()函数显示最终的字符串,但是程序定义了show_bstr()函数,以4位一组打印字符串,方便阅读。

15.3.9 另一个例子

要编写的函数用于切换一个值中的后n位,待处理值和n都是函数的参数。
~运算符切换一个字节的所有位,而不是选定的少数为,但是,^运算符可用于切换单个位。假设创建了一个掩码,把后n位设置为1,其余未设置为0.然后使用^组合掩码和待切换的值便可以切换该值的最后n位,而且其他位不变。方法如下:
int inver_end(int num,int bits)
{
    int mask = 0;
    int bitval = 1;

    while(bits -->0)
    {
        mask |=bitval;
        bitval <<=1;
    }
    return num^mask;
}
while循环用于创建所需的掩码。最初,mask的所有位都为0.
第一轮循环将mask的0号位设置为1.然后第2轮循环mask的1号位设置为1,以此类推。循环bits此,mask的后bits位就都被设置位1,最后,num ^ mask 运算即得所需的结果。
把这个函数放入到前面的程序中,测试该函数。
//binbit.c
#include<stdio.h>
#include<limits.h>            //提供CHAR_BIT的定义,CHAR_BIT表示每字节的位数
char *itobs(int,char *);
void show_bstr(const char *);
 int inver_end(int num,int bits);

int main(void)
{
    char bin_str[CHAR_BIT*sizeof(int)+1];
    int number;

    puts("Enter integers and see them binary.");
    puts("Non-numeric input terminates program.");
    while(scanf("%d",&number) == 1)
    {
        itobs(number,bin_str);
        printf("%d is \n",number);
        show_bstr(bin_str);
        putchar('\n');
        number = inver_end(number,4);
        printf("Inverting the last 4 bits gives\n");
        show_bstr(itobs(number,bin_str));
        putchar('\n');
    }   
    puts("Bye!");

    return 0;
} 
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;
}

//4位一组显示二进制字符串
void show_bstr(const char *str)
{
    int i = 0;

    while(str[i])       //不是一个空字符
    {
        putchar(str[i]);
        if(++i%4==0 && str[i])
            putchar(' '); 
    }    
 } 

 int inver_end(int num,int bits)
{
    int mask = 0;
    int bitval = 1;

    while(bits -->0)
    {
        mask |=bitval;
        bitval <<=1;
    }
    return num^mask;
}

file

15.4 位字段

操控位的第2种方法是位字段(bit field)。位字段是一个signed int或unsigned int类型变量中的一组相邻的位(C99和C11新增加了_Bool类型的位字段)。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。
例如:下面的声明建立了一个4个1位的字段:
struct{
    unsigned int autfd : 1;
    unsigned int bldfc : 1;
    unsigned int undln : 1;
    unsigned int itals : 1;
}prnt;
根据该声明,prnt包含4个1位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值。
prnt.itals = 0;
prnt.undln = 1;
由于每个字段恰好为1位,所以只能为其赋值1或0。变量prnt被储存在int大小的内存单元中,但是在本例中只使用了其中的4位。
带有位字段的结构提供一种记录设置的方便途径。许多设置(如,字体的粗体或斜体)就是简单的二选一。例如,开或关、真或假。如果只需要使用1位,就不需要使用整个变量。内含位字段的结构允许在一个存储单元中储存多个设置。
有时,某些设置也有多个选择,因此需要多位来表示。这没问题,字段不限制1位大小。可以使用如下的代码:
struct {
    unsigned int code1 : 2;
    unsigned int code2 : 2;
    unsigned int code3 : 8;
}procode;
以上代码创建了两个2位的字段和一个8位的字段。可以这样赋值
procode.code1 = 0;
procode.code2 = 3;
procode.code3 = 102;
但是,要确保所赋的值不超过字段可容纳的范围。
如果声明的总位数超过了一个unsigned int类型的大小会怎样?会用到下一个unsigned int类型的存储位置。一个字段不允许跨越两个unsigned int之间的边界编译器会自动鱼洞跨界的字段,保持unsigned int的边界对齐。一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”。
可以用未命名的字段宽度“填充”未命名的“洞”。使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐:
struct{
    unsigned int field1     : 1;
    unsigned int            : 2;
    unsigned int field2     : 1;
    unsigned int            : 0;
    unsigned int field3     : 1;
}stuff;
这里,在stuff.filed1和stuff.filed2之间有一个2位的空隙:stuff.field3将储存在下一个unsigned int中。
字段储存在一个int中的顺序取决于机器,在有些机器上,存储顺序是从左往右,而另一些机器上,是从右往左的。另外,不同机器中两个字段边界的位置也有区别。由于这些原因,位字段通常都不容易移植。尽管如此,有些情况下却要用到这种不可抑制的特性,如:以特定硬件设备所用的形式储存数据。

15.4.1 位字段示例

通常,把位字段作为一种更紧凑储存数据的方式,如,假设要在屏幕上表示一个方框的属性。为简化问题,假设方框具有如下属性:
- 方框是透明的或不透明的;
- 方框的填充色选自一下调色板:黑色、红色、绿色、黄色、蓝色、紫色、青色或白色;
- 边框可见或隐藏;
- 边框颜色与填充色使用相同的调色板;
- 边框可以使用实现、点线或虚线样式。
可以使用单独的变量或全长(full-sized)结构成员来表示每个属性,但是这样有些浪费位。例如,只需1位即可以表示方框是否透明,只需1位即可以表示边框是显示还是隐藏。8种颜色可以使用3位单元的8个可能的值来表示,而3种边框样式也只需2位单元即可表示。总共10位就足够表示方框的5个属性设置。
一种方案是:一个字节储存方框的内部(透明还和填充色)的属性,一个字节储存方框边框的舒总,每个字节种的空袭用未命名名字字段填充,struct box_props声明如下:
struct box_props{
    bool opaque     : 1 ;
    unsigned int fill_color     : 3;
    unsigned int        : 4;
    bool show_border        : 1;
    unsigned int border_color       : 3;
    unsigned int border_style       : 2;
    unsigned int            : 2;
};
加上未命名的字段,该结构共占16位。如果不使用填充,改结构占用10位。但是要记住,C以unsigned int作为位字段结构的基本布局单元,因此,即使一个结构唯一的成员是1位字段,改结构的大小也是一个unsigned int类型的大小,unsigned int在我们的系统中是32位的。另外,以上代码假设C99新增的—Bool类型可用,在stdbool.h中,bool是_Bool的别名。
对于opaque成员,1表示方框不透明,0表示透明,show_border成员也用类似方法。
对于颜色。可以用简单的RGB表示。这些颜色都是三原色的混合。显示器通过混合红、绿、蓝像素来产生不同的颜色。
早期的计算机色彩中,每个像素都可以打开或关闭,所以可以使用用1位来表示三原色中每个二进制颜色的亮度。常用的顺序是,左侧位标识蓝色亮度、中间位标识绿色、右红色的亮度。
fill_color成员合border_color成员可以使用这些组合。最后,border_style成员可以使用0、1、2来表示实线、点线和虚线样式。

file

使用box_props结构,该程序用#define创建供结构成员使用的符号常量。
注意,只打开一位即可以表示三原色之一。其他颜色用三原色的组合来表示。例如:紫色由打开的蓝色位和红色位组成,所以,紫色可以表示位BLUE|RED
//fields.c      --定义并使用字段
//fields.c  --定义并使用字段
#include<stdio.h>
#include<stdbool.h>       //C99定义了bool、true、false 
//线的样式 
#define SOLID 0
#define DOTTED 1
#define DASHED 2
//三原色
#define BLUE 4
#define GREEN 2
#define RED 1
//混合色
#define BLACK 0
#define YELLOW (RED | GREEN)
#define MAGENTA (RED | BLUE)
#define CYAN (GREEN | BLUE)
#define WHITE (RED | GREEN | BLUE)

const char *colors[8] = {"black","red","green","yellow","blue","magenta","cyan","white"};
struct box_props{
    bool opaque                 : 1 ;
    unsigned int fill_color     : 3 ;
    unsigned int                : 4 ;
    bool show_border            : 1 ;
    unsigned int border_color   : 3 ;
    unsigned int border_style   : 2 ;
    unsigned int                : 2 ;
}; 
void show_settings(const struct box_props *pb);

int main(void)
{
    //创建并初始化box_props结构
    struct box_props box = {true,YELLOW,true,GREEN,DASHED};

    printf("Original box settings:\n");
    show_settings(&box);

    box.opaque = false;
    box.fill_color = WHITE;
    box.border_color = MAGENTA;
    box.border_style = SOLID;
    printf("\nModified box setting:\n");
    show_settings(&box);

    return 0;
}
void show_settings(const struct box_props *pb)
{
    printf("Box is %s.\n",pb->opaque == true? "opaque":"transparent");
    printf("The fill color is %s.\n",colors[pb->fill_color]);
    printf("Border %s.\n",pb->show_border == true?"shown":"not shown");
    printf("The border color is %s.\n",colors[pb->border_color]);
    printf("The border style is ");
    switch(pb->border_style)
    {
        case SOLID:printf("solid.\n");
            break;
        case DOTTED:printf("dotted.\n");
            break;
        case DASHED:printf("dashed.\n");
            break;
        default:    printf("Unknow type.\n");
    }
}

file

该程序。
首先,初始化字段结构与初始化普通结构的语法相同:
struct box_props box = {true,YELLOW,true,GREEN,DASHED};
类似的,也可以给位字段成员赋值:
box.fill_color = WHITE;
另外,switch语句中也可以使用位字段成员,甚至可以把位字段成员用作数组的小标:
printf("The border color is %s.\n",colors[pb->border_color]);
注意:根据colors数组的定义,每个缩影对应一个表示颜色的字符串,而每种颜色都把索引值作为该颜色的数值。例如,索引1对应字符串“red”.枚举常量red的值是1.

15.4.2 位字段和按位运算

在同类型的编程问题中,位字段和按位运算符是两种可替换的方法,用哪种方法都可以。
可以通过一个联合把结构方法和位方法放在一起。假定声明了struct box_props类型,然后这样声明联合:
union Views         //把数据看作结构或unsigned short类型的变量
{
    struct box_props st_view;
    unsigned short us_view;
};
在某些系统中,unsigned int和box_props类型的结构都占用16位内存。但是,在其他系统中,unsigned int 和box_props都是32位。无论哪种情况,通过联合,都可以使用st_view成员把一块内存看作是一个结构,或者使用us_view成员把相同的内存块看作是一个unsigned short。结构的哪一位字段与unsigned short中的哪一位相对应?这取决于实现和硬件。下面的程序示例假设从字节的低阶位端载入结构。也就是说,结构中的第1个位字段对应计算机的0号位。

file

下述程序清单使用Views联合来比较位字段和按位运算符这两种方法。
在该程序中,box是View联合,所以box.st_view是一个使用位字段的box_props类型的结构,box.us_view把相同的数据看作是一个unsigned short类型的变量。联合只允许初始化第1个成员,所以初始化值必须与结构相匹配。该程序分别通过两个函数显示box的属性,一个函数接受一个结构,一个函数接受一个unsigned short类型的值。这两种方法都能访问数据,但是所用的技术不同。该程序还使用了本章前面定义的itobs()函数,以二进制字符串形式显示数据,以便读者查看每个位的开闭情况。

擦肩而过的概率