器→工具, 编程语言

C语言学习之位操作

钱魏Way · · 10 次浏览
!文章内容如有错误或排版问题,请提交反馈,非常感谢!

C 语言中的位操作(Bit Manipulation)。是一种直接在底层操作整型数据(char, short, int, long 等及其 unsigned 版本)中单个比特(位)的技术。它在系统编程、嵌入式开发、驱动开发、加密算法、性能优化等领域非常重要。

C语言中的位操作

核心概念

  • 比特(Bit):计算机中最小的数据单位,只能是 0 或 1。
  • 二进制表示:所有参与位操作的整型变量在内存中都是以二进制形式存储的。例如,unsigned char a = 5;在内存中表示为 00000101(假设一个字节有 8 位)。
  • 位操作运算符:作用于整数的单个或多个比特位。
  • 位掩码(Bitmask):一个预先定义好模式(哪些位是 1,哪些位是 0)的整数,用于与目标数据执行位操作,以达到设置、清除、翻转或测试特定比特位的目的。

位操作运算符

C 语言提供了以下位运算符(按位操作):

运算符 描述 规则
& 按位与 若两个相应额二进制位都为1,则该位的结果为1,否则为0
| 按位或 两个相应的二进制位中只要有一个为1,则该位的结果为1,否则为0
^ 按位异或 (XOR) 若两个二进制位相同,则结果为0,不同则为1
~ 按位反 (NOT) 按位取反,即0变1,1变0
>> 右移 将一个数的二进制位全部右移若干位。不同系统下右移的结果不同。
<< 左移 将一个数的二进制位全部左移若干位,左移1位相当于乘2,左移n位,相当于乘2的n次方

AND &

OR |

XOR ^

NOT ~

Shift Right >>

Shift Left <<

重要细节和注意事项:

  • 操作数类型: 位运算符只能作用于整型类型(int, unsigned int, char, short, long, long long 及其unsigned 版本)。不能用于浮点数或指针(除非通过显式转换)。
  • 优先级: 位运算符的优先级通常低于算术运算符和比较运算符。强烈建议使用括号()来明确运算顺序,避免混淆。例如value & mask == 0 等价于 value & (mask == 0)(可能不是你想要的),最好写成 (value & mask) == 0。
  • 副作用与移位:
    • 左移(<<): 低位补 0。如果高位移出的是非零值(符号位可能被改变),则结果可能不符合预期(溢出行为未定义?C99/C11 明确:如果值因位移而无法表示,则行为未定义)。
    • 右移(>>): 对于无符号整数 (unsigned),保证是逻辑右移(高位补 0)。 对于有符号整数 (signed),行为由编译器实现定义:可以是逻辑右移(某些平台)或算术右移(大多数平台为保持符号位正确)。 编写可移植代码时,强烈建议仅对有符号整数进行非负位移或对负整数移位时特别小心(或避免对有符号整数使用>>),或者显式使用无符号整数进行位操作。
    • 位移位数: 如果移动的位数大于或等于操作数类型的宽度(如对int 移 32 位或更多),或者移动负数的位数,行为是未定义的(Undefined Behavior, UB),可能导致任意结果或崩溃。务必确保移位位数在 [0, bitsize-1] 范围内(bitsize 是类型宽度,如 int 通常是 32)。例如:sizeof(int) * CHAR_BIT 可以得到 int 的位数(CHAR_BIT在<limits.h>中定义)。
  • 按位取反(~)与逻辑非(!):~a对每一个位取反。!a是逻辑非操作,a != 0时结果为0,a == 0时结果为1。

核心位操作技术

设置位(Set Bit): 将指定位设置为 1。

value = value | (1UL << n);  // 设置第 n 位(从最低位 0 开始计数)
// 或使用 |=
value |= (1UL << n);         // 等效写法
  • 1UL << n:创建一个掩码,其中只有第n 位是 1(其余位是 0)。UL 后缀确保为无符号长整型,避免移位错误。
  • value | mask:value的任何位与 mask 的 1 位进行 OR,结果该位必为 1。其他位保留 value 的原值。

