为什么char类型不同于有符号字符型和无符号字符型?

10

cppreference.com指出,char类型可以等同于signed char或者unsigned char,但是char是一种不同于signed charunsigned char的独立类型。

这意味着char类型可以持有与unsigned char或者signed char完全相同的值,但是不与它们兼容。

我认为这意味着char类型可以持有与unsigned char或者signed char完全相同的值,但是不与它们兼容。那么,为什么决定采用这种方式?为什么未经限定的char不能表示平台适当符号的char,就像其他整数类型一样,其中int恰好表示与signed int相同的类型?


8
与其他整数类型不同,char 类型的实现决定了它是有符号还是无符号的。因此,规范需要单独处理它。 - Some programmer dude
2
@Tau 这是语言的一个特性,而不是系统的。原因可能是历史上的:在标准制定时,不同的编译器版本可能已经使用了具有不同符号的 char - Weather Vane
2
但是我认为最有说服力的原因与规范中“兼容”的技术细节相关,并且对C抽象机器行为的影响,以及语言语义的定义在这方面扮演了重要角色。尽管不是从您所考虑的方向,但它主要仍然涉及到兼容性问题。 - John Bollinger
3
@JohnBollinger说:“我在这些评论中已经数出了大约六个原因。” 目前可见的评论中没有说明为什么C委员会决定“char是与signed charunsigned char不同的类型”,而不是“char与实现定义的signed charunsigned char之一是相同的类型”。Weather Vane关于定义signed charunsigned char以支持可移植性而又不破坏依赖char某些语义的旧代码的评论,暗示了可能的原因,但没有给出具体原因,因为如何引起这种破坏并没有说明。 - Eric Postpischil
2
早期版本的C语言没有unsignedsigned关键字。所有整数类型都是有符号的,除了依赖于实现的char类型。为了避免规定char应该是有符号的(这将与现有的实现冲突),添加了两种新类型。 - Ian Abbott
显示剩余17条评论
5个回答

7

TL;DR

回溯兼容性可能是原因。或者是因为他们必须做出选择并且不在意这个问题。但我没有确切的答案。

长版本

介绍

就像OP一样,我更喜欢来自可靠来源的确切答案。在没有这种情况下,合格的猜测和推测总比没有好。

C中的许多东西都源于回溯兼容性。当决定char是否与signed charunsigned char相同是实现定义时,已经有很多C代码存在,其中一些使用signed chars,其他使用unsigned chars。强制它成为其中之一肯定会破坏某些代码。

为什么这(可能)不重要

为什么未经资格认证的字符不表示平台适当签名的字符

这并不重要。使用有符号字符的实现保证CHAR_MIN等于SCHAR_MINCHAR_MAX等于SCHAR_MAX,无符号字符也是如此。因此,未经限定的char将始终具有与其限定版本完全相同的范围。
来自标准5.2.4.2.1p2
如果在表达式中使用char类型的对象的值被视为有符号整数,则CHAR_MIN的值应与SCHAR_MIN的值相同,而CHAR_MAX的值应与SCHAR_MAX的值相同。否则,CHAR_MIN的值应为0,而CHAR_MAX的值应与UCHAR_MAX的值相同。
这指向我们的方向,即他们并没有真正关心这个问题,或者它“感觉更安全”。
C标准中另一个有趣的提到是:
所有枚举都有一个基础类型。基础类型可以使用枚举类型说明符进行明确指定,是其固定的基础类型。如果没有明确指定,则基础类型是枚举兼容类型,这是有符号或无符号整数类型(不包括位精确的整数类型)或 char。
破坏此规则可能会带来潜在问题(推测)。我正在尝试想出一个可能导致问题的方案。其中一个可能会导致问题的是,如果您使用 signed char 使用一个编译器将源文件编译为共享库,然后使用另一个使用 unsigned char 编译的编译器将该库用于源文件中,那么就会出现问题。
即使那样也不会导致问题,想象一下,共享库是使用 pre-ansi 编译器编译的。好吧,我不能确定这是否会导致问题。但我可以想象它可能会导致问题。
Steve Summit 在评论部分提出了另一种推测:
我在推测,如果标准要求按照Eric的说法,"char是一个实现定义的signed charunsigned char选择的相同类型",那么如果我正在使用一个charsigned char相同的平台上,我可以混合使用两者而不会收到警告,并创建不能在默认情况下为无符号的机器上运行的代码。因此,定义"char是一个与signed charunsigned char不同的类型"有助于强制人们编写可移植的代码。

向后兼容性是一项神圣的功能

