g++/gcc 中 char 的符号及其历史

The signedness of char in g++/gcc and its history

首先让我先说我知道 charsigned charunsigned char 在 C++ 中是不同的类型。从标准的快速阅读来看, char 是否 signed 似乎也是实现定义的。为了让事情变得更有趣,g++ 决定了 char 是否是 signed 是基于每个平台的!

所以无论如何,让我介绍一下我在使用这个玩具程序时遇到的一个错误:

#include <stdio.h>

int main(int argc, char* argv[])
{
    char array[512];
    int i;
    char* aptr = array + 256;

    for(i=0; i != 512; i++) {
        array[i] = 0;
    }

    aptr[0] = 0xFF;
    aptr[-1] = -1;
    aptr[0xFF] = 1;
    printf("%d\n", aptr[aptr[0]]);
    printf("%d\n", aptr[(unsigned char)aptr[0]]);

    return 0;
}

预期的行为是对 printf 的两次调用都应输出 1。当然,在 linux/x86_64 上运行的 gccg++ 4.6.3 上发生的情况是第一个 printf 输出 -1 而第二个输出 1。这与被签名的字符一致,并且 g++ 合理地解释 -1 的负数组索引(这在技术上是未定义的行为)。

这个错误似乎很容易修复,我只需要将 char 转换为 unsigned,如上所示。我想知道的是这段代码是否曾被期望在使用 gcc/g++ 的 x86 或 x86_64 机器上正常工作?看起来这可能在 ARM 平台上按预期工作,显然 chars 是无符号的,但我想知道这段代码在使用 g++ 的 x86 机器上是否一直存在错误?

The intended-behavior is that both calls to printf should output 1

你确定吗?

aptr[0] 的右值是一个带符号的 char 并且是 -1,它再次用于索引到 aptr[] 因此你得到的是第一个 printf() 的 -1。

第二个 printf 也是如此,但是在那里,使用类型转换确保它被解释为无符号字符,因此最终得到 255,并使用它索引到 aptr[] 你得到 1来自第二个 printf().

我认为您对预期行为的假设是不正确的。

编辑 1:

It appears this may work as intended on ARM platform where apparently chars are unsigned, but I would like know whether this code has always been buggy on x86 machines using g++?

根据此声明,您似乎知道 x86 上的 char 是有符号的(与某些人的假设相反)。因此,我提供的解释应该很好,即将 char 视为 x86 上的有符号 char。

编辑 2:

Using a negative array index is perfectly fine as long as the pointer operand is to an interior element: whosebug.com/questions/3473675/negative-array-indexes-in-c – ecatmur

这是@ecatmur 对问题的评论之一。这澄清了与某些人的想法相反,负指数是好的。

我在你的程序中没有看到未定义的行为。负数组索引不一定无效,只要将索引添加到前缀的结果指向有效的内存位置即可。 (如果前缀是数组对象的名称或指向数组对象第 0 个元素的指针,则负数数组索引是无效的(即具有未定义的行为),但这里不是这种情况。)

在这种情况下,aptr 指向 512 元素数组的元素 256,因此有效索引从 -256 到 +255(+256 产生刚好超过数组末尾的有效地址,但不能取消引用)。假设 CHAR_BIT==8signed charunsigned char 或普通 char 中的任何一个范围是数组有效索引范围的子集。

如果普通 char 被签名,那么:

aptr[0] = 0xFF;

会将 int0xFF (255) 隐式转换为 char,转换的结果是实现定义的——但它将是在普通 char 的范围内,几乎可以肯定是 -1。如果普通 char 是无符号的,那么它会将值 255 赋给 aptr[0]。因此,代码的行为取决于普通 char 的符号性(并且可能取决于将超出范围的值转换为符号类型的实现定义的结果),但不存在未定义的行为。

(从 C99 开始,将超出范围的值转换为有符号类型也可能会引发实现定义的信号,但我知道没有任何实现会真正做到这一点。在转换时引发信号0xFFchar 可能会破坏现有代码,因此编译器编写者极力避免这样做。)

数组的类型与索引无关(底层内存访问除外)。

例如:

signed int a[25];
unsigned int b[25];

int value = a[-1];
unsigned int u_value = b[-5];

两种情况的索引公式为:

memory_address = starting_address_of_array
               + index * sizeof(array_type);

char 而言,它的大小无论如何都是 1(根据语言规范的定义)。

char在算术表达式中的用法可能取决于它是有符号的还是无符号的。

您的 printf 语句与:

printf("%d\n", aptr[(char)255]);
printf("%d\n", aptr[(unsigned char)(char)255]);

因此,这些转化显然取决于平台的行为。

What I want to know is whether this code was ever expected to work correctly on an x86 or x86_64 machines using gcc/g++?

用 'correctly' 来表示您描述的行为,不,在 char 已签名的平台上,绝对不应该期望这种行为。

char 被签名(并且不能表示 255)时,您将获得一个由实现定义并在可表示范围内的值。对于 8 位二进制补码表示,这意味着您将获得 [-128, 127] 范围内的某个值。这意味着唯一可能的输出:

printf("%d\n", aptr[(char)255]);

是“0”和“-1”(忽略 printf 失败的情况)。常见的实现定义了打印“-1”的转换结果。


代码定义明确,但在定义不同 char 签名的实现之间不可移植。编写可移植代码包括不依赖于 char 是有符号还是无符号,这反过来意味着如果索引被限制在 [0, 127] 范围内,您应该只使用 char 值作为数组索引。