为什么C语言中的字符字面量是整型而不是字符型?

120
在C++中,sizeof('a') == sizeof(char) == 1。这个很容易理解,因为'a'是一个字符字面值,而sizeof(char) == 1是由标准定义的。
但在C语言中,sizeof('a') == sizeof(int)。也就是说,C语言中的字符字面值实际上是整数。有没有人知道为什么呢?我能找到很多关于这个C语言怪癖的提及,但没有解释它存在的原因。

sizeof 只会返回一个字节的大小,不是吗?char 和 int 的大小不相等吗? - Josh Smeaton
1
这可能与编译器(和架构)有关。能说一下你在用什么吗?标准(至少到89年)非常宽松。 - dmckee --- ex-moderator kitten
2
一个char类型始终是1字节大小,因此在C++中sizeof('a') == 1总是成立的。而int类型理论上可以是1字节大小,但这需要一个字节至少有16位,这是非常不可能的 :) 因此,在大多数实现中,C++中sizeof('a') != sizeof(int)是非常可能的。 - Johannes Schaub - litb
2
在C语言中,这总是错误的。 - Johannes Schaub - litb
25
在C语言中,“a”是一个整数类型 - 就是这样。 C语言率先创建了规则。 C++改变了这些规则。你可以认为C++的规则更有意义,但是改变C的规则会带来更多的损害而不是好处,因此C标准委员会明智地没有触及这一点。 - Jonathan Leffler
Jonathan,只是为了明确一下 - 我的“在C语言中总是错误”的意思并不是说C语言总是错的 :) 它意味着sizeof('a')== sizeof(int)在C语言中总是成立。你的评论听起来像是在评论我在评论中说的某些内容 :) - Johannes Schaub - litb
11个回答

40

关于同一主题的讨论

"更具体地说是整数提升。在K&R C中,几乎不可能使用字符值而不首先将其提升为int,因此在第一次将字符常量设置为int时,就可以消除这一步骤。仍然存在多字符常量,例如'abcd'或适合int的任意数量."


多字符常量在不同编译器之间甚至在同一台机器上也不具备可移植性(尽管GCC在各个平台上似乎是自洽的)。请参见:https://dev59.com/unRC5IYBdhLWcg3wVvjL - Jonathan Leffler
11
请注意:a)这个引语没有标出出处;引文仅说“您是否不同意此观点,在过去的讨论中提出?” b)这是荒谬的,因为char变量不是int,所以使字符常量成为int是一种特殊情况。而且很容易在不提升它的情况下使用字符值:c1 = c2;。但是,c1 = 'x'是一个向下转换。最重要的是,sizeof(char) != sizeof('x'),这是一种严重的语言错误。至于多字节字符常量:它们是原因,但已经过时了。 - Jim Balter

36

最初的问题是“为什么?”

原因在于字面字符的定义已经演变和改变,而同时又试图保持与现有代码的向后兼容性。

在早期C语言的黑暗时代,根本没有任何类型。当我第一次学习C语言编程时,已经加入了类型,但函数没有原型来告诉调用者参数类型。相反,规定所有传递的参数都会作为int的大小(包括所有指针),或者它将是一个double。

这意味着,在您编写函数时,所有不是double的参数都将以int的方式存储在堆栈中,无论您如何声明它们,编译器都会为您处理这些代码。

这使得事情有些不一致,因此当K&R编写他们著名的书时,他们制定了规则,即在任何表达式中,字符字面值将始终提升为int,而不仅仅是作为函数参数。

当ANSI委员会首次标准化C语言时,他们更改了这个规则,因此字符字面值将简单地成为int,因为这似乎是实现相同目标的更简单方法。

在设计C++时,要求所有函数具有完整的原型(尽管这在C语言中仍不是必需的,但被普遍认为是良好实践)。正因为如此,决定将字符字面值存储在char中。在C++中的优点是,带有char参数的函数和带有int参数的函数具有不同的签名。这种优势在C语言中并不适用。

这就是它们不同的原因。演化...


2
我很赞同作者给出的"+1",因为他真正回答了 "why?"。但是我不同意最后一句话—— "在C++中的优势是一个带有char参数的函数和一个带有int参数的函数具有不同的签名" —— 在C ++ 中,仍然有可能出现两个具有相同大小但签名不同的函数,例如 void f(unsigned char)void f(signed char) - Peter K
4
@PeterK 约翰的措辞可能不太好,但他的话基本上是准确的。C++改变的动机是,如果你写了 f('a'),你很可能希望重载决议为该调用选择 f(char) 而不是 f(int)。正如你所说的那样,intchar 的相对大小并不相关。 - zwol

