c语言 - 使用 gcc 时导致字符有符号或无符号的原因



C(使用gcc)中的char是有符号还是无符号,是什么原因?我知道该标准不会规定一个而不是另一个,我可以检查CHAR_MIN并从限制中CHAR_MAX.h,但我想知道在使用 gcc 时是什么触发了一个而不是另一个

如果我从 libgcc-6 读取 limits.h,我会看到有一个宏__CHAR_UNSIGNED__它定义了"默认"字符有符号或无符号,但我不确定这是否由编译器在(他的)构建时设置。

我试图列出 GCC 预定义的 makros

$ gcc -dM -E -x c /dev/null | grep -i CHAR
#define __UINT_LEAST8_TYPE__ unsigned char
#define __CHAR_BIT__ 8
#define __WCHAR_MAX__ 0x7fffffff
#define __GCC_ATOMIC_CHAR_LOCK_FREE 2
#define __GCC_ATOMIC_CHAR32_T_LOCK_FREE 2
#define __SCHAR_MAX__ 0x7f
#define __WCHAR_MIN__ (-__WCHAR_MAX__ - 1)
#define __UINT8_TYPE__ unsigned char
#define __INT8_TYPE__ signed char
#define __GCC_ATOMIC_WCHAR_T_LOCK_FREE 2
#define __CHAR16_TYPE__ short unsigned int
#define __INT_LEAST8_TYPE__ signed char
#define __WCHAR_TYPE__ int
#define __GCC_ATOMIC_CHAR16_T_LOCK_FREE 2
#define __SIZEOF_WCHAR_T__ 4
#define __INT_FAST8_TYPE__ signed char
#define __CHAR32_TYPE__ unsigned int
#define __UINT_FAST8_TYPE__ unsigned char

但找不到__CHAR_UNSIGNED__

背景:我有一些代码,我在两台不同的机器上编译:

台式电脑:

  • Debian GNU/Linux 9.1 (stretch)
  • gcc 版本 6.3.0
  • 20170516 (Debian 6.3.0-18)
  • 英特尔® 酷睿™ i3-4150
  • libgcc-6-dev: 6.3.0-18
  • char已签名

树莓派3

  • Raspbian GNU/Linux 9.1 (stretch)
  • gcc 版本 6.3.0
  • 20170516 (Raspbian 6.3.0-18+rpi1)
  • ARMv7 处理器修订版 4 (v7l)
  • libgcc-6-dev: 6.3.0-18+RPI
  • char是无符号

所以唯一明显的区别是 CPU 架构...

根据 C11 标准(阅读 n1570),char可以是signedunsigned(所以你实际上有两种 C 风格)。它到底是什么是特定于实现的。

一些处理器和指令集架构或应用程序二进制接口偏爱signed字符(字节)类型(例如,因为它很好地映射到某些机器代码指令),其他处理器和指令倾向于unsigned

gcc甚至有一些你几乎不应该使用的-fsigned-char-funsigned-char选项(因为更改它会破坏调用约定和 ABI 中的一些极端情况),除非你重新编译所有内容,包括你的 C 标准库。

你可以在 Linux 上使用 feature_test_macros(7) 和<endian.h>(参见字节序(3))或 autoconf 来检测你的系统有什么。

在大多数情况下,您应该编写可移植的 C 代码,它不依赖于这些内容。你可以找到跨平台的库(例如glib)来帮助你。

顺便说一句,gcc -dM -E -x c /dev/null还提供了__BYTE_ORDER__等,如果您想要一个无符号的 8 位字节,您应该使用<stdint.h>及其uint8_t(更便携、更易读)。标准 limits.h 定义了CHAR_MINSCHAR_MIN以及CHAR_MAXSCHAR_MAX(您可以比较它们的相等性以检测signed char的实现)等等......

顺便说一句,您应该关心字符编码,但当今大多数系统在任何地方都使用 UTF-8。像 libunistring 这样的库很有帮助。另请参阅此内容并记住,实际上用 UTF-8 编码的 Unicode 字符可以跨越多个字节(即char-s)。

