“char”带有符号是什么意思?

31

考虑到有符号整数和无符号整数使用相同的寄存器等,只是以不同的方式解释位模式,而C字符基本上只是8位整数,那么C中有符号字符和无符号字符之间有什么区别? 我知道char的有符号性是实现定义的,但我简单地无法理解它如何可能有所不同,至少当char用于保存字符串而不是执行数学运算时。


2
答案很简单,你认为字符不用于数学运算的假设是错误的。我经常在系统代码中使用 "uint8_t" 和 "int8_t",它们通常被定义为无符号和有符号字符。 - Evan Teran
我认为这部分是因为我忘记了C语言中没有明确的字节/无符号字节类型。 - dsimcha
可能是signed/unsigned char之间的区别的重复问题。 - Ciro Santilli OurBigBook.com
9个回答

37

对于字符串来说不会有什么影响。但是在C语言中,当使用char进行数学计算时,它会产生影响。

实际上,在受限内存的环境下工作,比如嵌入式的8位应用程序中,char经常被用于做数学计算,这时它就非常重要了。这是因为在C语言中默认没有byte类型。


5
他的意思是说,没有叫做“byte”的类型,而不是任何类型都不能是字节型。 - Eagle-Eye
在C语言中有uint8_t类型。除非系统完全不透明,否则字节是8位。 - Lundin
2
这是否解释了为什么即使使用signed char,所有字符都可以用unsigned char表示?(我假设负值不编码为任何内容)。在我看来,unsigned char应该是默认类型,而不是signed char。 - einstein

32

就它们所代表的值而言:

unsigned char:

  • 跨越值范围0..255(00000000..11111111)
  • 值在低端溢出时为:

    0 - 1 = 255(00000000 - 00000001 = 11111111)

  • 值在高端溢出时为:

    255 + 1 = 0(11111111 + 00000001 = 00000000)

  • 按位右移运算符(>>)进行逻辑移位:

    10000000 >> 1 = 01000000(128/2 = 64)

signed char:

  • 跨越值范围-128..127(10000000..01111111)
  • 值在低端溢出时为:

    -128 - 1 = 127(10000000 - 00000001 = 01111111)

  • 值在高端溢出时为:

    127 + 1 = -128(01111111 + 00000001 = 10000000)

  • 按位右移运算符(>>)进行算术移位:

    10000000 >> 1 = 11000000(-128/2 = -64)

我包括二进制表示法以显示值包裹行为是纯净、一致的二进制算术,与char是有符号/无符号无关(除了右移)。

更新

