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

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

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

我一直在寻找这个问题的解释,但是没有找到答案。那么为什么会这样呢?

18981 次浏览

它之所以有效,是因为 ASCII 和派生编码中的“ a”和“ A”之间的差值是32,而32也是第六位的值。用独占 OR 翻转第6位,从而在上位和下位之间转换。

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

enter image description here

我们看到小写数字和大写数字的值之间有一个完全不同的 32。因此,如果我们使用 ^= 32(相当于切换第6个最低有效位) ,它会在小写字母和大写字母之间变化。

请注意,它适用于所有的符号,而不仅仅是字母。它将一个字符与第6位不同的相应字符进行切换,从而产生一对在两个字符之间来回切换的字符。对于这些字母,各自的大小写字符组成这样一对。NUL会变成 Space,反过来,@与反勾切换。基本上,这个图表第一列中的任何字符都会与第一列之上的字符切换,第三和第四列也是如此。

不过我不会使用这种黑客技术,因为不能保证它在任何系统上都能正常工作。只需使用 Toupper再低一点,以及诸如 Isupper之类的查询即可。

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

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

32是 0100000这是小写字母和大写字母的唯一区别。所以切换这个位就可以切换字母的大小写。

这使用的事实比 ASCII 值已经被真正聪明的人选择。

foo ^= 32;

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

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

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

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

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

使用32(00100000二进制)的 Xoring 设置或重置第六位(从右边开始)。这完全等同于32的加减。

这里有很多很好的答案来描述它是如何工作的,但是它为什么这样工作是为了提高性能。按位操作比处理器中的大多数其他操作都要快。你可以通过简单的翻转位(那些设计 ASCII 表的家伙非常聪明)而不去查看决定大小写的位或者将大小写改为上/下,从而快速地进行不区分大小写的比较。

显然,由于更快的处理器和 Unicode,这在今天并不像1960年(当时 ASCII 首次开始工作)那么重要,但是仍然有一些低成本的处理器,只要你能保证只有 ASCII 字符,这可能会带来显著的不同。

Https://en.wikipedia.org/wiki/bitwise_operation

在简单的低成本处理器上,按位操作通常是 比除法快很多倍 乘法,有时明显快于加法。

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

请允许我说这是——尽管看起来很聪明——一个非常非常愚蠢的黑客行为。如果有人在2019年向你推荐这个,打他。用力打他。
当然,如果你知道除了英语以外你永远不会使用任何语言,那么你可以在你自己的软件中做这件事。否则,不能去。

30-35年前,当计算机除了用 ASCII 语言和 也许吧语言学习一两种主要的欧洲语言外,并没有做什么其他事情的时候,这次黑客攻击还算“可以”。但是... 现在不是了。

这种黑客手段之所以有效,是因为美国-拉丁语的大写和小写字母之间恰好是 0x20,而且顺序相同,只有一点点不同。事实上,这个黑客程序会切换。

现在,为西欧以及后来的统一码联盟创建代码页的人非常聪明,他们把这个方案留给了德国元音和法国口音的元音。但是对于 ß 来说就不是这样了(直到2017年有人说服了统一码联盟,一家大型的假新闻印刷杂志写了关于它的文章,实际上说服了都登——没有评论)。现在它的 是的存在作为普遍,但两个是 0x1DBF位置分开,而不是 0x20

然而,实现者对 没有的考虑足以使其继续下去。例如,如果你使用一些东欧语言或类似的语言(我不知道西里尔字母) ,你会得到一个令人讨厌的惊喜。所有这些“斧头”字符都是例子,小写字母和大写字母是分开的。这样黑客就可以在那里正常工作。

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

甚至不要去想这个黑客会对泰语或中文做什么(它只会给你完全的胡说八道)。

30年前,节省几百个 CPU 周期可能是非常值得的,但是现在,真的没有理由正确地转换字符串。有一些库函数可以用来执行这个重要的任务。
现在转换几十 KB 的文本 适当地所花费的时间是可以忽略不计的。

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.(在这里发现的)。

在 ASCII 编码系统中,小写和大写字母范围不会跨越 %32“对齐”边界。

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

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


相关的仅 ASCII 技巧: 您可以检查字母 ASCII 字符通过强制使用小写字母 c |= 0x20,然后检查是否(无符号) c - 'a' <= ('z'-'a')。所以只有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 的操作数)

如何访问一个字符数组并将小写字母改为大写字母,反之亦然 (C 语言具有 SIMD 内部特性,标量 x86 asm 大小写翻转用于字母 ASCII 字符,其他字符未修改。)


这些技巧只有在用 SIMD (例如 SSE2或 NEON)手动优化一些文本处理时才有用,在检查向量中的 char都没有高位集之后。(因此,没有一个字节是单个字符的多字节 UTF-8编码的一部分,它们可能有不同的大小写逆序)。如果找到了,您可以回到标量来处理这个16字节的块,或者处理字符串的其余部分。

甚至有些语言环境中,ASCII 范围内的某些字符上的 toupper()tolower()会产生超出该范围的字符,特别是土耳其语中的 I something 和 something i 在这些语言环境中,您需要更复杂的检查,或者可能根本不尝试使用这种优化。


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

但是如果你可以验证它的安全性,你可以使用 toupper中等长度的字符串比循环中调用 toupper()快得多(比如5x) ,使用 上次我测试 Boost 1.58boost::to_upper_copy<char*, std::string>()快得多(boost::to_upper_copy<char*, std::string>()对每个字符都执行一个愚蠢的 dynamic_cast)。