为什么在处理二进制数据时要使用无符号字符(unsigned char)?

59

在一些处理字符编码或二进制缓冲区的库中,使用unsigned char来保存二进制数据是否真的必要?为了理解我的问题,请看下面的代码 -

char c[5], d[5];
c[0] = 0xF0;
c[1] = 0xA4;
c[2] = 0xAD;
c[3] = 0xA2;
c[4] = '\0';

printf("%s\n", c);
memcpy(d, c, 5);
printf("%s\n", d);

无论是printf的输出还是memcpy复制char所持有的位,都没有问题,其中f0 a4 ad a2是Unicode码点 U + 24B62()的编码(hex)。

什么样的推理可能支持使用unsigned char而不是普通的char

在其他相关问题中,unsigned char受到关注,因为它是C规范保证不填充的唯一(byte/smallest)数据类型。但正如上面的示例所示,输出似乎不会受到任何填充的影响。

我使用VC++ Express 2010和MinGW来编译上述内容。虽然VC给出了警告

warning C4309: '=' : truncation of constant value

但输出似乎并未反映出这一点。

P.S. 这可能被标记为Should a buffer of bytes be signed or unsigned char buffer?的可能重复项,但我的意图是不同的。我想知道为什么看起来使用char也可以正常工作,为什么要输入unsigned char

更新: 引用N3337中的内容,

Section 3.9 Types

2 对于任何一个平凡可复制类型T的对象(除了基类子对象),无论对象是否持有T类型的有效值,组成对象的底层字节(1.7)都可以被复制到char或unsigned char数组中。如果将char或unsigned char数组的内容复制回对象,则对象随后应保持其原始值。

鉴于上述事实以及我的原始示例是在Intel机器上,其中char默认为signed char,我仍然不确定是否应该优先使用unsigned char而不是char

还有其他事项吗?


如果这只是惯例,那么我很乐意遵循。但是背后是否有任何技术上的、逻辑上的原因呢? - nightlytrails
3
如果您提供用于处理二进制和非二进制数据的函数,signed char 可以更方便。当您处理字符串时,不得不进行无符号 char 的转换会很麻烦。 - goji
你链接的问题给出了一个技术原因 - 这一点非常明确。 - Björn Pollex
4
请注意,ifstreambasic_ifstream<char>,而不是 basic_ifstream<unsigned char>。我不知道这是否会影响你刚刚做出的修复,但它并不像“在 C++ 中,流数据是无符号字符”这么简单。标准流有所不同。 - Steve Jessop
其中一个原因是兼容性:因为在你的系统上它能够运行,并不意味着它能在其他系统上运行(有一些非常奇怪的系统,有些字符的大小和表示完全不同)。 - Olivier Dulac
显示剩余5条评论
8个回答

101

在C语言中,unsigned char数据类型是唯一一个同时具有以下三个属性的数据类型:

  • 它没有填充位,即所有存储位都对数据值做出贡献
  • 从该类型的值开始进行的任何按位操作,在转换回该类型时都不会产生溢出、陷阱表示或未定义行为
  • 它可以别名其他数据类型,而不违反“别名规则”,即通过以不同类型录入的指针访问相同数据将保证看到所有修改

如果这是您正在寻找的“二进制”数据类型的属性,那么您绝对应该使用unsigned char

对于第二个属性,我们需要一个unsigned类型。对于这些类型,所有的转换都采用模算术进行定义,其中模是UCHAR_MAX+1,在大多数99%的体系结构中是256。因此,将更宽的值转换为unsigned char只对最低有效字节进行截断。

另外两种字符类型通常不起作用。 signed char是带符号的,因此对不适合它的值进行转换是没有定义好的。 char不能固定为带符号或无符号,但在您的代码被移植到的特定平台上,它可能是带符号的,即使在您的平台上它是无符号的。


11
非常中立,坚持事实。+1 - Prof. Falken
你能更好地解释第二个属性或者举个例子吗? - sop
它可以别名其他数据类型而不违反“别名规则”,这也适用于char - Calmarius
@Calmarius 如果char是有符号的,那么只需添加两个char值就可能会溢出并导致未定义的行为。 - Andrew Henle

17

当比较每个字节的内容时,您将遇到大多数问题:

char c[5];
c[0] = 0xff;
/*blah blah*/
if (c[0] == 0xff)
{
    printf("good\n");
}
else
{
    printf("bad\n");
}

由于取决于编译器,c [0] 将被符号扩展为-1,与0xff完全不同,因此可能会打印“bad”。


13

普通的char类型存在问题,除字符串外不应该再使用。 char的主要问题在于您无法知道它是有符号还是无符号:这是实现定义的行为。这使得charint等不同,int始终保证是有符号的。

虽然 VC 给出了警告...常数值截断

它告诉你正在尝试将整数文字存储在字符变量中。这可能与符号有关:如果您尝试将值> 0x7F的整数存储在已签名的字符中,则可能会发生意外的事情。从形式上讲,在C中,这是未定义的行为,但是实际上,如果尝试将存储在(已签名)字符中的整数值打印为整数值,则会得到奇怪的输出。

在这种特定情况下,警告应该没有关系。

编辑:

在其他相关问题中,强调了无符号字符,因为它是C规范保证没有填充的唯一(字节/最小)数据类型。

理论上,除无符号字符和已签名字符之外,所有整数类型都允许包含“填充位”,如C11 6.2.6.2所述:

“对于除unsigned char之外的无符号整数类型,对象表示的位应分为两组:值位和填充位(不需要有后者)。”

