g++/gcc 中 char 的符号及其历史
The signedness of char in g++/gcc and its history
首先让我先说我知道 char
、signed char
和 unsigned 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
上运行的 gcc
和 g++ 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==8
、signed char
、unsigned char
或普通 char
中的任何一个范围是数组有效索引范围的子集。
如果普通 char
被签名,那么:
aptr[0] = 0xFF;
会将 int
值 0xFF
(255
) 隐式转换为 char
,转换的结果是实现定义的——但它将是在普通 char
的范围内,几乎可以肯定是 -1
。如果普通 char
是无符号的,那么它会将值 255
赋给 aptr[0]
。因此,代码的行为取决于普通 char
的符号性(并且可能取决于将超出范围的值转换为符号类型的实现定义的结果),但不存在未定义的行为。
(从 C99 开始,将超出范围的值转换为有符号类型也可能会引发实现定义的信号,但我知道没有任何实现会真正做到这一点。在转换时引发信号0xFF
到 char
可能会破坏现有代码,因此编译器编写者极力避免这样做。)
数组的类型与索引无关(底层内存访问除外)。
例如:
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
值作为数组索引。
首先让我先说我知道 char
、signed char
和 unsigned 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
上运行的 gcc
和 g++ 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==8
、signed char
、unsigned char
或普通 char
中的任何一个范围是数组有效索引范围的子集。
如果普通 char
被签名,那么:
aptr[0] = 0xFF;
会将 int
值 0xFF
(255
) 隐式转换为 char
,转换的结果是实现定义的——但它将是在普通 char
的范围内,几乎可以肯定是 -1
。如果普通 char
是无符号的,那么它会将值 255
赋给 aptr[0]
。因此,代码的行为取决于普通 char
的符号性(并且可能取决于将超出范围的值转换为符号类型的实现定义的结果),但不存在未定义的行为。
(从 C99 开始,将超出范围的值转换为有符号类型也可能会引发实现定义的信号,但我知道没有任何实现会真正做到这一点。在转换时引发信号0xFF
到 char
可能会破坏现有代码,因此编译器编写者极力避免这样做。)
数组的类型与索引无关(底层内存访问除外)。
例如:
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
值作为数组索引。