🍐位操作
2021-1-26
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

 

二进制数、位和字节

用二进制系统可以把任意整数(如果有足够的位)表示为0和1的组合。由于数字计算机通过关闭和打开状态的组合来表示信息,这两种状态分别用0和1来表示,所以使用这套数制系统非常方便

二进制整数

通常,1字节包含8位。C语言用字节(byte)表示储存系统字符集所需的大小,所以C字节可能是8位、9位、16位或其他值。不过,描述存储器芯片和数据传输率中所用的字节指的是8位字节。为了简化起见,假设1字节是8位(计算机界通常用八位组这个术语特指8位字节)。可以从左往右给这8位分别编号为7~0。在1字节中,编号是7的位被称为高阶位,编号是0的位被称为低阶位。每 1位的编号对应2的相应指数。
notion image
这里,128是2的7次幂,以此类推。该字节能表示的最大数字是把所有位都设置为1:11111111。这个二进制数的值是:
而该字节最小的二进制数是00000000,其值为0。因此,1字节可储存0~255范围内的数字,总共256个值。或者,通过不同的方式解释位组合(bit pattern),程序可以用1字节储存-128~+127范围内的整数,总共还是256个值。例如,通常unsigned char用1字节表示的范围是0~255,而signedchar用1字节表示的范围是-128~+127。
 

有符号整数

如何表示有符号整数取决于硬件,而不是C语言。也许表示有符号数最简单的方式是用1位(如,高阶位)储存符号,只剩下7位表示数字本身(假设储存在1字节中)。用这种符号量表示法,10000001表示−1,00000001表示1。因此,其表示范围是−127~+127。
这种方法的缺点是有两个0:+0和-0。这很容易混淆,而且用两个位组合来表示一个值也有些浪费。
二进制补码(two’s-complement)方法避免了这个问题,是当今最常用的系统。以1字节为例二进制补码用1字节中的后7位表示0~127,高阶位设置为0。目前,这种方法和符号量的方法相同。另外,如果高阶位是1,表示的值为负。这两种方法的区别在于如何确定负值。从一个9位组合100000000(256的二进制形式)减去一个负数的位组合,结果是该负值的量。例如,假设一个负值的位组合是 10000000,作为一个无符号字节,该组合为表示 128;作为一个有符号值,该组合表示负值(编码是 7的位为1),而且值为100000000-10000000,即1000000(128)。因此,该数是-128(在符号量表示法中,该位组合表示−0)。类似地,10000001 是−127,11111111 是−1。该方法可以表示−128~+127范围内的数。
要得到一个二进制补码数的相反数,最简单的方法是反转每一位(即0变为1,1变为0),然后加1。因为1是00000001,那么−1则是11111110+1,或11111111。
二进制反码(one’s-complement)方法通过反转位组合中的每一位形成一个负数。例如,00000001是1,那么11111110是−1。这种方法也有一个−0:11111111。该方法能表示-127~+127之间的数。
 

二进制浮点数

浮点数分两部分储存:二进制小数和二进制指数。
二进制小数
一个普通的浮点数0.527,表示如下:
从左往右,各分母都是10的递增次幂。在二进制小数中,使用2的幂作为分母,所以二进制小数.101表示为:
用十进制表示法为:
许多分数(如,1/3)不能用十进制表示法精确地表示。与此类似,许多分数也不能用二进制表示法准确地表示。实际上,二进制表示法只能精确地表示多个1/2的幂的和。因此,3/4和7/8可以精确地表示为二进制小数,但是1/3和2/5却不能。
 
浮点数表示法
为了在计算机中表示一个浮点数,要留出若干位(因系统而异)储存二进制分数,其他位储存指数。一般而言,数字的实际值是由二进制小数乘以2的指定次幂组成。例如,一个浮点数乘以4,那么二进制小数不变,其指数乘以2,二进制分数不变。如果一份浮点数乘以一个不是2的幂的数,会改变二进制小数部分,如有必要,也会改变指数部分
 

其他进制数

八进制

