如何设置、清除和切换单个位

3120
如何设置、清除和切换一个位?

90
阅读此链接:http://graphics.stanford.edu/~seander/bithacks.html,当您掌握了它后,请阅读此链接:http://realtimecollisiondetection.net/blog/?p=78。 - ugasoft
22
您可能还想查看The Bit Twiddler, Bit Twiddling HacksThe Aggregate Magic Algorithms. 这些网站会对您有所帮助。 - anon
这引出了一个问题,即多个位的规范问题是什么。 - Peter Mortensen
27个回答

4326

设置一个位

使用按位或运算符 (|) 来设置一个位。

number |= 1UL << n;

将会设置number的第n位。如果您想设置第1位,n应该为零,一直到第n位,n-1

如果numberunsigned long更宽,请使用1ULL;在评估1UL << n之前,不会提升1UL << n,在这里,左移多于一个long的宽度是未定义行为。 其他示例也适用。

清除一位

使用按位与运算符(&)来清除一位。

number &= ~(1UL << n);

这将清除 number 的第 n 位。您需要使用位NOT运算符 (~) 反转比特串,然后进行AND运算。

翻转一个比特位

XOR 运算符 (^) 可以用于翻转一个比特位。

number ^= 1UL << n;

这将切换number的第n位。

检查位

虽然您没有要求,但我也可以添加一些内容。

要检查位,请将数字n向右移动,然后进行按位与操作:

bit = (number >> n) & 1U;

这将把number的第n位的值放入变量bit中。

将第n位改为x

在2进制补码C++实现中,以下代码可实现将第n位设置为10

number ^= (-x ^ number) & (1UL << n);

如果x1,则会设置第n位,如果x0,则会清除该位。如果x具有其他值,则会得到垃圾值。 x = !!x将其转换为布尔值0或1。

为了使其独立于2的补码取反行为(在1的补码或符号/大小端C ++实现中,其中-1的所有位都已设置,与之不同),请使用无符号取反。

number ^= (-(unsigned long)x ^ number) & (1UL << n);
或者
unsigned long newbit = !!x;    // Also booleanize to force 0 or 1
number ^= (-newbit ^ number) & (1UL << n);

通常情况下,在进行可移植位操作时使用无符号类型是一个好的选择。

或者

number = (number & ~(1UL << n)) | (x << n);

(number & ~(1UL << n))会清除第n位,并且(x << n)会将第n位设置为x。一般来说最好不要直接复制/粘贴代码,所以许多人使用预处理器宏(如下面的社区wiki答案)或某种封装操作。


163
需要翻译的内容:我想指出,在具有原生位集合/清除支持(例如,AVR 微控制器)的平台上,编译器通常会在 x 为常量(例如 (1 << 5) 或 const unsigned x = 5)时将 'myByte |= (1 << x)' 转换为本地位设置/清除指令。 - Aaron
55
如果变量bit不是_Bool类型(<stdbool.h>),则语句"bit = number & (1 << x);"不能将第x位的值赋给变量bit。为了正确地将第x位的值赋给变量bit,可以使用"bit = !!(number & (1 << x));"这个语句。 - Chris Young
24
为什么不将最后一个更改为 bit = (number >> x) & 1 - aaronman
49
“1” 是一个带符号的“int”字面量。所以这里的所有操作都针对带符号的数字进行,这在标准中定义得不够明确。标准不能保证使用二进制补码或算术移位,因此最好使用“1U”。 - Siyuan Ren
71
我更喜欢使用 number = number & ~(1 << n) | (x << n); 来将第n位改为x。 - leoly
显示剩余32条评论

555
使用标准C++库:std::bitset<N>
或者使用Boost版本:boost::dynamic_bitset
没有必要自己编写:
#include <bitset>
#include <iostream>

int main()
{
    std::bitset<5> x;

    x[1] = 1;
    x[2] = 0;
    // Note x[0-4]  valid

    std::cout << x << std::endl;
}

./a.out

输出:

00010

