为什么C语言没有二进制文字?

53

我经常希望能够像这样在 c 中做某些事情:

val1 &= 0b00001111; //clear high nibble
val2 |= 0b01000000; //set bit 7
val3 &= ~0b00010000; //clear bit 5

这种语法似乎是C语言中非常有用的补充,我想不出任何缺点,而且在位操作比较常见的低级语言中,它似乎是一件很自然的事情。

编辑:我看到了其他一些很好的替代方案,但当存在更复杂的掩码时,它们都会崩溃。例如,如果reg是控制微控制器上I/O引脚的寄存器,并且我想同时将2、3和7号引脚设置为高电平,我可以写reg = 0x46;,但我需要花费10秒钟思考它(并且每次阅读这些代码之后的一两天里,我可能还需要再花费10秒钟),或者我可以写reg = (1 << 1) | (1 << 2) | (1 << 6);,但我个人认为这比只需编写`reg = 0b01000110;' 要不清晰得多。尽管我同意这种方法无法很好地扩展到8位或16位架构以外。不过我从来没有需要制作32位掩码。


2
它有十六进制,我认为这甚至更好,如果你花10分钟来感受关系。 - vroomfondel
8
C语言有“二进制”字面量,但只有两个:0和1。;-) - chux - Reinstate Monica
6
就这个问题而言,C++14将具有这些功能 - Nemo
2
#define B00000000 0 #define B00000001 1 #define B00000010 2 #define B00000011 3 ... #define B10100100 0xA4 ... 可能需要强制类型转换为 unsigned char - pmg
1
@AnT:更准确地说,0是一个八进制常量 - chqrlie
显示剩余7条评论
11个回答

59
根据国际标准理由 - 编程语言C §6.4.4.1 整数常量

由于缺乏先例和实用性不足,添加二进制常量的提议被拒绝。

虽然它不在标准C中,但是GCC作为扩展支持它,前缀为0b0B

 i = 0b101010;

详见 此处


太棒了,但我不使用GCC :( 你知道它为什么不在标准中吗? - Drew
@Drew 请看更新。换句话说,委员会认为它的使用可以通过十六进制常量来覆盖,我认为。 - Yu Hao
3
有时候我会觉得制定标准的人并没有在他们正在标准化的语言中编写任何代码。或者,他们都属于同一个小众技术领域,无法想象其他人使用该语言进行与他们不同的事情。 - Kurt E. Clothier

23

这就是促使十六进制成为... 十六进制。 "... 十六进制表示法的主要用途是作为计算机和数字电子设备中二进制编码值的人类友好表示..."。它将如下所示:

val1 |= 0xF;
val2 &= 0x40;
val3 |= ~0x10;

十六进制:

  1. 一个十六进制数字可以表示半个字节(4位或8进制的一半)。
  2. 两个十六进制数字可以表示一个字节(8位)。
  3. 在扩展到更大掩码时,十六进制比较紧凑。

稍微练习一下,转换十六进制和二进制之间的转换就会变得非常自然。试着手写出你的转换结果,不要使用在线的二/十六进制转换器--这样几天后它就会变得自然而然(并且更快)。

另外: 即使二进制文字不是C标准,如果你使用GCC编译,也可以使用二进制文字,它们应该以'0b'或'0B'为前缀。有关更多信息,请参见 此处 的官方文档。示例:

int b1 = 0b1001; // => 9
int b2 = 0B1001; // => 9

2
是的,这就是我最终总是做的事情,但我总是不得不在我的脑海中进行一堆计算,以记住二进制在十六进制中代表什么。特别是如果我想清除最低的6位。我同意二进制文字对于32位平台来说会变得很长,但在这种情况下,你可以选择不使用它们。 - Drew
5
经过一些练习,用十六进制思考会变得轻车熟路。十六进制的优点是比二进制更易读。 - markgz
1
@Drew,我理解你的观点,对于较小的掩码来说,从视觉上来看可能更容易思考。一旦你练习足够多,这将变得相当自然(就像生活中的一切都会变得自然)。我建议你手动计算所有的数值,并在创建掩码时使用计算器进行双重检查,以便你能够更好地在两种表示法之间转换。 - Jacob Pollack
一个好的程序员需要在脑海中进行的所有操作/计算中,想象由十六进制数表示的二进制值实际上需要多少心算呢?我认为这是最简单的操作之一。 - lurker
+1 我真的不知道十六进制符号的主要用途是什么。好知道! - The Mask
3
一个nibble是4位二进制数,而不是4个字节。请将第一条项目中的“4个字节”更改为“4个位”。 - Hans Dampf

16

你提供的所有例子都可以更清晰地表达:

val1 &= (1 << 4) - 1; //clear high nibble
val2 |= (1 << 6); //set bit 6
val3 &=~(1 << 3); //clear bit 3

(我已经擅自修改了注释,让它们从零开始计数,就像自然界本来就是这样的。)

您的编译器将折叠这些常量,因此以这种方式编写它们不会带来性能损失。并且,这比0b...版本更容易阅读。


@Jerry 好的,这会教会我不要在第一个错误后停止思考。谢谢。 - Nemo
如果我们考虑字节序,那么(1 << 4) - 1真的等同于0xF吗?也许不是。 - Abraham Sanchez
@AbrahamSanchez:是的,在所有平台上都完全相同。像左移这样的算术运算是独立于字节序定义的。(事实上,除非你强制转换指针或使用联合体,否则无法检测到字节序。) - Nemo
无论是@Nemo,数组,结构体还是任何其他变量,只要使用类似于memcpy的某些技巧,就可以实现任何事情。 - yyny
@YoYoYonnY:正如我所说,所有这些都需要“转换指针或使用联合”。 - Nemo
@Nemo 我认为“解引用”是一个更好的术语,但你是对的,我很抱歉。我只是想指出,你可以使用任何内存类型来检查字节序,使用类似memcpy或memset的技巧,只要它的大小超过1个字节。 - yyny

12

我认为易读性是首要考虑因素。虽然低级,但阅读和维护你的代码的是人类,而不是机器。

如果你错误地输入了0b1000000000000000000000000000000(0x40000000),而实际上你想要输入的是0b10000000000000000000000000000000(0x80000000),那么这是否容易让你发现呢?


1
这似乎是迄今为止最好的理由。不过,在这些情况下,您并不一定要使用二进制。而且,您有多经常制作32位掩码呢? - Drew
1
由于在大多数情况下有更好的替代方案(十六进制),我猜委员会只是通过不提供它来关闭这种错误的大门。 - Eric Z
1
但这会使其他情况更难理解,例如如果那是一个寄存器,其中位可能具有不同的含义,必须拿出计算器来查看哪些位被启用/禁用只是一种复杂化。这是处理i2c / spi传感器时的常见情况。 - Lesto
1
@lesto,这就是十六进制形式大多数被使用的地方。从十六进制中很容易看出二进制代码。 - Eric Z
5
仅仅因为某个特性可以编写难以阅读的代码,并不是不允许使用它的理由。如果想要的话,你在任何语言中都可以编写难以阅读的代码。 - 12431234123412341234123
你说“从十六进制和二进制代码区分很容易”,但你的问题基于难以区分十六进制和二进制。我并不完全认同这一点。即使我认同,仅仅因为它可能被误用(当一个二进制字面量有大约20个数字时,你不会真正使用它),并不意味着它有时不能帮助到你。 - 463035818_is_not_a_number

3
例如,如果reg是控制微控制器I/O引脚的寄存器,那么我不禁认为这是一个糟糕的例子。控制寄存器中的位具有特定的功能(与连接到单个IO位的任何设备一样)。在头文件中提供符号常量以表示位模式比在代码中计算二进制更明智。将二进制转换为十六进制或八进制很简单,但记住当您将01000110写入IO寄存器时会发生什么事情,尤其是如果您没有数据表或电路图方便的话,则不是那么容易。因此,您不仅可以节省尝试计算二进制代码的10秒钟,还可以节省更长时间来了解它的功能!

2

我建议在C语言中使用C宏来避免编译器警告或其他问题。我使用Ox(就像“Ohio”)代替0x。

#define Ob00000001 1
#define Ob10000000 (1 << (8-1))
#define Ob00001111 15
#define Ob11110000_8 (Ob00001111 << (8 - 4))
#define Ob11110000_16 (Ob00001111 << (16 - 4))
#define Ob11110000_32 (((uint32_t) Ob00001111) << (32 - 4))
#define Ob11110000_64 (((uint64_t) Ob00001111) << (64 - 4))
#define Ox0F Ob00001111
#define OxF0 Ob11110000_8
#define OxF000 Ob11110000_16
#define OxF0000000 Ob11110000_32
#define OxF000000000000000 Ob11110000_64

int main() {
    #define Ob00001110 14
    // bitwise operations work
    if (Ob00001110 == (Ob00001111 & ~Ob00000001)) {
        printf("true\n");
    }
}

1
我的方法是:

/* binmacro.h */

#define BX_0000 0
#define BX_0001 1
#define BX_0010 2
#define BX_0011 3
#define BX_0100 4
#define BX_0101 5
#define BX_0110 6
#define BX_0111 7
#define BX_1000 8
#define BX_1001 9
#define BX_1010 A
#define BX_1011 B
#define BX_1100 C
#define BX_1101 D
#define BX_1110 E
#define BX_1111 F

#define BIN_A(x) BX_ ## x

#define BIN_B(x,y) 0x ## x ## y
#define BIN_C(x,y) BIN_B(x,y)

#define BIN_B4(x,y,z,t) 0x ## x ## y ## z ## t
#define BIN_C4(x,y,z,t) BIN_B4(x,y,z,t)

#define BIN(x,y) BIN_C(BIN_A(x),BIN_A(y))
#define BIN4(x,y,z,t) BIN_C4(BIN_A(x),BIN_A(y),BIN_A(z),BIN_A(t))

/*---- test ... ---*/

BIN(1101,0100)

BIN4(1101,0010,1100,0101)

预处理成...

$  cpp binmacro.h
0xD4

0xD2C5

1
看起来C23现在有二进制字面量。

0

二进制在控制器上设置特定输出时最有用。我使用的是一种技术上非法但始终有效的黑客方法。如果您只需要打开一个LED,使用整个int甚至char都会冒犯每个人的感官。不要忘记,对于这些东西,我们可能并不谈论编译复杂性的极致。因此,为了个体可读性和群体控制,我使用位域:

struct DEMAND
{
    unsigned int dOil   :   1; // oil on
    unsigned int dAir   :   1; // air on
    unsigned int dHeat  :   1; // heater on
    unsigned int dMtr1  :   1; // motor 1 on
    unsigned int dMtr2  :   1; // motor 2 on
    unsigned int dPad1  :   10;// spare demand o/p's
    unsigned int dRunCycle: 1; // GO !!!!
    unsigned int dPad2  :   15;// spare o/p's
    unsigned int dPowerOn:  1; // Power on
}DemandBF;

当单独使用时,它们很容易处理,或者为了更彻底的控制,它们可以被视为无符号整数,而对K&R不予理会:

void *bitfPt = &DemandBF;
unsigned int *GroupOuts = (unsigned int *)bitfPt;

DemandBF.dAir = 1;   // Clearly describes what's turning on
DemandBF.dPowerOn = 1;

*GroupOuts ^= 0x04; // toggle the heater

*GroupOuts = 0; // kill it all

这对我总是有效的,可能不太可移植,但实际上谁会像这样移植它呢?试一试吧。


2
“但是,到底是谁会移植这样的东西呢?”那些为同一硬件更换编译器的人。大约有100%的嵌入式系统程序员在职业生涯中必须这样做。 - Lundin

0

自从C++14标准中包含了二进制字面量,就像您建议的那样:

二进制字面量是字符序列0b或字符序列0B,后跟一个或多个二进制数字(0、1)

int b = 0b101010;

自C++11以来,还有一种声明自定义字面量的方法。二进制字面量最初未被纳入标准的原因已不再重要。


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