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

4
首先,我想说的是我知道在C++中,char、signed char和unsigned char是不同的类型。根据标准的快速阅读,char是否为signed是由实现定义的。而且让事情变得更有趣的是,似乎g++会根据平台决定char是否为signed!所以,有了这个背景,让我们介绍一下我在使用这个玩具程序时遇到的一个bug:
#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。这与chars被视为有符号并且g++合理地解释了-1(这在技术上是未定义的行为)一致。
修复此错误似乎很容易,我只需要将char强制转换为unsigned,如上所示。我想知道的是,这段代码是否曾经预期在使用gcc/g++的x86或x86_64机器上正常工作?显然,在ARM平台上可能按预期工作,因为chars显然是无符号的,但我想知道这段代码在使用g++的x86机器上是否一直存在缺陷?

3
顺带一提,GCC 提供了编译选项,可以强制 char 类型具有你所喜欢的任何有符号性。它存在的目的正是为了解决像这样非可移植错误的代码。 :) - Lightness Races in Orbit
3
只要指针操作数指向内部元素,使用负数数组索引是完全可以的。 - ecatmur
1
@Pramod 为什么你认为“printf的两个调用都应该输出1”? - Vlad from Moscow
@Joachim Pileborg,请不要说傻话。整数字面值-1与整数字面值0xffffffff并不相同,尽管它们可以具有相同的内部表示。 - Vlad from Moscow
1
@JoachimPileborg:为什么要将值0xFFFFFFF添加到索引中,而不是从索引或指针中减去1? - Thomas Matthews
显示剩余4条评论
4个回答

4
我在你的程序中没有发现任何未定义的行为。负数数组索引不一定无效,只要将索引与前缀相加得到的结果是指向有效内存位置的就可以了。(如果前缀是数组对象的名称或指向数组对象第0个元素的指针,则负数数组索引是无效的(即具有未定义的行为),但这里不是这种情况。)
在这种情况下,`aptr`指向512个元素数组的第256个元素,因此有效索引范围从-256到+255(+256产生的地址刚好位于数组末尾,但不能被解引用)。假设`CHAR_BIT == 8`,任何`signed char`、`unsigned char`或普通的`char`都有一个范围,它是数组有效索引范围的子集。
如果普通的`char`是带符号的,那么:
aptr[0] = 0xFF;
int类型的值0xFF255)将隐式转换为char类型,该转换的结果是实现定义的,但它将在平凡char范围内,并且几乎肯定为-1。如果平凡char是无符号的,则会将值255赋给aptr[0]。因此,代码的行为取决于平凡char的符号(可能还取决于将超出范围的值转换为有符号类型的实现定义结果),但没有未定义的行为。

(从C99开始,将超出范围的值转换为有符号类型也可能引发实现定义的信号,但我不知道任何实际上这样做的实现。对0xFF转换为char引发信号可能会破坏现有代码,因此编译器编写者高度不愿意这样做。)


1
@RD445:这取决于你所说的“允许”的含义。越界数组访问具有未定义的行为。(我使用“无效”一词作为简称。) - Keith Thompson
@RD445 负索引可以有效,当且仅当它们“在范围内”时才有效。否则,它们会产生未定义的行为。无论经验如何,都不能消除语言规范中定义的这一段内容(例如,请参见 C++ 规范,第 5.7 条 [expr.add] 第 5 段)。如果它过去能工作,那只是你运气好或不好而已。 - bames53
@bames53,没有越界索引会产生任何“未定义”的行为。你似乎从未处理过这样的问题,因此你不知道自己在说什么。难道我一直很幸运,我与之合作的数百名同事也一直很幸运吗?我会查看标准并回复你的。 - RcnRcf
@RD445:未定义行为的无限结果之一是代码似乎“正常工作”。给定 int arr [10];,评估 arr [-1] 具有未定义行为。了解特定 C 编译器如何实现数组索引并不会改变这一点。(如果您认为它具有定义行为,则应该能够准确解释 C 标准 如何定义行为。) - Keith Thompson
@bames53:要小心使用“非法”一词。C标准没有使用该术语。有些构造是语法错误或约束违规,需要编译时诊断。其他的则具有未定义的行为,不需要诊断。 (在条件编译中生存下来的#error指令要求拒绝翻译单元; 这几乎是唯一的情况。) - Keith Thompson
显示剩余9条评论

1

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

例如:

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”可能取决于它是带符号还是无符号的。

这不像是对问题的回答。 - RcnRcf

0
预期行为是printf的两个调用都输出1。
你确定吗?
aptr[0]的rvalue是有符号字符,为-1,再次用于索引到aptr[]中,因此第一个printf()输出-1。
第二个printf也是同样的情况,但是使用类型转换确保它被解释为无符号字符,因此您从第二个printf()得到255,并使用它来索引aptr[],从而得到1。
我认为您对预期行为的假设是不正确的。
编辑1:
引用:“看起来在ARM平台上可能按预期工作,因为显然char是无符号的,但我想知道这段代码在使用g++的x86机器上是否一直存在错误?”
根据这个声明,似乎您知道x86上的char是有符号的(与某些人所假设的相反),因此我提供的解释应该是正确的,即将char视为有符号字符在x86上。
编辑2:
使用负数数组索引是完全可以的,只要指针操作数是指向内部元素的:stackoverflow.com/questions/3473675/negative-array-indexes-in-c - ecatmur。这是@ecatmur对问题的评论之一,澄清了负索引是可以的,与某些人的想法相反。

给那些点踩的人一个机会,做个好事。请在评论中说明你点踩的原因,这样其他人就能理解你的想法了。 - RcnRcf
根据问题的文本,意图是让aptr[0]成为一个值为255的char,而不是-1。所以,是的,他确定预期的输出是“1”。 - bames53
1
您假设char是有符号的(它不一定是这样,OP肯定没有假设),并且进一步假设将255(0xFF)转换为(有符号)char将产生-1(再次强调,并不一定正确)。 - T.C.
@T.C. 你能演示一下你所提到的吗? - RcnRcf
@bames53 我指的是使用实际代码进行演示。 - RcnRcf
显示剩余6条评论

0

你的 printf 语句与以下语句相同:

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

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

我想知道的是,这段代码是否曾经预期在使用gcc / g ++的x86或x86_64机器上能够正确运行?

如果“正确地”意味着您描述的行为,则不应该预期在 char 为有符号数的平台上以这种方式工作。

char 是有符号的(无法表示255)时,您将获得一个实现定义的值,并且在可表示范围内。对于8位、二进制补码表示,这意味着您会得到范围 [-128,127] 中的一些值。这意味着以下唯一可能的输出:

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

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


代码定义良好,但在定义不同的char有符号性之间不可移植。编写可移植代码包括不依赖于char是否有符号或无符号,这又意味着如果索引限制在[0, 127]范围内,则只应使用char值作为数组索引。


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