Boost版本允许与标准库编译时大小的bitset相比,具有运行时大小的bitset。

43
请注意,虽然std::bitset不能从“C”使用,但是由于作者在问题中标记了“C ++”,据我所知,您的答案是最好的......如果了解其优缺点,则std :: vector <bool>是另一种方法。 - paercebal
29
vector<bool> 是一个特化的类型,不幸的是它将值作为位来存储。请参阅 http://www.gotw.ca/publications/mill09.htm 了解更多信息。 - Niklas
92
也许没有人提到它是因为它被标记为嵌入式系统。在大多数嵌入式系统中,你要像避开瘟疫一样避免STL(标准模板库)。而且,在大多数嵌入式编译器中,找到Boost支持可能非常罕见。 - Lundin
21
非常正确。除了像STL和模板这样的特定性能杀手之外,许多嵌入式系统甚至避免使用整个标准库,因为它们非常难以验证。大部分嵌入式领域正在采用MISRA等标准,该标准要求使用静态代码分析工具(任何软件专业人士都应该使用这种工具,而不仅仅是嵌入式工程师)。通常,人们有更重要的事情要做,而不是在整个标准库中运行静态分析——如果他们甚至可以在特定编译器上获得其源代码的话。 - Lundin
47
@Lundin:你的陈述过于笼统(因此无法争论)。我确信我可以找到它们适用的情况。但这并不改变我的观点。这两个类在嵌入式系统中使用完全没有问题(我知道它们被使用过)。关于STL/Boost在嵌入式系统中不被使用的初步观点也是错误的。我确定有一些系统不使用它们,即使使用它们的系统也会谨慎使用,但说它们没有被使用是不正确的(因为有使用它们的系统)。 - Martin York
显示剩余15条评论

284

另一种选择是使用位域:

struct bits {
    unsigned int a:1;
    unsigned int b:1;
    unsigned int c:1;
};

struct bits mybits;

定义一个 3 位字段(实际上是三个 1 位字段)。现在,位操作变得更加简单:

设置或清除一个位:

mybits.b = 1;
mybits.c = 0;

切换一位:

mybits.a = !mybits.a;
mybits.b = ~mybits.b;
mybits.c ^= 1;  /* all work */

检查一位:

if (mybits.c)  //if mybits.c is non zero the next line below will execute

这仅适用于固定大小的位域。否则,您必须采用先前帖子中描述的位操作技术。


82
我一直认为使用位域是一个不好的主意。你无法控制分配位的顺序(从上到下或从下到上),这使得除了逐位操作外,以稳定/可移植的方式序列化值变得不可能。而且,DIY的位运算和位域无法混合使用,例如制作一种同时测试多个位的掩码。当然你可以使用&&并希望编译器会正确地进行优化... - R.. GitHub STOP HELPING ICE
45
"位域在很多方面都有问题,我几乎可以写一本书来讲述。事实上,为了一个需要符合 MISRA-C 标准的位域程序,我几乎不得不这样做。MISRA-C 要求所有实现定义行为都要有文档记录,所以我最终写了一篇关于位域可能出错的各个方面的文章。包括位序、字节序、填充位、填充字节、各种对齐问题、从位域进行隐式和显式类型转换,如果没有使用 int 还会有未定义行为等。相比之下,使用位运算符可以减少错误并编写可移植代码。位域完全是多余的。" - Lundin
51
像大多数语言特性一样,位域可以正确使用,也可以被滥用。如果你需要将几个小的值打包进一个 int 中,位域可以非常有用。另一方面,如果你开始假设位域如何映射到实际包含的 int 上,那么你只会自找麻烦。 - Ferruccio
5
@endolith:那样并不是一个好主意。你可能可以使它工作,但它未必能在不同的处理器、不同的编译器甚至相同编译器的下一个版本中移植过来。 - Ferruccio
4
@Yasky和Ferruccio针对这种方法使用sizeof()得出不同的答案,这说明了兼容性问题不仅存在于编译器之间,还存在于硬件之间。有时我们会自欺欺人地认为通过语言或定义运行时我们已经解决了这些问题,但实际上问题归根结底在于“它能在我的设备上运行吗?”你们这些嵌入式开发人员拥有我的尊重(也同情)。 - Kelly S. French
显示剩余7条评论

