如何在C/C++中读写任意位

54
假设我有一个二进制值为11111111的字节b。
例如,如何读取从第二位开始的3比特整数值,或者写入从第五位开始的4比特整数值?

5
你需要使用位运算符,如 &、<<、>>、|。 - Salepate
1
可能是如何在C中访问变量的特定位组?的重复问题。 - Bo Persson
1
对于这个问题,有一个更通用的回答,尽管是针对非新手(借用你的形容词):去看《Hacker's Delight》这本书。这本书中的大部分技巧普通人可能永远都不需要实现,但如果你需要一本关于位操作的烹饪书,那它可能是这个领域最好的书籍。 - Brian Vandenberg
1
@BrianVandenberg - 这个问题的目的是为了理解位访问的本质,而不是一些超级厉害的黑客技巧,这会让人们感到困惑。此外,去年SO改变了其关于书籍建议等方面的政策。 - dtech
4
你的回应最开始让我想要离开,但我仍感到有责任去帮助你。在正则表达式领域,“精通正则表达式”被广泛认为是最好的参考书,而“Hacker's Delight”则是学习位操作的最佳参考书。这本书解释了算法并提供了证明(或者草图)。如果读者对算法感到困惑,那更多是由于他们缺乏经验,而不是因为这本书有问题。 - Brian Vandenberg
显示剩余2条评论
8个回答

153

在我提出这个问题的两年后,我想以当时我还是一个完全新手时我希望解释的方式来解释它,这将对想要理解该过程的人最有益。

首先,忘记"11111111"的示例值,它并不适合用于过程的视觉说明。因此,让初始值为10111011(187十进制),这将更能说明过程。

1-如何从第二位开始读取3位值:

    ___  <- those 3 bits
10111011 

该值为101,或十进制下的5,有两种可能的获得方式:

  • 掩码和移位

在这种方法中,首先使用值00001110(十进制14)对所需的位进行掩码处理,然后将其移位到指定位置:

    ___
10111011 AND
00001110 =
00001010 >> 1 =
     ___
00000101

表达式为:(value & 14) >> 1

  • 移位和掩码

这种方法类似,但操作顺序相反,意味着原始值会先进行移位,然后使用00000111(7)进行掩码操作,只保留最后3位:

    ___
10111011 >> 1
     ___
01011101 AND
00000111
00000101

表达式为:(value >> 1) & 7

这两种方法的复杂度相同,因此性能不会有所不同。

2 - 如何从第二位开始编写3位值:

在这种情况下,初始值已知,如果代码中也是如此,您可能可以想出一种方法将已知值设置为使用更少操作的另一个已知值,但实际情况很少这样,大多数时候代码既不知道初始值,也不知道要写入的值。

这意味着为了成功地将新值“拼接”到字节中,目标位必须设置为零,然后将移位后的值“拼接”到位,这是第一步:

    ___ 
10111011 AND
11110001 (241) =
10110001 (masked original value)
第二步是将我们想要写入的值向左移动3位,假设我们想将其从101(5)更改为110(6)。
     ___
00000110 << 1 =
    ___
00001100 (shifted "splice" value)
第三步是将掩码原始值与移位后的“拼接”值拼接起来。
10110001 OR
00001100 =
    ___
10111101
整个过程的表达式为:(value & 241) | (6 << 1) 奖励 - 如何生成读写掩码:
自然地,使用二进制转十进制转换器远非优雅,特别是在32位和64位容器的情况下 - 十进制值变得非常大。 可以使用表达式轻松生成掩码,编译器可以在编译期间有效地解析它们:
- “掩码与位移”的读取掩码:((1 << fieldLength) - 1) << (fieldIndex - 1),假设第一位的索引为1(而不是零) - “位移和掩码”​​的读取掩码:(1 << fieldLength) - 1(索引在这里不起作用,因为它总是被移位到第一位) - 写入掩码:只需对“掩码与位移”掩码表达式使用~运算符进行反转。
它是如何工作的(从上面的示例开始的第二位的3位字段)?
00000001 << 3
00001000  - 1
00000111 << 1
00001110  ~ (read mask)
11110001    (write mask)

同样的例子适用于更宽的整数和任意位数和字段位置,移位和掩码值相应地变化。

还要注意,这些例子假设使用无符号整数,这是您想要使用整数作为可移植位域替代方案(常规位域在标准中无法保证可移植性)所需的。左右移运算都会插入填充的0,但对带符号整数进行右移运算时不是这种情况。

更容易的方法:

使用这组宏(但仅限于C++,因为它依赖于成员函数的生成):

#define GETMASK(index, size) ((((size_t)1 << (size)) - 1) << (index))
#define READFROM(data, index, size) (((data) & GETMASK((index), (size))) >> (index))
#define WRITETO(data, index, size, value) ((data) = (((data) & (~GETMASK((index), (size)))) | (((value) << (index)) & (GETMASK((index), (size))))))
#define FIELD(data, name, index, size) \
  inline decltype(data) name() const { return READFROM(data, index, size); } \
  inline void set_##name(decltype(data) value) { WRITETO(data, index, size, value); }

你可以选择最简单的方式:

struct A {
  uint bitData;
  FIELD(bitData, one, 0, 1)
  FIELD(bitData, two, 1, 2)
};

并将位字段实现为属性,您可以轻松访问:

A a;
a.set_two(3);
cout << a.two();

在C++11之前,使用gcc的typeof代替decltype