清除位(Clear Bit): 将指定位设置为 0。

value = value & ~(1UL << n);  // 清除第 n 位
// 或使用 &=
value &= ~(1UL << n);         // 等效写法
  • ~(1UL << n):创建一个掩码,其中只有第n 位是 0(其余位是 1)。
  • value & mask:value的任何位与 mask 的 0 位进行 AND,结果该位必为 0。其他位保留 value 的原值。

翻转位(Toggle/Filp Bit): 将指定位取反(0 变 1,1 变 0)。

value = value ^ (1UL << n);  // 翻转第 n 位
// 或使用 ^=
value ^= (1UL << n);         // 等效写法
  • 1UL << n:创建一个掩码,其中只有第n 位是 1(其余位是 0)。
  • value ^ mask:value的位与 mask 的 0 位(XOR)保持不变;与 mask 的 1 位(XOR)被取反。

检查位(Check/Test Bit): 判断指定位是否为 1。

if (value & (1UL << n)) {  // 注意:条件判断中的位操作
    // 第 n 位是 1
} else {
    // 第 n 位是 0 或未设置
}
// 或者需要明确布尔值
bit_is_set = (value & (1UL << n)) != 0;  // True if set
// bit_is_set = (value >> n) & 1;       // 另一种检查方式:右移后取最低位
  • value & (1UL << n):如果value 的第 n 位是 1,则结果非零(为 1 << n);如果是 0,则结果为 0。
  • 在条件判断(如if)中,非零即 true,零即 false。

提取位域(Extract Bit Field): 取出连续的一段位。

unsigned int field = (value >> start_bit) & mask;
  • 右移start_bit 位,将目标比特域移动到最低有效位(LSB)。
  • 用mask(宽度等于目标位域宽度,所有位为 1)进行 & 操作,清除高位多余的比特。
  • 例子:提取value 中从第 5 位开始的 4 个比特(即 bit5 到 bit8):
field = (value >> 5) & 0x0F;  // 0x0F 是二进制 00001111, 4 个 1

设置位域(Set Bit Field): 修改连续的一段位。

value = (value & ~(mask << start_bit)) | ((new_field & mask) << start_bit);

步骤:

  • mask << start_bit:创建一个目标位域全为 1 的掩码field_mask。
  • ~(field_mask):创建一个清除目标位域的掩码(目标位域为 0, 其余为 1)。
  • value & ~(field_mask):将value 的目标位域清零,其他位保留。
  • new_field & mask:确保new_field 的值不会超出目标位域的宽度(清除 new_field 中多余的位)。
  • (new_field & mask) << start_bit:将处理后的new_field 值移到目标位置。
  • |:将“已清零位域的 value” 与 “移到位置的 new_field 值” 合并。

实际应用场景

  • 嵌入式系统 / 硬件接口 (GPIO):
    • 设置或读取微控制器 I/O 寄存器的特定控制位或状态位。
    • PORTB |= (1 << PIN0); // 设置 PB0 为高电平
    • if (PINA & (1 << PIN7)) { … } // 检查 PA7 是否高电平
    • 配置设备寄存器选项(如设置波特率、数据位等)。
  • 设备驱动:
    • 与硬件寄存器交互,配置设备模式、读取设备状态、发送命令等。
  • 高效存储状态标志:
    • 用单个整数的不同比特位来表示多个布尔状态,节省内存(尤其是需要大量标志时)。例如:
#define FLAG_A (1 << 0) // 0x01
#define FLAG_B (1 << 1) // 0x02
#define FLAG_C (1 << 2) // 0x04
unsigned int status = 0;
status |= FLAG_A; // 设置标志 A
status &= ~FLAG_B; // 清除标志 B
if (status & FLAG_C) { ... } // 检查标志 C
  • 算法优化:
    • 某些位操作计算比除法或取模快得多。注意:现代优化编译器通常能自动转换,不必手工做过于晦涩的优化。
      • 判断奇偶:if (x & 1) { /* 奇数 */ }
      • 快速乘/除 2 的幂(使用移位):x * 8等效 x << 3, x / 4 等效 x >> 2 (对无符号数或正有符号数有效,对有符号负数需格外小心右移)。
      • 检查是否是 2 的幂:(x != 0) && ((x & (x – 1)) == 0)
      • 交换两个变量(无临时变量):a ^= b; b ^= a; a ^= b;(注意:有副作用的操作数可能导致问题)。
      • 位级加密算法。
    • 压缩数据:
      • 将多个小的整数压缩到一个更大的整数空间中进行存储或传输(利用移位和掩码组合/分解)。
    • 图形和图像处理:
      • 像素操作(处理像素值的各个分量 R, G, B, A,常涉及移位和掩码提取/组合)。
      • 位图操作(单色/索引颜色图像)。

结构体位域

C 语言提供了一种语法结构直接定义结构体中成员占据的比特位数:

struct packed_flags {
    unsigned int flag_a : 1; // 占用 1 位
    unsigned int flag_b : 1; // 占用 1 位
    unsigned int reserved : 6; // 保留 6 位
    unsigned int value : 4; // 占用 4 位,能表示 0-15
    unsigned int mode : 3; // 占用 3 位,能表示 0-7
};
  • 优点:语法清晰,可以节省内存(相对于定义多个布尔变量),特别适合描述硬件寄存器布局或需要严格内存布局的场景。
  • 缺点:
    • 可移植性差:内存中位的顺序(大端序还是小端序)、成员在结构体内的具体内存偏移位置由编译器实现决定,可能与硬件文档要求不一致。
    • 访问位域成员通常比直接位操作稍慢(编译器会将其转换为对应的位操作,但可能有额外的指令开销)。
    • 不能取址(&packed_flags.flag_a是无效的)。
    • 通常仅适合在同一个编译器、平台下使用。
  • 一般建议:对于对内存布局有严格要求(尤其是跨平台场景)或性能关键的部分,优先考虑显式使用位操作和掩码。

51单片机的位操作

51 单片机(通常指基于 Intel MCS-51 指令集架构的单片机,如 STC89C52、AT89S52 等)在硬件层面提供了强大的位寻址能力,这是它与标准通用计算机在架构上的显著区别之一,对于嵌入式系统开发(尤其是控制、IO操作)极其重要。位操作在51单片机编程中不仅是高效的,而且是访问控制特定功能寄存器(SFR)引脚状态的主要方式和必要手段

位寻址空间 (Bit-Addressable Area)

片内 RAM (低 128 字节):

20H – 2FH: 这是 16 字节(128位)的可位寻址区。这 128 个位(00H – 7FH)中的每一个都有独立的位地址。

  • 位地址00H – 07H 对应字节地址 20H 的 bit0 – bit7。
  • 位地址08H – 0FH 对应字节地址 21H 的 bit0 – bit7。
  • … 如此类推 …
  • 位地址78H – 7FH 对应字节地址 2FH 的 bit0 – bit7。

特殊功能寄存器 (Special Function Registers – SFRs, 高 128 字节 80H – FFH):

可位寻址的 SFRs: 当 SFR 的字节地址能被 8 整除(即地址以 0 或 8 结尾,如 80H, 88H, 90H, 98H, A0H, A8H, B0H, B8H, …, F0H, F8H)时,该 SFR 的 8 个位中的每一个也都有一个独立的位地址(通常范围在 80H – FFH)。

