为什么C字符文字是整型而不是char?


103

在C ++中,sizeof('a') == sizeof(char) == 1。这是直观的意义,因为'a'它是字符文字,并且sizeof(char) == 1由标准定义。

但是在C中,sizeof('a') == sizeof(int)。即,看来C字符文字实际上是整数。有人知道为什么吗?我可以找到很多关于此C怪癖的提及,但没有解释为什么它存在。


sizeof会返回一个字节的大小,不是吗?字符和整数大小不相等吗?
乔什·斯梅顿

1
这可能取决于编译器(和体系结构)。想说您正在使用什么?该标准(至少达到'89)非常宽松。
dmckee ---前主持人小猫,

2
没有。一个char始终为1个字节大,所以的sizeof(“A”)== 1总是(在C ++),而int可以理论上是的sizeof 1,但是这将需要具有至少16位,这是一个字节非常不大可能: ),因此sizeof('a')!= sizeof(int)在大多数实现中在C ++中很有可能
Johannes Schaub-litb

2
......而它总是错误的C.
约翰内斯·绍布- litb

22
“ a”是C-期间的整数。C首先到达那里-C制定了规则。C ++更改了规则。您可以说C ++规则更有意义,但是更改C规则带来的损害大于弊,因此C标准委员会没有明智地做到这一点。
乔纳森·莱夫勒,

Answers:


36

关于同一主题的讨论

“更具体地讲,整数提升。在K&R C中,如果不先将字符值提升为int,就几乎不可能使用(?)字符值,因此首先使字符恒定为int就消除了这一步骤。存在并且仍然是多字符常数,例如'abcd'或任何其他常数都可以放入int中。”


即使在单台机器上的编译器之间,多字符常量也不是可移植的(尽管GCC在各个平台上似乎是自洽的)。参见:stackoverflow.com/questions/328215
乔纳森·莱夫勒

8
我会注意到:a)此报价未归因;引文只说:“您是否不同意这一观点?该观点发表在过去讨论该问题的话题中?” ... and b)很荒谬,因为char变量不是int,所以将字符常量设为1是一种特殊情况。而且很容易使用字符值而不提升它:c1 = c2;。OTOH,c1 = 'x'是向下转换。最重要的sizeof(char) != sizeof('x')是,这是严重的语言缺陷。至于多字节字符常量:这是原因,但已过时。
Jim Balter

27

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

原因是文字字符的定义已经发展和改变,同时试图保持与现有代码的向后兼容性。

在C早期的黑暗日子里,根本没有类型。当我第一次学习用C语言编程时,已经引入了类型,但是函数没有原型来告诉调用者参数类型是什么。取而代之的是标准化了所有作为参数传递的内容,要么是一个int的大小(包括所有指针),要么是一个双精度数。

这意味着,当您编写函数时,所有不是双精度的参数都以ints形式存储在堆栈中,无论您如何声明它们,编译器都会在函数中放入代码来为您处理。

这使得事情有些前后矛盾,因此,当K&R撰写他们的著名著作时,他们提出了一个规则,即始终将字符文字提升为任何表达式中的int形式,而不仅仅是函数参数。

当ANSI委员会首次对C进行标准化时,他们更改了此规则,以使字符文字只是一个int,因为这似乎是实现同一目标的一种更简单的方法。

在设计C ++时,所有功能都必须具有完整的原型(尽管C被普遍接受为良好实践,但C中仍然不需要此功能)。因此,决定可以将字符文字存储在char中。在C ++中,这样做的优点是带有char参数的函数和带有int参数的函数具有不同的签名。在C中不是这种优势。

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


2
向我+1了,他们实际上回答了“为什么?”。但是我不同意最后一条语句-“在C ++中,这样做的优势在于,具有char参数的函数和具有int参数的函数具有不同的签名” –在C ++中,两个函数仍可能具有相同的大小和不同的签名,如void f(unsigned char)Vs的void f(signed char)
彼得·K

3
@PeterK John可能会说得更好,但是他说的基本上是正确的。编写C ++的动机是,如果您编写此代码,则f('a')可能希望f(char)为该调用选择重载分辨率,而不是f(int)。正如您所说,int和的相对大小char无关紧要。
zwol

21

我不知道为什么C中的字符文字是int类型的具体原因。但是在C ++中,有充分的理由不这样做。考虑一下:

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

print('a');

您希望打印调用选择带有字符的第二个版本。将字符文字设置为int将使这不可能。请注意,在C ++中,具有多个字符的文字尽管其值是由实现定义的,但仍然具有int类型。因此,'ab'有类型int,而'a'有类型char


是的,“ C ++的设计和演变”说,重载的输入/输出例程是C ++更改规则的主要原因。
Max Lybbert 09年

5
麦克斯,是的,我被骗了。我在兼容性部分的标准中进行了查找:)
Johannes Schaub-litb

18

在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位,就像您怀疑的那样,但是字符文字是一个整数。


7
+1表示有趣。人们通常认为sizeof(“ a”)和sizeof(“”)是char *的值,应给出4(或8)。但是实际上它们是char []的所在(sizeof(char [11])给出11)。新手陷阱。
paxdiablo

3
字符文字不提升为整数,它已经是整数。如果对象是sizeof运算符的操作数,则不会进行任何升级。如果有的话,这将破坏sizeof的目的。
克里斯·杨

@克里斯·杨:是的。检查一下 谢谢。
dmckee ---前主持人小猫,

8