但请记住,C标准背后的人们非常关心不破坏向后兼容性。甚至到了他们不想改变一些库函数的签名以返回const值的程度,因为这会产生警告。不是错误。可以轻松禁用警告。相反,他们只是在标准中写入了修改值的未定义行为。您可以在此处阅读更多信息:https://thephd.dev/your-c-compiler-and-standard-library-will-not-help-you 所以,每当您在C标准中遇到非常奇怪的设计选择时,可以很好地打赌向后兼容性是原因。这就是为什么您可以使用0来初始化指向NULL的指针,即使对于NULL不是零地址的机器也是如此。以及为什么bool是关键字_Bool的宏。
这也是位运算符 | 和 & 的优先级高于 == 的原因,因为有很多(安装在3台机器上的几百千字节的源代码 :))包括类似 if (a==b & c==d) 的东西。Dennis Ritchie 承认他应该改变它。https://www.lysator.liu.se/c/dmr-on-or.html 所以我们至少可以确定,有些设计选择考虑了向后兼容性,但后来这些选择的制定者承认是错误的,并且我们有可靠的来源证明。
C++
还要记住您的来源指向 C++ 源。在这种语言中,有一些不适用于 C 的原因。比如函数重载。

7
三种 C 字符类型 char, signed char, 和 unsigned char 存在是为了编码遗留的 C 实现和用法。
将 C 编码为第一个 C 标准(现在称为 C89)的 XJ311 委员会在其 Rational 中阐述了他们的目的(原文斜体):

1.1 目的

委员会的总体目标是为 C 编程语言开发一个清晰、一致和明确的标准,该标准编码了 C 的通用、现有定义,并促进用户程序在 C 语言环境中的可移植性。

X3J11 宪章明确授权委员会 编码常见的现有实践。...

注意:X3J11 委员会特别强调他们正在编码 C 的现有实现和常见用法/惯例,以促进可移植性。
换句话说,“标准” C 从未被创建 - 现有的 C 代码、用法和实践被编码化了。

根据同一份理由3.1.2.5类型(我加粗):

指定了三种char类型:signed,plain和unsigned。 plain char可以表示为signed或unsigned,具体取决于实现,就像以前的做法一样。引入了signed char类型,以便在那些将plain char实现为unsigned的系统上提供一个一字节的有符号整数类型。...

委员会的话很清楚:存在三种char类型,因为plain char必须是signed或unsigned才能与“以前的做法”相匹配。因此,plain char必须是单独的 - 可移植代码不能依赖于plain char是signed还是unsigned,但是signed char和unsigned char都必须可用。

由于可移植性的考虑,三种字符类型无法以任何方式兼容。符合标准的C代码的可移植性是XJ311委员会的主要目标之一。
如果在一个普通char为unsigned的系统上,extern char buffer[10]与unsigned char buffer[10]兼容,那么当在一个普通char为signed的系统上编译代码时,代码将表现出不同的行为,并且与unsigned char buffer[10]不兼容。例如,通过extern char buffer[10]声明或unsigned char buffer[10]定义访问buffer的位移将根据情况而改变,从而破坏了可移植性。
char已经可以在这种情况下具有不同的行为(即被视为signed或unsigned),委员会不能改变这一点,否则就会违反他们"对C语言常见、已存在的定义进行编码"的目标。
但是,如果目标是促进可移植性,就没有任何理由创造一个疯狂的、导致可移植性噩梦的情况,其中“有时char与这个不兼容,有时char与那个不兼容”。

* - 如果代码编译了 - 但这只是一个假设,旨在说明为什么三种char类型必须不兼容。


4

不强制要求对于普通的char类型使用已签名或未签名的一部分原因是IBM大型机特别使用EBCDIC码集。

C标准在§6.2.5 Types ¶3中指出:

声明为char类型的对象足够大,可以存储基本执行字符集中的任何成员。如果将基本执行字符集的成员存储在char对象中,则保证其值为非负数

强调添加。

现在,在EBCDIC中,小写字母的编码点为0x81-0x89,0x91-0x99,0xA2-0xA9;大写字母的编码点为0xC1-0xC9,0xD1-0xD9,0xE2-0xE9;数字的编码点为0xF0-0xF9。因此:
- 字母表不是连续的。 - 小写字母排序在大写字母之前。 - 数字比字母高。 - 并且由于§6.2.5¶3,普通char类型必须是无符号的。
前三个点与ASCII(以及ISO 8859和ISO 10646即Unicode)相反。

我刚刚瞥了一眼K&R第一版2.2,其中有一个有趣的总结,介绍了20世纪70年代一些奇特计算机使用的不同类型。不仅是具有EBCDIC、8位字符和32位整数的IBM 370,还有带有9位字符和36位整数的Honeywell 6000。由于计算机仍然非常实验性,因此C类型系统最终出现了如此多的实现定义行为和兼容性问题,这或许并不奇怪。 - Lundin

1
原因是为了向后兼容。以下是与其背后历史相关的一些研究。它只使用权威第一手资料,如C语言创始人Dennis M. Ritchie的出版物或ISO。
最初只有int和char。C语言早期草案称为“NB”(新B),包含了这些在前身B和BCPL中不存在的新类型[Ritchie,93]:
“...似乎需要一个类型方案来处理字符和字节寻址,并为即将到来的浮点硬件做准备。”

