^ = 32背后的想法是什么,将小写字母转换为高位字母,反之亦然?

Dev*_*on 146 c++ ascii bit-manipulation

我在解决代码问题上遇到了一些问题.通常我首先检查字符是英文字母的上部还是下部,然后减去或添加32以将其转换为相应的字母.但我发现有人^= 32做了同样的事情.这里是:

char foo = 'a';
foo ^= 32;
char bar = 'A';
bar ^= 32;
cout << foo << ' ' << bar << '\n'; // foo is A, and bar is a
Run Code Online (Sandbox Code Playgroud)

我已经搜索了这方面的解释并没有找到答案.那么为什么会这样呢?

Han*_*Lee 149

我们来看看二进制的ASCII码表.

A 1000001    a 1100001
B 1000010    b 1100010
C 1000011    c 1100011
...
Z 1011010    z 1111010
Run Code Online (Sandbox Code Playgroud)

而32是0100000小写和大写字母之间的唯一区别.因此,切换该位会切换字母的大小写.

  • "切换案例"*仅适用于ASCII (49认同)
  • @Mooing仅用于ASCII中的A-Za-z.小写"["是*不*"{". (39认同)
  • Trivia花絮:在7位区域,德国计算机将[\] {|}重新映射到ÄÖÜäöü,因为我们需要的元音更多的是元音,所以在这种情况下,{(ä)实际上*是*小写[(Ä). (25认同)
  • @dbkk` {`比`[`更短,所以它是一个"更低"的情况.没有?好的,我会告诉自己:D (21认同)
  • @GuntramBlohm进一步的琐事,这就是为什么[IRC服务器考虑](https://tools.ietf.org/html/rfc1459.html#section-2.2)`foobar []`和`foobar {}`是相同的昵称,因为昵称是*不敏感*,而IRC起源于斯堪的纳维亚:) (14认同)
  • @KeyWeeUsr我现在必须找到一个反驳你的论点的字体.... (2认同)

YSC*_*YSC 117

这使用了真实聪明人选择的ASCII值.

foo ^= 32;
Run Code Online (Sandbox Code Playgroud)

这个翻转第六最低位1foo(ASCII排序的大写标志),转化的ASCII上壳体的下壳体和反之亦然.

+---+------------+------------+
|   | Upper case | Lower case |  32 is 00100000
+---+------------+------------+
| A | 01000001   | 01100001   |
| B | 01000010   | 01100010   |
|            ...              |
| Z | 01011010   | 01111010   |
+---+------------+------------+
Run Code Online (Sandbox Code Playgroud)

'A' ^ 32

    01000001 'A'
XOR 00100000 32
------------
    01100001 'a'
Run Code Online (Sandbox Code Playgroud)

并且通过XOR的财产,'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'));
Run Code Online (Sandbox Code Playgroud)

1)当32是1 << 5(2到5的幂)时,它翻转第6位(从1开始计数).

  • 我不知道打卡,但是在纸带上使用了ASCII*.这就是为什么删除字符被编码为1111111:所以你可以通过在磁带上的列中冲出所有孔来标记任何字符为"已删除". (65认同)
  • @Bathsheba作为一个没有使用过穿孔卡的人,很难围绕EBCDIC智能设计的想法. (23认同)
  • 一些非常聪明的人也选择了EBCDIC:在穿孔卡上工作非常好.ASCII这是一个烂摊子.但这是一个很好的答案,+ 1. (16认同)
  • @ dan04请注意"'MASSE'的小写形式是什么?".对于那些不知道的人,德语中有*两个*单词,其大写形式为MASSE; 一个是"Masse",另一个是"Maße".德语中适当的"tolower"不仅需要字典,还需要能够解析其含义. (11认同)
  • @LordFarquaad恕我直言,维基百科图片中的字母是如何写在一个穿孔卡上的,这清楚地说明了EBCDIC如何为这种编码做出一些(但不是全部,见/ vs S)感觉.https://en.wikipedia.org/wiki/EBCDIC#/media/File:Blue-punch-card-front-horiz_top-char-contrast-stretched.png (9认同)
  • _"这个技巧只适用于ASCII平台."_ - 的确,类似的`^ = 64`虽然适用于EBCDIC!(但不再适用于ASCII) (6认同)
  • @MartinBonner [关于Unicode的案例映射很难](https://blogs.msdn.microsoft.com/oldnewthing/20030905-00/?p=42643/) (2认同)

Dam*_*mon 35

请允许我说这是 - 虽然它看起来很聪明 - 真的,非常愚蠢的黑客.如果有人在2019年向你推荐这个,打他.尽可能地打他.
当然,如果你知道除了英语之外你永远不会使用任何语言,你可以在你自己的软件中使用你自己的软件.否则,不去.

大约30-35年前,当计算机实际上并没有做多少时,黑客可以称之为"OK",而英语则是ASCII,也许是一两种主要的欧洲语言.但是......不再如此.

黑客攻击是有效的,因为美国 - 拉丁语的上下两个区域完全相互0x20分开,并且以相同的顺序出现,这只是差异的一点点.事实上,这有点黑客,切换.

现在,为西欧创建代码页的人们,以及后来的Unicode联盟,都非常聪明,可以保留这种方案,例如德国元音和法语重音的元音.对于ß来说并非如此(直到有人在2017年确认了Unicode联盟,以及大型假新闻印刷杂志上写到这一点,实际上说服杜登 - 没有评论)甚至不存在作为一个版本(转换为SS) .现在它确实存在,但两者是0x1DBF分开的,不是0x20.

然而,实施者并没有考虑到这一点.例如,如果你用一些东欧语言或类似的方式应用你的黑客(我不会知道西里尔语),你会得到一个令人讨厌的惊喜.所有这些"斧头"字符都是其中的例子,小写和大写是相互分开的.因此黑客在那里不能正常工作.

还有更多要考虑的事情,例如,一些字符根本不会简单地从低级变换为大写(它们被不同的序列替换),或者它们可能会改变形式(需要不同的代码点).

甚至不要考虑这个黑客会对泰国人或中国人做些什么(它只是给你完全无稽之谈).

30年前节省了几百个CPU周期可能非常值得,但是现在,没有理由正确地转换字符串.有用于执行这个非平凡任务的库函数.现在正确地
转换几十千字节文本所花费的时间可以忽略不计.

  • 我完全同意 - 尽管每个程序员都知道它的工作原理是个好主意 - 甚至可能会提出一个很好的面试问题.这是什么以及何时应该使用:) (2认同)

