如果我定义某种类型的变量(据我所知,只是为变量的内容分配数据),它如何跟踪它是哪种类型的变量?
如果我定义某种类型的变量(据我所知,只是为变量的内容分配数据),它如何跟踪它是哪种类型的变量?
Answers:
变量(或更笼统地说:C的“对象”)在运行时不存储其类型。就机器代码而言,只有未类型化的内存。相反,对此数据的操作会将数据解释为特定类型(例如,浮点数或指针)。这些类型仅由编译器使用。
例如,我们可能有一个struct或class struct Foo { int x; float y; };
和一个variable Foo f {}
。如何auto result = f.y;
编译字段访问?编译器知道这f
是类型的对象,Foo
并且知道Foo
-objects 的布局。根据特定于平台的详细信息,它可能被编译为“将指针指向的开头f
,添加4个字节,然后加载4个字节,然后将此数据解释为浮点数。”在许多机器代码指令集中(包括x86-64) )有不同的处理器指令可用于加载浮点数或整数。
C ++类型系统无法为我们跟踪类型的一个示例是类似的联合union Bar { int as_int; float as_float; }
。联合最多可包含一个不同类型的对象。如果我们将对象存储在联合中,则这是联合的活动类型。我们只能尝试使该类型退出联合,否则任何其他情况都是未定义的行为。要么在编程活动类型时“知道”,要么创建标记的联合,在其中单独存储类型标签(通常是枚举)。这是C语言中的常用技术,但是由于我们必须使并集和类型标记保持同步,因此这很容易出错。一个void*
指针是类似于工会,但只能容纳指针的对象,除了函数指针。
C ++提供了两种更好的机制来处理未知类型的对象:我们可以使用面向对象的技术来执行类型擦除(仅通过虚拟方法与对象进行交互,因此我们不需要知道实际的类型),或者可以use std::variant
,一种类型安全的联合。
在一种情况下,C ++确实存储了对象的类型:如果对象的类具有任何虚方法(“多态类型”,也称为接口)。虚拟方法调用的目标在编译时未知,并在运行时根据对象的动态类型(“动态调度”)进行解析。大多数编译器通过在对象的开头存储一个虚拟函数表(“ vtable”)来实现此目的。vtable也可用于在运行时获取对象的类型。然后,我们可以在编译时已知的表达式静态类型和运行时对象的动态类型之间进行区分。
C ++允许我们使用typeid()
为我们提供std::type_info
对象的运算符来检查对象的动态类型。要么编译器在编译时就知道对象的类型,要么编译器已经在对象内部存储了必要的类型信息,并且可以在运行时检索它。
void*
)。
typeid(e)
内省表达式的静态类型e
。如果静态类型是多态类型,则将评估表达式并检索该对象的动态类型。您不能将typeid指向未知类型的内存并获得有用的信息。例如,联合的typeid描述联合,而不是联合中的对象。a的typeid void*
只是一个空指针。并且不可能取消引用a void*
以获得其内容。在C ++中,除非以这种方式进行显式编程,否则不会进行装箱。
另一个答案很好地说明了技术方面,但是我想添加一些常规的“如何考虑机器代码”。
编译后的机器代码相当愚蠢,它实际上只是假设一切正常。假设您有一个简单的功能,例如
bool isEven(int i) { return i % 2 == 0; }
它需要一个int,然后吐出一个布尔值。
编译后,您可以将其视为类似于此自动橙色榨汁机的东西:
它吸收橘子,然后返回果汁。它可以识别进入的对象类型吗?不,他们应该只是橘子。如果它得到一个苹果而不是一个橘子怎么办?也许会破裂。没关系,因为负责任的所有者不会尝试以这种方式使用它。
上面的函数是相似的:它被设计为采用整数,并且在喂入其他东西时可能会中断或做不相关的事情。(通常)没有关系,因为编译器(通常)会检查它永远不会发生-而且在格式正确的代码中它也永远不会发生。如果编译器检测到函数将获得错误的类型值的可能性,则它拒绝编译代码,而是返回类型错误。
需要注意的是,在某些情况下,编译器将通过格式错误的代码。例如:
void*
到orange*
时,有上的指针的另一端一个苹果,如前所述,编译后的代码就像榨汁机一样-它不知道它要处理什么,它只是执行指令。如果指示错误,则会中断。这就是为什么C ++中的上述问题会导致无法控制的崩溃。
void*
胁迫到foo*
,通常的算术促销,union
类型双关语,NULL
对nullptr
,哪怕只是有一个坏的指针是UB等,但我不认为上市的所有这些事情出来会实质改善你的答案,所以它可能是最好的休假照原样。
void*
中不会隐式转换为foo*
,并且union
不支持punning类型(具有UB)。
变量具有像C这样的语言的许多基本属性:
在您的源代码中,位置(5)是概念性的,并且该位置由其名称(1)引用。因此,变量声明用于为值(6)创建位置和空间,在源代码的其他行中,我们通过在某些表达式中命名变量来引用该位置及其所拥有的值。
仅在某种程度上简化一下,一旦程序被编译器翻译为机器代码,该位置(5)就是一些内存或CPU寄存器的位置,任何引用该变量的源代码表达式都将翻译为引用该内存的机器代码序列或CPU寄存器位置。
因此,当翻译完成并且程序在处理器上运行时,变量的名称实际上在机器代码中被忘记了,并且编译器生成的指令仅引用变量的分配位置(而不是变量的位置)。名称)。如果您要调试和请求调试,则与名称关联的变量的位置会添加到程序的元数据中,尽管处理器仍会使用位置(而不是元数据)看到机器代码指令。(这过于简化了,因为某些名称存在于程序的元数据中,用于链接,加载和动态查找的目的—处理器仍然只是执行程序所告知的机器代码指令,并且在该机器代码中,名称具有已转换为位置。)
对于类型,范围和生存期也是如此。编译器生成的机器代码指令知道位置的机器版本,该机器版本存储值。其他属性(例如type)作为访问变量位置的特定指令编译到翻译后的源代码中。例如,如果所讨论的变量是有符号的8位字节与无符号的8位字节,则源代码中引用该变量的表达式将转换为有符号字节负载与无符号字节负载,根据需要满足(C)语言的规则。因此,变量的类型被编码为将源代码转换为机器指令,机器指令每次使用变量的位置时,都会命令CPU如何解释内存或CPU寄存器的位置。
实质是我们必须通过处理器的机器代码指令集中的指令(以及更多指令)来告诉CPU该做什么。处理器对它刚刚执行或被告知的操作记忆很少-仅执行给定的指令,而编译器或汇编语言程序员的工作是为其提供完整的指令序列集以正确地操纵变量。
处理器直接支持一些基本数据类型,例如字节/字/整数/长符号/无符号,浮点数,双精度等。如果您将相同的内存位置交替地视为有符号或无符号,则处理器通常不会抱怨或反对。例如,即使那通常是程序中的逻辑错误。编程的工作是在每次与变量的交互时指示处理器。
除了这些基本的基本类型之外,我们还必须在数据结构中编码事物,并使用算法根据这些基本类型来操纵它们。
在C ++中,涉及多态性的类层次结构中的对象通常在对象的开头有一个指针,该指针指向特定于类的数据结构,这有助于虚拟分派,强制转换等。
总而言之,处理器否则不知道或不记得存储位置的预期用途-它执行程序的机器代码指令,告诉它如何操作CPU寄存器和主存储器中的存储。因此,编程是软件(和程序员)的工作,以有意义地使用存储,并向处理器提供一组一致的机器代码指令,以忠实地执行程序。
useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);
,clang和gcc倾向于假设指向的指针unionArray[j].member2
不能访问,unionArray[i].member1
即使它们都是从相同的派生出来的unionArray[]
。
如果我定义某种类型的变量,它如何跟踪变量的类型。
这里有两个相关阶段:
C编译器将C代码编译为机器语言。编译器具有可以从您的源文件(和库,以及执行其工作所需的其他任何东西)中可以获得的所有信息。C编译器跟踪什么意味着什么。C编译器知道,如果您将变量声明为char
,则为char。
它通过使用所谓的“符号表”来做到这一点,其中列出了变量的名称,变量的类型以及其他信息。它是一个相当复杂的数据结构,但是您可以将其视为仅跟踪人类可读名称的含义。在编译器的二进制输出中,不再出现这样的变量名(如果我们忽略程序员可能会请求的可选调试信息)。
编译器的输出-编译后的可执行文件-是机器语言,由操作系统加载到RAM中,并由CPU直接执行。在机器语言中,根本没有“类型”的概念-它只有在RAM中某个位置上运行的命令。该命令确实有他们与经营固定式(即,有可能是机器语言指令“添加存储在RAM位置为0x100和0x521这两个16位整数”),但没有信息任何地方在系统中的这些位置上的字节实际上代表整数。这里根本没有针对类型错误的保护措施。
char *ptr = 0x123
C语言)。我相信在这种情况下,我对“指针”一词的用法应该很清楚。如果没有,请给我一个提示,然后在答案中加上一句话。
在几个重要的特殊情况下,C ++确实在运行时存储类型。
经典的解决方案是有区别的联合:一个数据结构,其中包含几种类型的对象中的一种,再加上一个字段,它说明当前包含的类型。模板化版本在C ++标准库中为std::variant
。通常,标记是enum
,但是如果您不需要数据的所有存储位,则可能是位字段。
另一个常见的情况是动态类型。当您class
拥有一个virtual
函数时,该程序会将指向该函数的指针存储在虚拟函数表中,该表将针对class
构建时的每个实例进行初始化。通常,对于所有类实例,这将意味着一个虚函数表,并且每个实例都拥有一个指向相应表的指针。(这节省了时间和内存,因为该表将比单个指针大得多。)当您virtual
通过指针或引用调用该函数时,程序将在虚拟表中查找该函数指针。(如果它在编译时知道确切的类型,则可以跳过此步骤。)这允许代码调用派生类型的实现,而不是基类的实现。
在这里使之相关的是:每个都ofstream
包含一个指向ofstream
虚拟表的指针,每个都ifstream
指向ifstream
虚拟表,等等。对于类层次结构,虚拟表指针可以用作标记,告诉程序类对象具有什么类型!
尽管语言标准没有告诉设计编译器的人员他们必须如何在后台实现运行时,但这是您可以期望dynamic_cast
并typeof
起作用的方式。