执行C程序时,是否将诸如“ int”和“ char”之类的数据类型声明符存储在RAM中?


74

当C程序运行时,数据存储在堆或堆栈中。这些值存储在RAM地址中。但是类型指示器(例如intchar)呢?他们也存储吗?

考虑以下代码:

char a = 'A';
int x = 4;

我读到A和4存储在这里的RAM地址中。但是,我们ax?最令人困惑的是,执行如何知道a是char和xint?我的意思是,int并且char在RAM中提到了?

假设某个值存储在RAM的某个位置,为10011001;如果我是执行代码的程序,我怎么知道此10011001是a char还是an int

我不明白的是,当计算机从诸如10001之类的地址中读取变量的值时,无论它是an int还是,计算机如何知道char。想象一下,我单击了一个名为的程序anyprog.exe。代码立即开始执行。该可执行文件是否包含有关存储的变量是类型int还是变量的信息char


24
这些信息在运行时会完全丢失。您(和您的编译器)必须事先确保可以正确解释内存。这是您所追求的答案吗?
5gon12eder 2015年

4
没有。因为它假定您知道自己在做什么,所以它将在您提供的内存地址中找到的所有内容都写入到stdout中。如果所写的任何内容均与可读字符相对应,它将最终以可读字符形式出现在某人的控制台上。如果不一致,它将显示为乱码,或可能是随机可读的字符。
罗伯特·哈维

22
@ user16307简短的答案是,在静态类型的语言中,每当您打印出char时,编译器将产生与打印出int时不同的代码。在运行时,不再有任何关于xchar的知识,而是要运行的char打印代码,因为这是编译器选择的。
Ixrec 2015年

13
@ user16307始终以数字65的二进制表示形式存储。它是以65还是A形式输出取决于您的编译器为打印出来的代码。65旁边没有元数据说它实际上是一个char或一个int(至少不是在像C这样的静态类型语言中)。
Ixrec 2015年

2
完全理解您在此处询问的概念并自己实现这些概念后,您可能需要上一门编译器课程,例如Coursera的
mucaho 2015年

Answers:


122

为了解决您在多个评论中发布的问题(我认为您应该将其编辑到您的帖子中):

我不了解的是,当计算机从int或char类型的地址(例如10001)中读取变量的值时,计算机如何知道允许。想象一下,我单击了一个名为anyprog.exe的程序。代码立即开始执行。此exe文件是否包含有关变量是否存储为in或char的信息?

因此,让我们添加一些代码。假设您写:

int x = 4;

并假设它存储在RAM中:

0x00010004: 0x00000004

第一部分是地址,第二部分是值。当您的程序(作为机器代码执行)运行时,它看到的0x00010004只是value 0x000000004。它不“知道”该数据的类型,也不知道如何“假定”使用它。

那么,您的程序如何找出正确的方法呢?考虑以下代码:

int x = 4;
x = x + 5;

我们在这里有读和写。当您的程序x从内存中读取时,它将在其中找到0x00000004。并且您的程序知道要添加0x00000005到它。您的程序“知道”这是有效操作的原因是因为编译器通过类型安全性确保该操作有效。你的编译器已经证实,您可以添加45在一起。因此,当您的二进制代码运行(exe)时,无需进行验证。假设一切正常,它只是盲目地执行每个步骤(坏事情实际上是不好的,会发生)。

另一种思考的方式是这样的。我给你这些信息:

0x00000004: 0x12345678

与以前相同的格式-左边的地址,右边的值。值是什么类型?至此,您知道的有关该值的信息与计算机执行代码时所获得的信息一样多。如果我告诉您在该值上加上12743,则可以这样做。您不知道该操作将对整个系统产生什么影响,但是您确实擅长将两个数字相加,因此您可以做到。这会使价值成为事实int吗?不一定-您看到的只是两个32位值和加法运算符。

也许有些困惑然后使数据被撤回。如果我们有:

char A = 'a';

计算机如何知道要a在控制台中显示?好吧,这有很多步骤。第一种是转到A内存中的s位置并读取它:

0x00000004: 0x00000061

aASCII 的十六进制值为0x61,因此上面的内容可能会在内存中显示。因此,现在我们的机器代码知道整数值。如何知道将整数值转换为字符以显示它?简而言之,编译器确保完成所有必要步骤以进行过渡。但是您的计算机本身(或程序/ exe)不知道该数据的类型。该32位值可以是- intchara的一半double,指针,数组的一部分,a的string一部分,指令的一部分等。


这是您的程序(exe)与计算机/操作系统可能进行的简短交互。

程序:我要启动。我需要20 MB的内存。

操作系统:查找20个未使用的可用MB内存并将其移交给

(重要的是,这可以返回任何 20 MB的可用内存,它们甚至不必是连续的。这时,程序现在可以在其拥有的内存中运行,而无需与OS进行对话。)

程序:我将假设内存中的第一个位置是32位整数变量x