“对于已签名整数类型,对象表示的位应分为三组:值位,填充位和符号位。不需要填充位;已签名字符不得有填充位。”

C标准故意模糊不清,允许这些理论上的填充位,因为:

  • 它允许不同于标准8位符号表的符号表。
  • 它允许实现定义的有符号性和奇怪的已签名整数格式,例如一补数或“符号和幅度”。
  • 整数可能不一定会使用分配的所有位。

然而,在C标准之外的现实世界中,以下内容适用:

  • 符号表几乎肯定是8位(UTF8或ASCII)。存在一些奇怪的例外,但干净的实现在实现大于8位的符号表时使用标准类型。
  • 有符号性总是采用二进制补码。
  • 整数始终使用所有分配的位。

因此,没有实际理由使用无符号字符或有符号字符来规避C标准中的某些理论情况。


关于第二个注释,请查看我链接的问题。 - nightlytrails
1
@Lundin,整数数据类型可能具有填充位而不是字节。是的,“unsigned char”是唯一保证没有填充位的类型。 - Jens Gustedt
@JensGustedt 我误解了问题。无论如何,根据C11标准,似乎unsigned charsigned char都不能包含填充位。 - Lundin
@Lundin,严格来说,在大多数架构上,位运算甚至不直接作用于字符类型。如果int比字符类型宽,则首先进行转换为int,然后执行操作,最后将结果最终转换回字符类型。 - Jens Gustedt
1
@JensGustedt市场上大多数8位或16位MCU都有8位指令集。尽管C标准强制执行整数提升规则,但对于它们来说将char类型提升为int类型只是不方便而已。这样的MCU通常会优化掉整个隐式整数提升过程,但在这样做的同时,它们会保留由提升引起的任何意外怪异行为,例如改变符号。 - Lundin
显示剩余3条评论

8

字节通常被用来表示无符号8位整数。

现在,char并没有指定整数的符号:在某些编译器上,char可能是有符号的,而在其他编译器上,它可能是无符号的。

如果我在你写的代码中添加位移操作,那么我将会得到一个未定义的行为。添加的比较也会有意想不到的结果。

char c[5], d[5];
c[0] = 0xF0;
c[1] = 0xA4;
c[2] = 0xAD;
c[3] = 0xA2;
c[4] = '\0';
c[0] >>= 1; // If char is signed, will the 7th bit go to 0 or stay the same?

bool isBiggerThan0 = c[0] > 0; // FALSE if char is signed!

printf("%s\n", c);
memcpy(d, c, 5);
printf("%s\n", d);

关于编译时的警告:如果 char 是有符号的,则您正在尝试分配值 0xf0,它不能在有符号 char(范围为 -128 到 +127)中表示,因此它将被转换为有符号值(-16)。

将 char 声明为无符号将消除警告,并且始终拥有没有任何警告的清洁构建是很好的。


你是不是想说将char声明为unsigned? - Gabriel Devillers
@GabrielDevillers 感谢您发现错误。我已经修正了答案。 - Paolo Brandoli

4

char类型的有符号/无符号性是由实现定义的,因此除非您实际处理字符数据(使用平台的字符集 - 通常为ASCII),否则最好通过使用signed charunsigned char显式指定有符号/无符号性。

对于二进制数据,最好的选择很可能是unsigned char,特别是如果将在数据上执行按位操作(特别是位移操作,对于有符号类型和无符号类型的行为不同)。


2
我想知道为什么看起来使用char也能正常工作,为什么还要使用unsigned char?
如果您做的事情不符合标准,那么就会依赖于未定义的行为。您的编译器今天可能会按照您的意愿进行操作,但您不知道明天会发生什么。您不知道GCC或VC++ 2012会发生什么。甚至行为是否取决于外部因素、Debug/Release编译等等。一旦您离开了标准的安全路径,您可能会遇到麻烦。

2
标准是否规定使用 unsigned char 来表示二进制? - nightlytrails
1
@nightlytrails,是的,在它自己的语言中。unsigned char 是唯一保证不具有填充位且没有任何位操作会受到溢出和其他不可预测行为影响的类型。 - Jens Gustedt

2

那么,你所谓的“二进制数据”是什么?这是一堆位,没有任何特定软件分配给它们的含义。哪种最接近原始数据类型,传达了对其中任何一位缺乏特定含义的概念?我认为是unsigned char


2
“在一些涉及字符编码或二进制缓冲区的库中,使用unsigned char来保存二进制数据真的是必需的吗?”
“真的”必需吗?并非如此。
但这是个很好的想法,有许多原因支持这种做法。
你的例子使用了printf,它并不是类型安全的。也就是说,printf是从格式字符串而不是数据类型中获取其格式化信息。你也可以尝试这样做:
printf("%s\n", (void*)c);

如果你使用 unsigned char 代替 plain char,结果是相同的。但如果你在 c++ iostreams 中尝试同样的操作,结果将会不同(取决于 c 的符号)。

有什么理由支持使用 unsigned char 而不是 plain char 呢?

signed 指定数据的最高有效位(对于 unsigned char 来说是第 8 个位)表示符号。由于显然不需要这一点,所以应该指定你的数据为 unsigned("符号" 位表示数据,而不是其他位的符号)。


好的,%s 在 C 语言中表示一个以 null 结尾的普通 char 数组。这正是我所暗示的 - charunsigned char 更加合适。 - nightlytrails
你说得对 - printf的数据类型无关紧要,只要地址指向正确位置即可。我认为这不是使用char而不是unsigned char的理由,更应该避免在C++中使用printf函数族。 - utnapistim

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