八进制(octal)是指八进制记数系统。该系统基于8的幂,用0~7表示数字(正如十进制用0~9表示数字一样)。例如,八进制数451(在C中写作0451)表示为: (十进制) 了解八进制的一个简单的方法是,每个八进制位对应3个二进制位。这种关系使得八进制与二进制之间的转换很容易。例如,八进制数0377的二进制形式是11111111。即,用111代替0377中的最后一个7,再用111代替倒数第2个7,最后用011代替3,并舍去第1位的0。这表明比0377大的八进制要用多个字节表示。这是八进制唯一不方便的地方:一个3位的八进制数可能要用9位二进制数来表示。注意,将八进制数转换为二进制形式时,不能去掉中间的0。例如,八进制数0173的二进制形式是01111011,不是0111111。
notion image

十六进制

十六进制(hexadecimal或hex)是指十六进制记数系统。该系统基于16的幂,用0~15表示数字。但是,由于没有单独的数(digit,即0~9这样单独一位的数)表示10~15,所以用字母A~F来表示。例如,十六进制数A3F(在C中写作0xA3F)表示为: (十进制) 由于A表示10,F表示15。在C语言中,A~F既可用小写也可用大写。因此,2623也可写作0xa3f。 每个十六进制位都对应一个4位的二进制数(即4个二进制位),那么两个十六进制位恰好对应一个8位字节。第1个十六进制表示前4位,第2个十六进制位表示后4位。因此,十六进制很适合表示字节值。 下表列出了各进制之间的对应关系。例如,十六进制值0xC2可转换为11000010。相反,二进制值11010101可以看作是1101 0101,可转换为0xD5。
notion image
 

按位运算符

按位逻辑运算符

按位取反:~

一元运算符~把1变为0,把0变为1。 假设val的类型是unsigned char,已被赋值为2。在二进制中,00000010表示2。那么,~val的值是11111101,即253。该运算符不会改变val的值。
如果要把val的值改为~val,使用下面这条语句:
 

按位与:&

二元运算符&通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位都为1时,结果才为1(从真/假方面看,只有当两个位都为真时,结果才为真)。
 
C有一个按位与和赋值结合的运算符:&=
 

按位或:|

二元运算符|,通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位为1,结果就为1(从真/假方面看,如果两个运算对象中相应的一个位为真或两个位都为真,那么结果为真)。
C有一个按位或和赋值结合的运算符:|=
 

按位异或:^

二元运算符^逐位比较两个运算对象。对于每个位,如果两个运算对象中相应的位一个为1(但不是两个为1),结果为1(从真/假方面看,如果两个运算对象中相应的一个位为真且不是两个为同为1,那么结果为真)。
C有一个按位异或和赋值结合的运算符:^=
 

掩码(mask)

按位与运算符常用于掩码(mask)。所谓掩码指的是一些设置为开(1)或关(0)的位组合。
例如,假设定义符号常量MASK为2 (即,二进制形式为00000010),只有1号位是1,其他位都是0。下面的语句:
把flags中除1号位以外的所有位都设置为0。这个过程叫作“使用掩码”,因为掩码中的0隐藏了flags中相应的位。
notion image
用&=运算符可以简化前面的代码
 
下面这条语句是按位与的一种常见用法:
过oxff的二进制形式是11111111,八进制形式是0377。这个掩码保持ch中最后8位不变,其他位都设置为0。无论ch原来是8位、16位或是其他更多位,最终的值都被修改为1个8位字节。
 

打开位(设置位)

有时,需要打开一个值中的特定位,同时保持其他位不变。例如,一台IBM PC 通过向端口发送值来控制硬件。例如,为了打开内置扬声器,必须打开 1 号位,同时保持其他位不变。这种情况可以使用按位或运算符(|)。
用|=运算符可以简化上面的代码
 

关闭位(清空位)

和打开特定的位类似,有时也需要在不影响其他位的情况下关闭指定的位。假设要关闭变量flags中的1号位。同样,MASK只有1号位为1(即,打开)。可以这样做:
由于MASK除1号位为1以外,其他位全为0,所以~MASK除1号位为0以外,其他位全为1。使用&,任何位与1组合都得本身,所以这条语句保持1号位不变,改变其他各位。另外,使用&,任何位与0组合都的0。所以无论1号位的初始值是什么,都将其设置为0。
 