Jac*_*ley 33

这是有效的,因为在发生这种情况时,ASCII和派生编码中'a'和A'之间的差异是32,而32也是第六位的值.使用异或将第6位翻转,从而在上下之间进行转换.


Bla*_*aze 22

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

在此输入图像描述

我们看到32小写和大写数字的值之间存在差异.因此,如果我们这样做^= 32(相当于切换第6个最低有效位),它会在小写和大写字符之间切换.

请注意,它适用于所有符号,而不仅仅是字母.它使用第6位不同的相应字符切换字符,从而产生一对在其间来回切换的字符.对于字母,相应的大写/小写字符形成这样的一对.A NUL会改变Space,@反之亦然,然后用反推来切换.基本上,此图表第一列中的任何字符都会切换一列的字符,同样适用于第三列和第四列.

我不会使用这个hack,因为不能保证它可以在任何系统上运行.只需使用touppertolower,以及诸如isupper之类的查询.

  • '@'和''不是"字母".只有`[az]`和`[AZ]`是"字母".其余的是遵循同样规则的巧合.如果有人问你"大写",那会是什么?它仍然是"]" - "}"不是"]"的"大写". (5认同)
  • @MatthieuBrucher:另一种说明这一点的方法是,小写和大写字母范围不会跨越ASCII编码系统中的'%32`"对齐"边界.*这就是为什么位"0x20"是同一个字母的大写/小写版本之间的唯一区别.如果不是这种情况,则需要添加或减去"0x20",而不仅仅是切换,对于某些字母,会有进位以翻转其他更高位.(并且相同的操作无法切换,并且首先检查字母字符会更难,因为你不能`| = 0x20`来强制lcase.) (4认同)
  • 好吧,它不适用于所有相差32的字母.否则,它会在'@'和''之间起作用! (2认同)
  • @MatthieuBrucher它正在工作,`32 ^ 32`是0,而不是64 (2认同)
  • +1提醒我所有这些访问asciitable.com盯着那个确切的图形(和扩展的ASCII版本!!)为最后,我不知道,15或20年? (2认同)

Bri*_*ian 15

这里有很多好的答案描述了它是如何工作的,但是为什么它以这种方式工作就是提高性能.按位运算比处理器内的大多数其他运算更快.您可以快速进行不区分大小写的比较,只需不查看确定大小写的位或通过翻转位来将大小写更改为大/小(那些设计ASCII表的人非常聪明).

显然,由于更快的处理器和Unicode,这在1960年(当时的工作首次开始使用ASCII)时,这并不像今天的交易那么大,但仍有一些低成本的处理器,这可能会产生显着的差异只要你能保证只有ASCII字符.

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

