位操作

掌握整数位模式、同宽无符号掩码、合法移位、位字段ABI限制和C11对齐特性

学习目标

  • 能用匹配的 printf 格式展示无符号整数进制,并为固定宽度寄存器构造同宽掩码
  • 能判断移位计数、整数提升、signed 左移和负数右移何时有定义,并修复宽度不匹配的清位代码
  • 能比较手动位编码、位字段和 _Alignas/_Alignof 的用途,说明哪些布局事实只由目标 ABI 保证

机制总览

位操作:机制路径

  1. 1

    为什么要在「位」这一层思考?

    程序里的 int flags 看起来是一个整数,但在 CPU 和硬件寄存器里,它是一串 0/1 位 。嵌入式驱动要读「第 3 位是否为 1」、网络协议要把多个小标志塞进 2 字节、权限系统要用一个 unsigned 同时表示读/写/执行——这些都离不开 按位操作 。

  2. 2

    多种进制:同一数值的不同写法

    日常用 十进制 ;硬件与调试更常用 二进制 、 八进制 、 十六进制 ——它们都是同一整数的不同 书写方式 ,不是不同数值。

  3. 3

    按位运算符:& ^ ~

    注意: 按位 & 与逻辑 && 不同 。 a & b 对每一位运算; a && b 只看「是否为 0」,且会短路。标志位操作几乎总是用 unsigned 配合按位运算符。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

位操作:机制与证据

切换《位操作》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 为什么要在「位」这一层思考?

程序里的 int flags 看起来是一个整数,但在 CPU 和硬件寄存器里,它是一串 0/1 位 。嵌入式驱动要读「第 3 位是否为 1」、网络协议要把多个小标志塞进 2 字节、权限系统要用一个 unsigned 同时表示读/写/执行——这些都离不开 按位操作 。

可核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么要在「位」这一层思考?」的实际行为。

学完《位操作》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

位操作:失效与核验

为什么要在「位」这一层思考?

典型失效

若只记语法而忽略「为什么要在「位」这一层思考?」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。

核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么要在「位」这一层思考?」的实际行为。

多种进制:同一数值的不同写法

典型失效

若只记语法而忽略「多种进制:同一数值的不同写法」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。

核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「多种进制:同一数值的不同写法」的实际行为。

按位运算符:& ^ ~

典型失效

若只记语法而忽略「按位运算符:& ^ ~」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。

核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「按位运算符:& ^ ~」的实际行为。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么要在「位」这一层思考?

程序里的 int flags 看起来是一个整数,但在 CPU 和硬件寄存器里,它是一串 0/1 位。嵌入式驱动要读「第 3 位是否为 1」、网络协议要把多个小标志塞进 2 字节、权限系统要用一个 unsigned 同时表示读/写/执行——这些都离不开按位操作

想象一排开关面板:每个开关只占一位,你可以单独拨动某个开关,而不必把整个面板换成新数字。C 的 就是直接拨这些开关。

没有位运算,你只能除法取余、多次比较——慢、难读,也写不出紧凑的寄存器映射。这一章还涉及 bit-field,让 struct 成员只占若干 bit。

多种进制:同一数值的不同写法

日常用十进制;硬件与调试更常用 二进制八进制十六进制——它们都是同一整数的不同书写方式,不是不同数值。

unsigned n = 42u;
printf("dec %u oct %o hex %x\n", n, n, n);
/* dec 42 oct 52 hex 2a */

C11 字面量可写 052(八进制)和 0x2A(十六进制),但尚不支持 0b00101010;二进制整数常量到 C23 才进入标准。printf 的 %o%x 期待 unsigned int,必须让实参类型匹配;格式与实参类型不匹配会产生未定义行为。

与硬件位一一对应。十六进制每个数字恰表示 4 bit,便于读长位串(如 0xFF 表示低 8 位全 1)。

按位运算符:& | ^ ~

四个二元(逐 bit)与一个一元:

运算符名称含义(每位)
&按位与两位都为 1 才 1
|按位或有 1 则 1
^按位异或不同为 1
~按位取反0↔1

注意:按位 & 与逻辑 && 不同a & b 对每一位运算;a && b 只看「是否为 0」,且会短路。标志位操作几乎总是用 unsigned 配合按位运算符。

掩码:置位、清位、翻转、测试

#define FLAG_READ  (1u << 0)   /* 0b0001 */
#define FLAG_WRITE (1u << 1)   /* 0b0010 */
#define FLAG_EXEC  (1u << 2)   /* 0b0100 */
 
unsigned flags = 0;
flags |= FLAG_READ;              /* 置位 */
flags &= ~FLAG_WRITE;            /* 清位 */
flags ^= FLAG_EXEC;              /* 翻转 */
if (flags & FLAG_READ) { ... }   /* 测试 */

常由 1u << n 或若干位组合,但目标若比 unsigned 更宽,掩码也要用 UINT64_C(1) 等同宽类型。~ 翻转的是提升后操作数类型的全部位;问题核心是类型宽度和整数转换,不是含糊的“符号扩展”。

位移:左移和右移

运算符:

unsigned x = 5;      /* 0b...0101 */
unsigned a = x << 2; /* 0b...010100 → 20 */
unsigned b = x >> 1; /* 0b...0010  → 2  */
分步1 / 3

第一步:左移