250

我使用在头文件中定义的宏来处理位的设置和清除:

/* a=target variable, b=bit number to act upon 0-n */
#define BIT_SET(a,b) ((a) |= (1ULL<<(b)))
#define BIT_CLEAR(a,b) ((a) &= ~(1ULL<<(b)))
#define BIT_FLIP(a,b) ((a) ^= (1ULL<<(b)))
#define BIT_CHECK(a,b) (!!((a) & (1ULL<<(b))))        // '!!' to make sure this returns 0 or 1

#define BITMASK_SET(x, mask) ((x) |= (mask))
#define BITMASK_CLEAR(x, mask) ((x) &= (~(mask)))
#define BITMASK_FLIP(x, mask) ((x) ^= (mask))
#define BITMASK_CHECK_ALL(x, mask) (!(~(x) & (mask)))
#define BITMASK_CHECK_ANY(x, mask) ((x) & (mask))

20
哦,我意识到这篇文章已经五年了,但是这些宏中没有任何重复的参数,Dan。 - Robert Kelly
15
BITMASK_CHECK(x,y) ((x) & (y)) 必须改为 ((x) & (y)) == (y),否则在多位掩码(例如 5 对比 3)时会返回错误的结果。/向所有挖坟人问好 :)/ - brigadir
9
1 应该被写成 (uintmax_t)1 或类似的形式,以防止任何人试图在 long 或更大类型上使用这些宏。 - M.M
4
BITMASK_CHECK_ALL(x,y) can be implemented as !~((~(y))|(x)) - Handy999
5
通过应用德摩根定理并重新排列顺序,可以更容易地看出为什么这样有效:!(~(x) & (y)) - Tavian Barnes
显示剩余4条评论

142

有时候使用 enum 来给位(bits)命名是值得的:

enum ThingFlags = {
  ThingMask  = 0x0000,
  ThingFlag0 = 1 << 0,
  ThingFlag1 = 1 << 1,
  ThingError = 1 << 8,
}

稍后使用名称。即写入

thingstate |= ThingFlag1;
thingstate &= ~ThingFlag0;
if (thing & ThingError) {...}

设置、清除和测试。这种方法可以将魔数从代码的其他部分隐藏起来。

除此之外,我推荐Paige Ruten的解决方案


2
你可以创建一个 clearbits() 函数来代替 &= ~。为什么要使用枚举?我认为枚举是用于创建一堆具有隐藏任意值的唯一变量,但你正在为每个变量分配明确的值。那么与将它们定义为变量相比,有什么好处呢? - endolith
5
在C语言编程中,使用枚举类型来表示相关常量集已有很长历史。我认为在现代编译器中,它们与const short或其他方式相比唯一的优势是可以明确地将它们分组在一起。当你需要它们用于除位掩码之外的其他目的时,它们可以自动编号。当然,在C++中,它们也形成独立的类型,这为您提供了一些额外的静态错误检查。 - dmckee --- ex-moderator kitten
1
如果您未为位的每个可能值定义常量,则会进入未定义的枚举常量。例如,对于ThingError | ThingFlag1,'enum ThingFlags' 的值是什么? - Luis Colorado
8
如果您使用这种方法,请记住枚举常量始终是有符号类型int,由于对有符号类型进行隐式整数提升或按位运算可能会导致各种微妙的错误。例如,thingstate = ThingFlag1 >> 1将调用实现定义的行为。 thingstate =(ThingFlag1 >> x)<< y可能会调用未定义的行为。为了安全起见,请始终转换为无符号类型。 - Lundin
3
从C++11开始,你可以设置枚举类型的底层类型,例如:enum My16Bits: unsigned short { ... }; - Aiken Drum
为什么 ThingMask 是零? - wizzwizz4