23

我不知道为什么在C中,字符字面量的类型是int。但在C++中,有一个很好的理由不采用这种方式。考虑以下情况:

void print(int);
void print(char);

print('a');

你会期望调用print函数选择第二个版本,即使用char类型作为参数。但是,如果字符字面值是int类型,这将变得不可能。请注意,在C++中,具有多个字符的字面值仍然具有int类型,尽管它们的值是实现定义的。因此,'ab' 的类型是 int,而 'a' 的类型是 char


是的,“C++的设计与演化”一书指出,重载输入/输出例程是C++改变规则的主要原因。 - Max Lybbert
5
Max,是的,我作弊了。我在兼容性部分查看了标准 :) - Johannes Schaub - litb

19

在我的 MacBook 上使用 gcc,我尝试执行以下操作:

#include <stdio.h>
#define test(A) do{printf(#A":\t%i\n",sizeof(A));}while(0)
int main(void){
  test('a');
  test("a");
  test("");
  test(char);
  test(short);
  test(int);
  test(long);
  test((char)0x0);
  test((short)0x0);
  test((int)0x0);
  test((long)0x0);
  return 0;
};

运行时会产生以下结果:

'a':    4
"a":    2
"":     1
char:   1
short:  2
int:    4
long:   4
(char)0x0:      1
(short)0x0:     2
(int)0x0:       4
(long)0x0:      4

这句话的意思是,字符占8位(就像你怀疑的那样),但字符字面值是int类型。

9
因为内容有趣,我给你点赞。人们常常认为sizeof("a")和sizeof("")是char*类型的,应该返回4(或8)。但实际上,在那一点上它们是char[]类型的(sizeof(char[11])返回11)。这是新手容易掉进的陷阱。 - paxdiablo
3
字符字面量不会被提升为整数,它已经是一个整数。如果对象是sizeof运算符的操作数,则根本没有进行任何提升。如果有提升,这将使sizeof失去作用。 - Chris Young
@Chris Young:好的,检查过了,谢谢。 - dmckee --- ex-moderator kitten

8

在C语言诞生之时,PDP-11的MACRO-11汇编语言具备了以下特点:

MOV #'A, R0      // 8-bit character encoding for 'A' into 16 bit register

这种情况在汇编语言中相当普遍 - 低 8 位将保留字符代码,其他位清零。PDP-11甚至有类似的情况:
MOV #"AB, R0     // 16-bit character encoding for 'A' (low byte) and 'B'

这提供了一种方便的方式,将两个字符加载到16位寄存器的低字节和高字节中。然后,您可以将它们写入其他位置,更新一些文本数据或屏幕内存。

因此,字符被提升为寄存器大小的想法非常正常和可取。但是,假设您需要将'A'放入寄存器中,而不是作为硬编码操作码的一部分,而是从包含以下内容的主存储器的某个位置获取:

address: value
20: 'X'
21: 'A'
22: 'A'
23: 'X'
24: 0
25: 'A'
26: 'A'
27: 0
28: 'A'