在评论中提到的一些特定于实现的行为:

  • char != signed char。“char”类型没有“signed”或“unsigned”是实现定义的,这意味着它可以像已签名或未签名类型一样运作。
  • 有符号整数溢出会导致未定义行为,程序可以做任何事情,包括转储核心或越界缓冲区。

  • 1
    嗯...有符号类型的溢出行为不是实现定义的吗? - Martin Ba
    @MartinBa 我不知道。你知道有任何不同的情况吗,还是你只是在问?我的直觉告诉我行为应该是一致的,因为我不会想象C实现会做超出底层CPU对某些ADD机器指令所做的事情 -- 在我有限的CPU知识范围内,它是相同的按位加法。 - Ates Goral
    4
    相比之下,C标准规定有符号整数溢出会导致未定义的行为,程序可以执行任何操作。由于历史原因,C标准还允许使用补码或者反码算术实现。 - Martin Ba
    1
    @Ates:是的,例如,如果您的循环索引是有符号整数类型,优化器将生成更有效的循环代码,因为它们不必担心在溢出情况下执行预期操作。 - user1084944
    3
    @Altes Goral:我喜欢你的答案,但我认为你应该提到char!=signed char。没有“signed”或“unsigned”的类型“char”是实现定义的,这意味着它可以像有符号或无符号类型一样运作。 我认同你的答案,但需要补充说明char与signed char是不同的数据类型。在C语言中,没有指定char类型是有符号还是无符号的,因此它的行为将取决于具体实现。 - FrozenTarzan
    显示剩余2条评论

    11
    #include <stdio.h>
    
    int main(int argc, char** argv)
    {
        char a = 'A';
        char b = 0xFF;
        signed char sa = 'A';
        signed char sb = 0xFF;
        unsigned char ua = 'A';
        unsigned char ub = 0xFF;
        printf("a > b: %s\n", a > b ? "true" : "false");
        printf("sa > sb: %s\n", sa > sb ? "true" : "false");
        printf("ua > ub: %s\n", ua > ub ? "true" : "false");
        return 0;
    }
    
    
    [root]# ./a.out
    a > b: true
    sa > sb: true
    ua > ub: false
    

    在排序字符串时这很重要。


    2
    默认情况下,char是有符号的。正如楼主所说,这是实现定义的。 - Steve Fallows

    3

    有几个区别。最重要的是,如果您通过分配太大或太小的整数来溢出char的有效范围,并且char为带符号的,则结果值是实现定义的,甚至可能会引发一些信号(在C中),就像所有带符号的类型一样。相比之下,当您将某些东西分配给无符号字符时,它的大小超出了范围:该值会环绕,您将获得明确定义的语义。例如,将-1分配给无符号字符,您将获得UCHAR_MAX。因此,每当您拥有一个字节,例如从0到2 ^ CHAR_BIT的数字,您应该真正使用无符号字符来存储它。

    当传递给vararg函数时,符号也会有所不同:

    char c = getSomeCharacter(); // returns 0..255
    printf("%d\n", c);
    

    假设分配给c的值太大,char无法表示,并且机器使用二进制补码。许多实现对于将过大的值分配给char的情况都有特殊处理,即位模式不会改变。如果int能够表示char的所有值(对于大多数实现来说是这样的),那么在传递给printf之前,char将被提升为int。因此,传递的值将为负数。提升为int将保留该符号。因此,您将得到一个负结果。但是,如果char是无符号的,则该值是无符号的,并且将其提升为int将产生正整数。您可以使用unsigned char,这样您将获得对变量赋值和传递给printf的精确定义行为,然后打印出一些正数。
    请注意,char、unsigned char和signed char都至少有8位宽度。没有要求char的宽度恰好为8位。但是,对于大多数系统而言,这是正确的,但是对于某些系统,您会发现它们使用32位字符。在C和C++中,字节的大小定义为char的大小,因此在C中,字节的宽度也不总是恰好为8位。
    另一个区别是,在C中,unsigned char必须没有填充位。也就是说,如果您发现CHAR_BIT为8,则unsigned char的值必须从0到2 ^ CHAR_BIT-1。如果char是无符号的,则情况也是如此。对于signed char,即使您知道编译器如何实现符号(二进制补码或其他选项),也不能假设任何值的范围,因为其中可能有未使用的填充位。在C ++中,三种字符类型都没有填充位。

    2
    "一个字符被标记为signed意味着什么?"
    传统上,ASCII字符集由7位字符编码组成。(与8位EBCIDIC相反。)
    当C语言被设计和实现时,这是一个重要的问题。(由于各种原因,如通过串行调制解调器设备进行数据传输。)额外的位具有像奇偶校验等用途。
    "signed字符"恰好适合此表示。
    另一方面,二进制数据只是获取每个8位数据块的值,因此不需要符号。

    1
    在字符中,有符号性与其他整数类型基本相同。正如您所指出的那样,字符实际上只是一个字节的整数。(不一定是8位!这是有区别的;在某些平台上,一个字节可能比8位更大,并且由于char和sizeof(char)的定义,char与字节密切相关。在C++的或中定义的CHAR_BIT宏将告诉您char中有多少位。)
    在C和C++中,没有标准类型叫做“byte”。对于编译器来说,“char”就是字节,反之亦然,并且它们之间没有区别。有时候,你需要这样做--有时候你希望那个“char”成为一个一字节的数字,在这种情况下(特别是字节能够拥有的范围很小),你通常也关心这个数字是有符号的还是无符号的。我个人使用有符号(或无符号)来表示某个“char”是一个(数值型的)“byte”,而不是一个字符,并且它将被用作数字。如果没有指定有符号性,那么这个“char”确实是一个字符,并且旨在用作文本。
    我曾经这样做,但现在较新版本的C和C++有“(u?)int_least8_t”(目前在“”或“”中进行了typedef),它们更明确地表示数字(虽然它们通常只是有符号和无符号的“char”类型的typedef)。

    1

    在计算机图形学中(其中通常使用8位值存储颜色),字节的算术运算非常重要。除此之外,我能想到两种主要情况需要考虑字符符号:

    • 转换为较大的整数
    • 比较函数

    讨厌的是,如果所有字符串数据都是7位,则不会出现这些问题。然而,如果您尝试使C/C++程序具备8位清洁性,则这将是一个无休止的晦涩错误源。


    0
    我能想象到唯一会出现问题的情况是你选择在字符上进行数学运算。编写以下代码是完全合法的。
    char a = (char)42;
    char b = (char)120;
    char c = a + b;
    

    根据 char 的符号,c 可能是两个值中的一个。如果 char 是无符号的,则 c 将为 (char)162。如果它们是有符号的,则会出现溢出情况,因为有符号 char 的最大值为 128。我猜测大多数实现只会返回 (char)-32。


    0
    关于有符号字符的一件事是,您可以测试c >= ' '(空格),并确保它是一个普通可打印的ASCII字符。当然,这不是可移植的,所以并不是非常有用。

    1
    标准库包含一个名为isprint的函数,可以检查字符是否可打印,因此这个技巧根本没有用处。 - Nate879
    但 ASCII 码 127 是“删除”,不是可见的图形字符,您提出的检查将欣然接受它。从被终端可靠地解释的意义上来说,它甚至不具备可移植性。 - underscore_d

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