切换位

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

检查位的值

有时,需要检查某位的值。例如,flags中1号位是否被设置为1?
 
 
 

移位运算符

左移:<<
移运算符(<<)将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。
右移:>>
右移运算符(>>)将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出右末端位的值丢。对于无符号类型,用0 填充空出的位置;对于有符号类型,其结果取决于机器。空出的位置可用0填充,或者用符号位(即,最左端的位)的副本填充:
下面是无符号值的例子:
 
移位运算符针对2的幂提供快速有效的乘法和除法:
 
移位运算符还可用于从较大单元中提取一些位。例如,假设用一个unsigned long类型的值表示颜色值,低阶位字节储存红色的强度,下一个字节储存绿色的强度,第 3 个字节储存蓝色的强度。随后你希望把每种颜色的强度分别储存在3个不同的unsigned char类型的变量中。那么,可以使用下面的语句:
以上代码中,使用右移运算符将 8 位颜色值移动至低阶字节,然后使用掩码技术把低阶字节赋给指定的变量。
 
 
 
 
 

位字段

操控位的第2种方法是位字段(bit field)。位字段是一个signed int或unsigned int类型变量中的一组相邻的位(C99和C11新增了_Bool类型的位字段)。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。
prnt包含4个1位的字段。现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值:
由于每个字段恰好为1位,所以只能为其赋值1或0。变量prnt被储存在int大小的内存单元中,但是在本例中只使用了其中的4位。
 
带有位字段的结构提供一种记录设置的方便途径。许多设置(如,字体的粗体或斜体)就是简单的二选一。例如,开或关、真或假。如果只需要使用 1 位,就不需要使用整个变量。内含位字段的结构允许在一个存储单元中储存多个设置。 有时,某些设置也有多个选择,因此需要多位来表示。这没问题,字段不限制 1 位大小
以上代码创建了两个2位的字段和一个8位的字段。可以这样赋值:
如果声明的总位数超过了一个unsigned int类型的大小会怎样?会用到下一个unsigned int类型的存储位置。一个字段不允许跨越两个unsigned int之间的边界。编译器会自动移动跨界的字段,保持unsigned int的边界对齐。一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”。
可以用未命名的字段宽度“填充”未命名的“洞”。使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐:
这里,在stuff.field1和stuff.field2之间,有一个2位的空隙;stuff.field3将储存在下一个unsigned int中。 字段储存在一个int中的顺序取决于机器。在有些机器上,存储的顺序是从左往右,而在另一些机器上,是从右往左。另外,不同的机器中两个字段边界的位置也有区别。由于这些原因,位字段通常都不容易移植。尽管如此,有些情况却要用到这种不可移植的特性。例如,以特定硬件设备所用的形式储存数据。
 
 
通常,把位字段作为一种更紧凑储存数据的方式。例如,假设要在屏幕上表示一个方框的属性。为简化问题,我们假设方框具有如下属性:
  • 方框是透明的或不透明的;
  • 方框的填充色选自以下调色板:黑色、红色、绿色、黄色、蓝色、紫色、青色或白色;
  • 边框可见或隐藏;
  • 边框颜色与填充色使用相同的调色板;
  • 边框可以使用实线、点线或虚线样式。
可以使用单独的变量或全长(full-sized)结构成员来表示每个属性,但是这样做有些浪费位。例如,只需1位即可表示方框是透明还是不透明;只需1位即可表示边框是显示还是隐藏。8种颜色可以用3位单元的8个可能的值来表示,而3种边框样式也只需2位单元即可表示。总共10位就足够表示方框的5个属性设置。
一种方案是:一个字节储存方框内部(透明和填充色)的属性,一个字节储存方框边框的属性,每个字节中的空隙用未命名字段填充。struct box_props声明如下:
加上未命名的字段,该结构共占用 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来表示实线、点线和虚线样式。
notion image
notion image
初始化位字段结构与初始化普通结构的语法相同:
类似地,也可以给位字段成员赋值:
另外,switch语句中也可以使用位字段成员,甚至还可以把位字段成员用作数组的下标:
 