重要例子:

  • P0(端口 0):地址 80H (可位寻址)
    • P0^0(或0) 位地址 80H (最低位)
    • P0^1位地址81H
    • P0^7位地址87H(最高位)
  • TCON(定时器/计数器控制寄存器): 地址 88H (可位寻址)
    • TR0(启动 T0) 位地址 8CH
    • TF0(T0 溢出) 位地址 8DH
    • TR1(启动 T1) 位地址 8EH
    • TF1(T1 溢出) 位地址 8FH
  • SCON(串行口控制寄存器): 地址 98H (可位寻址)
    • RI(接收中断) 位地址 98H
    • TI(发送中断) 位地址 99H
  • IE(中断允许寄存器):地址 A8H (可位寻址)
    • EA(总允许) 位地址 AFH
    • ET0(T0 中断) 位地址 A9H
    • … 等等 …
  • IP(中断优先级寄存器): 地址 B8H (可位寻址)
    • P1(端口 1): 地址 90H (可位寻址)
    • P2(端口 2): 地址 A0H (可位寻址)
    • P3(端口 3): 地址 B0H (可位寻址) (注意:P3^0 RXD, P3^1 TXD, P3^2 INT0, P3^3 INT1, P3^4 T0, P3^5 T1, P3^6 WR, P3^7 RD 都有额外功能)

C 语言中的位操作访问 (针对 51 编译器扩展)

51 编译器(如 Keil C51, SDCC)提供了专门的语法来方便地进行位操作,特别是在访问可位寻址的 SFRs 和位寻址 RAM 区:

sbit 关键字 (声明单个位变量)

sbit LED = P1^0;  // 将 LED 定义为 P1.0 (位地址 90H)
sbit KEY = P3^2;  // 将 KEY 定义为 P3.2 / INT0 (位地址 B2H)
sbit TF0 = TCON^5; // 或 sbit TF0 = 0x8D; 定义 TF0 标志位 (位地址 8DH)
sbit flagBit = 0x20; // 定义片内RAM 20H 地址处的 flagBit 位 (位地址 00H)

使用 sbit 定义的位变量可以直接赋值和读取:

LED = 1;    // 将 P1.0 置高 (开灯)
if (KEY == 0) { // 检测 P3.2 是否被拉低
    // 按键按下
}
TF0 = 0; // 清除 T0 溢出标志

bit 关键字 (声明一个位变量, 自动分配在位寻址区)

bit keyPressed;   // 在可位寻址RAM区自动分配一个位变量
bit timerFlag;
void main() {
    keyPressed = 0; // 初始化
    // ...
    if (keyPressed) {
        // 处理按键事件
        keyPressed = 0; // 清除标志
    }
}

bit 类型的变量存储在位寻址 RAM 区 (20H-2FH),编译器会自动管理其位置。

bdata 存储类型修饰符 (声明位于位寻址区的字节变量)

unsigned char bdata statusReg; // statusReg 位于可位寻址RAM区
sbit statusBit0 = statusReg^0; // 定义其最低位
sbit statusBit3 = statusReg^3; // 定义其 bit3
void main() {
    statusReg = 0x00;    // 操作整个字节
    statusBit3 = 1;      // 单独设置 bit3
    if (statusBit0) {     // 单独检查 bit0
        // ...
    }
}

bdata 允许你将一个字节变量放在 20H-2FH 区域,然后可以用 sbit 来定义这个字节变量的各个位,实现对该字节内位的便捷访问。

标准 C 位操作运算符的应用

除了上述专用语法,标准的 &, |, ^, ~, <<, >> 位操作符在 51 单片机编程中同样适用,尤其是在处理不可位寻址的 SFR 或变量时:

// 读取 P1 的值, 判断 bit0 (与)
if (P1 & 0x01) { ... } // 相当于 if (P1.0)
// 设置 P2 的 bit4, bit5 (或)
P2 |= 0x30; // 等同于 P2 |= (1<<4) | (1<<5); 或 P2.4=1; P2.5=1;
// 清除 P0 的 bit7 (与+取反)
P0 &= ~0x80; // 等同于 P0 &= ~(1<<7); 或 P0.7=0;
// 翻转 P3 的 bit1 (异或)
P3 ^= 0x02; // 等同于 P3 ^= (1<<1); 或 P3.1 = !P3.1;

