在C语言中,"??!??!"运算符是什么作用?

2538

我看到了一行看起来像这样的C代码:

!ErrorHasOccured() ??!??! HandleError();

这段代码编译正确且似乎正常运行。它似乎在检查是否发生了错误,如果发生了错误,则进行处理。但是我不确定它实际上在做什么或者是如何完成的。看起来程序员试图表达他们对错误的感受。

我从未在任何编程语言中见过??!??!,也找不到它的文档(谷歌搜索像??!??!这样的搜索术语无法帮助)。它是用来做什么的,以及这个代码示例是如何工作的?


87
不幸的是,这个宝石般的程序在C++17及更高版本中无法运行 - phoenix
14
ISO C23标准将删除三字符组。 - Keith Thompson
这是Kanetkar的谜题之一吗? - Peter Mortensen
4个回答

2029

??! 是一个将被翻译为 |三字符组合。因此,它的含义是:

!ErrorHasOccured() || HandleError();

由于短路,这等价于:

if (ErrorHasOccured())
    HandleError();

Guru of the Week (处理 C++ 相关的问题,但在此处也相关),我从那里获得了这个信息。

三字符序列的可能来源 或如 @DwB 在评论中指出,更可能是由于 EBCDIC 而变得困难 (再一次)。 这篇 IBM developerworks 论坛上的讨论似乎支持了这个理论。

来自 ISO/IEC 9899:1999 §5.2.1.1,注脚12 (感谢@Random832):

三字符序列使得可以输入在 ISO/IEC 646 描述的不在不变代码集合内,该集合是七位美国 ASCII 代码集合的子集的字符。


549
三连符最初是为了解决键盘没有 "|" 符号的问题。在这里,要么是程序员故意捣乱,要么是一些奇怪的编辑器"功能"。 - Martin Beckett
37
不一定是EBCDIC编码——需要使用三字符组的字符集几乎完全对应于在ISO-646(即旧的“国家ASCII”标准)中不是不变的字符集。 - Random832
95
如果你习惯于Shell脚本编程,那么一个完全可读的替代方法是ErrorHasOccurred() && HandleError(); - Yam Marcovic
13
请注意,许多编码标准明确禁止使用三重字和双重字,而且许多编译器和静态分析器会标记它们的使用。 - Luciano
13
自 C++17 起不再有效。 - val is still with Monica
显示剩余4条评论

590

一般来说,为什么这个存在的原因可能与它在你的例子中存在的原因不同。

事情始于半个世纪前,通过将硬拷贝通信终端重新用作计算机用户界面。 在最初的Unix和C时代,ASR-33 Teletype是这样一个设备。

该设备速度慢(10 cps),嘈杂且难看,并且其ASCII字符集的视图仅限��0x5f,因此它没有(仔细查看图片)任何键:

{ | } ~ 

三字符组(trigraphs)的定义是为了解决一个具体的问题。想法是C程序可以使用ASR-33和其他环境中缺少高ASCII值的ASCII子集。

你的示例实际上是两个??!,每个都表示|,因此结果是||

然而,写C代码的人基本上都有现代设备,1所以我猜测:这是某个人在展示或自娱自乐,留下了一种彩蛋让你去发现。

确实奏效了,它引起了一个非常受欢迎的SO问题。

ASR-33 Teletype

                                             ASR-33 电传打字机


1. 说到这里,三字符组是由ANSI委员会发明的,该委员会在C成为一项热门技术之后才首次开会,因此原始的C代码或编写者都不会使用它们。