位字段和按位运算符

在同类型的编程问题中,位字段和按位运算符是两种可替换的方法,用哪种方法都可以。例如,前面使用和unsigned int类型大小相同的结构储存图形框的信息。也可使用unsigned int变量储存相同的信息。如果不想用结构成员表示法来访问不同的部分,也可以使用按位运算符来操作。一般而言,这种方法比较麻烦。接下来研究这两种方法。
可以通过一个联合把结构方法和位方法放在一起。假定声明了 struct box_props 类型,然后这样声明联合:
在某些系统中,unsigned int和box_props类型的结构都占用16 位内存。但是,在其他系统中(例如我们使用的系统),unsigned int和box_props都是32位。无论哪种情况,通过联合,都可以使用 st_view 成员把一块内存看作是一个结构,或者使用 us_view 成员把相同的内存块看作是一个unsigned short。结构的哪一个位字段与unsigned short中的哪一位对应?这取决于实现和硬件。下面的程序示例假设从字节的低阶位端到高阶位端载入结构。也就是说,结构中的第 1 个位字段对应计算机字的0号位(为简化起见,下图以16位单元演示了这种情况)。
notion image
使用Views联合来比较位字段和按位运算符这两种方法。
在该程序中,box是View联合,所以box.st_view是一个使用位字段的box_props类型的结构,box.us_view把相同的数据看作是一个unsigned short类型的变量。联合只允许初始化第1 个成员,所以初始化值必须与结构相匹配。该程序分别通过两个函数显示 box 的属性,一个函数接受一个结构,一个函数接受一个 unsigned short 类型的值。这两种方法都能访问数据,但是所用的技术不同。该程序还使用了本章前面定义的itobs()函数,以二进制字符串形式显示数据,以便查看每个位的开闭情况。
 

对齐特性(C11)

C11 的对齐特性比用位填充字节更自然,它们还代表了C在处理硬件相关问题上的能力。在这种上下文中,对齐指的是如何安排对象在内存中的位置。例如,为了效率最大化,系统可能要把一个 double 类型的值储存在4 字节内存地址上,但却允许把char储存在任意地址。大部分程序员都对对齐不以为然。但是,有些情况又受益于对齐控制。例如,把数据从一个硬件位置转移到另一个位置,或者调用指令同时操作多个数据。
_Alignof运算符给出一个类型的对齐要求,在关键字_Alignof后面的圆括号中写上类型名即可:
假设d_align的值是4,意思是float类型对象的对齐要求是4。也就是说,4是储存该类型值相邻地址的字节数。一般而言,对齐值都应该是2的非负整数次幂。较大的对齐值被称为stricter或stronger,较小的对齐值被称为weaker。
可以使用_Alignas 说明符指定一个变量或类型的对齐值。但是,不应该要求该值小于基本对齐值。例如,如果float类型的对齐要求是4,不要请求其对齐值是1或2。该说明符用作声明的一部分,说明符后面的圆括号内包含对齐值或类型:
 
在我们的系统中,double的对齐值是8,这意味着地址的类型对齐可以被8整除。以0或8结尾的十六进制地址可被8整除。这就是地址常用两个double类型的变量和char类型的变量cz(该变量是double对齐值)。因为char的对齐值是1,对于普通的char类型变量,编译器可以使用任何地址。
在程序中包含stdalign.h头文件后,就可以把alignasalignof分别作为_Alignas_Alignof的别名。这样做可以与C++关键字匹配。
 
C11在stdlib.h库还添加了一个新的内存分配函数,用于对齐动态分配的内存:
第1个参数代表指定的对齐,第2个参数是所需的字节数,其值应是第1个参数的倍数。与其他内存分配函数一样,要使用free()函数释放之前分配的内存。
  • C
  • 函数指针C预处理器
    目录