【问题标题】:Variable types in C and who keeps track of itC 中的变量类型以及跟踪它的人
【发布时间】:2017-12-24 11:02:01
【问题描述】:

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

我的理解是该命令(例如,在 main 函数中)int a = 5 在堆栈上保留了一些字节(大部分为 4)的内存,并在那里放置了一系列代表 @ 的 0 和 1 987654327@。

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

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

我非常感谢为 C 初学者量身定制的答案。

【问题讨论】:

  • 在写这个问题并思考如何设计计算机时,我得出的结论是,C 编译器肯定需要跟踪如何解释内存块的内容。但我仍然需要得到肯定知道的人的确认。
  • 没有人跟踪它。编译器通过进行类型检查来帮助您,但是通过一些努力,可以以您喜欢的任何方式解释内存单元的内容。因此,程序员有责任选择正确的解释。大多数情况下,预期的解释与程序中声明的数据类型匹配。
  • 您(人类程序员)负责跟踪和处理变量。在运行时,变量不再存在,进程有内存位置(其中一些对应于源代码中的变量)。
  • 计算机的tagged memory 的内存单元将类型与值一起存储。

标签: c cs50


【解决方案1】:

对于 C 语言,它是编译器。

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

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

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

【讨论】:

  • “那是不可能的”——也就是说,在过去存在带有类型或“标记”内存的计算机。请参阅Tagged_architecture 在这些架构中,处理器可以检查例如用于条件跳转的值是否实际上是布尔值。
  • @Ivan 是的,我在 1990 年代一直在使用 Symbolics Lisp Machine,具有这样的标记架构,但所有当前的内存子系统都将内存组织为固定大小单词的同质数组(通常字节)。我将编辑我的答案。
【解决方案2】:

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

C++ 保留了这一理念,但还为多态类型提供了运行时类型信息(与编译时 类型信息相反)。很明显,每个多态对象都必须在某处携带额外的信息(但不一定靠近数据)。但那是 C++,不是 C。

【讨论】:

    【解决方案3】:

    主要是 C 编译器负责跟踪。

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

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

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

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

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

    但是在汇编代码生成过程中,编译器需要最终将其全部弄清楚。所以对于intdouble 的转换:

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

    编译器会生成代码来实现这种转换。

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

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

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

      • RE “对于给定的执行范围,编译器知道哪个变量位于相对于该堆栈指针的位置”:对于来自不同翻译单元的函数参数来说,情况并非如此;-)。编译器完全取决于你告诉它的内容。 通常该信息是通过调用者和被调用者的共同标头来传达的,该标头描述了参数,但这只是一个约定。 C++ 编译器对函数名称中的参数进行编码以便于函数重载,因此您将创建未定义的符号。但在 C 中,函数名不包含任何信息。
      【解决方案5】:

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

      让我们看下面的代码:

      #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
      

      不同的助记符 - 我们使用的是 %eax,而不是 movq,而不是 movwmovswl,这是 %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)。

      所以基本上翻译后的数据就不需要再打上类型信息了;该类型信息被“嵌入”到机器代码本身中。

      【讨论】:

        猜你喜欢
        • 2017-12-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-07-20
        • 2016-02-19
        相关资源
        最近更新 更多