在 C++ 中,sizeof('a') == sizeof(char) == 1
. 这具有直观意义,因为'a'
它是字符文字,并且sizeof(char) == 1
由标准定义。
然而,在 C 中,sizeof('a') == sizeof(int)
. 也就是说,看起来 C 字符文字实际上是整数。有谁知道为什么?我可以找到很多关于这个 C 怪癖的提及,但没有解释它为什么存在。
同一主题的讨论
“更具体地说,积分提升。在 K&R C 中,几乎(?)不可能使用字符值而不首先将其提升为 int,因此首先使字符常量 int 消除了该步骤。过去存在并且仍然存在多字符诸如 'abcd' 之类的常量,或者很多都适合 int。”
最初的问题是“为什么?”
原因是文字字符的定义已经演变和改变,同时试图保持与现有代码的向后兼容。
在早期 C 的黑暗日子里,根本没有类型。当我第一次学习用 C 编程时,已经引入了类型,但是函数没有原型来告诉调用者参数类型是什么。相反,作为参数传递的所有内容都是标准化的,要么是 int 的大小(包括所有指针),要么是 double。
这意味着当您编写函数时,所有不是双精度的参数都以整数形式存储在堆栈中,无论您如何声明它们,编译器都会将代码放入函数中为您处理。
这使得事情有些不一致,所以当 K&R 写他们著名的书时,他们制定了这样的规则:在任何表达式中,字符文字总是会被提升为 int,而不仅仅是函数参数。
当 ANSI 委员会首次对 C 进行标准化时,他们更改了此规则,以便字符文字只是一个 int,因为这似乎是实现相同目标的更简单方法。
在设计 C++ 时,要求所有函数都有完整的原型(这在 C 中仍然不需要,尽管它被普遍接受为良好实践)。因此,决定字符文字可以存储在 char 中。在 C++ 中这样做的好处是带有 char 参数的函数和带有 int 参数的函数具有不同的签名。这个优势在 C 中是没有的。
这就是它们不同的原因。进化...
我不知道 C 中的字符文字是 int 类型的具体原因。但在 C++ 中,有充分的理由不这样做。考虑一下:
void print(int);
void print(char);
print('a');
您会期望 print 调用选择采用 char 的第二个版本。将字符文字作为 int 会使这成为不可能。请注意,在 C++ 中,具有多个字符的文字仍然具有 int 类型,尽管它们的值是实现定义的。所以,'ab'
有类型int
,而'a'
有类型char
。
在我的 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。
早在编写 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 )
我记得阅读 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;
}
我还没有看到它的基本原理(C char 文字是 int 类型),但这是 Stroustrup 不得不说的(来自 Design and Evolution 11.2.1 - Fine-Grain Resolution):
在 C 中,字符文字的类型,例如
'a'
isint
。令人惊讶的是,在 C++ 中给出'a'
类型char
不会导致任何兼容性问题。除了病态的例子sizeof('a')
,可以用 C 和 C++ 表达的每个构造都给出相同的结果。
所以在大多数情况下,它应该不会引起任何问题。
其历史原因在于,C 及其前身 B 最初是在各种型号的 DEC PDP 小型机上开发的,具有各种字长,支持 8 位 ASCII 但只能对寄存器进行算术运算。(但不是 PDP-11;那是后来出现的。)C 的早期版本被定义int
为机器的本机字长,任何小于 an 的值int
都需要扩大到int
才能传递给函数或从函数传递,或用于按位、逻辑或算术表达式,因为这就是底层硬件的工作方式。
这也是为什么整数提升规则仍然说任何小于 an 的数据类型int
都被提升为int
. 出于类似的历史原因,C 实现也允许使用补码数学而不是二进制补码。与十六进制相比,八进制字符转义和八进制常量是一等公民的原因同样是那些早期的 DEC 小型计算机的字长可分为三字节块,但不能分为四字节半字节。
这是正确的行为,称为“整体提升”。它也可能发生在其他情况下(如果我没记错的话,主要是二元运算符)。
编辑:可以肯定的是,我检查了我的Expert C Programming: Deep Secrets副本,并确认 char 文字不以int类型开头。它最初是char类型,但在表达式中使用时,它被提升为int。以下内容摘自书中:
字符文字具有 int 类型,它们通过遵循从 char 类型提升的规则到达那里。这在 K&R 1 的第 39 页上被简单地介绍过,它说:
表达式中的每个 char 都转换为 int....请注意,表达式中的所有 float 都转换为 double....由于函数参数是表达式,因此在将参数传递给函数时也会发生类型转换:特别是 char 和 short 变成 int,float 变成 double。
我不知道,但我猜想以这种方式实现它更容易,这并不重要。直到 C++ 类型可以确定哪个函数将被调用时,它才需要修复。
我确实不知道这一点。在原型存在之前,任何比 int 更窄的东西在用作函数参数时都会被转换为 int。这可能是解释的一部分。
这仅与语言规范相切,但在硬件中,CPU 通常只有一个寄存器大小——比方说 32 位——因此,只要它实际上在一个 char 上工作(通过加、减或比较它),就会有将 int 加载到寄存器中时隐式转换为 int。编译器会在每次操作后正确屏蔽和移动数字,这样如果您将 2 添加到 (unsigned char) 254,它将环绕到 0 而不是 256,但在硅内部它实际上是一个 int直到您将其保存回内存。
这是一种学术观点,因为该语言无论如何都可以指定一个 8 位文字类型,但在这种情况下,语言规范恰好更准确地反映了 CPU 实际在做什么。
(x86 专家可能会注意到,例如,有一个本地 addh 操作可以一步添加短宽寄存器,但在 RISC 内核内部,这转换为两个步骤:添加数字,然后扩展符号,就像一个 add/extsh 对电源PC)