C和C++是否保证[a-f]和[A-F]字符的ASCII码?

28

我在查看以下代码,用于测试一个十六进制数字并将其转换为整数。该代码有些巧妙,因为它利用了大写字母和小写字母之间的差异是32,即第5位,所以代码执行了一个额外的OR,但节省了一个JMP和两个CMP

static const int BIT_FIVE = (1 << 5);
static const char str[] = "0123456789ABCDEFabcdef";

for (unsigned int i = 0; i < COUNTOF(str); i++)
{
    int digit, ch = str[i];

    if (ch >= '0' && ch <= '9')
        digit = ch - '0';
    else if ((ch |= BIT_FIVE) >= 'a' && ch <= 'f')
        digit = ch - 'a' + 10;
    ...
}

C和C ++是否保证ASCII或[a-f]和[A-F]字符的值?这里,保证意味着大小写字符集始终将以可以用一个位表示的常量值相差(对于上述技巧)。如果没有,标准如何描述它们?

(抱歉使用C和C ++标签。我对两种语言在这个主题上的立场感兴趣)。


10
考虑 EBCDIC。 - chris
3
在“保证ASCII或[a-f]和[A-F]的值”的语境中,“guarantee”的意思是确保这些字符集的存在和对应的值。 - c-smile
3
他们是相邻的,而且大小写之间的差异是32。 - Neil Kirk
4
@teppic: 你确定吗?我现在正在查看n1516第5.2.1节,它并没有说那个,它只保证0-9是顺序的。确实,在EBCDIC和ASCII中,大写字母A-F和小写字母a-f也是顺序的。 - Dietrich Epp
2
据我所知,甚至没有保证 a < b。但这将使我们进入真正邪恶的领域。 - MSalters
显示剩余15条评论
3个回答

40
不,它并没有。
C标准保证十进制数字和大写字母、小写字母以及其他一些字符是存在的。它还保证了十进制数字是连续的,例如'0' + 9 == '9',并且所有的基本执行字符集成员都具有非负值。但它明确不保证字母是连续的。(关于所有细节,请参见C标准N1570草案第5.2.1节;关于基本字符是非负的保证在6.2.5p3中,在char类型的讨论中。)
假设 'a' .. 'f''A' .. 'F' 具有连续的编码是几乎可以肯定的。在 ASCII 和所有基于 ASCII 的字符集中,26 个小写字母是连续的,26 个大写字母也是连续的。即使在EBCDIC中,ASCII 的主要竞争对手,整个字母表也不是连续的,但字母 'a' ..'f''A' .. 'F' 是连续的(EBCDIC 在 'i''j''r''s''I''J''R''S' 之间有间隙)。
然而,在 EBCDIC 中,设置表示中的第 5 位将大写字母转换为小写字母的假设是无效的。在 ASCII 中,小写字母和大写字母的编码差异为 32;在 EBCDIC 中,它们的差异为 64。
这种为了节省一两条指令而进行的位操作,在标准库中或已知对性能至关重要的代码中可能是合理的。对基于ASCII字符集的隐含假设,我认为至少应该通过注释来明确说明。一个包含256个元素的静态查找表可能会更快,但需要额外的一点存储空间。

6
如果需要100%的保证,可以使用#if来验证这些字母是否连续,如果它们不连续,则会触发错误。最好在编译时失败,而不是在运行时失败。 - Boann
7
@Boann:(更正我之前的评论)不,那并不保证可行。标准明确指出,#if 'z' - 'a' == 25 不一定会像 if ('z' - 'a' == 25) 那样计算;因为在编译时和运行时可以使用不同的字符集。N1570 6.10.1。 - Keith Thompson
2
一个人必须检查所有字符。理论上,可能存在一种疯狂的字符集,其中a..z不是连续的,但是z恰好与a具有相同的距离。 - Sebastian Mach
1
@jww 你知道你可以更改你接受的答案吗? - Hong Ooi
1
@HongOoi:是的,但那样我就得不到大众化徽章了!8-)} - Keith Thompson
显示剩余8条评论

26

不能保证特定的值,但你不需要担心,因为你的软件可能从未遇到与ASCII不兼容的系统。假设空格始终是32,A始终是65,在现代世界中这很好用。

C标准仅保证字母A-Z和a-z存在,并且它们适合于单个字节内。

它确保0-9是连续的。

在源和执行基本字符集中,上述十进制数字列表中每个字符的值都应比前一个大1。

正当理由

