【问题标题】:This source code is switching on a string in C. How does it do that?此源代码在 C 中打开一个字符串。它是如何做到的?
【发布时间】:2024-05-16 21:45:01
【问题描述】:

我正在阅读一些模拟器代码,我反驳了一些真正奇怪的东西:

switch (reg){
    case 'eax':
    /* and so on*/
}

这怎么可能?我以为您只能在整数类型上使用switch。是不是发生了一些宏诡计?

【问题讨论】:

  • 不是字符串'eax',而是枚举常量整数值
  • 单引号,不是双引号。一个字符常量被提升为int,所以它是合法的。但是,多字符常量的值是实现定义的,因此代码可能无法在另一个编译器上按预期工作。例如,eax 可能是 0x650x6561780x656178000x7861650x6165 或其他名称。
  • @Davislor:鉴于变量“reg”的名称,以及 eax 是一个 x86 寄存器的事实,我猜想实现定义的行为本来是可以的,因为它是相同的它在代码中使用的任何地方。当然,只要'eax' != 'ebx',它只会使您的一两个示例失败。尽管在某处可能有一些代码实际上假定为*(int*)("eax") == 'eax',因此您的大多数示例都失败了。
  • @SteveJessop 我不同意你所说的,但存在真正的危险,即有人可能会尝试在不同的编译器上编译代码,即使对于相同的架构,也会得到不同的行为。例如,'eax' 可能比较等于 'ebx''ax',并且 switch 语句不会按预期工作。
  • 如果您查找/向我们展示了 reg 的数据类型,那么所有这些谜团都会很快被解开。

标签: c switch-statement label constants


【解决方案1】:

(只有你可以回答“宏诡计”部分 - 除非你粘贴更多代码。但这里没有太多宏可以处理 - 正式地,你不允许重新定义 关键字;这样做的行为是未定义的。)

为了实现程序的可读性,机智的开发人员正在利用实现定义的行为'eax' 不是字符串,而是多字符常量。注意eax 周围的单引号字符。在您的情况下,它很可能会为您提供int,这是该字符组合所独有的。 (通常每个字符在 32 位 int 中占据 8 位)。每个人都知道您可以在intswitch

最后,一个标准参考:

C99 标准说:

6.4.4.4p10:“一个整数字符常量的值包含多个字符(例如,'ab'),或包含一个字符或 不映射到单字节执行的转义序列 字符,是实现定义的。”

【讨论】:

  • 以防万一有人看到并恐慌,“实现定义”需要工作并由编译器以某种适当的方式记录(标准不要求行为是直观的或该文档是否很好,但是...)。对于完全理解他们所写内容的编码人员来说,这是“安全的”,而不是“未定义”。
  • @Justin 虽然可以,但这很不正当。如果它没有按照答案建议的最有可能进行,那么下一个可能性可能是它只使用第一个字符而忽略其余字符。
  • @ZanLynx 我不肯定,但我相信该功能早于 Unicode 和其他 MBCS 标准。看起来像内存转储中的文本的“幻数”和 RIFF 样式的文件格式块 ID 是我所知道的第一个应用程序。
  • @jpmc26 这不是未定义的行为,它是实现定义的。所以除非编译器文档提到恶魔,否则你的鼻子是安全的。
  • @ZanLynx:恐怕最初的意图比 Unicode、UTF-8 和任何多字节字符编码早了近 20 年。 多字符常量 只是表达表示 2、3 或 4 个字节组的整数的一种方便方式(取决于字节和 int 大小)。实现和架构之间的不一致导致委员会将其声明为实现定义,这意味着没有可移植的方法来从'a''b' 计算'ab' 的值。
【解决方案2】:

根据 C 标准(6.8.4.2 的 switch 语句)

3 每个case标签的表达式为整数常量 表达...

和(6.6 常量表达式)

6 整数常量表达式应该是整数类型并且应该 只有整数常量、枚举常量的操作数, 字符常量,结果为整数常量的 sizeof 表达式,以及作为直接操作数的浮点常量 演员表。整数常量表达式中的强制转换运算符只能 将算术类型转换为整数类型,但作为 sizeof 运算符的操作数。

现在'eax' 是什么?

C 标准(6.4.4.4 字符常量)

