109

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

然而,在 C 中,sizeof('a') == sizeof(int). 也就是说,看起来 C 字符文字实际上是整数。有谁知道为什么?我可以找到很多关于这个 C 怪癖的提及,但没有解释它为什么存在。

4

12 回答 12

39

同一主题的讨论

“更具体地说,积分提升。在 K&R C 中,几乎(?)不可能使用字符值而不首先将其提升为 int,因此首先使字符常量 int 消除了该步骤。过去存在并且仍然存在多字符诸如 'abcd' 之类的常量,或者很多都适合 int。”

于 2009-01-11T23:21:04.487 回答
31

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

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

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

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

这使得事情有些不一致,所以当 K&R 写他们著名的书时,他们制定了这样的规则:在任何表达式中,字符文字总是会被提升为 int,而不仅仅是函数参数。

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

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

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

于 2014-04-23T16:04:22.050 回答
23

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

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

print('a');

您会期望 print 调用选择采用 char 的第二个版本。将字符文字作为 int 会使这成为不可能。请注意,在 C++ 中,具有多个字符的文字仍然具有 int 类型,尽管它们的值是实现定义的。所以,'ab'有类型int,而'a'有类型char

于 2009-01-11T23:26:28.110 回答
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。

于 2009-01-11T23:08:41.360 回答
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 )

于 2011-03-29T06:26:51.917 回答
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;
}
于 2009-01-11T22:51:05.763 回答
5

我还没有看到它的基本原理(C char 文字是 int 类型),但这是 Stroustrup 不得不说的(来自 Design and Evolution 11.2.1 - Fine-Grain Resolution):

在 C 中,字符文字的类型,例如'a'is int。令人惊讶的是,在 C++ 中给出'a'类型char不会导致任何兼容性问题。除了病态的例子sizeof('a'),可以用 C 和 C++ 表达的每个构造都给出相同的结果。

所以在大多数情况下,它应该不会引起任何问题。

于 2009-01-11T23:53:57.650 回答
2

其历史原因在于,C 及其前身 B 最初是在各种型号的 DEC PDP 小型机上开发的,具有各种字长,支持 8 位 ASCII 但只能对寄存器进行算术运算。(但不是 PDP-11;那是后来出现的。)C 的早期版本被定义int为机器的本机字长,任何小于 an 的值int都需要扩大到int才能传递给函数或从函数传递,或用于按位、逻辑或算术表达式,因为这就是底层硬件的工作方式。

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

于 2018-07-10T05:14:55.427 回答
1

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

编辑:可以肯定的是,我检查了我的Expert C Programming: Deep Secrets副本,并确认 char 文字不以int类型开头。它最初是char类型,但在表达式中使用时,它被提升为int。以下内容摘自书中:

字符文字具有 int 类型,它们通过遵循从 char 类型提升的规则到达那里。这在 K&R 1 的第 39 页上被简单地介绍过,它说:

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

于 2009-01-11T23:45:54.997 回答
0

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

于 2009-01-11T22:59:24.640 回答
0

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

于 2009-01-11T23:07:13.140 回答
0

这仅与语言规范相切,但在硬件中,CPU 通常只有一个寄存器大小——比方说 32 位——因此,只要它实际上在一个 char 上工作(通过加、减或比较它),就会有将 int 加载到寄存器中时隐式转换为 int。编译器会在每次操作后正确屏蔽和移动数字,这样如果您将 2 添加到 (unsigned char) 254,它将环绕到 0 而不是 256,但在硅内部它实际上是一个 int直到您将其保存回内存。

这是一种学术观点,因为该语言无论如何都可以指定一个 8 位文字类型,但在这种情况下,语言规范恰好更准确地反映了 CPU 实际在做什么。

(x86 专家可能会注意到,例如,有一个本地 addh 操作可以一步添加短宽寄存器,但在 RISC 内核内部,这转换为两个步骤:添加数字,然后扩展符号,就像一个 add/extsh 对电源PC)

于 2009-01-11T23:54:30.283 回答