“^= 32”背后的想法是什么?它是如何将小写字母转换为大写字母或反之亦然的?

146

我正在 Codeforces 上解决一些问题。通常我首先检查字符是否为大写或小写英文字符,然后减去或加上32以将其转换为相应的字符。但我发现有人使用 ^= 32 来完成同样的事情。这是代码:

char foo = 'a';
foo ^= 32;
char bar = 'A';
bar ^= 32;
cout << foo << ' ' << bar << '\n'; // foo is A, and bar is a

我已经搜索了这个问题的解释,但没有找到。那么为什么它有效呢?


5
以下是需要翻译的内容:https://en.wikipedia.org/wiki/File:USASCII_code_chart.png 提示:你可以使用“^ 32”将“@”转换为“`”。这张图片显示了美国信息交换标准代码(ASCII)的字符编码表,包括控制字符和可打印字符。ASCII是一种将字符映射到数字代码的编码系统,它被广泛用于计算机系统中。在表中,每个字符都有一个唯一的十进制、十六进制和八进制的值。此外,还提供了一些特殊的转义序列,用于表示在普通文本中无法直接输入的字符,比如退格、换行和制表符。注意:如果要将“@”转换为“`”,可以使用“^ 32”。 - KamilCuk
113
就这个字符集来说它似乎“有效”,但实际上不是所有字符集都适用。你应该使用 touppertolower 函数来切换字符大小写。 - NathanOliver
7
有时在线比赛的“意图”是以一种混淆不清的方式编写代码,以至于它永远无法通过严格的审查。 - 463035818_is_not_a_number
21
^= 使用异或运算来转换值。大写 ASCII 字母在相应的位上有一个零,而小写字母则为一。尽管如此,请不要这样做!使用正确的字符(Unicode)例程来在小写字母和大写字母之间进行转换。只使用 ASCII 已经过时了。 - Hans-Martin Mosner
14
它不仅仅只能与某些字符集一起使用。即使我们假设全世界都使用UTF-8编码(这可能是一个美好的乌托邦目标),它也只能处理由26个字母A到Z组成的字符集。如果你只关心英语(而且不使用拼写为"naïve"、像"café"这样的单词或带有变音符号的名字……),那就没问题了,但世界不仅仅只有英语。 - ilkkachu
显示剩余12条评论
10个回答

147

让我们来看一下ASCII码表的二进制表示。

A 1000001    a 1100001
B 1000010    b 1100010
C 1000011    c 1100011
...
Z 1011010    z 1111010

32 的二进制表示是 0100000 ,这是区分大小写字母的唯一差别。因此,切换该位可以切换字母的大小写。