在简单的低成本处理器上,通常,按位运算比除法快得多,比乘法快几倍,有时比加法快得多.

注意:出于多种原因(可读性,正确性,可移植性等),我建议使用标准库来处理字符串.如果您已经测量了性能,那么只能使用位翻转,这是您的瓶颈.


Bat*_*eba 14

这就是ASCII的工作原理,就是这样.

但是在利用它时,你放弃了可移植性,因为C++不坚持使用ASCII作为编码.

这就是函数std::toupperstd::tolower在C++标准库中实现的原因- 您应该使用它们.

  • 但是有些协议要求使用ASCII,例如DNS.事实上,某些DNS服务器使用"0x20技巧"将额外的熵插入到DNS查询中作为反欺骗机制.DNS不区分大小写,但也应该保留大小写,因此如果发送带有随机大小写的查询并获得相同的大小写,则可以很好地指示响应未被第三方欺骗. (6认同)
  • @CaptainMan:绝对.UTF-8是一种纯粹的美丽.希望它能够"吸收"到IEEE754具有浮点的C++标准中. (5认同)

Iir*_*ayn 11

请参见http://www.catb.org/esr/faqs/things-every-hacker-once-knew/#_ascii的第二个表格,以及下面的说明,转载如下:

键盘上的Control修饰符基本上清除了您键入的任何字符的前三位,保留底部五位并将其映射到0..31范围.所以,例如,Ctrl-SPACE,Ctrl- @和Ctrl-`都意味着同样的事情:NUL.

非常旧的键盘,只需切换32位或16位,就可以进行Shift操作,具体取决于键; 这就是为什么ASCII中的小写和大写字母之间的关系是如此规则的原因,如果你眯着眼睛,数字和符号以及一些符号对之间的关​​系是有规律的.ASR-33是一个全大写的终端,甚至可以通过移动16位来生成一些没有键的标点字符; 因此,例如,Shift-K(0x4B)变为[(0x5B)

设计ASCII时,可以在没有太多(或者可能是任何)逻辑的情况下实现shiftctrl键盘键ctrl- shift可能只需要几个门.它可能至少与任何其他字符编码存储有线协议一样有意义(不需要软件转换).

链接的文章解释了许多奇怪的黑客约定,例如And control H does a single character and is an old^H^H^H^H^H classic joke.(在此处找到).


Yve*_*ust 8

Xoring 32(二进制00100000)设置或重置第六位(从右侧).这相当于添加或减去32.

  • 另一种说法是XOR是无需携带的. (2认同)

Pet*_*des 7

小写和大写字母范围不跨越%32ASCII编码系统中的"对齐"边界.

这就是为什么bit 0x20是同一个字母的大写/小写版本之间的唯一区别.

如果不是这种情况,则需要加或减0x20,而不仅仅是切换,对于某些字母,会有进位以翻转其他更高的位.(并且没有一个操作可以切换,并且首先检查字母字符会更难,因为你不能| = 0x20来强制lcase.)


相关的纯ASCII技巧:您可以通过强制使用小写c |= 0x20然后检查(无符号)来检查字母ASCII字符c - 'a' <= ('z'-'a').所以只需3个操作:OR + SUB + CMP对常数25.

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
Run Code Online (Sandbox Code Playgroud)

另请参见将C++中的字符串转换为大写字母((c>='a' && c<='z')仅适用于ASCII的SIMD字符串,使用该检查屏蔽XOR的操作数.)

还有如何访问char数组并将小写字母更改为大写字母,反之亦然 (C使用SIMD内在函数,标量x86 asm case-flip用于字母ASCII字符,而其他字母则不加修改.)


这些技巧大多只有在使用SIMD(例如SSE2或NEON)手动优化某些文本处理后,在检查c|=0x20向量中的所有s 都没有设置其高位后才有用.(因此,对于单个字符,没有字节是多字节UTF-8编码的一部分,这可能具有不同的大写/小写反转).如果你发现任何一个,你可以回退到这个16字节的块或者字符串的其余部分的标量.

甚至有一些语言环境在ASCII范围内的某些字符int或其中的toupper某些字符产生该范围之外的字符,特别是土耳其语,我andı和↔↔i. 在这些区域设置中,您需要更复杂的检查,或者可能根本不尝试使用此优化.


但在某些情况下,您可以假设ASCII而不是UTF-8,例如具有char(POSIX语言环境)的Unix实用程序,不是toupper()或者其他.

但是,如果你可以验证它是安全的,可以tolower()中等长度字符串比调用快得多LANG=C在一个循环(如5次),并最后我升压1.58测试,很多很多的速度比en_CA.UTF-8它做了愚蠢的toupper每个字符.