世界上有许多字符编码。如果你关心可移植性,你可以使你的程序适用于不同的字符集,或者你可以选择一种字符集在所有地方使用(例如Unicode)。我将大致分类现有的大多数字符编码:

  1. 兼容ISO/IEC 646的单字节字符编码。数字0-9和字母A-Z和a-z总是占据相同的位置。

  2. 多字节字符编码(Big5、Shift JIS、基于ISO 2022的编码)。在这些编码中,你的程序可能已经出错了,如果你关心的话,你需要花时间去修复它。然而,解析数字仍将按预期工作。

  3. Unicode编码。数字0-9和字母A-Z、a-z总是占据相同的位置。如果你使用的是小于128的代码点,则可以自由地使用代码点或代码单元工作,将得到相同的结果。(你正在使用UTF-7吗?不是的话,你只应该在电子邮件中使用它。

  • EBCDIC。数字和字母被分配的值与ASCII中的值不同,但是0-9和A-F,a-f仍然是连续的。即使如此,你的代码在EBCDIC系统上运行的几率基本为零。

  • 所以问题是:您认为未来会发明第五个假设选项,它是否与Unicode不兼容/更难使用?

    您关心EBCDIC吗?

    我们可以整天想出奇怪的系统……假设CHAR_BIT为11,或sizeof(long)=100,或假设我们使用一的补数算术,或malloc()总是返回NULL,或假设您显示器上的像素排列成六边形网格。假设您的浮点数不是IEEE 754,假设您所有的数据指针都有不同的大小。最终,这并没有让我们更接近在实际现代系统上编写可以工作的软件的目标(偶尔除外)。


    10
    @jww说的"undefined behavior"指的是 ch >= 'a' && ch <= 'f' 可能会导致程序崩溃、重置硬盘并喷漆染成粉色。而"Implementation defined"则意味着由实现者决定其结果,实现者有一个字符编码,字符编码的选择将决定这些问题的答案。请注意,实际上没有人使用A-F不连续的编码,所以也没有人关心这个问题。 - Dietrich Epp
    12
    哇,真是教人不要思考抽象概念的好方法。无论这个回答多么冷静事实上准确,它都是一个糟糕的回答。 - Lightness Races in Orbit
    6
    @DietrichEpp:请将“但你不应该在意”的部分改为“实际上,在今天最常见的平台上遇到的情况下”,如果您意识到平台依赖性(最好使用static_assert进行检查),那么这种依赖性是可以接受的。你的回答很好,直截了当,但第一句话让我的脚趾甲翻起来,这并不是一个好的表现。 - peterchen
    3
    @LightningRacisinObrit:我认为这里的目标不是教人们在不必要的情况下思考抽象概念。编写可移植代码的成本很高,而编写适用于既非ASCII也非EBCDIC兼容但某种假设的第三选项字符集的代码的好处为零,除非有人能够证明相反。EBCDIC可移植性的好处通常也为零。 - Dietrich Epp
    4
    @LightningRacisinObrit: 我很难理解你怎么会认为编写可移植代码与编写不可移植代码相比没有成本...通常来说,你似乎很精明。 - Dietrich Epp
    显示剩余8条评论

    24

    为了最大限度地提高可移植性、清晰度和速度,我建议使用一个简单的开关:

    int hex_digit_value(char x)
    {
        switch (x)
        {
        case '0': return 0;
        case '1': return 1;
        case '2': return 2;
        case '3': return 3;
        case '4': return 4;
        case '5': return 5;
        case '6': return 6;
        case '7': return 7;
        case '8': return 8;
        case '9': return 9;
        case 'A':
        case 'a': return 10;
        case 'B':
        case 'b': return 11;
        case 'C':
        case 'c': return 12;
        case 'D':
        case 'd': return 13;
        case 'E':
        case 'e': return 14;
        case 'F':
        case 'f': return 15;
        default: return -1;
        }
    }
    

    clang -O1 -S 将其转换为简单的表查找:

        addl    $-48, %edi
        cmpl    $54, %edi
        ja  .LBB0_2
    
        movslq  %edi, %rax
        movl    .Lswitch.table(,%rax,4), %eax
        retq
    .LBB0_2:
        movl    $-1, %eax
        retq
    

    为了完整起见,以下是生成的查找表:

    .Lswitch.table:
    .long   0                       # 0x0
    .long   1                       # 0x1
    .long   2                       # 0x2
    .long   3                       # 0x3
    .long   4                       # 0x4
    .long   5                       # 0x5
    .long   6                       # 0x6
    .long   7                       # 0x7
    .long   8                       # 0x8
    .long   9                       # 0x9
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   10                      # 0xa
    .long   11                      # 0xb
    .long   12                      # 0xc
    .long   13                      # 0xd
    .long   14                      # 0xe
    .long   15                      # 0xf
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   4294967295              # 0xffffffff
    .long   10                      # 0xa
    .long   11                      # 0xb
    .long   12                      # 0xc
    .long   13                      # 0xd
    .long   14                      # 0xe
    .long   15                      # 0xf
    

    感谢您对“达夫设备”的优化目标;) - dhein
    5
    @Zaibis:这不是达夫设备。 - Zan Lynx
    @ZanLynx:编译器端触发的优化是基于通过Duff's Device进行的研究。我从未说过这就是Duff's Device。 - dhein

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