2 整数字符常量是一个或多个的序列 用单引号括起来的多字节字符,如 'x'...

所以'eax'根据同一节的第10段是一个整数字符常量

  1. ...包含多个字符的整数字符常量的值 字符(例如,'ab'),或包含字符或转义 不映射到单字节执行字符的序列是 实现定义。

所以根据第一次提到的引用,它可以是整数常量表达式的操作数,可以用作 case 标签。

请注意,字符常量(用单引号括起来)的类型为int,与具有字符数组类型的字符串文字(用双引号括起来的字符序列)不同。

【讨论】:

    【解决方案3】:

    正如其他人所说,这是一个int 常量,它的实际值是实现定义的。

    我假设其余的代码看起来像

    if (SOMETHING)
        reg='eax';
    ...
    switch (reg){
        case 'eax':
        /* and so on*/
    }
    

    您可以确定第一部分中的“eax”与第二部分中的“eax”具有相同的值,所以一切正常,对吧? ……错了。

    @Davislor 在评论中列出了 'eax' 的一些可能值:

    ...0x650x6561780x656178000x7861650x6165 或其他内容

    注意到第一个潜在价值了吗?那只是'e',忽略其他两个字符。问题是程序可能使用'eax''ebx', 等等。如果所有这些常量的值都与'e' 相同,那么您最终会得到

    switch (reg){
        case 'e':
           ...
        case 'e':
           ...
        ...
    }
    

    这看起来不太好,是吗?

    关于“实现定义”的好处是程序员可以检查他们的编译器的文档,看看它是否对这些常量做了一些有意义的事情。如果是的话,免费回家。

    不好的部分是其他一些可怜的家伙可以获取代码并尝试使用其他编译器编译它。即时编译错误。该程序不可移植。

    正如@zwol 在 cmets 中指出的那样,情况并没有我想象的那么糟糕,在糟糕的情况下代码无法编译。这至少会给你一个确切的文件名和问题的行号。不过,你不会有一个工作程序。

    【讨论】:

    • 除了某种形式的 assert('eax' != 'ebx'); //if this fails you can't compile the code because... 之外,原作者可以做些什么来防止其他编译器失败而不完全替换该构造>
    • 具有相同值的两个 case 标签是违反约束的(6.8.4.2p3:“...同一 switch 语句中的任何两个 case 常量表达式在转换后都不应具有相同的值”)因此,只要所有代码都将这些常量的值视为不透明的,就可以保证编译成功或编译失败。
    • 更糟糕的是,在另一个编译器上编译的可怜家伙可能不会看到任何 compile-time 错误(打开 int 很好);相反,run-time 错误会突然出现...
    【解决方案4】:

    代码片段使用了一个历史奇点,称为多字符字符常量,也称为多字符

    'eax' 是一个整数常量,其值由实现定义。

    这是一个关于多字符以及如何使用但不应该使用的有趣页面:

    http://www.zipcon.net/~swhite/docs/computers/languages/c_multi-char_const.html


    再往后看,这里是丹尼斯·里奇(Dennis Ritchie)在过去的美好时光(https://www.bell-labs.com/usr/dmr/www/cman.pdf)的原始 C 手册如何指定字符常量。

    2.3.2 字符常量

    字符常量是用单引号“'”括起来的 1 或 2 个字符。在字符常量中,单引号前面必须有一个反斜杠“\”。某些非图形字符以及“\”本身可以根据下表进行转义:

        BS \b
        NL \n
        CR \r
        HT \t
        ddd \ddd
        \ \\
    

    转义“\ddd”由反斜杠后跟 1、2 或 3 个八进制数字组成,用于指定所需字符的值。这种结构的一个特例是“\0”(后面不跟数字),它表示一个空字符。

    字符常量的行为与整数完全一样(尤其不像字符类型的对象)。根据 PDP-11 的寻址结构,长度为 1 的字符常量具有给定字符的代码 低位字节和高位字节为 0;长度为 2 的字符常量在低字节中具有第一个字符的代码,在高字节中具有第二个字符的代码。具有多个字符的字符常量本质上是与机器相关的,应该避免使用。

    关于这个奇怪的结构,你只需要记住最后一句话:具有多个字符的字符常量本质上是依赖于机器的,应该避免。

    【讨论】:

      最近更新 更多