2
将最终示例翻译成C语言还需要一些工作。您需要使用typedef struct A A;来定义a才能正常工作。此外,在C语言中,您不能在结构的范围内定义函数,这意味着需要进行一些重大更改(需要将结构传递给函数等——符号变化是非常重要的)。 - Jonathan Leffler
1
你说得对。我没有严格关注C,因为原始问题也标记了C++。它仍然可以应用于C,但需要使用“伪”成员函数,即手动传递显式的this指针(最好是为了C++编译器兼容性而使用self)。 - dtech
1
你在哪里定义value?它是字符数组吗?谢谢! - tommy.carstensen
2
@tommy.carstensen - 我不确定我理解你的问题,这个值只是一个无符号整数,为了简洁起见表示为单个字节。 - dtech

17

你需要进行移位和掩码操作,例如...

如果您想读取前两位,您只需要如下进行掩码操作:

int value = input & 0x3;

如果你想要进行偏移,你需要将其向右移动N位,然后屏蔽掉你想要的位:

int value = (intput >> 1) & 0x3;

根据您在问题中的要求,读取三个位。

int value = (input >> 1) & 0x7;

10

只需使用此方法,无需担心:

#define BitVal(data,y) ( (data>>y) & 1)      /** Return Data.Y value   **/
#define SetBit(data,y)    data |= (1 << y)    /** Set Data.Y   to 1    **/
#define ClearBit(data,y)  data &= ~(1 << y)   /** Clear Data.Y to 0    **/
#define TogleBit(data,y)     (data ^=BitVal(y))     /** Togle Data.Y  value  **/
#define Togle(data)   (data =~data )         /** Togle Data value     **/

例如:
uint8_t number = 0x05; //0b00000101
uint8_t bit_2 = BitVal(number,2); // bit_2 = 1
uint8_t bit_1 = BitVal(number,1); // bit_1 = 0

SetBit(number,1); // number =  0x07 => 0b00000111
ClearBit(number,2); // number =0x03 => 0b0000011

7
你需要进行移位和掩码(AND)操作。 设b为任意字节,p为要从中获取 n 个比特(>=1)的位的索引(>=0)。
首先,你需要将b向右移动 p 次:
x = b >> p;

第二步,您需要使用n个1遮蔽结果。
mask = (1 << n) - 1;
y = x & mask;

您可以将所有内容放在宏中:

#define TAKE_N_BITS_FROM(b, p, n) ((b) >> (p)) & ((1 << (n)) - 1)

3

例如,我如何读取从第二位开始的3位整数值?

int number = // whatever;
uint8_t val; // uint8_t is the smallest data type capable of holding 3 bits
val = (number & (1 << 2 | 1 << 3 | 1 << 4)) >> 2;

我假设"second bit"是指第二个比特,也就是实际上的第三个比特。


1
直接使用0x7更容易,因为它与0b111相同,这与(1 << 2 | 1 << 3 | 1 << 4)相同。此外,您将其移位到第3位,而不是第2位。 - Geoffrey
1
@Geoffrey 请注意最后一句话关于位编号。此外,任何体面的编译器都会优化冗长的移位和或操作部分,至少你可以一眼看出你正在/曾经做什么。 - user529758
1
如果你想让它更简单,只需使用0b语法,那么移位逻辑,虽然会被编译器优化掉,但阅读起来很困难,例如(number >> 2) & 0b111 - Geoffrey
1
@Geoffrey,0b语法是什么?这不是标准的C语言。 - user529758
1
我可能把它和另一种语言混淆了,或者GCC接受它,但是没错,它不是标准的C语言。 - Geoffrey

2

使用std::bitset读取字节

const int bits_in_byte = 8;

char myChar = 's';
cout << bitset<sizeof(myChar) * bits_in_byte>(myChar);

要进行编程,你需要使用位运算符,例如 & ^ | & << >>。请确保了解它们的作用。

例如,要得到 00100100,你需要将第一个比特设置为 1,并使用 << >> 运算符移动 5 次。如果你想继续编写,只需继续设置第一个比特并移动它。这非常像一台旧式打字机:你写入内容,然后移动纸张。

对于 00100100:将第一个比特设置为 1,移动 5 次,然后再将第一个比特设置为 1 并移动 2 次:

const int bits_in_byte = 8;

char myChar = 0;
myChar = myChar | (0x1 << 5 | 0x1 << 2);
cout << bitset<sizeof(myChar) * bits_in_byte>(myChar);

1
如果您需要从数据中获取位,您可以使用位域。您只需设置一个结构并用1和0加载它即可:
struct bitfield{
    unsigned int bit : 1
}
struct bitfield *bitstream;

然后稍后像这样加载它(将char替换为int或任何你要加载的数据):

long int i;
int j, k;
unsigned char c, d;

bitstream=malloc(sizeof(struct bitfield)*charstreamlength*sizeof(char));
for (i=0; i<charstreamlength; i++){
    c=charstream[i];
    for(j=0; j < sizeof(char)*8; j++){
        d=c;
        d=d>>(sizeof(char)*8-j-1);
        d=d<<(sizeof(char)*8-1);
        k=d;
        if(k==0){
            bitstream[sizeof(char)*8*i + j].bit=0;
        }else{
            bitstream[sizeof(char)*8*i + j].bit=1;
        }
    }
}

然后访问元素:
bitstream[bitpointer].bit=...

或者

...=bitstream[bitpointer].bit

所有这些都是在假设您正在使用 i86/64,而不是 arm 的情况下进行的,因为 arm 可以是大端或小端。

4
我不喜欢位域的原因是标准没有规定实现方式。不能保证在不同平台上布局相同。手动操作可以确保这一点,并允许快速有效地进行二进制序列化/反序列化。 - dtech

1
int x = 0xFF;   //your number - 11111111

例如,我如何读取从第二位开始的3位整数值?
int y = x & ( 0x7 << 2 ) // 0x7 is 111
                         // and you shift it 2 to the left

2
你还需要向右移动2位,以获得0-7之间的数字。此外,只需使用“0x1c”即可简化掩码。 - Esailija

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