含有三字符组的字符字面值对于不可表示字符的含义

4
在使用ASCII作为字符集的C编译器上,字符字面值'??<'的值将等同于'{',即0x7B。如果编译器的字符集没有'{'字符,那么该字面值的值是多少呢?
在字符串字面值之外,编译器可以推断出'??<'应该具有与开括号字符定义的相同含义,即使编译器的字符集没有开括号字符。实际上,三字符序列的整个目的就是允许使用可表示的字符序列来代替不可表示的字符。规范要求甚至在字符串字面值内也要处理三字符序列,这让我感到困惑。如果编译器的字符集包含'{'字符,那么编译器可以允许'{'被表示为'??<',但如果字符集不包含'{',似乎程序员没有理由不直接使用其他可表示的字符。然而,如果字符集不包含'{',似乎使用三字符序列是唯一的选择,那么编译器会用什么可表示的字符来替换'??<'呢?

2
字符集可能包含“{”,但使用编写程序的键盘输入“{”可能并不容易,甚至不可能。 - Mankarse
@supercat 这是一个合理的解决方案,但是C89标准化委员会选择了另一种解决方案。 - Potatoswatter
也许可以看一下C89有关三字符序列的解释 - mafso
@mafso:关于0x7B,我说“如果字符集已知”。否则,除了??/之外的任何三字母组合是否提供了任何功能,这些功能不能通过指定一个带有字符宏的.h文件来实现[例如,#define __LBR { / #define __clbr 0x7B / #define __SLBR __STRINGIZE(<:)]? - supercat
字符集对于严格符合的代码来说始终是未知的。也许你想要针对ASCII和EBCDIC系统编写代码,但键盘上缺少某些按键。关于你的宏问题:是的,从上面的链接看起来是可能的:“一些用户可能希望为一些或所有三字母序列定义预处理宏。”如果你想的话,可以自己编写该头文件,反之则不可能,所以选择这种方式至少更加灵活。 - mafso
显示剩余6条评论
2个回答

5
如果编译器的字符集中没有 { 字符,那么这个字面值的价值是多少?
没有这样(符合规范的)编译器。{ 是基本源字符集的一部分(C99 的 5.2.1/3,C++11 的 [lex.charset]/1)。基本执行字符集(程序在运行时使用的字符集)应至少包含基本源字符集的所有成员(相同的 C99 的 5.2.1/3,C++11 的[lex.charset]/3)。
正如 @Mankarse 所指出的,三字符序列的发明并不是为了支持缺少某些字符的编译器(再次强调,没有这样的编译器),而是为了支持无法输入这些字符所必需的键的人类打字。

1
[lex.charset] 是一个 C++ 交叉引用。这个问题与 C++ 无关。相应的 C11 段落是 §5.2.1/3。 - Potatoswatter
1
此外,在C和C++中的要求是“必须”,这是一个绝对要求,比“应该”更强。 - Potatoswatter
你在哪里读到了三字符序列的理由?评论中引用的理由表明,其目的是允许具有有限可显示或打印字形的系统,并将字符代码分配给像 éè 而不是 {} 这样的字形。 - supercat
@supercat Digraphs and trigraphs:"C编程语言的基本字符集是ASCII字符集的子集,包括九个字符,这些字符位于ISO 646不变字符集之外。当使用的编码(以及可能的键盘)不支持这九个字符中的任何一个时,这可能会对编写源代码造成问题。ANSI C委员会发明了三连符作为一种使用支持任何版本的ISO 646字符集的键盘输入源代码的方法。" - Igor Tandetnik
@supercat编译器不会“渲染”任何东西,我不确定你在说什么。如果您正在谈论磁盘上物理文件的编码,则5.1.1.2/1“物理源文件多字节字符以一个实现定义的方式映射到源字符集”(我所强调的)。我想,除了键盘外,曾经存在无法表示{(使用ISO 646编码保存文件)的文本编辑器,并发明了三字符序列来支持在这些编辑器中编写代码。渲染文本文件中的字符的是文本编辑器,而不是编译器。 - Igor Tandetnik
显示剩余4条评论

1
当涉及环境问题时,特别是文件方面的考虑,C标准故意变得相当模糊。以下是有关三字符序列及其对应字符编码的保证:
C11(n1570)5.1.1.2 p1(“翻译阶段”)[强调我]
物理源文件多字节字符以实现定义的方式映射到源字符集(必要时引入换行符作为行末指示符)。如果需要,“三字符序列将被替换为相应的单字符内部表示”。因此,三字符序列必须映射到一个单字节。这个单字节字符必须在基本字符集中与基本字符集中的任何其他字符不同。编译器在翻译过程中如何处理它们在实际上不可观察的行为,因此无关紧要。
如果写入文本流,则可以将其转换(根据我的理解,如果底层编码没有某个字符的编码,则可能会重新变成三字符序列)。它可以再次读取,并且如果被认为是打印字符,则必须相等。Ibid. 7.21.2 p2:
从文本流中读取的数据只有在以下条件下才会与之前写入该流的数据相等:数据仅由打印字符和控制字符水平制表符和换行符组成;没有换行符紧接着空格字符;最后一个字符是换行符。术语“打印字符”指的是区域设置特定字符集中的一个成员,每个字符占据显示设备上的一个打印位置;术语“控制字符”指的是不是打印字符的区域设置特定字符集中的一个成员。所有字母和数字都是打印字符。“在使用七位US ASCII字符集的实现中,打印字符的值介于0x20(空格)到0x7E(波浪号)之间;控制字符的值介于0(NUL)到0x1F(US)之间,以及字符0x7F(DEL)。对于二进制流,参见Ibid. 7.21.2 p3:”
一个二进制流是一组有序的字符,可以透明地记录内部数据。从二进制流中读取的数据应该与在相同实现下之前写入该流的数据相等。但是,这样的流可能会在流的末尾附加实现定义数量的空字符。
printf("int main(void) ??< ??>\n");     // (1) 
printf("int main(void) ?\?< ?\?>\n");   // (2)

始终适用于代码生成,该语句的输出保证是可编译的。我找不到规范参考要求 isprint('??<') 等(对于 (1))甚至 isprint('<') 等(对于 (2))返回非零,但是 关于流的 C89 解释 表示:

在文本流 I/O 中需要保留的字符集是编写 C 程序所需的字符集;标准的意图是允许以最大可移植的方式编写 C 转换器。回退等控制字符并不是为此目的所必需的,因此它们在文本流中的处理没有被规定。

当将 '??<' 等写入二进制流时,它必须映射到一个单字节,打印为这样的字节,唯一且可区分于任何其他基本字符,并在读回时与 '??<' 相等。


相关:C89关于三字符组的解释

1
谢谢。因此,系统可以任意选择“??<”和“'??>'”的字符代码,但它们需要是不同的打印字符。我很好奇康柏64的任何C编译器可能会做什么;我认为有一些,但那台机器没有像“〜”,“\”,“{”或“}”这样的字形;而类似“|”和“_” 的唯一东西是一个框线字符(一个居中的垂直线和一个底部的框线水平线)。 ASCII 0x5E是一个向上箭头(足够接近“^”以简单地称之为),但0x5F是一个向后箭头。如果我要为该系统设计编译器... - supercat
1
我可能会将box-bottom字符解释为标识符中与back-arrow同义,但不是字面值;同时可能接受一些box-drawing字符作为括号或竖线的同义词(因为它们在键盘上很容易输入)。我可能会接受£作为反斜杠的同义词(它的代码是0x5C),因为我想不到其他更好的图形字符。对于波浪符,我不确定;也许可以将其视为顶部框字符(因为它的含义类似于数字信号描述中的上横线)。 - supercat
你的C64示例是为什么三字符序列可能很方便的好例子:假设你在那台机器上编码。你可以直接在源代码中使用你提到的字符。如果你现在想将你的代码移植到另一台机器上(比如使用UTF-8),所有非ISO646字符都会被C64转换成UTF8转换器错误地转换,但三字符序列会被正确地转换(成为三字符序列)。 - mafso
char * foo = #$"foo\bar$n" 会将 foo 设置为一个包含反斜杠和换行符的字符串。如果编译器使用 这样奇怪的字符集来表示大括号,而这些字符无法可靠地转换为 ASCII,则让 printf("int main() ??<doSomething();??>;"); 使用相同的字符是有意义的,尽管如果使用 printf("int main() ┤doSomething();├");,则在移植代码时可能会遇到问题,而三字符代码的输出也同样存在翻译问题,而 printf("int main() <: doSomething();:>"); 则既不会对代码也不会对输出产生翻译问题。 - supercat
1
在您的计算机上,后面的代码将输出 int main {doSomething();},但在使用 作为大括号的机器上,它将输出 int main ┤doSomething();├。顺便说一下,我简要查看了 C64 C 编译器,看起来它们内置了编辑器,可以重新编程字符集以包含 ASCII 字符,这让我对字符串字面值应该如何解释产生了好奇。C64 有两个可选择的预加载字符集;一个将 0x53 和 0x73 定义为 S;另一个则是 sS [按照这个顺序]。在 ASCII 中,它们是 Ss [不同的顺序]。 - supercat
显示剩余3条评论

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