Embryonic C

NB existed so briefly that no full description of it was written. It supplied the types int and char, arrays of them, and pointers to them, declared in a style typified by

int i, j;
char c, d;
无符号类型(unsigned)是后来添加的[Ritchie, 93]:
在1973年至1980年期间,该语言有所增长:类型结构增加了unsigned、long等。
需要注意的是,在这一点上,它指的是独立的“类型限定符” unsigned,相当于 unsigned int。
大约在1978年左右,《C程序设计语言》第一版出版[Kernighan, 78],第2.7章提到与char相关的类型转换问题:
关于将字符转换为整数的一个微妙点是,语言没有指定char类型的变量是有符号还是无符号量。当char转换为int时,它是否会产生负整数?不幸的是,这因机器而异,反映了架构差异。在某些机器上(例如PDP-11),最左边位为1的char将被转换为负整数("符号扩展")。在其他机器上,通过在左端添加零,将char升级为int,并因此始终为正数。
此时,问题描述为向int的类型提升,而不是char的符号性,甚至没有指定。以上文本在第二版[Kernighan, 88]中基本保持不变。
然而,在版本之间,类型本身的描述是不同的。在第一版[Kernighan, 78,2.2]中,unsigned只能应用于int并被视为限定符:
此外,还有一些限定符可以应用于int:short、long和unsigned。
而第二版与标准C保持一致[Kernighan, 88,2.2]:
限定符signed或unsigned可以应用于char或任何整数。/--/普通字符是有符号的还是无符号的是机器相关的,但可打印字符始终为正数。
因此,在第一版和第二版之间,他们发现了将新的unsigned/signed(现在称为类型说明符而不是限定符[ANSI / ISO,90])应用于char类型的向后兼容性问题,具有与已经确定的类型转换相同的问题在第一版中。
这个兼容性问题在80年代后期标准化期间仍然存在。我们可以从各种解释中读出,例如[ISO,98,6.1.2.5 §30]
三种类型的“char”被指定:signed、plain和unsigned。普通的“char”可以表示为有符号或无符号,这取决于实现方式,就像以前的做法一样。引入了“signed char”类型,在那些将普通“char”实现为无符号的系统上提供了一个一字节带符号整数类型。出于对称的原因,关键字“signed”被允许作为其他整数类型名称的一部分。指定了两种整数类型的变体:signed和unsigned。如果没有使用任何说明符,则默认为signed。在基本文档中,唯一的unsigned类型是unsigned int。
这实际上表明,允许使用“signed int”是为了使“int”更对称,而不是相反。
来源: [ANSI/ISO, 90] ANSI/ISO 9899:1990 - 编程语言 - C [ISO, 98] Rationale for International Standard - Programming Language - C, WG14 / N802 J11 / 98-001 [Kernighan, 78] Kernighan, Brian W.,Ritchie, Dennis M. - The C Programming Language,第1版(1978) [Kernighan, 88] Kernighan, Brian W.,Ritchie, Dennis M. - The C Programming Language,第2版(1988) [Ritchie, 93] Ritchie, Dennis M. - The Development of the C Language(1993)

0
你引用的那行代码实际上并不来自于C标准,而是来自于C++标准。你链接的网站(cppreference.com)主要是关于C++的,其中的C内容只是附带而已。
这对于C++很重要(但对于C来说并不重要),因为C++允许基于类型进行重载,但你只能重载不同的类型。事实上,char必须与signed charunsigned char都不同,这意味着你可以安全地重载所有三个类型:
// 3 overloads for fn
void fn(char);
void fn(signed char);
void fn(unsigned char);

这样你就不会因为重载歧义等问题而出现错误了。


我引用的文章非常具体地涵盖了C语言的算术类型。我认为那里使用的措辞既不出现在C标准中,也不出现在C++标准中。在C语言中,字符类型的不兼容仍然很重要:例如,通过不兼容的函数类型值(例如参数为char而不是signed char)调用函数会导致未定义的行为。 - Tau
@Tau:这是不正确的,因为在函数调用中,char、signed char和unsigned char都将通过默认参数提升(6.5.2.2)转换为int。 - Chris Dodd
如果您指的是6.5.2.2.6(C17,最新草案,我很穷(:)),那只适用于具有没有原型的类型的函数值。 - Tau
由于(非静态)函数可以从可能没有原型的其他编译单元调用,因此适用于任何非静态函数。 - Chris Dodd
@Dodd 它仅适用于通过无原型声明调用函数时。当使用的声明的原型(或对于无原型声明,参数的提升类型)与函数定义的原型不兼容时,UB会发生。这也是类型兼容性在C中重要的原因之一。另一个原因是:在_Generic选择中命名的类型可能彼此不兼容;因此,这整个主题也与C的“重载”版本相关。 - Tau
1
抱歉我这么挑剔,但我正在编写一个编译器,所以对于所有这些愚蠢的标准术语,我必须非常精确。 - Tau

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