57
snip-c.zip的bitops.h中:
/*
**  Bit set, clear, and test operations
**
**  public domain snippet by Bob Stout
*/

typedef enum {ERROR = -1, FALSE, TRUE} LOGICAL;

#define BOOL(x) (!(!(x)))

#define BitSet(arg,posn) ((arg) | (1L << (posn)))
#define BitClr(arg,posn) ((arg) & ~(1L << (posn)))
#define BitTst(arg,posn) BOOL((arg) & (1L << (posn)))
#define BitFlp(arg,posn) ((arg) ^ (1L << (posn)))

好的,让我们来分析一下...
在所有这些中,你似乎对一个常见的表达式"(1L << (posn))"有些困惑。这只是创建一个带有单个位并且适用于任何整数类型的掩码。"posn"参数指定了你想要的位的位置。如果posn==0,那么这个表达式将会被计算为:
0000 0000 0000 0000 0000 0000 0000 0001 binary.

如果 posn==8,它将评估为:
0000 0000 0000 0000 0000 0001 0000 0000 binary.

换句话说,它只是在指定位置创建一个由0组成的字段,并在该位置上放置一个1。唯一棘手的部分在于BitClr()宏中,我们需要在由1组成的字段中设置一个单独的0位。这是通过使用同一表达式的1的补码来实现的,即使用波浪符(~)运算符表示的补码。
一旦创建了掩码,就可以像你建议的那样将其应用于参数,通过使用位与(&)、或(|)和异或(^)运算符。由于掩码的类型是long,所以这些宏在char、short、int或long上同样适用。
底线是,这是一个解决整个问题类的通用解决方案。当然,每次需要时,可以使用显式的掩码值重写任何这些宏的等效版本,这样做也是可能的,甚至是适当的,但为什么要这样做呢?请记住,宏替换发生在预处理器中,因此生成的代码将反映编译器将这些值视为常量的事实,即使用通用宏与每次需要进行位操作时“重新发明轮子”一样高效。
不相信吗?这里有一些测试代码 - 我使用了Watcom C进行了完全优化,而且没有使用_cdecl,所以生成的反汇编代码会尽可能干净:

----[ TEST.C ]----------------------------------------------------------------

#define BOOL(x) (!(!(x)))

#define BitSet(arg,posn) ((arg) | (1L << (posn)))
#define BitClr(arg,posn) ((arg) & ~(1L << (posn)))
#define BitTst(arg,posn) BOOL((arg) & (1L << (posn)))
#define BitFlp(arg,posn) ((arg) ^ (1L << (posn)))

int bitmanip(int word)
{
      word = BitSet(word, 2);
      word = BitSet(word, 7);
      word = BitClr(word, 3);
      word = BitFlp(word, 9);
      return word;
}

----[ TEST.OUT (disassembled) ]-----------------------------------------------

Module: C:\BINK\tst.c
Group: 'DGROUP' CONST,CONST2,_DATA,_BSS

Segment: _TEXT  BYTE   00000008 bytes
 0000  0c 84             bitmanip_       or      al,84H    ; set bits 2 and 7
 0002  80 f4 02                          xor     ah,02H    ; flip bit 9 of EAX (bit 1 of AH)
 0005  24 f7                             and     al,0f7H
 0007  c3                                ret

No disassembly errors

----[ finis ]-----------------------------------------------------------------

