类型检查
静态类型检查
静态类型检查 是在 编译时 对程序中的所有变量和表达式进行类型检查,以确保每个操作数、变量、函数调用等的类型都是一致和兼容的。
类型声明和定义
在静态类型语言中,每个变量、函数参数和返回值等的类型在代码编写时就已经明确指定。这些类型信息是编译器进行类型检查的基础。
符号表(Symbol Table)
编译器在编译过程中维护一个符号表,用于存储所有变量、函数及其类型信息。符号表在语法分析阶段创建,并在语义分析阶段进行扩展和检查。
类型推导
有些静态类型语言支持类型推导,编译器可以根据上下文自动推导出变量的类型(如C++中的auto
关键字)
类型检查
编译器在编译过程中对所有表达式和语句进行类型检查,以确保类型一致性。例如,变量赋值时检查类型兼容性,函数调用时检查参数类型匹配,运算符操作时检查操作数类型。
类型转换
静态类型语言通常支持显式类型转换和隐式类型转换。编译器在类型转换时进行检查,确保转换是合法的。
编译器的工作流程
编译器的工作流程通常分为以下几个阶段,每个阶段都会涉及类型检查:
词法分析(Lexical Analysis)
将源代码转换为一系列标记(tokens),这些标记代表代码中的基本元素,如关键字、标识符、操作符等。
语法分析(Syntax Analysis)
根据语法规则将标记组织成语法树(parse tree 或 AST, Abstract Syntax Tree),表示程序的结构。
语义分析(Semantic Analysis)
进行类型检查,确保每个操作符和操作数的类型一致。建立和维护符号表,跟踪变量和函数的类型信息。
中间代码生成(Intermediate Code Generation)
将语法树转换为中间表示(IR, Intermediate Representation),这种表示比源代码更接近机器代码,但仍然与具体机器无关。
优化(Optimization)
对中间代码进行各种优化,提高程序执行效率。
目标代码生成(Code Generation)
将优化后的中间代码转换为目标机器代码。
静态检查的优缺点
优点:
- 提高性能: 静态类型检查消除了运行时类型检查的开销,生成的代码更高效。
- 早期错误检测: 类型错误在编译时被捕捉,减少了运行时错误的发生。
- 代码可维护性: 明确的类型定义提高了代码的可读性和可维护性。
缺点:
- 灵活性较低: 类型必须在编译时确定,减少了代码的灵活性和动态性。
- 开发速度较慢: 需要显式地声明类型,增加了编码工作量。
RTTI
虚函数表(vtable)
当一个类包含虚函数时,编译器会为这个类生成一个虚函数表(vtable),其中包含了所有虚函数的指针。每个包含虚函数的类实例都有一个指向这个虚函数表的指针(称为vptr)。
RTTI数据
RTTI信息也会存储在虚函数表中。具体来说,编译器会在虚函数表中加入指向类型信息(如std::type_info对象)的指针。这个指针通常在虚函数表的某个固定位置。
RTTI的实现
RTTI的实现依赖于两个关键操作符:typeid
和dynamic_cast
。
typeid
操作符用于获取对象的类型信息。它返回一个std::type_info
对象,该对象包含类型的信息。type_info
对象的实际内容由编译器生成和维护。
#include <iostream>
#include <typeinfo>
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {};
int main() {
Base* base = new Derived();
std::cout << "Type of base: " << typeid(*base).name() << std::endl;
delete base;
return 0;
}
RTTI的工作流程
编译时:
编译器为每个包含虚函数的类生成虚函数表,并在虚函数表中添加指向类型信息的指针。
编译器生成type_info对象,用于表示每个类型的信息。
运行时:
typeid操作符通过访问虚函数表中的类型信息指针,获取对象的类型信息。
dynamic_cast操作符通过检查虚函数表中的类型信息,确定是否可以进行安全的类型转换。
RTTI的限制
性能开销:
RTTI会增加一些运行时开销,因为它需要在虚函数表中存储类型信息,并在运行时进行检查。
仅适用于多态类:
RTTI依赖于虚函数表,因此只适用于包含虚函数的类(多态类)。对于非多态类,RTTI无法使用。
可移植性:
不同编译器和平台对RTTI的实现可能有所不同,因此在某些情况下,RTTI可能会有可移植性问题。
动态类型检查
运行时动态类型检查是在程序运行时进行类型检查,以确保操作数和表达式的类型是有效和兼容的。与静态类型检查不同,动态类型检查是在程序执行过程中进行的,这意味着类型信息在运行时确定。
运行时确定类型: 变量和表达式的类型在运行时确定,允许更灵活的编程方式。
高灵活性: 由于类型信息在运行时可用,可以方便地进行类型转换、动态类型检查和反射。
潜在的性能开销: 运行时类型检查增加了程序的运行时开销,可能会影响性能。
延迟错误检测: 类型错误在运行时被捕获,这可能导致运行时错误,增加调试和维护的复杂性。
反射机制:
反射是指程序在 运行时 能够检查和修改自身结构的能力,包括检查类,方法,属性等信息
反射的实现依赖
静态类型语言 通过 编译时 生成的元数据和运行时类型信息(RTTI)实现反射
动态类型语言 天然支持反射,因为类型信息在运行时就可用
-
灵活性和性能的权衡:
- 静态类型检查提供了更好的性能和类型安全性,但灵活性较低。
- 动态类型检查提供了更高的灵活性,但带来了性能和类型安全性的挑战。
- 反射增加了动态性,但也带来了额外的性能开销和潜在的安全问题。
偏移量在反射机制中的作用
属性访问:
反射机制通过属性的偏移量来访问对象中的成员变量。当我们在运行时想要读取或修改对象的某个属性值时,UE通过反射系统找到该属性的偏移量,然后使用该偏移量直接访问内存中的相应位置。
内存布局解析:
偏移量帮助引擎解析和理解对象的内存布局。反射系统需要知道每个属性在对象内存中的具体位置,以便能够正确地进行属性的读写操作。
序列化和反序列化:
偏移量在对象的序列化和反序列化过程中起重要作用。通过偏移量,反射系统可以精确地读取对象中的每个属性并将其转换为适当的格式进行存储,或者从存储格式中恢复对象的状态。
UE 反射机制
- UClass。存储类信息,用于反射。把它当成C#的Type来理解。
- GetClass()。获得一个UObject实例的UClass,是UObject成员函数。
- GetStaticClass()。不需要有实例就能获得UClass。是静态的,每次调用返回相同结果。
- ClassDefaultObject。类默认对象,可以获得UObject初始化时的值。注意GetClass()->GetDefaultObject()和T::StaticClass()->GetDefaultObject()不一样。
UE为什么要实现反射
编辑器支持
反射机制使得UE4编辑器能够动态地显示和修改对象的属性。这对于开发人员和设计师来说极其重要,因为它们可以在编辑器中直接调整游戏对象的属性,而无需重新编译代码。
- 属性面板: 通过反射,编辑器可以自动生成用户界面,让开发者和设计师在属性面板中查看和修改对象的属性。
- 蓝图编辑器: 蓝图是UE4中的可视化
脚本语言
,利用反射机制,蓝图编辑器可以动态地识别和调用C++类中的方法和属性。
序列化
反射机制使得对象的自动序列化和反序列化变得更加容易。UE4中使用反射来读取和写入对象的属性,以便保存和加载游戏状态、资源等。
- 保存和加载: 使用反射,可以自动将对象的属性保存到文件中,并在需要时重新加载。
- 网络同步: 在多人游戏中,反射机制可以用来同步游戏状态,使得对象的属性在网络上传输时能够自动序列化和反序列化。
动态对象创建
反射机制允许在运行时动态创建和管理对象。这在需要根据运行时条件动态加载和创建对象的场景中特别有用。
- 工厂模式: 反射可以用于实现工厂模式,动态创建对象实例,而无需在编译时确定具体的类。
- 模块和插件: 通过反射,可以动态加载和使用模块或插件中的类和对象,增强引擎的扩展性和灵活性。
脚本语言支持
UE4支持使用脚本语言(如Blueprint)来编写游戏逻辑。反射机制使得脚本语言能够访问和调用C++类中的属性和方法,从而实现脚本与引擎核心代码的无缝集成。
- 脚本调用C++方法: 通过反射,脚本语言可以调用C++类中的方法,访问对象的属性,实现高度的灵活性和动态性。
- 跨语言互操作: 反射机制使得不同语言之间能够方便地进行互操作,提高了引擎的扩展能力。
自动化测试和调试
反射机制在自动化测试和调试中也起到了重要作用。通过反射,可以动态地检查对象的状态,调用对象的方法,从而实现更加灵活和全面的测试。
- 自动化测试工具: 测试工具可以通过反射机制自动化地调用对象的方法,设置对象的状态,从而进行全面的测试覆盖。
- 调试工具: 调试工具可以利用反射机制动态检查对象的状态,帮助开发者快速定位和修复问题。
UE 如何实现反射机制
Unreal Engine(UE)通过一套宏、元数据和自定义的对象系统实现了反射机制。这些机制允许在运行时获取类型信息、访问和修改对象的属性和方法。以下是UE反射机制的核心部分及其实现方式:
核心组件
- UObject和UClass
- 宏系统
- 元数据系统
- 反射API
UObject 和 UClass
UObject: 所有支持反射的类都必须继承自 Uobject,UObject 是 UE 的所有反射类的基类,提供了反射所需的基础设置
UClass: UClass 是 UObject 的元类,包含了反射信息,如类的名称,属性和方法
宏系统
UE 使用一套宏类标记和生成反射信息,这些宏包括 UCLASS,USTRUCT,UPROPERTY,UFUNCTION 等
UCLASS:用来声明一个类支持反射
UCLASS()
class MYGAME_API AMyActor : public AActor {
GENERATED_BODY()
}
USTRUCT:用来声明一个结构体支持反射
USTRUCT()
struct FMyStruct {
GENERATED_BODY()
UPROPERTY()
int32 MyProperty;
};
UPROPERTY:用来声明类或结构体中的属性支持反射
UCLASS()
class MYGAME_API AMyActor : public AActor {
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Custom")
int32 MyProperty;
}
UFUNCTION:用来声明类中的方法支持反射
UCLASS()
class MYGAME_API AMyActor : public AActor {
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category="Custom")
void MyFunction();
}
元数据系统
UE的反射系统通过生成元数据文件来记录类、属性和方法的信息。这些元数据在编译时生成,并在运行时使用。
编译时生成:使用宏系统,UE在编译时生成元数据文件,描述类、属性和方法的反射信息。
运行时使用:在运行时,UE使用这些元数据来实现反射功能,如动态属性访问、方法调用等。
反射API
UE提供了一套反射API,用于在运行时访问和操作反射信息。这些API包括但不限于:
查找类和创建实例:FindObject 和 StaticConstructObject 用来查找类和创建其实例
UClass* MyActorClass = FindObject<UClass>(ANY_PACKAGE, TEXT("MyActor"));
if (MyActorClass) {
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
}
访问属性:Uproperty 类及其派生类(如 Property,UFloatProperty 等)用来访问和修改对象的属性
UClass* MyActorClass = AMyActor::StaticClass();
UProperty* MyProperty = FindField<UProperty>(MyActorClass, TEXT("MyProperty"));
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
if (MyProperty) {
MyProperty->SetPropertyValue_InContainer(MyActorInstance, 42);
}
调用方法:UFunction 类用于调用对象的方法
UClass* MyActorClass = AMyActor::StaticClass();
UFunction* MyFunction = MyActorClass->FindFunctionByName(TEXT("MyFunction"));
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
if (MyFunction) {
MyActorInstance->ProcessEvent(MyFunction, nullptr);
}
参考:【UE·底层篇】一文搞懂StaticClass、GetClass和ClassDefaultObject_ue staticclass-CSDN博客