50
将"toggles the case"转换为ASCII字符的反义大小写。 - Mooing Duck
39
仅限使用ASCII中的A-Za-z进行打字。 "[" 的小写不是“{”。 - dbkk
22
@dbkk说:"{"比"["短,所以它是“小写”的。不是吗?好吧,我自己走了:D" - Peter Badida
27
有趣的小细节:在7位编码的范围内,德国的计算机将 []{|} 重新映射为 ÄÖÜäöü,因为我们更需要Umlauts而不是这些字符,因此在这种情况下,{(ä)实际上成了小写的[(Ä)。 - Guntram Blohm
15
@GuntramBlohm 这是一个有趣的小知识点,这就是为什么IRC服务器认为foobar []foobar{}是相同的昵称,因为昵称是不区分大小写的,而IRC起源于斯堪的纳维亚 :) - ZeroKnight
1
值得了解的短语是“ISO 646”。就像在8位时代有许多国家/地区ASCII超集一样,在7位时代,ASCII只是许多与646兼容的字符集之一。因此,^= 32技巧实际上适用于(大多数?)基于ISO 646的字符集,而不仅仅是ASCII :D - Andrea

117

这是因为ASCII值是由非常聪明的人选择的事实。

foo ^= 32;

这个翻转1 foo 的第六低位(ASCII排序的大写标志),将 ASCII 大写字母转换为小写,反之亦然。

+---+------------+------------+
|   | Upper case | Lower case |  32 is 00100000
+---+------------+------------+
| A | 01000001   | 01100001   |
| B | 01000010   | 01100010   |
|            ...              |
| Z | 01011010   | 01111010   |
+---+------------+------------+

示例

'A' ^ 32

    01000001 'A'
XOR 00100000 32
------------
    01100001 'a'

通过异或的属性,'a' ^ 32 == 'A'

注意事项

C++不要求使用ASCII表示字符。另一种变体是EBCDIC。此技巧仅适用于ASCII平台。更具可移植性的解决方案是使用std::tolowerstd::toupper,提供的奖励是区域设置感知(它并不能自动解决所有问题,参见评论):

bool case_incensitive_equal(char lhs, char rhs)
{
    return std::tolower(lhs, std::locale{}) == std::tolower(rhs, std::locale{}); // std::locale{} optional, enable locale-awarness
}

assert(case_incensitive_equal('A', 'a'));

1) 由于32等于1 << 5(2的5次方),因此它会翻转第6位(从1开始计数)。


16
EBCDIC也是由一些非常聪明的人选择的:在使用打孔卡时非常好用,而ASCII则比较混乱。但这是一个很好的答案,加1。 - Bathsheba
66
我不知道穿孔卡片,但ASCII码曾经被用在纸带上。这就是为什么删除字符被编码为1111111的原因:你可以通过打出纸带上其所在列的所有孔来标记任何字符为“已删除”。 - dan04
24
作为一个没有使用过穿孔卡片的人,我很难理解EBCDIC是如何经过精心设计的这个概念。 - Lord Farquaad
9
在我看来,维基百科上展示字符如何被打印在穿孔卡上的图片很明显地说明了EBCDIC编码对于这种方式的一些(但不是全部,参见/ vs S)合理性。https://en.wikipedia.org/wiki/EBCDIC#/media/File:Blue-punch-card-front-horiz_top-char-contrast-stretched.png - Peteris
12
请注意:@dan04提到“MASSE”的小写形式是什么。对于那些不知道的人来说,德语中有两个单词的大写形式都是MASSE;一个是“Masse”,另一个是“Maße”。在德语中正确的小写形式不仅需要字典,还需要解析含义。 - Martin Bonner supports Monica
显示剩余11条评论

35

请允许我说,虽然看起来很聪明,但这真的是一个非常愚蠢的技巧。如果有人在2019年向您推荐此技巧,请打他。尽你所能狠狠地打他。
当然,如果您知道自己永远不会使用除英语以外的任何语言,那么您可以在自己的软件中使用此技巧,而其他人则不能。

30-35年前,计算机只能够处理ASCII编码下的英文和可能两种主要欧洲语言,因此该技巧曾经是可以接受的。但是......现在不再是这样了。

该技巧之所以有效,是因为美式拉丁大写字母和小写字母相距恰好0x20,并以相同的顺序出现,这只相差一位的二进制位被切换。

现在,为欧洲西部创建代码页(后来是Unicode联盟)的人足够聪明,如对于德语Umlauts和法语重音元音字母,他们保持了此方案。但是对于ß,他们没有这样做(直到有人在2017年说服Unicode联盟,并有一份大型虚假新闻印刷杂志报道此事,实际上说服了Duden——对此不发表评论),因为ß没有大写字母(转换为SS)。现在ß有了大写字母,但这两个字符相距0x1DBF而不是0x20

实现者们并没有考虑到这种情况。例如,如果您在一些东欧语言(例如西里尔语我不了解)中应用此技巧,您将会有一个不愉快的惊喜,所有这些“hatchet”字符都是例子,小写和大写相差一个单位,所以此技巧在那里不能正常工作。
还有许多要考虑的因素,例如,有些字符根本不会简单地从小写转换为大写(它们会被替换为不同的序列),或者它们可能会改变形式(需要不同的代码点)。
甚至不要想象此技巧对泰国语或汉语等文字会产生什么影响(它只会给您带来完全的无意义)。
节省几百个CPU周期也许在30年前非常值得,但现在,对字符串进行适当的转换真的没有借口。有库函数可以执行这个非平凡的任务。现在适当地转换几十千字节的文本所需的时间是微不足道的。

2
我完全同意,尽管让每个程序员知道为什么它起作用是一个好主意--这甚至可能成为一个好的面试问题..它是做什么的,何时应该使用 :) - Bill K

33

它可以工作是因为,在ASCII和派生编码中,“a”和“A”的差异是32,而32也是第六位的值。通过使用异或翻转第6个位,从而在大写字母和小写字母之间进行转换。