6
关于此事有两件事情需要注意:(1)在查看您的宏时,一些人可能会错误地认为这些宏实际上会在参数中设置/清除/翻转位,但实际上没有赋值操作;(2)您的test.c并不完整;我怀疑如果您运行更多测试用例,您会发现问题(读者练习)。 - Dan
26
这只是一种奇怪的混淆方法。永远不要通过将语言语法隐藏在宏背后来重新发明C语言,这是非常糟糕的做法。然后还有一些怪异之处:首先,1L是带符号的,这意味着所有位运算将在带符号的类型上执行。传递给这些宏的所有内容都将作为带符号长整型返回。不好。其次,当操作可以在整型级别上进行时,这将在较小的CPU上效率非常低下,因为它强制使用long。第三,类似于函数的宏是万恶之源:你根本没有类型安全性。此外,关于不能进行赋值的先前评论非常有效。 - Lundin
5
如果 arglong long,那么这会失败。1L 需要是最宽的类型,所以应该使用 (uintmax_t)1。(你也可以试试 1ull)。 - M.M
4
你是否优化了代码大小?在英特尔主流CPU上,当该函数返回后读取AX或EAX会出现部分寄存器停顿,因为它写入了EAX的8位组件。在AMD CPU或其他不单独重命名部分寄存器和完整寄存器的CPU上是可以的。[Haswell/Skylake不单独重命名AL,但它们会单独重命名AH。](https://dev59.com/Yuk5XIcBkEYKwwoY9-p1) - Peter Cordes

43
对于初学者,我想用一个例子来解释得更详细一些:
例子:
value is 0x55;
bitnum : 3rd.

使用&运算符来检查位:

0101 0101
&
0000 1000
___________
0000 0000 (mean 0: False). It will work fine if the third bit is 1 (then the answer will be True)

切换或翻转:
0101 0101
^
0000 1000
___________
0101 1101 (Flip the third bit without affecting other bits)
| 运算符:设置位
0101 0101
|
0000 1000
___________
0101 1101 (set the third bit without affecting other bits)

29

这是我最喜欢的位运算宏,适用于任何类型的无符号整数数组,从 unsigned charsize_t(这是最大的类型,应该是高效工作的):

#define BITOP(a,b,op) \
 ((a)[(size_t)(b)/(8*sizeof *(a))] op ((size_t)1<<((size_t)(b)%(8*sizeof *(a)))))

设置一个位:

BITOP(array, bit, |=);

澄清一下:

BITOP(array, bit, &=~);

要切换一个比特位:

BITOP(array, bit, ^=);

为了测试一下:

if (BITOP(array, bit, &)) ...

等等。


5
阅读是好的,但人们应该注意可能出现的副作用。在循环中使用BITOP(array, bit++, |=); 很可能不会达到调用者的预期效果。 - foraidt
确实。= )你可能更喜欢的一种变体是将其分成两个宏,一个用于寻址数组元素,另一个用于将位移动到位,如 BITCELL(a,b) |= BITMASK(a,b); (两者都使用 a 作为参数来确定大小,但后者永远不会评估 a,因为它只出现在 sizeof 中)。 - R.. GitHub STOP HELPING ICE
1
@R.. 这个回答确实很旧,但在这种情况下我可能更喜欢使用函数而不是宏。 - PC Luddite
小问题:第三个(size_t)转换似乎只是为了确保使用%进行一些无符号数学。可以用(unsigned)代替。 - chux - Reinstate Monica
(size_t)(b)/(8*sizeof *(a)) 这个表达式在除法之前将 b 狭窄化了,这是不必要的。这只会在非常大的位数组中出现问题。但这依然是一个有趣的宏定义。 - chux - Reinstate Monica

29

由于此处标记为“嵌入式”,我假设您正在使用微控制器。以上所有建议都是有效的并且可行(读取-修改-写入、联合、结构等)。

然而,在基于示波器的调试过程中,我惊奇地发现,与直接将值写入微控制器的PORTnSET/ PORTnCLEAR寄存器相比,这些方法在CPU周期方面有相当大的开销,这在有紧密循环/高频率ISR切换引脚的情况下会产生真正的影响。

对于不熟悉的人:在我的例子中,微控制器具有反映输出引脚状态的通用引脚状态寄存器PORTn,因此执行PORTn |= BIT_TO_SET会导致对该寄存器进行读取-修改-写入操作。但是,PORTnSET / PORTnCLEAR寄存器将“1”解释为“请使此位为1”(SET),将“0”解释为“请将此位清零”(CLEAR),将“0”解释为“无需更改引脚”。因此,您最终会得到两个端口地址,具体取决于您是设置还是清除该位(不总是方便的),但是会有一个更快的反应和较小的汇编代码。