(编译器确保对其他变量的访问不会触及内存中的该位置。系统上没有任何内容表明第一个字节是variable x,或者该变量x是整数。类推:您有一个袋子。您告诉人们您只会在袋子里放黄色的球。后来有人从袋子里拿出东西时,他们会掏出蓝色或立方体的东西,这真是令人震惊,这真是太糟糕了。计算机也是如此:您的程序现在假设第一个内存点是变量x,并且它是一个整数。如果在该内存字节上写了其他东西,或者假定是其他东西–发生了可怕的事情。不会发生)

程序:我现在将写入2假定的前四个字节x

程序:我想加5 x

  • 将X的值读入临时寄存器

  • 将5加到临时寄存器

  • 将临时寄存器的值存储回第一个字节,仍然假定为x

程序:我要假设下一个可用字节是char变量y

程序:我将写a变量y

  • 库用于查找以下内容的字节值 a

  • 该字节被写入程序假定的地址y

程序:我要显示的内容 y

  • 读取第二个存储点中的值

  • 使用库将字节转换为字符

  • 使用图形库更改控制台屏幕(将像素从黑色设置为白色,滚动一行等)

(从这里开始)

您可能会挂在嘴上的是-当内存中的第一个位置不再存在时会发生什么x?还是第二个不再了y?当某人读xchary指针时会发生什么?简而言之,坏事发生了。其中一些具有明确定义的行为,而另一些具有未定义的行为。未定义的行为恰恰是-任何事情都可能发生,从一无所有到崩溃的程序或操作系统。甚至明确定义的行为也可能是恶意的。如果我可以更改x程序的指针,并让您的程序将其用作指针,那么我可以使您的程序开始执行程序-这正是黑客所做的。编译器可以帮助确保我们不将其int x用作string,以及类似性质的东西。机器代码本身不知道类型,它只会执行指令指示的操作。在运行时还会发现大量信息:程序允许使用哪些内存字节?是否x启动的第一个字节或12号?

但是您可以想象,实际上编写这样的程序有多可怕(而且您可以用汇编语言编写)。您从“声明”变量开始-告诉自己字节1是x,字节2是y,并且在编写每一行代码,加载和存储寄存器时,(作为人类)您必须记住哪个是x哪个,哪个是哪个。一是y,因为系统不知道。而且,您(作为人类)必须记住类型x和类型y,因为同样,系统不知道。


惊人的解释。只有您写的部分“如何知道将整数值转换为字符以显示它?简单地说,编译器确保输入所有必需的步骤来进行该转换。” 我还是迷雾的 假设CPU从RAM寄存器中提取了0x00000061。从这一点上来说,您是说还有其他说明(在exe文件中)可以使我们过渡到屏幕上看到的内容吗?
2015年

2
@ user16307是的,还有其他说明。您编写的每一行代码都有可能变成许多指令。这里有说明要使用什么字符的说明,有要修改哪些像素以及它们改变为哪种颜色的说明,等等。还有一些您实际上看不到的代码。例如,使用std :: cout表示您正在使用库。您写入控制台的代码可能只有一行,但是您调用的函数将是更多行,并且每一行都可以变成许多机器指令。
沙兹2015年

8
@ user16307 Otherwise how can console or text file outputs a character instead of int 因为存在不同的指令序列,用于以整数或字母数字字符的形式输出存储位置的内容。编译器确实了解变量类型,并在编译时选择了适当的指令序列,并将其记录在EXE中。
查尔斯·格兰特

2
我会为“字节码本身”找到一个不同的短语,因为字节码(或字节码)通常是指一种中间语言(如Java字节码或MSIL),它实际上可以存储此数据以供运行时使用。另外,还不清楚在那种情况下应该指代什么“字节码”。否则,很好的答案。
2015年

6
@ user16307尽量不要担心C ++和C#。这些人说的是远远超出您当前对计算机和编译器如何工作的了解。出于您要了解的目的,硬件对类型,char或int或其他内容一无所知。当您告诉编译器某个变量是int时,如果它是int,它将生成可执行代码来处理内存位置。内存位置本身不包含有关类型的信息。只是您的程序决定将其视为int。忘记所有其他有关运行时类型信息的信息。
安德烈斯·F

43

我认为您的主要问题似乎是:“如果类型在编译时被擦除而在运行时未保留,那么计算机如何知道执行将其解释为的int代码还是执行将其解释为的代码char? ”

答案是……计算机没有。但是,编译器确实知道,并且它将只是简单地将正确的代码放在二进制文件中。如果将变量键入为char,则编译器不会放置将其视为int的代码,而会将代码视为char

