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


146

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

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

我已经搜索了对此的解释,但没有找到。那为什么行得通呢?


5
en.wikipedia.org/wiki/File:USASCII_code_chart.png提示:您可以使用转换@为` ^ 32
KamilCuk

112
FWIW,它并不是真的“有效”。它适用于此特定字符集,但有些其他字符集则不可用touppertolower请切换大小写。
NathanOliver

7
有时在网上比赛中,“主意”就是以一种难以理解的方式编写代码,这样它就永远不会通过认真的审查;)
idclev 463035818

21
^ =正在使用XOR转换值。大写ASCII字母的对应位为零,而小写字母的为1。也就是说,请不要!使用适当的字符(unicode)例程在小写和大写之间进行转换。ASCII时代早已过去。
汉斯·马丁·莫斯纳

14
不仅仅是它仅适用于某些字符集。即使我们假设所有的世界是UTF-8(可能至少是一个不错的乌托邦式的目标),它也只能用26个字母的作品AZ。只要您只关心英语(不要使用拼写“天真”,“咖啡馆”之类的单词或带有变音符号的名称...),那很好,但是世界不只是英语。
ilkkachu

Answers:


149

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

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

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


49
“切换大小写” *仅适用于ASCII
Mooing Duck

39
@Mooing仅适用于ASCII中的A-Za-z。“ [”的小写字母不是 “ {”。
dbkk

21
@dbkk {比短[,因此是“小写”的情况。没有?好吧,我将展示自己:D
彼得·巴迪达

25
Trivia tidbit:在7位区域中,德国计算机已将[] {|}重映射到ÄÖÜäöüü,因为我们需要Umlauts而不是这些字符,因此在这种情况下,{(ä)实际上小写的[(Ä))。
Guntram Blohm

14
@GuntramBlohm进一步的琐事花絮,这就是为什么IRC服务器考虑 foobar[]foobar{}使用相同的昵称,因为昵称不区分大小写,并且IRC起源于斯堪的纳维亚半岛:)
ZeroKnight,

117

这利用了事实,即真正聪明的人已经选择了ASCII值。

foo ^= 32;

这个翻转第六最低位1foo(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'

根据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'));

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


16
EBCDIC也被一些非常聪明的人选择:在打孔卡上的效果非常好。ASCII这是一团糟。但这是一个不错的答案,+ 1。
Bathsheba,

65
我不知道穿孔卡片,但是ASCII 在纸带上使用。这就是Delete字符编码为1111111的原因:因此,您可以通过在磁带上的列中打出所有孔来将任何字符标记为“已删除”。
dan04 '19

23
@Bathsheba还是一个没有使用打孔卡的人,所以很难将EBCDIC设计为智能的想法笼罩在脑海中。
法夸德勋爵(Lord Farquaad)

9
@LordFarquaad恕我直言,维基百科上如何在打孔卡上写字母的图片清楚地说明了EBCDIC如何使这种编码具有某种意义(但不完全是,见/ vs)。en.wikipedia.org/wiki/EBCDIC#/media/...
Peteris

11
@ dan04请注意提及“'MASSE'的小写形式是什么?”。对于那些不知道的人,德语中有两个单词,其大写形式为MASSE。一个是“ Masse”,另一个是“Maße”。正确tolower的德语不仅需要词典,还需要能够解析含义。
马丁·邦纳

35

让我说,这-尽管看起来很聪明-确实是一个非常愚蠢的黑客。如果有人在2019年向您推荐这个,请打他。尽力打他。
当然,如果您知道您绝不会使用除英语以外的任何语言,那么您当然可以在自己的软件中使用它,而您和其他人都不会使用。否则,不去。

大约30-35年前,当计算机并没有真正做很多事情,而是使用ASCII的英语(也许是一两种主要的欧洲语言)时,这种黑客论点就可以了。但是...不再如此。

该hack之所以有效,是因为US-Latin的大写和小写字母彼此完全0x20分开并且以相同的顺序出现,只是一点点差异。实际上,这有点hack。

现在,为西欧以及后来的Unicode联合会创建代码页的人们足够聪明,可以将这种方案保留给德国的Umlauts和法语的元音。ß并非如此(直到有人在2017年说服Unicode联盟,并且一本大型的Fake News印刷杂志对此进行了报道,实际上说服了Duden-对此没有评论)甚至没有(作为转换)(转换为SS) 。现在它的确存在,但两者是0x1DBF分开的,不是0x20

但是,实现者不够周到,无法继续进行下去。例如,如果您使用某些东欧语言或类似语言(我对西里尔语不了解)来进行黑客攻击,则会感到讨厌。所有那些“柴刀”字符就是这样的例子,小写字母和大写字母是分开的。因此,hack 无法在此处正常运行。

还有更多需要考虑的内容,例如,某些字符根本不会简单地从小写转换为大写(它们被替换为不同的序列),或者它们可能会更改形式(需要不同的代码点)。

甚至不用考虑这种黑客将对泰国或中国这样的东西产生什么影响(它只会使您完全胡说八道)。

节省数百个CPU周期在30年前可能是非常值得的,但如今,确实没有任何借口来正确转换字符串。有用于执行此重要任务的库函数。如今
正确转换几十千字节文本所花费的时间可以忽略不计。


2
我完全同意-尽管每个程序员都知道为什么它是一个好主意-甚至可能会提出一个很好的面试问题。这是做什么用的,何时应使用:)
Bill K,

33

之所以起作用,是因为ASCII和导出的编码中的“ a”和“ A”之间的差是32,并且32也是第六位的值。因此,使用异或将第6位翻转会在高位和低位之间转换。


22

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

在此处输入图片说明

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

请注意,它适用于所有符号,而不仅仅是字母。它在第6位不同的各个字符之间切换字符,从而在一对字符之间来回切换。对于字母,相应的大写/小写字符形成这样的一对。A NUL将变为Space@反之亦然,并使用反引号进行切换。基本上,此图表第一列中的任何字符都将字符切换为一列,第三列和第四列也是如此。

不过,我不会使用此技巧,因为无法保证它可以在任何系统上正常工作。只需使用touppertolower代替,然后使用isupper之类的查询。


2
好吧,它不适用于所有相差32的字母。否则,它将在'@'和''之间起作用!
Matthieu Brucher

2
@MatthieuBrucher它正在工作,32 ^ 32是0,而不是64
NathanOliver

5
'@'和''不是“字母”。只有[a-z][A-Z]是“字母”。其余都是遵循相同规则的巧合。如果有人要求您“大写],那会是什么?它仍然是“]”-“}”不是“]”的“大写”。
freen-m

4
@MatthieuBrucher:指出这一点的另一种方法是,%32在ASCII编码系统中,小写和大写字母范围都不会越过“对齐”边界。 就是为什么位0x20是同一字母的大写/小写版本之间唯一的区别。如果不是这种情况,则您需要添加或减去0x20,而不仅是切换,对于某些字母,可以进行进位来翻转其他更高的位。(而且相同的操作无法切换,并且首先要检查字母字符会更困难,因为您不能|= 0x20强制使用lcase。)
Peter Cordes

2
+1是让我想起过去15或20年间对asciitable.com的所有访问,他们盯着那个确切的图形(以及扩展的ASCII版本!),是吗?
AC

15

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

显然,由于处理器和Unicode的速度更快,今天这并不像1960年(当时刚开始研究ASCII时)那样重要,但是仍然有一些低成本的处理器可能会产生重大的变化只要您只能保证ASCII字符。

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

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

注意:出于多种原因(可读性,正确性,可移植性等),我建议使用标准库来处理字符串。仅在测量性能后才使用位翻转,这是您的瓶颈。


14

这就是ASCII的工作原理,仅此而已。

但是,在利用这一点时,您放弃了可移植性,因为C ++并不坚持将ASCII作为编码。

这就是为什么功能std::toupperstd::tolower在C ++标准库中实现-你应该使用这些。


6
但是,有些协议要求使用ASCII,例如DNS。实际上,某些DNS服务器使用“ 0x20技巧”将附加的熵作为反欺骗机制插入DNS查询中。DNS不区分大小写,但也应该保留大小写,因此,如果发送带有随机大小写的查询并返回相同的大小写,则很好地表明了该响应未被第三方欺骗。
Alnitak

值得一提的是,对于标准(非扩展)ASCII字符,许多编码仍然具有相同的表示形式。但是,如果您真的担心不同的编码,则应该使用适当的功能。
上尉曼

5
@CaptainMan:绝对。UTF-8真是太美了。希望它可以“吸收”到IEEE754具有浮点的C ++标准中。
Bathsheba,

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.在此处找到)。


1
可以为更多的ASCII w /实现移位切换foo ^= (foo & 0x60) == 0x20 ? 0x10 : 0x20,尽管这只是ASCII,因此由于其他答案中所述的原因是不明智的。可以通过无分支编程进行改进。
Iiridayn

1
啊,foo ^= 0x20 >> !(foo & 0x40)会更简单。这也是为什么简短代码通常被认为不可读的一个很好的例子。
Iiridayn


7

%32在ASCII编码系统中,小写字母和大写字母范围都没有越过“对齐”边界。

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

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


相关的纯ASCII技巧:您可以通过将小写字母强制为小写c |= 0x20然后检查(unsigned)是否可以检查字母ASCII字符c - 'a' <= ('z'-'a')。因此,只需3个操作:针对常数25的OR + SUB + CMP。当然,编译器知道如何(c>='a' && c<='z') 为您优化成这样的asm,因此,您最多应该c|=0x20自己完成这一部分。自己做所有必要的转换是很不方便的,特别是要解决对signed的默认整数提升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

另请参见将C ++中的字符串转换为大写toupper仅SIMD字符串用于ASCII,使用该检查为XOR屏蔽操作数​​。)

以及如何访问char数组并将小写字母更改为大写字母,反之亦然 (带有SIMD内部函数的C,以及字母ASCII字符的标量x86 asm大小写翻转,其他都保持不变。)


仅当在检查char向量中的s 均未设置高位之后,使用SIMD手动优化某些文本处理(例如SSE2或NEON)时,这些技巧才最有用。(因此,所有字节都不是单个字符的多字节UTF-8编码的一部分,该编码可能具有不同的大写/小写倒数)。如果找到任何内容,则可以针对这16个字节的块或字符串的其余部分退回到标量。

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


但是在某些情况下,允许您使用ASCII而不是UTF-8,例如,带有LANG=C(POSIX语言环境)的Unix实用程序,不是en_CA.UTF-8或不是。

但是,如果可以验证它的安全性,则toupper中等长度的字符串可以比toupper()在循环中调用(例如5x)快得多,最后我使用Boost 1.58进行了测试速度比每个字符boost::to_upper_copy<char*, std::string>()都愚蠢得多dynamic_cast

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.