回到编写C时,PDP-11的MACRO-11汇编语言具有:

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

这种事情在汇编语言中很常见-低8位将保存字符代码,其他位清除为0。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-25页的6.3.3 )


5

我记得读过K&R并看到一个代码片段,该片段将一次读取一个字符,直到达到EOF。由于所有字符都是文件/输入流中的有效字符,因此这意味着EOF不能为任何char值。代码要做的是将读取的字符放入一个int,然后测试EOF,然后如果不是,则转换为char。

我意识到这并不能完全回答您的问题,但是如果EOF字面量是,那么其余的字符字面量就是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

3
@gbjbaanb:当然可以。这是空字符。想一想。您是否认为文件不应包含任何零字节?
P爸爸2009年

1
阅读Wikipedia-“ EOF的实际值是一个与系统有关的负数,通常为-1,保证与任何有效的字符代码均不相等。”
Malx

2
正如Malx所说-EOF不是char类型-它是int类型。getchar()和朋友返回一个int,该int可以容纳任何char和EOF且不会发生冲突。这实际上并不需要文字字符具有int类型。
迈克尔·伯

2
EOF == -1早于C的字符常量,因此这不是答案,甚至都不相关。
Jim Balter

5

我还没有看到它的基本原理(C char文字是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

2

历史原因是C和它的前身B最初是在具有各种字长的DEC PDP微型计算机的各种模型上开发的,这些微型计算机支持8位ASCII,但只能对寄存器执行算术运算。(但是,不是PDP-11;稍后出现。)C的早期版本定义int为机器的本机字大小,小于该值的任何值都int需要扩展为int与函数之间的传递。或用于按位,逻辑或算术表达式,因为这是基础硬件的工作方式。

这就是为什么整数提升规则仍说任何小于的数据类型int都提升为的原因int。由于类似的历史原因,C实现也可以使用补码数学而不是补码。与十六进制相比,八进制字符转义符和八进制常量是一等公民的原因同样是那些早期的DEC小型计算机的字长可分为三字节的块,但不能分为四字节的半字节。


...并且char正好是3个八进制数字长
Antti Haapala

1

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

编辑:可以肯定的是,我检查了我的Expert C编程:Deep Secrets副本,并确认char文字不是int类型开头。它是最初类型的,但是当它在使用表达式,它被提升到一个INT。这本书引用了以下内容:

字符文字的类型为int,它们遵循char类型的提升规则到达那里。第39页的K&R 1太简单地介绍了这一点,其中指出:

表达式中的每个char都将转换为int ....注意,表达式中的所有float都将转换为double...。由于函数参数是表达式,因此将参数传递给函数时也会进行类型转换:特别是,char和short变为int,float变为double。


如果可以相信其他注释,则表达式'a' int类型开始 -在sizeof()内不执行任何类型提升。看起来“ a”具有int类型只是C的一个怪癖。
j_random_hacker

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

0

我不知道,但是我猜想以这种方式实现它会更容易,而且并不重要。直到C ++,类型可以确定调用哪个函数时,才需要对其进行修复。


0

我确实不知道这一点。在原型存在之前,将任何比int窄的内容都用作函数参数时会转换为int。这可能是解释的一部分。


1
另一个糟糕的“答案”。自动转换char,以int将使它很不必要的字符常量是整数。与此相关的是,该语言对待字符常量char和变量的方式有所不同(通过给它们提供不同的类型),而需要的是对此差异的解释。
Jim Balter

感谢您在下面给出的解释。您可能想要在答案中更全面地描述您的解释,该解释属于该问题,可以投票通过并且容易被访问者看到。另外,我从没说过我在这里有很好的答案。因此,您的价值判断无济于事。
Blaisorblade 2011年

0

这仅与语言规范相切,但在硬件中,CPU通常只有一个寄存器大小-可以说是32位-因此,每当它在char上实际工作时(通过加,减或比较它)当将其装入寄存器时,将其隐式转换为int。编译器会在每次操作后适当地掩盖和移位数字,以便您在2(无符号字符)254上加上2,它将环绕到0而不是256,但是在硅芯片内部,它实际上是一个int直到将其保存回内存。

这有点学术性,因为该语言无论如何都可以指定8位文字类型,但是在这种情况下,语言规范恰好更紧密地反映了CPU的实际功能。

(x86眨眼可能会注意到,例如有一个本机addh op一步添加了短宽度寄存器,但是在RISC内核内部,这转化为两个步骤:添加数字,然后扩展符号,例如对上的add / extsh对。 PowerPC)


1
另一个错误的答案。这里的问题是为什么字符文字和char变量具有不同的类型。反映硬件的自动提升无关紧要-它们实际上是反相关的,因为char变量是自动提升的,因此没有理由不使用字符文字char。真正的原因是现在已经过时的多字节文字。
Jim Balter

@Jim Balter多字节文字根本不会过时;有多字节的Unicode和UTF字符。
Crashworks

@Crashworks我们正在谈论多字节字符文字,而不是多字节字符串文字。一定要注意。
Jim Balter

4
Chrashworks确实写过角色。您应该已经写出字符文字(例如L'à')确实占用更多字节,但不称为多字节char文字。减少傲慢自大将有助于您提高自己的准确性。
Blaisorblade 2011年

@Blaisorblade宽字符文字在这里无关紧要-它们与我写的内容无关。我很准确,您缺乏理解力,您为纠正我的虚假尝试是自大的。
Jim Balter
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.