Micro使用Coldfire MCF52259,使用Codewarrior中的C语言。查看反汇编/asm是一个有用的练习,因为它显示了CPU执行甚至最基本操作所需经过的所有步骤。<br>我们还发现在时间关键的循环中存在其他占用CPU的指令-通过执行var%= max_val来限制变量每次都会消耗大量CPU周期,而通过执行if(var> max_val)var-=max_val仅使用几个指令。<br>一些更多技巧的好指南在这里: http://www.codeproject.com/Articles/6154/Writing-Efficient-C-and-C-Code-Optimization - John U
1
更重要的是,辅助内存映射I/O寄存器提供了原子更新机制。如果序列被中断,读取/修改/写入可能会出现严重问题。 - Ben Voigt
3
请注意,所有端口寄存器都将被定义为“volatile”,因此编译器无法对涉及这些寄存器的代码进行任何优化。因此,最好的做法是反汇编这样的代码,并查看它在汇编级别上的结果。 - Lundin
这不太清楚。什么更快?直接在内存映射的I/O上操作?还是将所有操作(可能)都放在CPU寄存器中,并将最终结果写入I/O寄存器? - Peter Mortensen
直接在I/O寄存器上操作可能会导致输出引脚上出现脉冲,这是因为位运算的中间结果被写入。 - Peter Mortensen

29

先假设几件事情
num = 55 整数用于执行位运算(设置、获取、清除、切换)
n = 4 基于0的位位置,用于执行位运算。

如何获取位?

  1. 要获取num的第n位,请右移num n次,然后执行按位与操作(&)与1。
bit = (num >> n) & 1;

它是如何工作的?

       0011 0111 (55 in decimal)
    >>         4 (right shift 4 times)
-----------------
       0000 0011
     & 0000 0001 (1 in decimal)
-----------------
    => 0000 0001 (final result)

如何设置一个二进制位?

  1. 要设置某个特定的二进制位,将 1 左移 n 次。 然后执行按位或 | 运算与 num
num |= (1 << n);    // Equivalent to; num = (1 << n) | num;

它是如何工作的?

       0000 0001 (1 in decimal)
    <<         4 (left shift 4 times)
-----------------
       0001 0000
     | 0011 0111 (55 in decimal)
-----------------
    => 0001 0000 (final result)

如何清除一个比特位?

  1. 将1向左移动n次,即1 << n
  2. 对上述结果执行按位补码操作。这样第n个比特位就变为未设置,其余位都变为已设置,即~ (1 << n)
  3. 最后,对上述结果和num执行按位与&操作。以上三个步骤可以写成 num & (~ (1 << n))

清除比特位的步骤

num &= (~(1 << n));    // Equivalent to; num = num & (~(1 << n));

它是如何工作的?

       0000 0001 (1 in decimal)
    <<         4 (left shift 4 times)
-----------------
     ~ 0001 0000
-----------------
       1110 1111
     & 0011 0111 (55 in decimal)
-----------------
    => 0010 0111 (final result)

如何切换一个比特位?

使用位异或^运算符来切换一个比特位。位异或运算符的计算结果为1,当且仅当两个操作数的相应比特位不同时,否则结果为0。

这意味着要切换一个比特位,我们需要对要切换的比特位和数字1执行异或操作。

num ^= (1 << n);    // Equivalent to; num = num ^ (1 << n);

它是如何工作的?

  • 如果要切换的位为0,则0 ^ 1 => 1
  • 如果要切换的位为1,则1 ^ 1 => 0
       0000 0001 (1 in decimal)
    <<         4 (left shift 4 times)
-----------------
       0001 0000
     ^ 0011 0111 (55 in decimal)
-----------------
    => 0010 0111 (final result)

推荐阅读 - C语言位运算练习及解答


感谢详细的解释。 以下是BIT Magic练习问题的链接 [link] (https://www.geeksforgeeks.org/c-programs-gq/bit-magic-programs-gq/) - Chandra Shekhar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接