C 中的变量类型以及谁跟踪它
Variable types in C and who keeps track of it
我正在上哈佛的 MOOC 课程 CS50。在第一堂课中,我们学习了不同数据类型的变量:int
、char
等
我的理解是该命令(例如,在 main
函数中)int a = 5
在堆栈上保留了一些字节(大部分为 4 个)的内存,并在其中放置了一系列代表 5
的零和一。
相同的零和一序列也可能表示某个字符。所以有人需要跟踪这样一个事实,即为 a
保留的内存位置中的零和一的序列将被读取为整数(而不是字符)。
问题是谁跟踪它?计算机的内存通过在内存中的这个地方贴上一个标签说"hey, whatever you find in these 4 bytes read as an integer"?或者 C 编译器,它知道(查看 a
的 int
类型)当我的代码要求它做某事时(更准确地说,生成一个机器代码做某事),值为 a
需要把这个值当成整数?
非常感谢为 C 初学者量身定制的答案。
对于主要部分,它是跟踪的 C 编译器。
在编译过程中,编译器构建了一个称为解析树的大型数据结构。它还跟踪所有变量、函数、类型……所有有名称(即标识符)的东西;这称为符号 table.
解析树和符号table的节点都有一个记录类型的条目。他们跟踪所有类型。
主要有这两个数据结构在手,编译器可以检查你的代码是否不违反类型规则。如果您使用不兼容的值或变量名,它允许编译器警告您。
C 确实允许类型之间的隐式对话。例如,您可以将 int
分配给 double
。但是在内存中,对于相同的值,这些是完全不同的位模式。
在编译过程的早期(更高抽象级别)阶段,编译器还不处理位模式(或太多),并在更高级别进行转换和检查。
但是在汇编代码生成过程中,编译器需要最终把它全部计算出来。所以对于 int
到 double
的转换:
int i = 5;
double d = i; // Conversion.
编译器将生成代码来实现这种转换。
但是在 C 中很容易出错和搞砸。这是因为 C 不是一种非常强类型的语言,而且相当灵活。所以程序员也需要注意。
因为 C 在编译后不再跟踪类型,所以当程序 运行 时,程序通常可以在执行您的一些错误后默默地继续 运行 错误的数据。而且,如果您 'lucky' 认为程序崩溃了,那么您的错误消息并没有(非常)有用。
你有一个堆栈指针,它给出了内存中最顶层堆栈帧的绝对偏移量。
对于给定的执行范围,编译器知道哪个变量相对于此堆栈指针,并发出对这些变量的访问作为堆栈指针的偏移量。因此,主要是编译器映射变量,但应用此映射的是处理器。
您可以轻松编写程序来计算或记住曾经有效或刚好在有效区域之外的内存地址。编译器不会阻止你这样做,只有具有引用计数和严格边界检查的高级语言在运行时才会这样做。
对于C语言来说,就是编译器
在 运行 时,堆栈上只有 32 位 = 4 个字节。
你问 "The computer's memory by sticking a tag to this place...":那是不可能的(对于当前的计算机体系结构 - 感谢@Ivan 的提示)。内存本身只有 8 位(0 或 1)字节。内存中没有任何地方可以用任何附加信息标记内存单元。
还有其他语言(例如 LISP,在某种程度上还有 Java 和 C#)将整数存储为数字的 32 位加上一些位或包含一些位的字节的组合-编码标记,这里我们有一个整数。所以他们需要例如6 个字节用于 32 位整数。但是对于 C,情况并非如此。您需要源代码中的知识才能正确解释内存中的位——它们不会自我解释。并且已经有支持硬件标记的特殊架构。
在C中,内存是无类型的;那里没有存储超出其价值的信息。所有类型信息都是在编译时根据表达式的类型(变量名、值计算、指针解引用等)计算的。此计算取决于程序员通过声明(也在 headers 中)或提供的信息演员表。如果该信息有误,例如因为函数原型的参数声明错误,所有的赌注都被取消了。编译器在同一个 "translation unit"(带有 headers 的文件)中警告或阻止 mis-declarations,但在翻译单元之间没有(或没有很多?)保护。这就是 C 具有 headers 的原因之一:它们在翻译单元之间共享公共类型信息。
C++ 保留了这个想法,但额外提供了 运行 时间类型信息(相对于 编译时间 类型信息)多态类型。很明显,每个多态 object 都必须在某处携带额外的信息(虽然不一定靠近数据)。但那是 C++,不是 C。
编译器在翻译过程中会跟踪所有类型信息,并会生成适当的机器代码来处理不同类型或大小的数据。
让我们看下面的代码:
#include <stdio.h>
int main( void )
{
long long x, y, z;
x = 5;
y = 6;
z = x + y;
printf( "x = %ld, y = %ld, z = %ld\n", x, y, z );
return 0;
}
在通过 gcc -S 运行ning 之后,赋值、加法和打印语句被翻译成:
movq , -24(%rbp)
movq , -16(%rbp)
movq -16(%rbp), %rax
addq -24(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rcx
movq -16(%rbp), %rdx
movq -24(%rbp), %rsi
movl $.LC0, %edi
movl [=11=], %eax
call printf
movl [=11=], %eax
leave
ret
movq
是将值移动到 64 位字 ("quadwords") 的助记符。 %rax
是用作累加器的通用 64 位寄存器。现在不要太担心剩下的事情。
现在让我们看看当我们将 long
s 更改为 short
s 时会发生什么:
#include <stdio.h>
int main( void )
{
short x, y, z;
x = 5;
y = 6;
z = x + y;
printf( "x = %hd, y = %hd, z = %hd\n", x, y, z );
return 0;
}
同样,我们运行它通过gcc -S生成机器码,瞧瞧:
movw , -6(%rbp)
movw , -4(%rbp)
movzwl -6(%rbp), %edx
movzwl -4(%rbp), %eax
leal (%rdx,%rax), %eax
movw %ax, -2(%rbp)
movswl -2(%rbp),%ecx
movswl -4(%rbp),%edx
movswl -6(%rbp),%esi
movl $.LC0, %edi
movl [=13=], %eax
call printf
movl [=13=], %eax
leave
ret
不同的助记符 - 我们使用 movw
和 movswl
而不是 movq
,我们使用 %eax
,这是 [=17= 的低 32 位], 等等
再一次,这次是浮点类型:
#include <stdio.h>
int main( void )
{
double x, y, z;
x = 5;
y = 6;
z = x + y;
printf( "x = %f, y = %f, z = %f\n", x, y, z );
return 0;
}
gcc -S 再次:
movabsq 17315517961601024, %rax
movq %rax, -24(%rbp)
movabsq 18441417868443648, %rax
movq %rax, -16(%rbp)
movsd -24(%rbp), %xmm0
addsd -16(%rbp), %xmm0
movsd %xmm0, -8(%rbp)
movq -8(%rbp), %rax
movq -16(%rbp), %rdx
movq -24(%rbp), %rcx
movq %rax, -40(%rbp)
movsd -40(%rbp), %xmm2
movq %rdx, -40(%rbp)
movsd -40(%rbp), %xmm1
movq %rcx, -40(%rbp)
movsd -40(%rbp), %xmm0
movl $.LC2, %edi
movl , %eax
call printf
movl [=15=], %eax
leave
ret
新助记符(movsd
),新寄存器(%xmm0
)。
所以基本上,在翻译之后,不需要用类型信息来标记数据;该类型信息对机器代码本身是 "baked in" 。
我正在上哈佛的 MOOC 课程 CS50。在第一堂课中,我们学习了不同数据类型的变量:int
、char
等
我的理解是该命令(例如,在 main
函数中)int a = 5
在堆栈上保留了一些字节(大部分为 4 个)的内存,并在其中放置了一系列代表 5
的零和一。
相同的零和一序列也可能表示某个字符。所以有人需要跟踪这样一个事实,即为 a
保留的内存位置中的零和一的序列将被读取为整数(而不是字符)。
问题是谁跟踪它?计算机的内存通过在内存中的这个地方贴上一个标签说"hey, whatever you find in these 4 bytes read as an integer"?或者 C 编译器,它知道(查看 a
的 int
类型)当我的代码要求它做某事时(更准确地说,生成一个机器代码做某事),值为 a
需要把这个值当成整数?
非常感谢为 C 初学者量身定制的答案。
对于主要部分,它是跟踪的 C 编译器。
在编译过程中,编译器构建了一个称为解析树的大型数据结构。它还跟踪所有变量、函数、类型……所有有名称(即标识符)的东西;这称为符号 table.
解析树和符号table的节点都有一个记录类型的条目。他们跟踪所有类型。
主要有这两个数据结构在手,编译器可以检查你的代码是否不违反类型规则。如果您使用不兼容的值或变量名,它允许编译器警告您。
C 确实允许类型之间的隐式对话。例如,您可以将 int
分配给 double
。但是在内存中,对于相同的值,这些是完全不同的位模式。
在编译过程的早期(更高抽象级别)阶段,编译器还不处理位模式(或太多),并在更高级别进行转换和检查。
但是在汇编代码生成过程中,编译器需要最终把它全部计算出来。所以对于 int
到 double
的转换:
int i = 5;
double d = i; // Conversion.
编译器将生成代码来实现这种转换。
但是在 C 中很容易出错和搞砸。这是因为 C 不是一种非常强类型的语言,而且相当灵活。所以程序员也需要注意。
因为 C 在编译后不再跟踪类型,所以当程序 运行 时,程序通常可以在执行您的一些错误后默默地继续 运行 错误的数据。而且,如果您 'lucky' 认为程序崩溃了,那么您的错误消息并没有(非常)有用。
你有一个堆栈指针,它给出了内存中最顶层堆栈帧的绝对偏移量。
对于给定的执行范围,编译器知道哪个变量相对于此堆栈指针,并发出对这些变量的访问作为堆栈指针的偏移量。因此,主要是编译器映射变量,但应用此映射的是处理器。
您可以轻松编写程序来计算或记住曾经有效或刚好在有效区域之外的内存地址。编译器不会阻止你这样做,只有具有引用计数和严格边界检查的高级语言在运行时才会这样做。
对于C语言来说,就是编译器
在 运行 时,堆栈上只有 32 位 = 4 个字节。
你问 "The computer's memory by sticking a tag to this place...":那是不可能的(对于当前的计算机体系结构 - 感谢@Ivan 的提示)。内存本身只有 8 位(0 或 1)字节。内存中没有任何地方可以用任何附加信息标记内存单元。
还有其他语言(例如 LISP,在某种程度上还有 Java 和 C#)将整数存储为数字的 32 位加上一些位或包含一些位的字节的组合-编码标记,这里我们有一个整数。所以他们需要例如6 个字节用于 32 位整数。但是对于 C,情况并非如此。您需要源代码中的知识才能正确解释内存中的位——它们不会自我解释。并且已经有支持硬件标记的特殊架构。
在C中,内存是无类型的;那里没有存储超出其价值的信息。所有类型信息都是在编译时根据表达式的类型(变量名、值计算、指针解引用等)计算的。此计算取决于程序员通过声明(也在 headers 中)或提供的信息演员表。如果该信息有误,例如因为函数原型的参数声明错误,所有的赌注都被取消了。编译器在同一个 "translation unit"(带有 headers 的文件)中警告或阻止 mis-declarations,但在翻译单元之间没有(或没有很多?)保护。这就是 C 具有 headers 的原因之一:它们在翻译单元之间共享公共类型信息。
C++ 保留了这个想法,但额外提供了 运行 时间类型信息(相对于 编译时间 类型信息)多态类型。很明显,每个多态 object 都必须在某处携带额外的信息(虽然不一定靠近数据)。但那是 C++,不是 C。
编译器在翻译过程中会跟踪所有类型信息,并会生成适当的机器代码来处理不同类型或大小的数据。
让我们看下面的代码:
#include <stdio.h>
int main( void )
{
long long x, y, z;
x = 5;
y = 6;
z = x + y;
printf( "x = %ld, y = %ld, z = %ld\n", x, y, z );
return 0;
}
在通过 gcc -S 运行ning 之后,赋值、加法和打印语句被翻译成:
movq , -24(%rbp)
movq , -16(%rbp)
movq -16(%rbp), %rax
addq -24(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rcx
movq -16(%rbp), %rdx
movq -24(%rbp), %rsi
movl $.LC0, %edi
movl [=11=], %eax
call printf
movl [=11=], %eax
leave
ret
movq
是将值移动到 64 位字 ("quadwords") 的助记符。 %rax
是用作累加器的通用 64 位寄存器。现在不要太担心剩下的事情。
现在让我们看看当我们将 long
s 更改为 short
s 时会发生什么:
#include <stdio.h>
int main( void )
{
short x, y, z;
x = 5;
y = 6;
z = x + y;
printf( "x = %hd, y = %hd, z = %hd\n", x, y, z );
return 0;
}
同样,我们运行它通过gcc -S生成机器码,瞧瞧:
movw , -6(%rbp)
movw , -4(%rbp)
movzwl -6(%rbp), %edx
movzwl -4(%rbp), %eax
leal (%rdx,%rax), %eax
movw %ax, -2(%rbp)
movswl -2(%rbp),%ecx
movswl -4(%rbp),%edx
movswl -6(%rbp),%esi
movl $.LC0, %edi
movl [=13=], %eax
call printf
movl [=13=], %eax
leave
ret
不同的助记符 - 我们使用 movw
和 movswl
而不是 movq
,我们使用 %eax
,这是 [=17= 的低 32 位], 等等
再一次,这次是浮点类型:
#include <stdio.h>
int main( void )
{
double x, y, z;
x = 5;
y = 6;
z = x + y;
printf( "x = %f, y = %f, z = %f\n", x, y, z );
return 0;
}
gcc -S 再次:
movabsq 17315517961601024, %rax
movq %rax, -24(%rbp)
movabsq 18441417868443648, %rax
movq %rax, -16(%rbp)
movsd -24(%rbp), %xmm0
addsd -16(%rbp), %xmm0
movsd %xmm0, -8(%rbp)
movq -8(%rbp), %rax
movq -16(%rbp), %rdx
movq -24(%rbp), %rcx
movq %rax, -40(%rbp)
movsd -40(%rbp), %xmm2
movq %rdx, -40(%rbp)
movsd -40(%rbp), %xmm1
movq %rcx, -40(%rbp)
movsd -40(%rbp), %xmm0
movl $.LC2, %edi
movl , %eax
call printf
movl [=15=], %eax
leave
ret
新助记符(movsd
),新寄存器(%xmm0
)。
所以基本上,在翻译之后,不需要用类型信息来标记数据;该类型信息对机器代码本身是 "baked in" 。