如果您想将主存中的一个'A'读入寄存器,那么应该读哪个地址?
一些CPU可能只支持将16位值直接读入16位寄存器,这意味着在20或22处进行读取需要清除来自'X'的位,并且根据CPU的字节顺序之一需要移动到低位字节。
一些CPU可能要求进行内存对齐读取,这意味着涉及的最低地址必须是数据大小的倍数:您可以从地址24和25读取,但无法从27和28读取。
因此,编译器生成代码以将'A'放入寄存器可能更喜欢浪费一些额外的内存,并将值编码为0 'A' 或 'A' 0 - 取决于字节顺序,并确保它被正确对齐(即不在奇数存储器地址)。
我猜测C语言只是简单地延续了这种基于CPU的行为模式,认为字符常量占据寄存器大小的内存,从而证明了C被称为“高级汇编语言”的普遍评价。
(请参见http://www.dmv.net/dec/pdf/macro.pdf第6.3.3页)

5
我没有看到它(C字符字面量是int类型)的合理解释,但这里有一些Stroustrup关于它的言论(来自Design and Evolution 11.2.1 - Fine-Grain Resolution):
在C中,像'a'这样的字符字面量的类型是int。令人惊讶的是,在C++中将'a'的类型设置为char并不会引起任何兼容性问题。除了病态例子sizeof('a')之外,可以在C和C++中表达的每个结构都给出相同的结果。
因此,在大多数情况下,它应该不会引起问题。

有趣!这有点与其他人关于C标准委员会“明智地”决定不从C中删除此怪癖的说法相矛盾。 - j_random_hacker

5
我记得在阅读K&R时看到一段代码片段,它会逐个字符地读取,直到遇到EOF。由于文件/输入流中的所有字符都是有效的字符,因此EOF不能是任何char值。该代码将读取的字符放入int中,然后测试EOF,如果不是,则转换为char。
我意识到这并没有完全回答你的问题,但如果EOF字面量是sizeof(int),那么其余字符字面量大小为sizeof(int)也许是有道理的。
int r;
char buffer[1024], *p; // don't use in production - buffer overflow likely
p = buffer;

while ((r = getc(file)) != EOF)
{
  *(p++) = (char) r;
}

我认为0不是一个有效的字符。 - gbjbaanb
4
当然可以。它就是空字符。想一想,你觉得一个文件中不应该包含任何零字节吗? - P Daddy
一个以 null 结尾的文件对于文本数据可能是有意义的,但如果是二进制数据,我认为 \0 应该被视为有效值。 - Kyle Cronin
1
阅读维基百科 - “EOF 的实际值是一个系统相关的负数,通常为-1,它保证不等于任何有效的字符代码。” - Malx
2
正如Malx所说 - EOF不是char类型,而是int类型。getchar()和其他函数返回int,可以容纳任何char以及EOF而不会发生冲突。这确实不需要将字面字符具有int类型。 - Michael Burr
2
EOF == -1 这个概念出现在 C 语言的字符常量之后,因此这不是一个答案,甚至也不相关。 - Jim Balter

3
这一历史原因在于 C 语言及其前身 B 语言最初是在各种 DEC PDP 迷你计算机上开发的,这些计算机具有不同的字长,支持 8 位 ASCII 码,但只能在寄存器上执行算术运算。(不过 PDP-11 并不是这样,它是后来才出现的。)早期版本的 C 语言将 int 定义为机器的本地字长,并且任何小于 int 的值都需要被拓展到 int 大小才能被传递给函数、从函数中返回或在按位、逻辑或算术表达式中使用,因为这是底层硬件工作的方式。
这也是为什么整数提升规则仍然指出任何比 int 小的数据类型都会被提升为 int。出于类似的历史原因,C 编译器也可以使用补码运算以代替二进制补码运算。八进制字符转义和常量是第一类公民,而相比之下十六进制并不如此,也是因为那些早期的 DEC 迷你计算机具有可被三字节块整除而不是四位半字节的字长。

...而char恰好是3个八进制数字长。 - Antti Haapala -- Слава Україні

-1

我不确定,但我猜这样实现起来更容易,而且并不重要。直到C++出现后,类型才能决定调用哪个函数,这时才需要修复它。


-1

这是正确的行为,称为“整数提升”。在其他情况下也可能发生(主要是二元运算符,如果我记得正确的话)。

编辑:为了确保,我检查了我的《Expert C Programming: Deep Secrets》副本,并确认字符字面量不以类型int开头。它最初是char类型,但当它在表达式中使用时,它会被提升为int类型。以下摘自该书:

字符字面量的类型为int, 它们通过遵循从char类型到int类型的提升规则而获得。 这在K&R 1的第39页上过于简略地介绍了: 表达式中的每个char都转换为int....请注意, 表达式中的所有float都转换为double....由于函数参数是一个表达式, 当参数传递给函数时也进行类型转换:特别是,char和short变成int, float变成double。


2
一个字符字面量确实具有int类型。 ANSI / ISO 99标准将它们称为“整数字符常量”(以将它们与具有wchar_t类型的“宽字符常量”区分开来),并明确表示:“整数字符常量具有int类型。” - Michael Burr
我的意思是它不是以int类型开始的,而是从char类型转换为int类型(答案已编辑)。当然,这可能只关系到编译器的编写者,因为转换总是会进行。 - PolyThinker
3
不行!如果你阅读ANSI/ISO 99 C标准,你会发现在C语言中,表达式'a' int类型开始。如果你有一个函数void f(int),和一个变量char c,那么f(c) 执行整数提升,但是f('a')不会,因为'a'的类型已经是int。奇怪但是真实的。 - j_random_hacker
2
“只是为了确保” - 您可以通过实际阅读语句“字符字面量具有int类型”来更加确定。 “我只能假设那是其中一个静默更改” - 您的假设是错误的。在C中,字符字面量始终是int类型。 - Jim Balter
-1 这个答案仍然是错误的,应该被删除。请参阅C11 6.4.4.4/10:“整数字符常量的类型为int”。要么“Deep Secrets”是错误的,要么你只是误解了它。 - Lundin
显示剩余3条评论

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