35
在键盘和字符集中,缺失字符并非唯一的情况。对于那些三十多岁以上的人来说,Commodore 64 更为熟悉 —— 显示的字符集都缺少花括号(很可能也没有竖线和波浪符)——这是因为 "ASCII" 并不是真正的 ASCII。在 ECMA-6 中(几乎总被称为 ASCII,但不是 US-ASCII),有18个区域特定代码,但我不知道它们是哪些代码。有一件事是肯定的 —— 在英国的 "ASCII" 中,# 被替换为 £。在其他地区,也许 "ASCII" 没有花括号等符号。 - user180247
9
Atari 8位电脑所使用的类似ATASCII字符集也缺少{ }、~ 和`。 - dan04
55
请查看这两篇维基百科文章:ASCII#Incompatibility_vs_interoperabilityISO/IEC 646。我已经到了足够年龄还记得7位国家字符集的时代(虽然我敢肯定它们仍然存在于某些阴暗未被打扫的角落),而我从学习C语言的书中得知有必要警告可能出现 if (x || y) { a[i] = '\0'; } 看起来像在错误的字符集中写成 if (x öö y) ä aÄiÅ = 'Ö0'; å 的情况。 - Ilmari Karonen
17
另一个有趣的历史注解是,Unix(C语言所驾驭的大平台)可能是第一个在默认情况下将字母值设为小写而不是大写的重要系统(也许是第一个整体)。虽然我没有用自己的眼睛看到许多现代系统,但我认为这是真正的精妙之处。除了是唯一可靠的操作系统外,Unix还将您的大写转换为小写,而不是反过来。那些家伙真的很酷。 - DigitalRoss
29
有个好笑的故事要告诉你……IBM RS/6000工作站的XL Fortran编译器是从XL C编译器发展而来的。在最初的几个版本中,他们不小心保留了三字符处理功能,因此有些合法的Fortran字符序列(我记得是在文字字符串中)被误解为C语言的三字符序列,导致了一些有趣的错误! - Phil Perry
显示剩余8条评论

203

这是一个C语言的三字符组??!代表|,因此??!??!表示逻辑运算符||


14
三键符号(trigraph)源自一段时期,那个时候有些键盘没有现在拥有的所有键。当一些文本编辑器为特殊用途保留了特殊字符时,三键符号也非常有用。它主要是过去的遗物,也是一个有趣的挑战辅助工具 ;) - Joel Falcou
9
因为一些键盘显然没有"|"键,所以有些人别无选择,只能不断地用头猛击键盘,直到出现一个三字符组,给他们需要的符号。 - Owl
4
还有一个 <iso646.h> 头文件。 - David R Tribble

201

如前所述,??!??! 本质上是两个 三字符组合(再次出现的 ??!??!)被拼接在一起并由预处理器替换为 ||,即逻辑或

下表列出了每个三字符组合,有助于消除其他三字符组合的歧义:

Trigraph   Replaces

??(        [
??)        ]
??<        {
??>        }
??/        \
??'        ^
??=        #
??!        |
??-        ~

来源:C语言参考手册第五版

因此,类似于??(??)的三字符序列最终会映射为[]??(??)??(??)将被替换为[][]等等,你明白了。

由于三字符序列在预处理期间被替换,您可以使用cpp来查看自己的输出,使用一个愚蠢的trigr.c程序:

void main(){ const char *s = "??!??!"; } 

并使用以下方式进行处理:

cpp -trigraphs trigr.c 

您将获得控制台输出

void main(){ const char *s = "||"; }

注意到,必须指定选项-trigraphs,否则 cpp 会发出警告; 这表明三字符标识符已成为过去,除了让可能遇到它们的人感到困惑之外,没有现代价值。


至于引入三字符标识符背后的原因,查看ISO/IEC 646的历史部分可以更好地理解:

ISO/IEC 646及其前身ASCII(ANSI X3.4)在电信行业中主要认可了现有的字符编码实践。

由于ASCII没有提供其他语言所需的一些字符,因此制作了一些国家变体,用所需的字符替换了一些不常用的字符。

(我强调的)

因此,在某些国家变体中,一些所需字符(存在三字符标识符)被替换。这导致使用由其他变体仍然具有的字符组成的三字符标识符的替代表示。


4
好的解释......这也说明了为什么使用 char *date = "??-??-??" 这样的占位符可能不会产生您所期望的结果(实际上会产生 char *date = "~~|";)。 - Andrew
1
似乎如果完全使用三字符组实现,大多数典型的C代码将非常难以阅读: if(data??(x??)??(y??)=='??/r' ??!??! data??(x??)??(y??)==0) ??< break; ??> - wojtow
2
@wojtow 不,你只是没写够硬核 :) 只需要添加一些 ?: 就可以提高可读性。 - quetzalcoatl

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