写出以下运算的16位掩码和C语言语句?


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

二进制数、 位和字节:

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

通常, 1字节包含8位。

而该字节最小的二进制数是,其值为0。 因此,1字节可储存0~255范围内的数字,总共256个值。 或者,通过不同的方式解释位组合(bit pattern),程序可以用1字节储存-128~+127范围内的整数,总共还是256个值。例如,通常unsigned char用1字节表示的范围是0~255,而signed char用1字节表示的范围是-128~+127。

如何表示有符号整数取决于硬件, 而不是C语言。 也许表示有符号数最简单的方式是用1位(如, 高阶位) 储存符号, 只剩下7位表示数字本身(假设储存在1字节中) 。 用这种符号量(sign-magnitude) 表示法, 表示-1, 表示1。 因此, 其表示范围是-127~+127。
这种方法的缺点是有两个0: +0 ()和-0 ()。 这很容易混淆, 而且用两个位组合来表示一个值也有些浪费。

二进制补码(two’s-complement) 方法避免了这个问题, 是当今最常用的系统。 我们将以1字节为例, 讨论这种方法。 二进制补码用1字节中的后7位表示0~127, 高阶位设置为0。 目前, 这种方法和符号量的方法相同。 另外, 如果高阶位是1, 表示的值为负。 这两种方法的区别在于如何确定负值。 从一个9位组合(256的二进制形式) 减去一个负数的位组合, 结果是该负值的量。 例如, 假设一个负值的位组合是 , 作为一个无符号字节, 该组合表示为 128; 作为一个有符号值, 该组合表示负值(编码是 7的位为1) , 而且值为000000, 即1000000(128) 。 因此, 该数是-128(在符号量表示法中, 该位组合表示-0) 。 类似地, 是-127, 是-1。 该方法可以表示-128~+127范围内的数。

要得到一个二进制补码数的相反数, 最简单的方法是反转每一位(即0变为1, 1变为0) , 然后加1。 因为1是, 那么-1则是,或。 这与上面的介绍一致。

二进制反码(one’s-complement) 方法通过反转位组合中的每一位形成一个负数。 例如, 是1, 那么是-1。 这种方法也有一个-0: 。 该方法能表示-127~+127之间的数。


补码:在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

【反码】规定:正数的反码和原码相同,负数的反码是原码除了符号位,其余位都取反

补码】规定:正数的补码是其本身,负数的补码为其反码加一

浮点数分两部分储存: 二进制小数和二进制指数。 
一个普通的浮点数0.527, 表示为: 从左往右, 各分母都是10的递减负次幂。

在二进制小数中, 使用2的幂作为分母, 从左往右,各分母都是2的递减负次幂,所以二进制小数.101表示为二进制分数(二进制的分数可以表示为2的负次幂的和):

许多分数(如,1/3)不能用十进制表示法精确地表示。与此类似,许多分数也不能用二进制表示法准确地表示。实际上,二进制表示法只能精确地表示多个1/2的幂的和。因此,3/4和7/8可以精确地表示为二进制小数,但是1/3和2/5却不能。

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

八进制(octal) 是指八进制记数系统。 该系统基于8的幂, 用0~7表示数字。例如, 八进制数451(在C中写作0451) 表示为:

十六进制(hexadecimal或hex) 是指十六进制记数系统。 该系统基于16的幂, 用0~15表示数字。例如, 十六进制数

C 提供按位逻辑运算符和移位运算符。在下面的例子中, 为了方便读者了解位的操作, 我们用二进制记数法写出值。 但是在实际的程序中不必这样, 用一般形式的整型变量或常量即可。 例如, 在程序中用25或031或0x19, 而不是。 另外, 下面的例子均使用8位二进制数, 从左往右每位的编号为7~0。

1.二进制反码或按位取反:
一元运算符~把1变为0, 把0变为1。 如下例子所示:

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

C有一个按位与和赋值结合的运算符: &=。 下面两条语句产生的最终结果相同:

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

C有一个按位或和赋值结合的运算符: |=。 下面两条语句产生的最终作用相同:

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

C有一个按位异或和赋值结合的运算符: ^=。 下面两条语句产生的最终作用相同:

按位与运算符常用于掩码(mask) 。 所谓掩码指的是一些设置为开(1) 或关(0) 的位组合。

在该例中, 掩码的宽度为8位。

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

用法: 关闭位( 清空位)
和打开特定的位类似, 有时也需要在不影响其他位的情况下关闭指定的位。 假设要关闭变量flags中的1号位。 同样, MASK只有1号位为1(即, 打开)。 可以这样做:

例如, 假设flags是, MASK是。 下面的表达式:

MASK中为1的位在结果中都被设置(清空) 为0。 flags中与MASK为0的位相应的位在结果中都未改变。

可以使用下面的简化形式:

切换位指的是打开已关闭的位, 或关闭已打开的位。 可以使用按位异或运算符(^) 切换位。
值与MASK为0的位相对应的位不变。 要切换flags中的1号位, 可以使用下面两种方法:

flags中与MASK为1的位相对应的位都被切换了, MASK为0的位相对应的位不变。
前面介绍了如何改变位的值。 有时, 需要检查某位的值。

这样做即使flags的1号位为1, 其他位的值会导致比较结果为假。 因此,必须覆盖flags中的其他位, 只用1号位和MASK比较:

为了避免信息漏过边界, 掩码至少要与其覆盖的值宽度相同。
左移运算符(<<) 将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。 左侧运算对象移出左末端位的值丢失,用0填充空出的位置。

右移运算符(>>) 将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。 左侧运算对象移出右末端位的值丢。 对于无符号类型, 用0 填充空出的位置; 对于有符号类型, 其结果取决于机器。 空出的位置可用0填充, 或者用符号位(即, 最左端的位) 的副本填充:

() // 在某些系统中的结果值 () // 在另一些系统上的结果值

下面是无符号值的例子:

() // 所有系统都得到该结果值

移位运算符针对2的幂提供快速有效的乘法和除法:

移位运算符还可用于从较大单元中提取一些位。 例如, 假设用一个unsigned long类型的值表示颜色值, 低阶位字节储存红色的强度, 下一个字节储存绿色的强度, 第 3 个字节储存蓝色的强度 (注:这与"#FF00FF"不同,它是0xFF00FF:一个16进制数)。随后你希望把每种颜色的强度分别储存在3个不同unsigned char类型的变量中。 那么, 可以使用下面的语句:

以上代码中, 使用右移运算符将 8 位颜色值移动至低阶字节, 然后使用掩码技术把低阶字节赋给指定的变量。


/* 使用位操作显示二进制 */
/* 以4位为一组, 显示二进制字符串 */
 



itobs()函数返回的地址与传入的地址相同, 可以把该函数作为printf()的参数。 在该函数中, 首次执行for循环时, 对01 & n求值。 01是一个八进制形式的掩码, 该掩码除0号位是1之外, 其他所有位都为0。 因此, 01 & n就是n最后一位的值。 该值为0或1。 但是对数组而言, 需要的是字符'0'或字符'1'。该值加上'0'即可完成这种转换(假设按顺序编码的数字, 如 ASCII)。 其结果存放在数组中倒数第2个元素中(最后一个元素用来存放空字符)。


顺带一提, 用1 & n或01 & n都可以。 我们用八进制1而不是十进制1, 只是为了更接近计算机的表达方式。


然后, 循环执行i--和n >>= 1。 i--移动到数组的前一个元素, n >>= 1使n中的所有位向右移动一个位置。 进入下一轮迭代时, 循环中处理的是n中新的最右端的值。 然后, 把该值储存在倒数第3个元素中,以此类推。itobs()函数用这种方式从右往左填充数组。


 
 
操控位的第2种方法是位字段(bit field)。 位字段是一个signed int或unsigned int类型变量中的一组相邻的位(C99和C11新增了_Bool类型的位字段)。位字段通过一个结构声明来建立, 该结构声明为每个字段提供标签,并确定该字段的宽度。
例如, 下面的声明建立了一个4个1位的字段:
根据该声明, 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位。、
对于opaque成员, 1表示方框不透明, 0表示透明。 show_border成员也用类似的方法。 对于颜色, 可以用简单的RGB(即red-green-blue的缩写) 表示。 这些颜色都是三原色的混合。 显示器通过混合红、 绿、 蓝像素来产生不同的颜色。 在早期的计算机色彩中, 每个像素都可以打开或关闭, 所以可以使用用 1 位来表示三原色中每个二进制颜色的亮度。 常用的顺序是, 左侧位表示蓝色亮度、 中间位表示绿色亮度、 右侧位表示红色亮度。 表15.3列出了这8种可能的组合。 fill_color成员和border_color成员可以使用这些组合。 最后, border_style成员可以使用0、 1、 2来表示实线、 点线和虚线样式。

程序清单中的程序使用box_props结构, 该程序用#define创建供结构成员使用的符号常量。 注意, 只打开一位即可表示三原色之一。 其他颜色用三原色的组合来表示。 例如, 紫色由打开的蓝色位和红色位组成, 所以, 紫色可表示为BLUE|RED。
/* 定义并使用字段 */
 



在同类型的编程问题中, 位字段和按位运算符是两种可替换的方法, 用哪种方法都可以。 例如, 前面的例子中, 使用和unsigned int类型大小相同的结构储存图形框的信息。 也可使用unsigned int变量储存相同的信息。 如果不想用结构成员表示法来访问不同的部分, 也可以使用按位运算符来操作。 一般而言, 这种方法比较麻烦。 接下来, 我们来研究这两种方法(程序中使用了这两种方法, 仅为了解释它们的区别, 我们并不鼓励这样做)。


可以通过一个联合把结构方法和位方法放在一起。 假定声明了 struct box_props 类型, 然后这样声明联合:




/* 位字段和按位运算符 */
/* 位字段符号常量 */
/* 按位方法中用到的符号常量 */
 