位操作的实际应用场景 (51 单片机中至关重要):

  • GPIO (输入/输出端口) 控制: 这是最频繁的操作。点亮/熄灭 LED(设置/清除位),检测按键输入(读取位状态)。
  • 中断系统: 设置/清除中断允许位 (EA,EX0, ..),检测中断请求标志 (IE0, TF0, RI, TI…)。
  • 定时器/计数器: 启动/停止定时器 (TR0,TR1),查询溢出标志 (TF0, TF1),设置工作模式 (M0, M1)。
  • 串行通信: 设置串口模式、波特率、允许接收 (SM0,SM1, SM2, REN),检测发送完成/接收完成标志 (TI, RI),设置中断优先级 (PS)。 (注意 SCON 各模式下的位定义不同)。
  • 状态标志: 在可位寻址 RAM 区 (bit,bdata+sbit) 高效存储程序状态标志、事件标志位。
  • 软件模拟 I2C, SPI: 直接操作 SCL, SDA (I2C) 或 SCK, MOSI, MISO (SPI) 引脚时,精确控制高低电平的时序依赖于位操作。

Keil C51 与 SDCC 编译环境的差异

在51单片机开发中,SDCC(Small Device C Compiler)Keil C51 是两种常用的编译器,它们在对位操作(特别是51特有的硬件位寻址)的支持上存在显著差异,主要体现在语法、关键字、对内存模型的利用以及实现细节上。

sbit 关键字的用法与定位方式

  • Keil C51:
    • 定义灵活且强大:sbit 可直接定位到绝对位地址(包括SFRs和可位寻址RAM区)、基于符号的可位寻址SFR(如 P1^0)或基于地址的偏移量(如 0x20^0)。
    • 允许任意绝对位地址:sbit flag = 0x20; (RAM位地址00H) 或 sbit TF0 = 0x8D; (SFR TCON.5的位地址) 完全合法。
  • SDCC:
    • 受限的定位方式:sbit 仅能用于SFRs的位定义,如 sbit LED = P1^0;。
    • 不支持绝对位地址赋值:sbit flag = 0x20; 是无效语法! SDCC无法用 sbit 直接指向RAM中的位地址。
    • 替代方案: 访问RAM位地址需使用位域或__bit类型。

RAM位寻址的访问方式

Keil C51:

  • bit关键字 + sbit:
bit flag1;              // 编译器自动在位寻址区分配一位
sbit flag2 = 0x20;      // 显式绑定到位地址0x20 (RAM位00H)
  • bdata+ sbit:
unsigned char bdata flags; // flags存放于20H-2FH的RAM区
sbit flagA = flags^0;     // 操作flags的bit0

SDCC:

  • __bit关键字: 用于声明一个位于位可寻址区(20H-2FH)的位变量。编译器自动分配位地址。
__bit flag1;           // 自动在位寻址区分配一位
  • 不支持bdata 修饰符和 sbit 定位RAM位:
  • 无法像Keil一样方便地在一个字节变量内精确定义位别名。若想操作RAM字节内的特定位,需使用:
  • 标准位运算:if (flags & 0x01) …判断最低位。
  • 位域 (struct bitfields): (更推荐SDCC下的“软”位域,见第4点)。

中断服务函数中位变量的处理

Keil C51:

  • 使用interrupt 关键字声明中断函数,并支持在其中直接使用bit/sbit变量:
void Timer0_ISR() interrupt 1 {
  if (TF0) {           // 直接访问sbit定义的标志位
    TF0 = 0; 
    // ... 
  }
}

SDCC:

  • 使用__interrupt 和中断号声明中断函数。
  • 关键限制:在中断函数内部使用__bit 变量存在风险或不支持! SDCC可能需要使用 __critical 或 **volatile** 修饰,并避免直接用 __bit 做跨函数状态传递(除非明确知晓其编译器行为)。
  • 替代方案: 在中断中使用非位寻址的全局变量(如unsigned char flags;)加标准位操作作为标志位:
volatile unsigned char flags;
#define FLAG_TF0 0x01
__interrupt(1) void Timer0_ISR() {
    if (TCON & 0x20) {        // 直接检查TCON的TF0位 (0x20是掩码)
        TCON &= ~0x20;        // 清除TF0
        flags |= FLAG_TF0;     // 设置全局标志
    }
}

结构体位域 (struct bitfields) 的支持与实现

Keil C51:

  • 硬件位域: 通过bdata 声明 + 位域结构体,编译器利用硬件的位寻址能力,生成高效代码(单指令操作位)。
  • 示例:
unsigned char bdata status;
struct sbits {
    unsigned char flag1 : 1;
    unsigned char flag2 : 1;
    unsigned char : 4;        // 保留位
    unsigned char mode : 2;
};
struct sbits *b = (struct sbits*)&status;
b->flag1 = 1;                 // 直接硬件位操作

SDCC:

  • “软”位域: 即使使用类似Keil的bdata 方法,位域访问也是通过编译器生成的 掩码与移位操作(一系列 &, |, <<, >> 指令)来实现,没有硬件加速。
  • 结果: 位域操作代码效率显著低于Keil。若需高效操作RAM区特定位,推荐直接使用位运算 + 宏,而非位域。
  • 示例 (高效做法):
#define STATUS_FLAG1  (1 << 0)
#define STATUS_MODE   (0x03 << 2)  // 2位宽的mode在bit2-bit3
unsigned char status;
// 设置flag1
status |= STATUS_FLAG1;
// 设置mode为2
status = (status & ~STATUS_MODE) | (2 << 2);

位变量类型 (bit vs __bit)

  • Keil C51:
    • 关键字:bit,用于声明自动位于可位寻址RAM区的位变量。
  • SDCC:
    • 关键字:__bit(注意双下划线),作用与Keil的bit 类似,声明位可寻址区的位变量。
    • 命名空间: 需使用双下划线前缀,表明是编译器扩展。

其他差异

  • 变量初始化:
    • Keil C51:bit 变量可初始化,如 bit flag = 0;。
    • SDCC:__bit 变量不能直接初始化(C标准限制),其初始值由编译器决定(通常为0)。安全做法:在函数开始时显式赋值 flag = 0;。
  • 位变量作用域:
    • 二者均支持全局__bit/bit 和局部 __bit/bit。
  • 函数返回值与参数:
    • 通常避免将__bit/bit 作为函数参数或返回值(存在潜在问题或编译器限制)。优先使用 int 或 unsigned char 传递位状态(0/1)。

核心差异与移植建议

特性 Keil C51 SDCC 移植建议 (Keil -> SDCC)
sbit用于RAM位地址 ✓ 支持(直接绝对地址) ✗ 不支持 删除或替换为 __bit + 标准位操作/宏
bdata类型 ✓ 支持(硬件位域核心) ✗ 不支持 移除 bdata,改用全局变量+位操作宏
bit关键字 bit __bit 重命名 bit -> __bit
SFR位寻址(sbit) ✓ 支持 sbit LED = P1^0; ✓ 支持 (相同语法) 通常无需修改
RAM区精确定位位 灵活 (sbit指向绝对位地址) 仅自动分配(__bit)或位运算/宏 优先用 __bit;特定位用位运算+掩码宏定义
结构体位域效率 高效硬件实现 低效软件实现 (移位+掩码) 避免SDCC下使用位域操作硬件位!改用宏/函数
中断函数中的位操作 ✓ 可安全使用sbit/bit ⚠️ 慎用__bit!用volatile变量+位操作* 在ISR中用volatile char + 位标志宏替代

强烈建议: 若需在SDCC下保持效率和可移植性:

  • SFR位操作: 保留sbit 对端口/寄存器的定义。
  • RAM位操作:
    • 独立标志 → 使用__bit。
    • 字节内位/位域 → 使用 位操作宏 + 全局unsigned char变量 +volatile修饰。
  • 中断标志: 使用全局volatile变量作为标志,在ISR和主循环间传递状态。
  • 彻底摒弃Keil的bdata+位域写法,因其在SDCC下既无效也低效。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注