C中的变量类型以及跟踪它的人

问题描述 投票:4回答:5

我正在参加哈佛大学的MOOC课程CS50。在最初的一个讲座中,我们了解了不同数据类型的变量:intchar等。

我所理解的是命令(例如,在main函数中)int a = 5保留了堆栈中内存的大量字节(大部分为4)并且放置了一系列零和一些代表5的内存。

零和一个相同的序列也可能意味着某个特征。所以有人需要跟踪这样一个事实,即为a保留的内存位置中的0和1序列将被读作整数(而不是字符)。

问题是谁跟踪它?计算机的内存是通过在内存中将标签粘贴到这个地方说“嘿,无论你在这4个字节中找到什么,都读成一个整数”?或者是C编译器,它知道(查看int的类型a)当我的代码要求它做某事(更准确地说,生成机器代码做某事)时使用a的值,它需要将此值视为整数?

我真的很感激为C初学者量身打造的答案。

c cs50
5个回答
3
投票

使用C语言,它是编译器。

在运行时,堆栈上只有32位= 4个字节。

你通过在这个地方贴一个标签来问“计算机的记忆......”:这是不可能的(使用当前的计算机架构 - 感谢来自@Ivan的暗示)。内存本身只有8位(0或1)字节。内存中没有可以用任何附加信息标记存储单元的地方。

还有其他语言(例如LISP,在某种程度上也包括Java和C#),它存储一个整数,作为数字的32位的组合加上包含一些位编码标记的几个位或字节,这里我们有一个整数。所以他们需要例如32位整数的6个字节。但是对于C,事实并非如此。您需要源代码中的知识才能正确解释内存中的位 - 它们不会解释自己。并且有一些特殊的架构支持硬件中的标记。


2
投票

在C中,内存是无类型的;没有超出其价值的信息存储在那里。所有类型信息都是在编译时根据表达式的类型(变量名,值计算,指针解除引用等)计算的。此计算取决于程序员通过声明(也在标题中)或强制转换提供的信息。如果该信息有误,例如因为函数原型的参数声明错误,所有的赌注都是关闭的。编译器警告或防止同一“翻译单元”(带有标题的文件)中的错误声明,但在翻译单元之间没有(或没有多少?)保护。这就是为什么C有标题的原因之一:它们在翻译单元之间共享公共类型信息。

C ++保留了这个想法,但另外提供了多态类型的运行时类型信息(而不是编译时类型信息)。很明显,每个多态对象都必须在某处携带额外的信息(尽管不一定接近数据)。但那是C ++,而不是C.


2
投票

对于主要部分,它是跟踪的C编译器。

在编译过程中,编译器会构建一个称为解析树的大型数据结构。它还跟踪所有变量,函数,类型,...具有名称(即标识符)的所有内容;这称为符号表。

解析树和符号表的节点都有一个记录类型的条目。他们跟踪所有类型。

主要使用这两个数据结构,编译器可以检查您的代码是否违反了类型规则。如果使用不兼容的值或变量名,它允许编译器发出警告。

C确实允许类型之间的隐式对话。例如,您可以将int指定给double。但在内存中,这些是相同值的完全不同的位模式。

在编译过程的早期(更高抽象级别)阶段,编译器尚未(或过多)处理位模式,并在更高级别进行转换和检查。

但是在汇编代码生成过程中,编译器需要最终以位为单位进行全部计算。所以对于intdouble的转换:

int    i = 5;
double d = i; // Conversion.

编译器将生成代码以进行此转换。

然而,在C中,很容易犯错误并搞砸了。这是因为C不是一种非常强类型的语言,而且非常灵活。所以程序员也需要注意。

因为C在编译之后不再跟踪类型,所以当程序运行时,程序通常可以在执行一些错误之后以错误的数据继续运行。如果你“幸运”程序崩溃,那么错误信息就不是(非常)提供信息。


1
投票

你有一个堆栈指针,它给出了内存中最顶层堆栈帧的绝对偏移量。

对于给定的执行范围,编译器知道哪个变量相对于此堆栈指针定位,并在对堆栈指针的偏移量上发出对这些变量的访问。所以它主要是编译器映射变量,但它是应用此映射的处理器。

您可以轻松编写计算或记住曾经有效的存储器地址的程序,或者只是在有效区域之外的程序。编译器不会阻止您这样做,只有具有引用计数和严格边界检查的更高级语言在运行时才会执行。


0
投票

编译器在转换过程中跟踪所有类型信息,并生成适当的机器代码以处理不同类型或大小的数据。

我们来看下面的代码:

#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运行后,赋值,加法和打印语句被转换为:

    movq    $5, -24(%rbp)
    movq    $6, -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    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    ret

movq是将值转换为64位字(“四字”)的助记符。 %rax是一个通用的64位寄存器,用作累加器。暂时不要过分担心其余部分。

现在让我们看看当我们将longs更改为shorts时会发生什么:

#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    $5, -6(%rbp)
    movw    $6, -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    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    ret

不同的助记符 - 而不是movq我们得到movwmovswl,我们使用%eax,这是%rax的低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 $4617315517961601024, %rax
    movq    %rax, -24(%rbp)
    movabsq $4618441417868443648, %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    $3, %eax
    call    printf
    movl    $0, %eax
    leave
    ret

新助记符(movsd),新寄存器(%xmm0)。

所以基本上,在翻译之后,不需要用类型信息标记数据;该类型信息被“烘焙”到机器代码本身。

© www.soinside.com 2019 - 2024. All rights reserved.