那里 原因在运行时保留类型:

  • 动态键入:在动态键入中,类型检查在运行时进行,因此,显然必须在运行时知道类型。但是C不是动态键入的,因此可以安全地删除这些类型。(不过请注意,这是一个非常不同的场景。动态类型和静态类型并不是真正的相同,并且在混合类型语言中,您仍然可以擦除静态类型,而仅保留动态类型。)
  • 动态多态性:如果您根据运行时类型执行不同的代码,则需要保持运行时类型不变。C没有动态多态性(实际上,它根本没有任何多态性,除了在某些特殊的硬编码情况下,例如+运算符),因此出于这个原因,它不需要运行时类型。但是,无论如何,运行时类型还是与静态类型有所不同,例如,在Java中,理论上您可以擦除静态类型,并且仍然保持运行时类型为多态。还要注意,如果您将类型查找代码进行分散化和专门化,然后将其放在对象(或类)中,则也不一定需要运行时类型,例如C ++ vtables。
  • 运行时反射:如果允许程序在运行时反映其类型,则显然需要在运行时保留类型。您可以使用Java轻松地看到它,它在运行时保留一阶类型,但是在编译时将类型参数擦除为泛型类型,因此您只能反映类型构造函数(“原始类型”),而不能反映类型参数。同样,C没有运行时反射,因此不需要在运行时保留类型。

在C中将类型保留在运行时的唯一原因是进行调试,但是,调试通常是在可用源代码的情况下完成的,然后您可以在源文件中简单地查找类型。

类型擦除是很正常的。这不会影响类型安全性:在编译时检查类型,一旦编译器确信程序是类型安全的,就不再需要类型(因此)。它不会影响静态多态性(也称为重载):一旦完成重载解析,并且编译器选择了正确的重载,就不再需要这些类型。类型也可以指导优化,但是,一旦优化器根据类型选择了优化,就不再需要它们了。

仅在要在运行时对类型进行操作时才需要在运行时保留类型。

Haskell是最严格,最严格,类型安全的静态类型语言之一,Haskell编译器通常会擦除所有类型。(我相信例外是类型类方法字典的传递。)


3
没有!为什么?该信息将需要什么?编译器输出用于将a读取char到已编译二进制文件中的代码。它不输出代码的int,它不输出代码为一个byte,它不输出到指针的代码,它简单地输出代码为char。没有基于类型的运行时决策。您不需要类型。这是完全不相关的。在编译时已经做出了所有相关决定。
约尔格W¯¯米塔格

2
没有。编译器只是将用于打印char的代码放入二进制文件中。期。编译器知道在该内存地址处有char,因此它将用于打印char的代码放在二进制文件中。如果由于某种奇怪的原因,该内存地址上的值恰好不是一个char,那么,一切都将变得一团糟。基本上,这就是整个安全漏洞利用方法的工作方式。
约尔格W¯¯米塔格

2
想想看:如果CPU以某种方式知道程序的数据类型,那么每当有人发明一种新类型的程序时,地球上的每个人都将不得不购买新的CPU。public class JoergsAwesomeNewType {};看到?我刚刚发明了一种新型!您需要购买一个新的CPU!
约尔格W¯¯米塔格

9
不,不是。编译器知道必须在二进制文件中放入什么代码。保持这些信息毫无意义。如果要打印int,则编译器将放置用于打印int的代码。如果要打印字符,则编译器将放置用于打印字符的代码。期。但这只是一种模式。用于打印char的代码将以某种方式来解释位模式,用于打印int的代码将以不同的方式来解释位,但是无法将作为int的位模式与用于是一个字符,它是一串位。
约尔格W¯¯米塔格

2
@ user16307:“ exe文件是否包含有关什么地址是什么类型的数据的信息?” 也许。如果使用调试数据进行编译,则调试数据将包含有关变量名称,地址和类型的信息。有时,调试数据会存储在.exe文件中(作为二进制流)。但这不是可执行代码的一部分,应用程序本身不使用它,仅由调试器使用。
Ben Voigt

12

计算机不“知道”什么是什么地址,而是“什么是什么”的知识被植入程序的指令中。

当您编写一个用于写入和读取char变量的C程序时,编译器会创建汇编代码,该汇编代码会将数据作为char写入某处,而其他地方的其他代码则将读取内存地址并将其解释为char。将这两个操作绑定在一起的唯一事情是该内存地址的位置。

当需要阅读时,指令不会说“看看那里有什么数据类型”,而只是说诸如“以浮点数加载该内存”之类的内容。如果要读取的地址已更改,或者某些内容用浮点数以外的内容覆盖了该内存,则CPU仍将很高兴地以浮点数的方式加载该内存,结果可能会发生各种奇怪的事情。

不好的类比时间:想象一个复杂的运输仓库,那里的仓库是内存,人们在捡东西是CPU。仓库“程序”的一部分将各种物品放在架子上。另一个程序可以从仓库中取出物品并将它们放入盒子。当它们被拉下时,不检查它们,它们只是进入垃圾箱。整个仓库的运作是通过一切事物同步进行的,正确的物品始终在正确的时间放置在正确的位置,否则所有事物都会崩溃,就像在实际程序中一样。


您将如何解释CPU是否在寄存器中找到0x00000061并获取它;并想象控制台程序应该将其输出为字符而不是int。您的意思是在该exe文件中有一些指令代码,这些指令代码知道0x00000061的地址是一个字符,并使用ASCII表转换为字符?
2015年

