56

是否真的有必要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's输出都正确,十六进制f0 a4 ad a2的Unicode代码点的编码在哪里。U+24B62 ()

甚至memcpy还正确地复制了 char 所持有的位。

有什么理由可以提倡使用 ofunsigned char而不是 a plain char

在其他相关问题unsigned char中突出显示,因为它是 C 规范保证没有填充的唯一(字节/最小)数据类型。但正如上面的示例所示,输出似乎不受任何填充的影响。

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

warning C4309: '=' : truncation of constant value

输出似乎没有反映这一点。

PS 这可能被标记为是否应该有符号的字节缓冲区或无符号字符缓冲区?但我的意图不同。我在问为什么char应该输入一些似乎可以正常工作的东西unsigned char

更新:引用 N3337,

Section 3.9 Types

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

鉴于上述事实以及我最初的示例是在char默认为Intel 的机器上signed char,我仍然不相信是否unsigned char应该首选char

还要别的吗?

4

8 回答 8

99

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

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

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

对于第二个属性,我们需要一个类型,即unsignedUCHAR_MAX+1对于这些,256在大多数 99% 的架构中,所有转换都是用模算术定义的,这里是 modulo 。更宽值的所有转换,unsigned char从而仅对应于截断到最低有效字节。

其他两种字符类型通常不一样。signed char无论如何,是有符号的,因此不适合它的值的转换没有很好的定义。char不固定为已签名或未签名,但在您的代码移植到的特定平台上,即使它在您的代码上未签名,它也可能已签名。

于 2012-11-30T10:06:26.977 回答
16

比较单个字节的内容时,您会遇到大部分问题:

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

可以打印“bad”,因为根据您的编译器,c[0] 将符号扩展为 -1,这与 0xff 不同

于 2012-11-30T10:46:50.397 回答
12

普通char类型是有问题的,不应该用于字符串以外的任何东西。主要问题char是你不知道它是有符号还是无符号:这是实现定义的行为。这使得charint等不同,int始终保证被签名。

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

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

在这种特定情况下,警告应该无关紧要。

编辑 :

在其他相关问题中,unsigned char 被突出显示,因为它是唯一(字节/最小)数据类型,C 规范保证没有填充。

理论上,除了 unsigned char 和 signed char 之外的所有整数类型都允许包含“填充位”,根据 C11 6.2.6.2:

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

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

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

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

但是,在 C 标准之外的现实世界中,以下情况适用:

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

所以没有真正的理由使用 unsigned char 或 signed char 只是为了避开 C 标准中的一些理论场景。

于 2012-11-30T09:46:18.090 回答
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 声明为 unsigned 将删除警告,并且在没有任何警告的情况下进行干净的构建总是好的。

于 2012-11-30T10:13:38.177 回答
4

普通类型的签名char是实现定义的,因此除非您实际处理字符数据(使用平台字符集的字符串 - 通常是 ASCII),通常最好使用signed char或明确指定签名unsigned char

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

于 2012-11-30T09:45:34.820 回答
2

我在问为什么应该输入 unsigned char 似乎与 char 一样好用的东西?

如果您做的事情在标准意义上不“正确”,那么您将依赖未定义的行为。你的编译器今天可能会按照你想要的方式来做,但你不知道它明天会做什么。你不知道 GCC 做什么或 VC++ 2012。或者即使行为取决于外部因素或调试/发布编译等。一旦你离开标准的安全路径,你可能会遇到麻烦。

于 2012-11-30T09:44:47.953 回答
2

好吧,你怎么称呼“二进制数据”?这是一堆位,没有任何由软件的特定部分赋予它们的任何意义,这些部分称为“二进制数据”。什么是最接近的原始数据类型,它向这些位中的任何一个传达了缺乏任何特定含义的想法?我认为unsigned char

于 2012-11-30T09:46:20.683 回答
2

真的有必要像在某些处理字符编码或二进制缓冲区的库中那样使用 unsigned char 来保存二进制数据吗?

“真的”有必要吗?不。

不过,这是一个非常好的主意,并且有很多原因。

您的示例使用 printf,它不是类型安全的。也就是说, printf 从格式字符串而不是数据类型中获取格式提示。你可以很容易地尝试:

printf("%s\n", (void*)c);

...结果是一样的。如果您使用 c++ iostreams 尝试相同的操作,结果会有所不同(取决于 c 的签名性)。

有什么理由可以提倡使用无符号字符而不是普通字符?

Signed 指定数据的最高有效位(对于 unsigned char 的第 8 位)表示符号。由于您显然不需要,您应该指定您的数据是无符号的(“符号”位代表数据,而不是其他位的符号)。

于 2012-11-30T10:57:20.550 回答