21

你的字符集实现很可能是ASCII。如果我们看一下下表:

enter image description here

我们可以看到,小写字母和大写字母之间的值恰好相差32。因此,如果我们执行^= 32(即切换第六位最低有效位),它就会在小写字母和大写字母之间切换。

请注意,它适用于所有符号,而不仅仅是字母。它会将一个具有不同第六位的相应字符进行切换,从而得到一对反复切换的字符。对于字母,相应的大/小写字符形成这样一对。 NUL 将变为 Space,反之亦然,而 @ 与反引号切换。基本上,此图表上第一列中的任何字符都会与其右侧的字符切换,第三列和第四列也是如此。

虽然它可以在某些系统上正常工作,但我不建议使用这个hack。相反,请使用touppertolower以及isupper等查询函数。


2
好的,它不适用于所有差32的字母。否则,它将在“@”和“!”之间起作用。 - Matthieu Brucher
2
@MatthieuBrucher 它正在工作,32 ^ 32 是0,而不是64。 - NathanOliver
5
'@'和' '不是“字母”,只有[a-z][A-Z]才是“字母”。其余的都是遵循相同规则的巧合。如果有人要求你将“]”变成大写字母,它仍然会是“]”-“}”不是“]”的“大写字母”。 - freedomn-m
5
另一种阐述这个观点的方式是,ASCII编码系统中的小写字母和大写字母的范围没有跨越%32的"对齐"边界。这就是为什么同一个字母的大小写版本之间唯一的区别是比特位0x20。如果不是这样,你需要添加或减去0x20,而不仅仅是切换大小写,并且对于某些字母,可能会发生进位以翻转其他更高位的比特位。 (同时,相同的操作无法切换大小写,并且检查字母字符本身会更加困难,因为你无法使用“|= 0x20”来强制小写。) - Peter Cordes
2
+1 提醒我那些访问asciitable.com并盯着那个图形(还有扩展ASCII版本!)的时光,这已经持续了15年或20年了吧? - A C
显示剩余8条评论

14
这里有很多好的答案描述了它是如何工作的,但为什么要这样做是为了提高性能。在处理器内部,按位操作比大多数其他操作更快。您可以通过简单地不查看确定大小写的位或翻转该位(设计ASCII表的那些家伙非常聪明)来快速进行不区分大小写的比较或更改大小写。

显然,由于更快的处理器和Unicode,今天这并不像1960年(当ASCII首次开始工作)那样重要,但仍然有一些低成本的处理器可以显著提高性能,只要您能保证只使用ASCII字符。

https://en.wikipedia.org/wiki/Bitwise_operation

在简单的低成本处理器上,通常比除法快几倍,比乘法快数倍,并且有时比加法显着更快。注意:我建议使用标准库来处理字符串,原因有很多(可读性、正确性、可移植性等)。仅在您测量了性能并且这是瓶颈时才使用位翻转。

13

这就是 ASCII 的工作原理。

然而,利用它时,您会失去 可移植性,因为 C++ 不会坚持使用 ASCII 编码。

这就是为什么在 C++ 标准库中实现了函数 std::toupperstd::tolower - 您应该使用这些函数。


6
有一些协议要求使用ASCII编码,例如DNS。实际上,某些DNS服务器使用“0x20技巧”将额外的熵插入DNS查询作为反欺诈机制。DNS对大小写不敏感,但也应该是大小写保持不变的,因此如果发送一个随机大小写的查询并获得相同大小写的响应,则表明响应没有被第三方欺诈。 - Alnitak
值得一提的是,许多编码仍然使用相同的表示方式来表示标准(非扩展)ASCII字符。但是,如果您真的担心不同的编码,您应该使用适当的函数。 - Captain Man
5
当然。UTF-8 是一件绝妙的事情。希望它能像 IEEE754 浮点数那样被吸收到 C++ 标准中。 - Bathsheba

10
请查看位于http://www.catb.org/esr/faqs/things-every-hacker-once-knew/#_ascii的第二个表格,以及以下的笔记,如下所述:

键盘上的Control修饰键基本上会清除您键入字符的前三位,只留下底部的五位并将其映射到0..31范围。因此,例如,Ctrl-SPACE、Ctrl-@和Ctrl-'都意味着NUL。