unsigned 左移在移位计数合法时等价乘 2 的 n 次方再按该无符号类型取模。signed 非负值仅在数学结果可由结果类型表示时有定义;signed 负值左移或不可表示结果都是未定义行为。

位字段 bit-field:在 struct 里压缩位宽

当多个成员各只需几位时,可用

struct Status {
    unsigned ready : 1;
    unsigned mode  : 3;
    unsigned error : 4;
    unsigned flags : 8;
};
 
struct Status s;
s.ready = 1;
s.mode = 5;

位字段不能取地址,也不能作为 sizeof 或 _Alignof 的操作数。C11 可移植位字段基础类型是 _Boolsigned intunsigned int,实现还可接受其他类型。未命名的零宽字段可以要求下一个字段从新分配单元开始,但字段高低位顺序、跨单元规则和 padding 仍由实现决定,因此不可用于可移植协议。手动位移与掩码也必须再定义整数宽度和字节序。

对齐特性:_Alignas 与 _Alignof

C11 的 可以让接口表达对象地址约束:

#include <stddef.h>
#include <stdio.h>
 
struct packet_buffer {
    _Alignas(max_align_t) unsigned char bytes[64];
};
 
int main(void) {
    printf("packet alignment = %zu\n",
           _Alignof(struct packet_buffer));
    return 0;
}

_Alignof(type-name) 产生 size_t 常量,查询类型要求而不创建对象。_Alignas 只能指定实现支持的有效对齐,多个说明取最严格者,不能削弱对象本来的自然对齐。<stdalign.h> 在 C11 中提供 alignasalignof 宏作为更易读拼写。

提高对象对齐可能增加结构体尾部 padding 和数组步长,并不决定位字段顺序、字节序或文件格式。把任意字节缓冲强制转换成某类型指针前,必须同时满足大小、对齐、有效对象表示和别名规则;“地址看起来是 16 的倍数”只解决其中一项。

代码详解:寄存器风格标志与提取

定义与读写 16 位「寄存器」

#include <stdint.h>
 
#define REG_ENABLE     (UINT16_C(1) << 0)
#define REG_MODE_MASK  UINT16_C(0xF)
#define REG_MODE_SHIFT 1u
 
uint16_t read_reg(void);   /* 假设从硬件读 */
void write_reg(uint16_t value);
 
void set_mode(uint16_t mode) {
    uint16_t reg = read_reg();
    uint16_t field_mask = (uint16_t)(REG_MODE_MASK << REG_MODE_SHIFT);
    reg = (uint16_t)((reg & (uint16_t)~field_mask) |
                     ((uint16_t)(mode << REG_MODE_SHIFT) & field_mask));
    write_reg(reg);
}

不用 bit-field 的可移植解析

uint16_t reg = read_reg();
uint16_t mode = (uint16_t)((reg >> REG_MODE_SHIFT) & REG_MODE_MASK);
int enabled = (reg & REG_ENABLE) != 0;

bit-field 示例(仅本机/嵌入式)

struct GPIO {
    unsigned pin0 : 1;
    unsigned pin1 : 1;
    unsigned pad  : 6;   /* 显式保留 */
};

容易踩的坑

小结

  • 同一整数可用十/八/十六/二进制书写;printf %o/%x 按格式输出
  • printf 的 %o/%x 需要 unsigned int;固定宽度值使用 inttypes.h 对应格式宏
  • 掩码与目标保持同一无符号宽度;移位计数必须小于提升后类型的 sizeof * CHAR_BIT
  • unsigned 左移按模运算,signed 左移有额外值域限制;负 signed 右移由实现定义
  • bit-field 的分配顺序、单元和 padding 不可移植;线上格式使用固定宽度字段和显式字节序
  • _Alignas/_Alignof 表达对象对齐要求,不规定位字段、结构体或文件布局

练习

问题 1(问答型) 十六进制 0xFF 对应多少位全 1?用 1u << n 如何写出等价掩码?

问题 2(改代码型) 下面代码想构造 32 位最高位掩码,哪里有问题?

#include <stdint.h>
 
uint32_t high = 1 << 31;

问题 3(综合实现型) 用 unsigned 实现:set_flagclear_flagtoggle_flagtest_flag 四个 inline 函数,并演示置 READ、清 WRITE、测 READ。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

位操作(bit fiddling)

直接读写整数二进制位的技巧;含按位运算、位移、掩码与位字段。见 BitwiseOperatorsDiagram。

二进制(binary)

以 2 为基数,每位为 0 或 1。整数对象由若干位组成,但 C11 仍允许不同 signed 表示和 padding;协议应选择无符号固定宽度类型。见 BinaryHexDiagram。

掩码(mask)

用于选中或修改字段的无符号位模式。掩码应与目标同宽,构造移位前须验证计数小于该类型位宽。

位移(shift)

对整数提升后的值使用左移或右移。计数为负或达到类型位宽时未定义;unsigned 与 signed 的结果和溢出规则不同。见 ShiftOperatorsDiagram。

位字段(bit-field)

结构成员指定位宽;不能取地址,分配单元、顺序、对齐和跨单元方式由实现决定,不适合作为可移植协议布局。见 BitFieldDiagram。

对齐特性

C11 的 _Alignof 查询类型对齐要求,_Alignas 为对象指定不弱于自然对齐的有效要求。二者不规定结构成员偏移、位字段顺序或字节序。

资料与写作方式声明

本章以C Primer Plus, Sixth Edition, Chapter 15: Bit Fiddling权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…