默认值取决于平台和本机代码集。 例如,使用 EBCDIC(通常为大型机)的机器必须使用unsigned char(或具有CHAR_BIT > 8),因为 C 标准要求基本代码集中的字符为正数,而 EBCDIC 使用像 240 这样的代码表示数字 0。 (C11 标准,§6.2.5 类型 ¶2 说:声明为类型char的对象足够大,可以存储基本执行字符集的任何成员。如果基本执行字符集的成员存储在char对象中,则保证其值为非负数。

您可以通过-fsigned-char-funsigned-char选项控制 GCC 使用哪个符号。这是否是一个好主意是一个单独的讨论。

字符类型charsignedunsigned,具体取决于平台和编译器。

根据此参考链接:

C 和 C++ 标准允许对字符类型字符进行签名无符号具体取决于平台和编译器

大多数系统,包括x86 GNU/Linux和Microsoft Windows,都使用签名字符

但 基于PowerPC和ARM处理器的处理器通常使用无符号处理器 字符。(二十九)

这可能会导致移植程序时出现意外结果 在对 char 类型具有不同默认值的平台之间。

GCC 提供了选项-fsigned-char-funsigned-char来设置默认的char类型。

至少在x86-64 Linux上,它是由x86-64 System V psABI定义的。

其他平台将有类似的ABI标准文档,这些文档指定了规则,让不同的C编译器在调用约定,结构布局等方面相互同意。 (请参阅 x86 标记 wiki 以获取指向其他 x86 ABI 文档的链接,或有关其他体系结构的其他地方。 大多数非 x86 体系结构只有一个或两个标准 ABI。

从 x86-64 SysV ABI:图 3.1:标量类型

C            sizeof      Alignment       AMD64
(bytes)         Architecture
_Bool*          1             1              boolean
-----------------------------------------------------------
char            1             1              signed byte
signed char
---------------------------------------------------------
unsigned char   1             1              unsigned byte
----------------------------------------------------------
...
-----------------------------------------------------------
int             4             4              signed fourbyte
signed int
enum***
-----------------------------------------------------------
unsigned int    4             4              unsigned fourbyte
--------------------------------------------------------------
...

* 此类型在C++中称为bool

C++和一些 C 的实现允许大于 国际。基础类型被碰撞到无符号的 int、long int 或 无符号长 int,按此顺序排列。


在这种情况下,char是否签名实际上直接影响调用约定,因为 clang 所依赖的当前未记录的要求:根据被调用方原型,窄类型在作为函数参数传递时是符号或零扩展为 32 位。

所以对于int foo(char c) { return c; },clang 将依靠调用者对参数进行符号扩展。 (代码 + asm 为此和 Godbolt 上的调用者)。

gcc:
movsx   eax, dil       # sign-extend low byte of first arg reg into eax
ret
clang:
mov     eax, edi       # copy whole 32-bit reg
ret

即使除了调用约定之外,C 编译器也必须同意,以便他们以相同的方式以.h编译内联函数。

如果(int)(char)x在同一平台的不同编译器中表现不同,它们就不会真正兼容。

gcc 有两个编译时选项来控制char的行为:

-funsigned-char
-fsigned-char

除非您确切知道自己在做什么,否则不建议使用这些选项中的任何一个。

默认值取决于平台,并在构建 gcc 本身时修复。选择它是为了与该平台上存在的其他工具具有最佳兼容性。

源。

一个重要的实际注意事项是 UTF-8 字符串文字的类型,如u8"...",是一个char数组,它必须以 UTF-8 格式存储。 基本集中的字符保证等效于正整数。 然而

如果任何其他字符存储在 char 对象中,则结果值是实现定义的,但应在可以用该类型表示的值范围内。

(在C++中,UTF-8 字符串常量的类型是const char []的,并且没有指定基本集之外的字符是否具有数字表示形式。

因此,如果您的程序需要摆动 UTF-8 字符串的位,则需要使用unsigned char. 否则,任何检查 UTF-8 字符串的字节是否在一定范围内的代码都将不可移植。

最好显式转换为unsigned char*,而不是编写char并期望程序员使用正确的设置进行编译以将其配置为unsigned char。 但是,您可以使用static_assert()来测试char范围是否包括从 0 到 255 的所有数字。

https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html 说

__CHAR_UNSIGNED__

GCC 定义此宏当且仅当数据类型 char 在>目标计算机上未签名时。它的存在是为了使标准头文件 limits.h>正常工作。您不应该自己使用此宏;相反,请参阅 限制中定义的标准宏。

因此,您没有在列表中看到它的原因是您在系统上进行了 char 签名并且根本没有在此类系统上定义宏。我已经确认它确实出现在我的一个手臂系统手臂系统的cc -dM -E -x c /dev/null | grep -i CHAR输出中。

C 标准将其留给实现,当然这并没有说太多,因为"实现"将一堆东西、编译器、操作系统、CPU 架构等混为一谈。

在 Linux 上,它取决于 CPU 系列。对于某些体系结构,这是有充分理由的。例如,早期的 arm 没有真正支持有符号字节。对于其他人来说,它似乎更加随意,可能是从在同一硬件上运行的其他操作系统复制而来的。

Afaict Windows和mac OS在所有目标架构(或至少是当前支持的所有架构)上使用签名字符。

最新更新