这里要讨论几个要点。 位字段视图和按位视图的区别是, 按位视图需要位置信息。 例如, 程序中使用BLUE表示蓝色, 该符号常量的数值为4。 但是, 由于结构排列数据的方式, 实际储存蓝色设置的是3号位(位的编号从0开始, 参见图15.1) , 而且储存边框为蓝色的设置是11号位。 因此, 该程序定义了一些新的符号常量:


这里, 0x8是3号位为1时的值, 0x800是11号位为1时的值。 可以使用第1个符号常量设置填充色的蓝色位, 用第2个符号常量设置边框颜色的蓝色位。 用十六进制记数法更容易看出要设置二进制的哪一位, 由于十六进制的每一位代表二进制的4位, 那么0x8的位组合是1000, 而0x800的位组合是, 0x800的位组合比0x8后面多8个0。 但是以等价的十进制来看就没那么明显, 0x8是8, 0x800是2048。
可以使用枚举代替#defined创建符号常量。 例如, 可以这样做:


注意, 按位运算符改变设置更加复杂。 例如, 要设置填充色为青色。 只打开蓝色位和绿色位是不够的:


问题是该颜色还依赖于红色位的设置。 如果已经设置了该位(比如对于黄色) , 这行代码保留了红色位的设置, 而且还设置了蓝色位和绿色位, 结果是产生白色。 解决这个问题最简单的方法是在设置新值前关闭所有的颜色位。 因此, 程序中使用了下面两行代码:


如果不先关闭所有的相关位, 程序中演示了这种情况:





这种情况下, 位字段版本更简单:


这种方法不用先清空所有的位。 而且, 使用位字段成员时, 可以为边框和框内填充色使用相同的颜色值。 但是用按位运算符的方法则要使用不同的值(这些值反映实际位的位置)。


其次, 比较下面两个打印语句:


第1条语句中, 表达式pb->border_color的值在0~7的范围内, 所以该表达式可用作colors数组的索引。 用按位运算符获得相同的信息更加复杂。 一种方法是使用ui>>9把边框颜色右移至最右端(0号位~2号位) , 然后把该值与掩码07组合, 关闭除了最右端3位以外所有的位。 这样结果也在0~7的范围内, 可作为colors数组的索引。


 

对齐特性( C11):

 
 
C11 的对齐特性比用位填充字节更自然, 它们还代表了C在处理硬件相关问题上的能力。 在这种上下文中, 对齐指的是如何安排对象在内存中的位置。 例如, 为了效率最大化, 系统可能要把一个 double 类型的值储存在4 字节内存地址上, 但却允许把char储存在任意地址。 大部分程序员都对对齐不以为然。 但是, 有些情况又受益于对齐控制。 例如, 把数据从一个硬件位置转移到另一个位置, 或者调用指令同时操作多个数据项。
_Alignof运算符给出一个类型的对齐要求, 在关键字_Alignof后面的圆括号中写上类型名即可:
假设d_align的值是4, 意思是float类型对象的对齐要求是4。 也就是说,4是储存该类型值相邻地址的字节数。 一般而言, 对齐值都应该是2的非负整数次幂。 较大的对齐值被称为stricter或stronger, 较小的对齐值被称为weaker。
可以使用_Alignas (:align as)说明符指定一个变量或类型的对齐值。 但是, 不应该要求该值小于基本对齐值。 例如, 如果float类型的对齐要求是4, 不要请求其对齐值是1或2。 该说明符用作声明的一部分, 说明符后面的圆括号内包含对齐值或类型:
无论_Alignas(type)说明符在类型说明符的前面还是后面, GCC 4.7.3都能识别, 后来Clang 3.3 版本也支持了这两种顺序。
在我们的系统中,double的对齐值是8,这意味着地址的类型对齐可以被8整除。以0或8结尾的十六进制地址可被8整除。这就是地址常用两个double类型的变量和char类型的变量cz(该变量是double对齐值)。因为char的对齐值是1,对于普通的char类型变量,编译器可以使用任何地址。

C11在stdlib.h库还添加了一个新的内存分配函数, 用于对齐动态分配的内存。 该函数的原型如下:
第1个参数代表指定的对齐,第2个参数是所需的字节数,其值应是第1个参数的倍数。与其他内存分配函数一样,要使用free()函数释放之前分配的内存。
 
C 区别于许多高级语言的特性之一是访问整数中单独位的能力。 该特性通常是与硬件设备和操作系统交互的关键。
C有两种访问位的方法。 一种方法是通过按位运算符, 另一种方法是在结构中创建位字段。
C11新增了检查内存对齐要求的功能, 而且可以指定比基本对齐值更大的对齐值。
通常(但不总是),使用这些特性的程序仅限于特定的硬件平台或操作系统,而且设计为不可移植的。

c语言教程,适合编程入门、大学考试、计算机证书和考验,喜欢的记得三连哦!侵权私信必删!

纯粉丝交流群: 公众号:编译 , 合作推广私信

我要回帖

更多关于 c语言中定义变量的语句有哪些 的文章

 

随机推荐