非常旧的键盘使用切换32或16位来完成Shift操作;这就是为什么ASCII中小写字母和大写字母之间的关系如此规律,数字和符号以及某些符号对之间的关系也有点规律。甚至可以通过移位16位来生成一些ASR-33不具备的标点符号; 因此,例如,Shift-K(0x4B)变成了 [(0x5B)。

ASCII被设计为使得shift和ctrl键可以实现而无需太多(或者也许没有ctrl需要任何)逻辑 - 可能只需要一些门。将电线协议存储为任何其他字符编码可能更有意义(不需要进行软件转换)。

这篇链接文章解释了许多奇怪的黑客惯例,比如And control H does a single character and is an old^H^H^H^H^H classic joke.在这里发现)。


1
可以使用foo ^= (foo & 0x60) == 0x20 ? 0x10 : 0x20来实现ASCII字符的移位切换,但由于其他答案中所述的原因,这仅适用于ASCII字符,因此不明智。它可能也可以通过无分支编程来改进。 - Iiridayn
1
啊,foo ^= 0x20 >> !(foo & 0x40)会更简单。同时也是为什么简洁的代码通常被认为难以阅读的好例子 ^_^。 - Iiridayn

7

使用32(二进制中的00100000)进行异或操作将设置或重置第六位(从右边开始计数)。这等价于加上或减去32。


2
另一种说法是,XOR 是不带进位的加法。 - Peter Cordes

6
小写字母和大写字母范围在ASCII编码系统中不跨越32%的“对齐”边界。这就是为什么同一字母的大小写版本之间唯一的区别是位0x20的原因。
如果不是这样,您需要添加或减去0x20,而不仅仅是切换,并且对于某些字母,可能会有进位来翻转其他更高的位。 (并且不会有单个操作可以切换,并且首先检查字母字符将更加困难,因为您无法使用|= 0x20强制小写。)

相关的ASCII技巧:通过强制小写字母c |= 0x20,然后检查是否(无符号)c - 'a' <= ('z'-'a'),可以检查字母ASCII字符。因此,只需3个操作:OR + SUB + CMP与常数25进行比较。当然,编译器知道如何优化(c>='a' && c<='z') 就像这样为您转换成汇编语言,所以最多应该自己做c|=0x20部分。自己完成所有必要的类型转换非常不方便,特别是为了解决默认整数提升为有符号int的问题。

unsigned char lcase = y|0x20;
if (lcase - 'a' <= (unsigned)('z'-'a')) {   // lcase-'a' will wrap for characters below 'a'
    // c is alphabetic ASCII
}
// else it's not

换句话说:

 unsigned char lcase = y|0x20;
 unsigned char alphabet_idx = lcase - 'a';   // 0-index position in the alphabet
 bool alpha = alphabet_idx <= (unsigned)('z'-'a');

另请参阅如何将C++字符串转换为大写(仅针对ASCII字符的SIMD字符串toupper,使用该检查掩码操作数进行XOR。)

还有如何访问字符数组并将小写字母变为大写字母,反之亦然 (使用SIMD内在函数的C语言和标量x86汇编大小写翻转,仅修改字母ASCII字符,不修改其他字符。)


这些技巧主要用于手动优化使用SIMD(例如SSE2或NEON)的文本处理,前提是检查向量中没有任何char的高位设置。 (因此,没有任何字节是单个字符的多字节UTF-8编码的一部分,可能具有不同的大/小写反转)。如果发现任何问题,则可以在该16字节块或整个字符串的其余部分上退回到标量。

甚至有一些语言环境,其中对ASCII范围内某些字符执行toupper()tolower()会产生超出该范围的字符,特别是土耳其语,其中I ↔ ı和İ ↔ i。在这些区域设置中,您需要进行更复杂的检查,或者根本不尝试使用此优化。


但在某些情况下,您可以假设ASCII而不是UTF-8,例如使用LANG=C(POSIX语言环境)的Unix实用程序,而不是en_CA.UTF-8或其他。

但如果您可以验证它是安全的,您可以比在循环中调用toupper()(如5倍)更快地将中等长度的字符串转换为大写,并且最后我测试了Boost 1.58,比执行每个字符的愚蠢的dynamic_castboost::to_upper_copy<char*, std::string>()要快得多。


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