7
请注意,“所有崩溃”实际上是最好的情况。“奇怪的事情发生”是第二好的场景,“奇怪的事情发生”则更糟,最坏的情况是“背后发生的事情是有人故意操纵以他们想要的方式进行事情”又名安全漏洞利用。
约尔格W¯¯米塔格

@ user16307:程序中的代码将告诉计算机获取该地址,然后根据所使用的编码来显示该地址。不管该存储位置中的数据是ASCII字符还是完整的垃圾,计算机都不必担心。其他原因负责设置该内存地址以在其中包含预期值。我认为尝试一些汇编编程可能会有所帮助。
whatsisname 2015年

1
@JörgWMittag:的确如此。我以提及缓冲区溢出为例,但认为这样做只会使事情更加混乱。
whatsisname 2015年

@ user16307:将数据显示到屏幕上的是一个程序。在传统的unixen上,它是一个终端(模拟DEC VT100串行终端的软件-一种带有监视器和键盘的硬件设备,该监视器将键盘中显示的内容显示给监视器,并将键盘上键入的内容发送给调制解调器)。在DOS上是DOS(实际上是VGA卡的文本模式,但是请忽略它),在Windows上是command.com。您的程序不知道它实际上是在打印字符串,而是在打印一系列字节(数字)。
slebetman

8

没有。一旦将C编译为机器代码,机器就会看到一堆比特。这些位的解释方式取决于对它们执行的操作,而不是一些其他元数据。

您在源代码中输入的类型仅适用于编译器。它采用您所说的数据应该是哪种类型,并尽其最大努力确保数据仅以有意义的方式使用。编译器在检查源代码的逻辑后尽其所能后,便将其转换为机器代码,并丢弃类型数据,因为机器代码无法表示(至少在大多数机器上) 。


我不明白的是,当计算机从int或char类型的地址中读取变量的值(例如10001)时,计算机如何知道允许。想象一下,我单击了一个名为anyprog.exe的程序。代码立即开始执行。此exe文件是否包含有关变量是否存储为in或char的信息?–
user16307

@ user16307不,没有关于任何东西是int还是char的额外信息。假设没有其他人击败我,我将在后面添加一些示例内容。
8bittree

1
@ user16307:exe文件间接包含该信息。执行程序的处理器并不关心编写程序时使用的类型,但是可以从用于访问各个内存位置的指令中推导出很多信息。
Bart van Ingen Schenau 2015年

@ user16307实际上有一些额外的信息。exe文件知道整数是4个字节,因此当您将“ int a”写入“ int a”时,编译器将为a变量保留4个字节,因此可以计算a和其他变量的地址。
Esben Skov Pedersen

1
@ user16307 int a = 65char b = 'A'在编译代码之后没有实际的区别(除了类型的大小)。

6

大多数处理器为处理不同类型的数据提供了不同的指令,因此类型信息通常被“嵌入”到生成的机器代码中。无需存储其他类型的元数据。

一些具体的例子可能会有所帮助。以下机器代码是在运行SuSE Linux Enterprise Server(SLES)10的x86_64系统上使用gcc 4.1.2生成的。

假定以下源代码:

int main( void )
{
  int x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

这是与上面的源相对应的生成的汇编代码的内容(使用gcc -S),我添加了注释:

main:
.LFB2:
        pushq   %rbp               ;; save the current frame pointer value
.LCFI0:
        movq    %rsp, %rbp         ;; make the current stack pointer value the new frame pointer value
.LCFI1:                            
        movl    $1, -12(%rbp)      ;; x = 1
        movl    $2, -8(%rbp)       ;; y = 2
        movl    -8(%rbp), %eax     ;; copy the value of y to the eax register
        addl    -12(%rbp), %eax    ;; add the value of x to the eax register
        movl    %eax, -4(%rbp)     ;; copy the value in eax to z
        movl    $0, %eax           ;; eax gets the return value of the function
        leave                      ;; exit and restore the stack
        ret

接下来还有一些其他内容ret,但这与讨论无关。

%eax是32位通用数据寄存器。 %rsp是保留用于保存堆栈指针的64位寄存器,该指针包含压入堆栈的最后一件事的地址。 %rbp是保留用于保存帧指针的64位寄存器,其中包含当前堆栈帧的地址。输入函数时,会在堆栈上创建一个堆栈框架,并为该函数的参数和局部变量保留空间。通过使用帧指针的偏移量来访问参数和变量。在这种情况下,该变量的内存x位于存储在中的地址“下方”的12个字节%rbp

在上述代码中,我们使用指令将x(1 的整数值,存储在-12(%rbp))复制到寄存器%eax中,该movl指令用于将32位字从一个位置复制到另一位置。然后addl,我们调用,将y(存储在-8(%rbp))的整数值添加到中已有的值%eax。然后-4(%rbp),我们将结果保存到z

现在让我们对其进行更改,以便处理double值而不是int值:

int main( void )
{
  double x, y, z;

  x = 1;
  y = 2;

  z = x + y;

  return 0;
}

gcc -S再次运行给我们:

main:
.LFB2:
        pushq   %rbp                              
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movabsq $4607182418800017408, %rax ;; copy literal 64-bit floating-point representation of 1.00 to rax
        movq    %rax, -24(%rbp)            ;; save rax to x
        movabsq $4611686018427387904, %rax ;; copy literal 64-bit floating-point representation of 2.00 to rax
        movq    %rax, -16(%rbp)            ;; save rax to y
        movsd   -24(%rbp), %xmm0           ;; copy value of x to xmm0 register
        addsd   -16(%rbp), %xmm0           ;; add value of y to xmm0 register
        movsd   %xmm0, -8(%rbp)            ;; save result to z
        movl    $0, %eax                   ;; eax gets return value of function
        leave                              ;; exit and restore the stack
        ret

几个差异。代替movladdl,我们使用movsdaddsd(分配并添加双精度浮点数)。%eax我们使用代替存储临时值%xmm0

这就是我说类型已“嵌入”到机器代码中时的意思。编译器只是生成正确的机器代码来处理该特定类型。


4

从历史上看,C将内存视为由许多类型的带编号的插槽组成的组unsigned char(也称为“字节”,尽管不一定总是8位)。任何使用内存中存储内容的代码都需要知道信息存储在哪个插槽中,并知道应该使用那里的信息来进行处理[例如,将以地址123:456开始的四个字节解释为32位。浮点值”或“将最近计算出的数量的低16位存储到两个字节中,从地址345:678开始。”内存本身既不知道也不在乎内存插槽“ meant”中存储的值。如果代码尝试使用一种类型写入内存,然后再读取另一种类型,则写入时存储的位模式将根据第二种类型的规则进行解释,可能会导致任何后果。

例如,如果代码要存储0x12345678到32位unsigned int,然后尝试unsigned int从其地址和上面的地址中读取两个连续的16位值,则根据unsigned int存储在其中的哪一半,代码可以读取这些值0x1234和0x5678,或0x5678和0x1234。

但是,C99 Standard 不再要求内存就像一堆带编号的插槽,它们对位模式表示什么一无所知。允许编译器的行为就像内存插槽知道存储在其中的数据类型一样,并且仅允许使用任何其他类型写入的数据(而不是unsigned char使用unsigned char与写入类型相同的类型读取)与; 进一步允许编译器的行为好像存储器插槽具有权力和倾向来任意破坏任何试图以与那些规则相反的方式访问存储器的程序的行为。

鉴于:

unsigned int a = 0x12345678;
unsigned short p = (unsigned short *)&a;
printf("0x%04X",*p);

有些实现可能会打印0x1234,而其他实现可能会打印0x5678,但是根据C99标准,实现必须打印“ FRINK RULES!”(FRINK RULES!)是合法的。或采取其他任何措施,在理论上认为,持有的内存位置a包括记录使用哪种类型的硬件进行写入的硬件,并使此类硬件以任何方式响应无效的读取尝试(包括“民谣规则!” 待输出。

请注意,实际上是否存在任何此类硬件都没有关系-此类硬件可以合法存在的事实使编译器生成行为类似于在此类系统上运行的代码合法。如果编译器可以确定某个特定的内存位置将被写为一种类型,而另一种则被读取,则它可以假装它正在某个系统上运行,该系统的硬件可以做出这种确定,并且可以以编译器作者认为合适的任意反复程度进行响应。

该规则的目的是允许知道以下内容的编译器推断出某个类型的值的一组字节在某个时间点上具有特定的值,并且此后未写入相同类型的值,以推断该组的字节数仍将保留该值。例如,一个处理器已将一组字节读入一个寄存器,然后又想在它仍在寄存器中时再次使用相同的信息,则编译器可以使用寄存器的内容而不必从内存中重新读取该值。有用的优化。大约在该规则的前十年,违反该规则通常意味着,如果写入的变量不是用于读取变量的类型,则写入可能会或可能不会影响读取的值。这种行为在某些情况下可能是灾难性的,但在其他情况下可能是无害的,

但是,大约在2009年左右,像CLANG这样的一些编译器的作者已经确定,由于标准允许编译器在内存使用一种类型写入并以另一种类型读取的情况下做他们想做的任何事情,因此编译器应该推断出程序永远不会收到可能导致这种事情发生。由于标准说,当收到这样的无效输入时,允许编译器做任何喜欢的事情,因此仅在标准不施加任何要求的情况下才可能起作用的代码(某些编译器作者认为应该省略)无关紧要。这将别名冲突的行为从类似于内存的方式更改为,在给定读取请求的情况下,其可能会任意返回使用与读取请求相同的类型写入的最后一个值或使用其他类型写入的任何最近的值,


1
在对不了解RTTI的人不知道的类型进行修剪时提及未定义的行为似乎违反直觉
Cole Johnson

@ColeJohnson:太糟糕了,没有99%的2009年前的编译器支持C语言的正式名称或标准,因为从教学的角度和实际的角度来看,它们都应被视为根本不同的语言。由于在过去的35年中,演变出许多可预测和可优化行为的方言都使用了相同的名称,为了实现所谓的优化目的而抛出此类行为的方言,在谈论在它们中作用不同的事物时,很难避免混淆。
超级猫

从历史上看,C在Lisp机器上运行,不允许这种类型的松散玩法。我非常确定,三十年前出现的许多“可预测和可优化的行为”仅能在VAX上的BSD Unix上运行。
prosfilaes,2015年

@prosfilaes:也许“ 1999年至2009年使用的99%的编译器”会更准确?即使编译器具有一些相当积极的整数优化的选项,它们也就是这些选项。我不知道我曾见过1999年之前的编译器,而该编译器没有一种模式不能保证给定int x,y,z;的表达式x*y > z除返回1或0之外不会做任何其他事情,否则混叠违例会产生任何影响除了让编译器任意返回旧值或新值之外。
2015年

1
... unsigned char用于构造类型“来自”的值。如果程序要将指针分解为unsigned char[],则在屏幕上短暂显示其十六进制内容,然后擦除该指针,,然后再unsigned char[]从键盘上接受一些十六进制数字,将其复制回指针,然后取消对该指针的引用。 ,在键入的数字与显示的数字匹配的情况下,行为将得到很好的定义。
2015年

3

在C语言中不是。其他语言(例如Lisp,Python)具有动态类型,但是C是静态类型的。这意味着您的程序必须知道正确解释数据的类型是字符,整数等。

通常,编译器会为您解决此问题,如果您做错了什么,则会收到编译时错误(或警告)。


我不明白的是,当计算机从int或char类型的地址中读取变量的值(例如10001)时,计算机如何知道允许。想象一下,我单击了一个名为anyprog.exe的程序。代码立即开始执行。此exe文件是否包含有关变量是否存储为in或char的信息?–
user16307

1
@ user16307基本上没有,所有这些信息都完全丢失了。机器代码的设计是否足够好,即使没有这些信息也可以正确完成其工作。计算机只关心地址中连续有八位10001。在编写机器或汇编代码时,手动跟上类似工作是您的工作,还是编译器的工作(视情况而定)。
Panzercrisis

1
请注意,动态类型并不是保留类型的唯一原因。Java是静态类型的,但是它仍然必须保留类型,因为它允许动态地反映类型。另外,它具有运行时多态性,即基于运行时类型的方法分派,为此它也需要该类型。C ++将方法分发代码放入对象(或类)本身,因此,它在某种意义上不需要类型(尽管vtable在某种意义上是类型的一部分,因此,实际上至少一部分类型保留),但是在Java中,方法的分发代码是集中的。
约尔格W¯¯米塔格

看看我写的“何时执行C程序?”这个问题。它们是否在指令代码之间间接存储在exe文件中,并最终在内存中占位?我再次为您编写该代码:如果CPU在寄存器中找到0x00000061并将其获取;并想象控制台程序应该将其输出为字符而不是int。该exe文件(机器/二进制代码)中是否存在一些指令代码,这些指令代码知道0x00000061的地址为char,并使用ASCII表转换为字符?如果是这样,则意味着char int标识符间接位于二进制文件中???
user16307

如果该值为0x61并被声明为char(即'a'),并且您调用了一个例程来显示它,则[最终]将有一个系统调用来显示该字符。如果已将其声明为int并调用显示例程,则编译器将知道生成代码以将0x61(十进制97)转换为ASCII序列0x39、0x37(“ 9”,“ 7”)。底线:生成的代码是不同的,因为编译器知道以不同的方式对待它们。
Mike Harris 2015年

3

你必须区分compiletimeruntime对一方面codedata另一方面。

从机械角度来看,这就是你所谓没有区别code或者instructions,你叫什么data。一切都取决于数字。但是某些序列(我们称之为“序列”)会code做一些我们认为有用的事情,而其他序列只是crash机器。

CPU完成的工作是一个简单的4步循环:

  • 给定地址获取“数据”
  • 解码指令(即将数字“解释为” instruction
  • 读取有效地址
  • 执行并存储结果

这称为指令周期

我读到A和4存储在这里的RAM地址中。但是a和x呢?

ax是变量,它们是地址的占位符,程序可以在其中找到变量的“内容”。因此,无论何时使用变量a,都将有效地包含所a使用内容的地址。

最令人困惑的是,执行如何知道a是一个字符而x是一个int?

执行不知道任何事情。根据引言中的内容,CPU仅获取数据并将此数据解释为指令。

printf的功能全被设计成“知道”,你把什么样的投入进去,即其产生的代码给出正确的指示如何处理特殊的内存段。当然,可以废话输出:使用一个地址,其中没有字符串与“%s”一起存储,printf()将导致废话输出仅由随机存储位置(0\0)所在)停止。

程序的入口点也是如此。在C64下,可以将程序放在(几乎)每个已知地址中。汇编程序以一条称为的指令开始,sys后跟一个地址:这sys 49152是放置汇编代码的常见位置。但是没有什么可以阻止您将图形数据加载到中的49152,从而导致从“启动”到此之后机器崩溃。在这种情况下,指令周期从读取“图形数据”并将其解释为“代码”开始(当然这没有任何意义)。效果令人震惊;)

假设某个值存储在RAM的某个位置,为10011001;如果我是执行代码的程序,我怎么知道此10011001是char还是int?

如前所述:“上下文”(即上一条指令和下一条指令)有助于按我们希望的方式处理数据。从机器角度看,任何存储位置都没有区别。int并且char仅仅是词汇,在中有意义compiletime;期间runtime(在装配级别),没有charint

我不明白的是,当计算机从诸如10001之类的地址中读取变量的值时,无论它是int还是char,计算机如何知道。

电脑什么都不知道。的程序员一样。编译后的代码生成上下文,这对于为人类生成有意义的结果是必需的。

此可执行文件是否包含有关存储的变量是int还是char类型的信息。

是的没有。信息,无论是int还是char丢失。但是另一方面,上下文(指示如何处理存储数据的存储位置的指令)得以保留;因此隐式地是,“信息”是隐式可用的。


编译时间和运行时之间有很好的区别。
迈克尔·布莱克本

2

让我们仅将讨论保留为C语言。

您所引用的程序是使用C等高级语言编写的。计算机仅理解机器语言。高级语言使程序员能够以更人性化的方式表达逻辑,然后将其翻译成可由微处理器解码和执行的机器代码。现在让我们讨论您提到的代码:

char a = 'A';
int x = 4;

让我们尝试分析每个部分:

char / int被称为数据类型。这些告诉编译器分配内存。在这种情况下char将是1个字节和int2个字节。(请注意,该内存大小再次取决于微处理器)。

a / x被称为标识符。现在,您可以说出RAM中内存位置的“用户友好”名称。

=告诉编译器将'A'存储在的存储位置a并将4存储在存储位置x

因此int / char数据类型标识符仅由编译器使用,而在程序执行期间不由微处理器使用。因此,它们不存储在存储器中。


好的int / char数据类型标识符不是作为变量直接存储在内存中,而是在指令代码中间接存储在exe文件中并最终在内存中发生了吗?我再次为您编写该代码:如果CPU在寄存器中找到0x00000061并将其获取;并想象控制台程序应该将其输出为字符而不是int。该exe文件(机器/二进制代码)中是否存在一些指令代码,这些指令代码知道0x00000061的地址为char,并使用ASCII表转换为字符?如果是这样,则意味着char int标识符间接位于二进制文件中???
2015年

否,CPU的所有编号。对于您的特定示例,在console上进行打印并不取决于变量是char还是int。我将用详细的流程来更新我的答案,该流程详细说明了在执行程序之前如何将高级程序转换为机器语言。
2015年

2

我的回答在某种程度上得到了简化,仅涉及C。

不,类型信息不会存储在程序中。

intchar不是CPU的类型指示器;只给编译器。

int如果将变量声明为,则由编译器创建的exe将具有操作s的指令int。同样,如果将变量声明为a char,则exe将包含操作a的指令char

在C中:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

由于和在RAM中具有相同的,因此该程序将打印其消息。charint

现在,如果您想知道如何为和为a printf输出,那是因为您必须在“格式字符串”中指定应如何对待value。 (例如,意味着将值视为a ,并且意味着将值视为整数;不过,两种方式都相同。)65intAcharprintf
%cchar%d


2
我希望有人会使用的示例printf。@OP:int a = 65; printf("%c", a)将输出'A'。为什么?因为处理器不在乎。对此,它所看到的只是位。您的程序告诉处理器在65处存储65(恰好是'A'ASCII 的值),a然后输出一个字符,这很高兴。为什么?因为它不在乎。
科尔·约翰逊

但是为什么有人在C#情况下说这不是故事呢?我读了一些其他评论,他们说在C#和C ++中,故事(数据类型的信息)有所不同,甚至CPU也无法进行计算。有什么想法吗?
2015年

@ user16307如果CPU不执行计算,则程序未运行。:)至于C#,我不知道,但是我认为我的答案也适用于此。至于C ++,我知道我的答案在那里适用。
BenjiWiebe,2015年

0

在最低级别上,在实际的物理CPU中根本没有任何类型(忽略浮点单元)。只是位模式。计算机通过非常,非常快速地操作位模式来工作。

这就是CPU曾经做过的一切,它曾经可以做的一切。没有int或char这样的东西。

x = 4 + 5

将执行为:

  1. 将00000100装入寄存器1
  2. 将00000101装入寄存器2
  3. I将寄存器1添加到寄存器2中,并存储在寄存器1中

iadd指令触发硬件,其行为就像寄存器1和2是整数一样。如果它们实际上不表示整数,那么以后所有事情都会出错。最好的结果通常是崩溃。

由编译器根据源代码中给出的类型选择正确的指令,但是在CPU执行的实际机器代码中,任何地方都没有类型。

编辑:请注意,实际的机器代码实际上并未提及4或5或任何地方的整数。它只是两种位模式,而一条指令则采用两种位模式,假设它们是整数,然后将它们加在一起。


0

简而言之,类型是在编译器生成的CPU指令中编码的。

尽管不直接存储有关信息类型或大小的信息,但是在访问,修改和存储这些变量中的值时,编译器仍会跟踪该信息。

执行如何知道a是一个字符而x是一个int?

不会,但是当编译器生成机器代码时,它就会知道。一个int和一个char可以具有不同的大小。在char为字节大小而int为4字节的体系结构中,变量x不在地址10001中,而是在10002、10003和10004中。当代码需要将的值加载x到CPU寄存器中时,它使用指令加载4个字节。加载char时,它使用指令加载1个字节。

如何选择两个说明中的哪一个?编译器在编译过程中决定,在检查内存中的值后并不会在运行时完成。

还要注意,寄存器的大小可以不同。在Intel x86 CPU上,EAX为32位宽,其中一半为AX,即16,而AX分为AH和AL,均为8位。

因此,如果要加载整数(在x86 CPU上),请对整数使用MOV指令,对字符使用MOV指令来加载字符。它们都被称为MOV,但是它们具有不同的操作码。实际上是两个不同的指令。变量的类型在使用说明中进行了编码。

其他操作也会发生相同的情况。根据操作数的大小,即使有符号的还是无符号的,都有许多执行加法的指令。请参阅https://en.wikipedia.org/wiki/ADD_(x86_instruction),其中列出了不同的可能添加项。

假设某个值存储在RAM的某个位置,为10011001;如果我是执行代码的程序,我怎么知道此10011001是char还是int

首先,一个char是10011001,但是一个int是00000000 00000000 00000000 10011001,因为它们的大小不同(在具有上述相同大小的计算机上)。但是让我们考虑一下signed charvs 的情况unsigned char

无论如何,都可以解释存储在存储单元中的内容。C编译器的部分职责是确保以一致的方式完成对变量的存储和读取。因此,并不是程序知道存储在内存中的内容是什么,而是事先同意它将始终在其中读取和写入相同类型的内容。(不包括类型转换等)。


但是为什么有人在C#情况下说这不是故事呢?我读了一些其他评论,他们说在C#和C ++中,故事(数据类型的信息)有所不同,甚至CPU也无法进行计算。有什么想法吗?
2015年

0

但是为什么有人在C#情况下说这不是故事呢?我读了一些其他评论,他们说在C#和C ++中,故事(数据类型的信息)有所不同,甚至CPU也无法进行计算。有什么想法吗?

在像C#这样的类型检查语言中,类型检查由编译器完成。Benji编写的代码:

int main()
{
    int a = 65;
    char b = 'A';
    if(a == b)
    {
        printf("Well, what do you know. A char can equal an int.\n");
    }
    return 0;
}

只会拒绝编译。类似地,如果您尝试将一个字符串和一个整数相乘(我想说add,但是运算符'+'会因字符串连接而重载,并且可能会起作用)。

int a = 42;
string b = "Compilers are awesome.";
double[] c = a * b;

不管您的字符串接吻了多少,编译器都只是拒绝从此C#生成机器代码。


-4

其他答案是正确的,因为实际上您将遇到的每个消费类设备都不存储类型信息。但是,过去(今天,在研究环境中)有几种使用标记架构的硬件设计 -它们既存储数据,又存储类型(可能还存储其他信息)。这些将最明显地包括Lisp机器

我隐约记得有一次听说过有关为面向对象编程设计的硬件体系结构的内容,但与此类似,但现在找不到。


3
这个问题特别指出它指的是C语言(未Lisp的),和C语言并没有存储可变的元数据。尽管C实现确实可以执行此操作,但由于标准不禁止这样做,因此实际上从未发生过。如果您有与问题相关的示例,请提供具体的引用并提供与C语言有关的参考。

好了,您可以为Lisp机器编写C编译器,但是在当今这个时代,通常没有人使用Lisp机器。顺便说一下,面向对象的体系结构是Rekursiv
内森·林戈2015年

2
我认为这个答案没有帮助。它使事情变得复杂,远远超出了对OP的当前了解。很明显,OP不了解CPU + RAM的基本执行模型,也不了解编译器如何将符号高级源转换为可执行二进制文件。在我看来,标记的内存,RTTI,Lisp等远远超出了问问者所需要知道的范围,只会使他/她更加困惑。
安德列斯·F

但是为什么有人在C#情况下说这不是故事呢?我读了一些其他评论,他们说在C#和C ++中,故事(数据类型的信息)有所不同,甚至CPU也无法进行计算。有什么想法吗?
2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.