ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

UE4 C++开发中指针与引用的核心区别、应用场景与最佳实践

UE4 C++开发中指针与引用的核心区别、应用场景与最佳实践 1. 项目概述为什么UE4 C开发者必须吃透和*如果你正在用UE4做C开发无论是写游戏逻辑、插件还是优化底层系统有两个符号你几乎每天都会遇到那就是引用和*指针。很多刚接触UE4 C的朋友甚至一些有经验的开发者对这两者的区别和具体应用场景都感到模糊。这直接导致代码里出现一些难以排查的bug比如空指针访问、意外的数据修改或者性能上的浪费。在UE4的庞大框架里和*不仅仅是C的基础语法更是理解引擎内存管理、对象生命周期、蓝图交互和性能优化的基石。比如为什么UPROPERTY()宏后面经常跟的是指针为什么函数参数里有时用const FString有时又直接用FStringTArrayAActor*和TArrayAActor哪个能用这些问题都绕不开对这两个符号的深刻理解。这篇文章我就结合自己十多年在UE4项目里摸爬滚打的经验把和*在UE4 C环境下的区别、用法、坑点以及最佳实践掰开揉碎了讲清楚。目标很明确让你看完之后不仅能回答面试题更能写出更安全、更高效、更符合UE4范式的代码。2. 核心概念拆解从内存模型理解本质区别要真正搞懂和*不能只记语法必须从它们在计算机内存中扮演的角色说起。这就像盖房子指针(*)是给你一张写着地址的纸条引用()是直接给这栋房子起了个别名。2.1 指针*持有地址的“导航员”指针的本质是一个变量这个变量里存储的是另一个变量或对象在内存中的地址。你可以把它想象成一个遥控器或者一张藏宝图。它本身不“是”那个对象它只是“指向”那个对象。1.1.1 基本语法与声明// 声明一个整型指针指向一个int类型变量的地址 int* IntPtr; // 声明一个指向AActor类对象的指针 AActor* ActorPtr; // 声明一个指向TArray容器的指针 TArrayFVector* LocationArrayPtr;这里的*紧挨着类型名int*或变量名*ActorPtr都可以但在UE4和现代C风格中更推荐int* IntPtr这种写法强调“指向int的指针”是一个类型。1.1.2 指针的核心操作指针有两大核心操作取地址和解引用*。这里容易混淆同一个符号用在变量前面是“取地址运算符”用在类型后面是“声明引用”。我们稍后讲引用时会再区分。int Health 100; int* HealthPtr Health; // Health 取得Health变量的内存地址赋值给指针 // 解引用通过指针访问或修改其指向的对象 *HealthPtr 150; // 等价于 Health 150; UE_LOG(LogTemp, Warning, TEXT(Health is: %d), *HealthPtr); // 输出1501.1.3 指针的特殊值nullptr指针可以处于一种“未指向任何有效对象”的状态在C11及UE4中我们用nullptr来表示这个空指针。这是指针与引用最关键的区别之一。AActor* MyActor nullptr; // 合法指针初始化为空 if (MyActor ! nullptr) { // 安全的操作先检查指针是否有效 MyActor-Destroy(); }在UE4中由于垃圾回收Garbage Collection机制的存在一个UObject指针可能被自动置为nullptr。因此在使用任何UObject指针前进行检查是至关重要的安全习惯。2.2 引用对象的“别名”引用可以理解为某个已存在变量的另一个名字。一旦一个引用被初始化为某个变量它就将一直作为那个变量的别名存在无法再指向其他变量。它本身不占用额外的存储空间在实现层面编译器通常会将其当作指针处理但对程序员透明。1.2.1 基本语法与声明int Mana 200; int ManaRef Mana; // ManaRef是Mana的引用即别名 ManaRef 250; // 直接修改ManaRef就是在修改Mana UE_LOG(LogTemp, Warning, TEXT(Mana is: %d), Mana); // 输出250注意引用必须在声明时就被初始化且之后不能改变其“绑定”的对象。int A 10; int B 20; int Ref A; // 正确Ref绑定到A // int Ref2; // 错误引用必须初始化 // Ref B; // 错误不能将Ref重新绑定到B。这行代码意图是将B的地址赋值给Ref但Ref是int不是指针。 Ref B; // 注意这行代码的含义是将B的值20赋值给Ref所引用的对象即A。执行后A的值变为20。1.2.2 引用作为函数参数最常用场景这是引用在UE4 C开发中最有价值的地方之一避免不必要的对象拷贝提升性能。// 低效做法传值会发生整个FVector的拷贝 void ProcessLocation_ByValue(FVector Location) { Location.X 100.0f; // 修改的是拷贝的副本不影响原数据 } // 高效做法传常量引用避免拷贝且防止函数内部修改 void ProcessLocation_ByConstRef(const FVector Location) { // Location.X 100.0f; // 编译错误const引用禁止修改 float XValue Location.X; // 只读访问安全高效 } // 需要修改原数据时的做法传非常量引用 void ModifyLocation_ByRef(FVector Location) { Location.X 100.0f; // 直接修改调用方传入的原始对象 } // 调用示例 FVector PlayerPosition GetPlayerLocation(); ProcessLocation_ByConstRef(PlayerPosition); // 高效只读 ModifyLocation_ByRef(PlayerPosition); // 直接修改PlayerPosition对于UE4中常见的大型对象如FString、TArray、TMap、FTransform等在函数间传递时几乎总是应该使用const 如果不修改或如果需要修改而不是直接传值。2.3 对比表格指针 vs 引用为了更直观地理解我把核心区别总结成下面这个表格特性指针 (*)引用 ()本质存储内存地址的变量已存在变量的别名是否可为空可以值为nullptr不可以必须初始化且始终代表一个有效对象能否重定向可以指向另一个地址不可以初始化后绑定关系不可变内存占用占用一个指针大小的内存通常4或8字节通常不占用额外存储编译器实现细节访问方式需使用解引用运算符 (*) 或箭头运算符 (-)像普通变量一样直接使用安全性较低需手动检查nullptr易出错较高总是指向有效对象理论上主要用途1. 动态内存分配 (new/delete)2. 可选参数可传nullptr3. 需要重指向的场景4. 实现多态基类指针指向派生类1. 函数参数传递避免拷贝2. 函数返回值某些情况下3. 范围for循环for (auto Item : Array)在UE4中的典型应用存储UObject派生类的实例AActor*,UWidget*传递大型结构体或容器const FString,TArrayint323. 在UE4实战中的具体应用与选择策略理解了基本概念我们来看看在真实的UE4项目里什么时候该用指针什么时候该用引用。这个选择直接影响到代码的健壮性、可读性和性能。3.1 必须使用指针的场景2.1.1 管理UObject及其派生类的生命周期UE4的核心对象系统建立在UObject之上包括AActor、UActorComponent等。这些对象由引擎的垃圾回收系统管理我们通过指针来持有和操作它们。UCLASS() class AMyCharacter : public ACharacter { GENERATED_BODY() public: // UPROPERTY宏必须用于指针成员以便垃圾回收器跟踪引用 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Combat) AWeapon* EquippedWeapon; // 指向另一个AActor的指针 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Components) UCapsuleComponent* CapsuleComp; // 指向一个UActorComponent的指针 // 函数返回或参数也使用指针 AWeapon* FindBestWeapon() const; void PickUpWeapon(AWeapon* WeaponToPickup); };为什么必须是指针因为UE4的反射系统和垃圾回收需要知道对象之间的引用关系。一个非指针的UObject成员变量意味着“值语义”的包含这与UE4的对象模型不符会导致编译错误或运行时问题。2.1.2 需要表达“可能不存在”的语义当你需要一个可选的、可能为空的参数或返回值时指针是唯一选择。// 在场景中查找一个特定标签的Actor可能找不到 AActor* FindActorByTag(const FName Tag) { for (TActorIteratorAActor It(GetWorld()); It; It) { if (It-ActorHasTag(Tag)) { return *It; // 返回找到的指针 } } return nullptr; // 找不到返回空指针 } // 调用方必须检查返回值 AActor* FoundActor FindActorByTag(FName(Target)); if (FoundActor) // 或者 if (FoundActor ! nullptr) { // 安全地使用FoundActor FoundActor-SetActorHiddenInGame(true); } // 如果FoundActor是nullptr这里什么也不做程序不会崩溃如果试图用引用来实现这个函数你将无法表示“未找到”的状态调用方也无法安全地处理这种情况。2.1.3 需要多态行为C的多态虚函数需要通过基类指针或引用来调用派生类的方法。虽然引用也能实现多态但指针更灵活因为你可以有指针容器如TArrayACharacter*并且可以方便地置空或更换指向的对象。UCLASS() class AEnemy : public ACharacter { GENERATED_BODY() public: virtual void PerformAttack() override { UE_LOG(LogTemp, Warning, TEXT(Enemy attacks!)); } }; UCLASS() class ABossEnemy : public AEnemy { GENERATED_BODY() public: virtual void PerformAttack() override { UE_LOG(LogTemp, Warning, TEXT(Boss launches special attack!)); } }; // 使用指针容器管理不同类型的敌人 TArrayAEnemy* EnemyArray; EnemyArray.Add(NewObjectAEnemy()); EnemyArray.Add(NewObjectABossEnemy()); for (AEnemy* Enemy : EnemyArray) { if (Enemy) { Enemy-PerformAttack(); // 多态调用根据实际对象类型执行不同逻辑 } }3.2 应该优先使用引用的场景2.2.1 函数参数传递尤其是输入参数这是引用最能发挥优势的地方。对于非UObject的大型数据结构体、容器使用const 传递可以避免昂贵的拷贝开销。// 好使用常量引用传递只读参数 float CalculateDistance(const FVector PointA, const FVector PointB) { return FVector::Distance(PointA, PointB); } // 好使用引用传递需要修改的输出参数 void GetPlayerViewPoint(FVector OutLocation, FRotator OutRotation) const { OutLocation GetActorLocation(); OutRotation GetActorRotation(); } // 不好传值会导致整个TArray的深拷贝性能杀手 void ProcessAllItems(TArrayFItemData Items) { for (auto Item : Items) { /* ... */ } } // 好传常量引用零拷贝开销 void ProcessAllItems(const TArrayFItemData Items) { for (const auto Item : Items) { /* ... */ } } // 需要修改原容器时传非常量引用 void RemoveInvalidItems(TArrayFItemData Items) { Items.RemoveAll([](const FItemData Item){ return !Item.IsValid(); }); }经验法则对于内置类型int,float,bool等传值开销很小可以直接传值。对于其他所有类型先考虑const 如果需要修改再考虑。2.2.2 范围for循环Range-based for loop在遍历容器时使用引用可以避免容器元素的拷贝尤其是当元素是复杂类型时。TArrayFVector PathPoints GetPath(); // 不好每次迭代都会拷贝一个FVector for (FVector Point : PathPoints) { Point.Z 100.0f; // 修改的是拷贝不影响原数组 } // 好使用引用直接操作原数组中的元素 for (FVector Point : PathPoints) { Point.Z 100.0f; // 直接修改原数组 } // 如果只需要读取使用常量引用 float TotalLength 0.0f; for (const FVector Point : PathPoints) { TotalLength Point.Size(); }2.2.3 实现操作符重载为了让自定义类型支持像内置类型一样的自然语法操作符重载经常返回引用。struct FMyVector { float X, Y, Z; // 重载操作符返回引用以支持链式调用 (a b c) FMyVector operator(const FMyVector Other) { X Other.X; Y Other.Y; Z Other.Z; return *this; // 返回当前对象的引用 } };3.3 模棱两可的场景与决策指南有些情况似乎两者都可以这时需要根据具体语义和未来扩展性来决定。2.3.1 函数返回一个“找到”的对象如果函数保证总是返回一个有效对象例如获取玩家控制的Pawn那么返回引用可能更清晰。但如果存在找不到的可能性必须返回指针或使用TOptional等现代C类型。// 场景获取游戏中的唯一玩家控制器假定总是存在 APlayerController GetFirstPlayerController() const { APlayerController* PC GetWorld()-GetFirstPlayerController(); check(PC ! nullptr); // 如果为空断言失败帮助在开发期发现问题 return *PC; // 解引用指针返回引用 } // 场景通过ID查找玩家可能找不到 APlayerState* FindPlayerStateById(int32 PlayerId) { // ... 查找逻辑 return FoundPlayer ? FoundPlayer : nullptr; // 可能返回空 }2.3.2 类成员变量存储非UObject对象对于非UObject的复杂类型如果该对象是当前类“拥有”的组成部分组合关系通常直接作为值成员。如果需要共享或可能为空则使用指针通常是智能指针TSharedPtr或TUniquePtr或引用但引用作为成员比较少见因为必须在构造函数初始化列表中初始化。class FMyComplexSystem { private: // 值成员MyConfig是FMySystemConfig类型的一部分生命周期与FMyComplexSystem一致 FMySystemConfig MyConfig; // 智能指针指向动态分配的资源可能为空所有权明确 TUniquePtrFExternalData ExternalDataPtr; // 原始指针指向一个不由本类管理的对象观察者模式 FSomeOtherSystem* ObservedSystem; // 需要小心生命周期管理 // 引用成员必须在构造函数中初始化且之后不能改变指向 // 通常用于注入依赖表示一个必须存在的协作对象 IAudioInterface AudioService; public: FMyComplexSystem(IAudioInterface InAudioService) : AudioService(InAudioService) // 引用成员必须在初始化列表中初始化 { // ObservedSystem可能稍后设置所以可以不在初始化列表 // MyConfig会调用默认构造函数 // ExternalDataPtr默认为空 } };在UE4中对于非UObject的大型数据成员直接作为值存储是常见做法因为UE4的结构体如FVector,FTransform通常设计为轻量且可快速拷贝。如果需要多态或者对象可能非常大且拷贝昂贵才考虑使用指针或引用。4. UE4特有的注意事项与深度解析UE4在标准C的基础上引入了一套自己的对象模型和内存管理机制这使得和*的使用出现了一些特殊的规则和最佳实践。4.1 UPROPERTY()宏与指针的强制绑定这是UE4 C中最具特色的一点。任何需要被UE4反射系统识别、参与序列化保存/加载、网络复制或垃圾回收的UObject派生类成员变量必须声明为指针并且用UPROPERTY()宏修饰。UCLASS() class AMyGameMode : public AGameModeBase { GENERATED_BODY() public: // 正确UObject派生类成员必须是指针并用UPROPERTY修饰 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category Players) TSubclassOfAPawn DefaultPawnClass; // TSubclassOf内部也是指针 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Components) USceneComponent* RootSceneComponent; UPROPERTY(BlueprintReadWrite, Category Game State) APlayerState* CurrentLeader; // 错误不能将UObject派生类作为非指针成员值语义 // AMyActor MyActorMember; // 编译错误 // 错误即使是指针缺少UPROPERTY()垃圾回收时将无法追踪可能导致悬空指针 // AMyActor* UnsafeActorPtr; };底层原理UE4的垃圾回收器通过UPROPERTY()宏标记的指针来构建对象引用图。如果一个UObject指针没有被UPROPERTY()修饰垃圾回收器就不知道有指针指向它当其他引用都消失时该对象会被错误地回收留下一个指向无效内存的“悬空指针”访问它会导致崩溃。4.2 常量正确性Const Correctness在UE4中的运用正确使用const与、*的组合是写出健壮接口的关键。它能明确表达函数的意图防止意外修改并帮助编译器进行优化。3.2.1 常量指针 vs 指向常量的指针这是一个经典的容易混淆的点AActor* Ptr; // 非常量指针指向非常量对象可以改指针也可以改对象 const AActor* PtrToConst; // 非常量指针指向常量对象可以改指针不能通过指针改对象 AActor* const ConstPtr; // 常量指针指向非常量对象不能改指针可以改对象 const AActor* const ConstPtrToConst; // 常量指针指向常量对象都不能改在UE4中最常用的是const AActor*指向常量对象的指针用于表示函数不会修改该对象。// 函数声明承诺不会通过该指针修改Actor void DisplayActorInfo(const AActor* ActorToDisplay) { if (ActorToDisplay) { FString Name ActorToDisplay-GetName(); FVector Loc ActorToDisplay-GetActorLocation(); // ActorToDisplay-Destroy(); // 错误不能通过const指针调用非const成员函数 } }3.2.2 常量引用作为函数参数这是UE4函数接口设计的黄金标准之一用于传递大型只读参数。// 优秀的函数签名 bool IsLocationInBounds(const FVector TestLocation, const FBox WorldBounds); void SortPlayerArray(TArrayAPlayerState* PlayersToSort); // 需要修改容器本身 void GetSortedPlayers(const TArrayAPlayerState* Players, TArrayAPlayerState* OutSortedPlayers); // 输入只读输出通过引用参数 // 在头文件中广泛应用的例子 (Engine/World.h) bool K2_LineTraceSingle(struct FHitResult OutHit, const FVector Start, const FVector End, ...);个人心得在设计函数时先问自己“这个参数需要被修改吗”如果不需要立刻加上const。这不仅是好习惯更能让代码的使用者一眼明白你的意图减少犯错的可能。4.3 TArray TMap TSet与引用/指针的协作UE4的容器类与引用、指针的配合使用非常频繁这里有些细节需要注意。3.3.1 容器存储指针当容器存储UObject指针时配合UPROPERTY()可以自动被垃圾回收管理。UCLASS() class AInventory : public AActor { GENERATED_BODY() public: // TArray存储指针UPROPERTY确保容器内指针被GC追踪 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category Inventory) TArrayAItem* Items; // 添加元素 void AddItem(AItem* NewItem) { if (NewItem !Items.Contains(NewItem)) { Items.Add(NewItem); // 存储的是指针不是拷贝 } } // 遍历元素 AItem* FindItemByName(const FString Name) const { for (AItem* Item : Items) // 使用范围for循环Item是指针 { if (Item Item-GetItemName() Name) { return Item; } } return nullptr; } };注意TArrayAItem*存储的是原始指针。如果这些AItem对象在其他地方被Destroy()了这个数组里就会留下悬空指针。UE4的垃圾回收只针对UObject本身不会自动把容器里的指针置空。安全做法是重写对象的BeginDestroy()或使用弱引用(TWeakObjectPtr)。3.3.2 容器存储非指针对象与引用遍历当容器存储的是结构体等值类型时在遍历修改时使用引用至关重要。// 假设FPlayerScore是一个结构体 struct FPlayerScore { FString PlayerName; int32 Score; }; TArrayFPlayerScore PlayerScores; // 需要更新分数时使用引用遍历 void UpdateScore(const FString PlayerName, int32 DeltaScore) { for (FPlayerScore ScoreEntry : PlayerScores) // 注意是 FPlayerScore { if (ScoreEntry.PlayerName PlayerName) { ScoreEntry.Score DeltaScore; // 直接修改原数组元素 break; } } } // 仅用于读取时使用常量引用遍历避免拷贝 int32 GetTotalScore() const { int32 Total 0; for (const FPlayerScore ScoreEntry : PlayerScores) // 注意是 const FPlayerScore { Total ScoreEntry.Score; } return Total; }4.4 智能指针TUniquePtr, TSharedPtr与原始指针的选用现代C推荐使用智能指针管理动态内存的所有权。UE4也提供了自己的智能指针TUniquePtr独占所有权和TSharedPtr共享所有权。它们与原始指针*和引用如何配合3.4.1 何时使用智能指针TUniquePtrT当你拥有一个对象的唯一所有权并且该对象不是UObjectUObject由GC管理。当TUniquePtr离开作用域时它会自动删除所指向的对象。适用于管理纯C类的生命周期。#include MyNonUObjectClass.h class FMyResourceManager { private: TUniquePtrFMyNonUObjectClass ExclusiveResource; // 独占资源 public: FMyResourceManager() : ExclusiveResource(MakeUniqueFMyNonUObjectClass()) { } // 析构时ExclusiveResource会自动删除其指向的对象 };TSharedPtrT/TSharedRefT当多个对象需要共享一个非UObject资源的所有权时使用。TSharedRef类似于引用不能为空TSharedPtr类似于指针可以为空。引用计数降为0时自动删除对象。class FTextureData { /* ... */ }; TSharedPtrFTextureData SharedTextureData MakeSharedFTextureData(); // 多个地方可以持有这个共享指针 class FMaterialA { TSharedPtrFTextureData TextureRef; }; class FMaterialB { TSharedPtrFTextureData TextureRef; }; // 当所有Material都不再引用TextureData时它会被自动释放。3.4.2 与原始指针和引用的交互智能指针可以方便地获取其内部的原始指针用于需要原始指针的API比如很多UE4函数接口。void ProcessTexture(FTextureData* RawPtr) { /* ... */ } TSharedPtrFTextureData SmartPtr MakeSharedFTextureData(); // 将智能指针获取的原始指针传递给函数 ProcessTexture(SmartPtr.Get()); // .Get() 返回 T* // 如果函数不会修改对象且你需要表达“可能为空”可以这样 if (SmartPtr.IsValid()) { ProcessTexture(SmartPtr.Get()); } // 对于TSharedRef因为它保证非空可以直接解引用或获取指针 TSharedRefFTextureData SharedRef MakeSharedFTextureData(); FTextureData DataRef *SharedRef; // 转换为引用 FTextureData* DataPtr DataRef; // 或获取指针重要原则在UE4中对于UObject派生类永远不要用new/delete或智能指针来管理其生命周期。UObject的生命周期由引擎的垃圾回收系统管理使用NewObjectT()或SpawnActorT()创建并通过UPROPERTY()标记的原始指针或TWeakObjectPtr来引用。智能指针仅用于非UObject的纯C类。5. 常见陷阱、调试技巧与最佳实践总结即使理解了概念在实际编码中仍然会踩坑。下面是我在项目中总结的一些常见问题和应对策略。5.1 悬空指针Dangling Pointer问题这是使用指针时最危险的错误。指针指向的内存已经被释放但指针本身还未被置空。// 危险代码示例 ACharacter* GetLocalPlayerCharacter() { APlayerController* PC GetWorld()-GetFirstPlayerController(); if (PC) { return PC-GetCharacter(); // 返回一个指针 } return nullptr; } void SomeFunction() { ACharacter* MyChar GetLocalPlayerCharacter(); // ... 一些其他代码 ... if (MyChar) // 此时检查可能还是true { // 但如果在这期间角色被Destroy了比如死亡、关卡切换 MyChar-AddMovementInput(FVector::ForwardVector, 1.0f); // 崩溃 } }解决方案对于UObject使用IsValid()函数进行检查它比简单的if (Ptr)更安全因为它会检查指针是否为空以及对象是否处于待销毁状态IsPendingKill。if (IsValid(MyChar)) { MyChar-AddMovementInput(FVector::ForwardVector, 1.0f); }使用弱引用如果不负责对象生命周期只是观察者使用TWeakObjectPtrT。TWeakObjectPtrACharacter WeakCharPtr GetLocalPlayerCharacter(); // ... if (ACharacter* Char WeakCharPtr.Get()) // .Get()会返回有效的指针或nullptr { Char-AddMovementInput(FVector::ForwardVector, 1.0f); }明确所有权理清代码中谁“拥有”对象谁只是“使用”对象。拥有者负责生命周期使用者使用弱引用或共享指针针对非UObject。5.2 引用绑定到临时对象生命周期问题引用必须绑定到一个具有足够生命周期的对象上。绑定到临时对象如函数返回值是未定义行为。// 错误示例 const FString GetBadReference() { FString LocalString TEXT(Hello); return LocalString; // 返回局部变量的引用LocalString在函数结束时被销毁。 } // 函数结束LocalString内存被释放返回的引用变成“悬空引用”。 void ProblematicCode() { const FString BadRef GetBadReference(); // BadRef绑定到了已销毁的内存 UE_LOG(LogTemp, Warning, TEXT(%s), *BadRef); // 可能崩溃或输出乱码 }解决方案如果函数需要返回一个新创建的对象直接返回值编译器通常会进行返回值优化RVO。FString CreateGreeting(const FString Name) { return FString::Printf(TEXT(Hello, %s!), *Name); // 正确返回FString对象 }如果需要返回一个已存在对象的引用确保该对象的生命周期长于引用。class AMyGameState { FString GlobalGameName; public: const FString GetGameName() const { return GlobalGameName; } // 安全返回成员变量的引用 };5.3 指针和引用的误用导致性能或逻辑错误4.3.1 不必要的指针拷贝对于小型结构体使用指针反而可能因为间接寻址而降低性能并增加代码复杂度。// 不必要FVector很小3个float传值或传引用即可 void ProcessVector(const FVector* InVector) { if (InVector) // 每次使用都要检查 { float X InVector-X; } } // 更好直接传常量引用 void ProcessVectorBetter(const FVector InVector) { float X InVector.X; // 无需检查更简洁高效 }4.3.2 混淆指针和引用的修改语义void TryToReassignPointer(AActor* ActorPtr) { AActor* NewActor GetNewActor(); ActorPtr NewActor; // 这只修改了函数内局部指针副本的指向不影响实参 } void TryToReassignReference(AActor* ActorRef) // 注意参数类型指针的引用 { AActor* NewActor GetNewActor(); ActorRef NewActor; // 这会修改调用方传入的指针变量本身 } void Caller() { AActor* MyActor GetActorA(); TryToReassignPointer(MyActor); // MyActor 仍然指向 GetActorA() 返回的对象 TryToReassignReference(MyActor); // MyActor 现在指向 GetNewActor() 返回的对象 }如果你想修改指针本身即让指针指向另一个对象你需要传递指针的引用AActor*或指针的指针AActor**。而如果你想修改指针所指向的对象直接通过指针解引用操作即可。5.4 UE4 C中和*的最佳实践清单根据上面的分析我总结了一份在UE4开发中关于和*的快速决策清单成员变量UObject派生类一律使用原始指针 (T*)并且如果需要在蓝图中访问、需要被序列化或网络复制必须加UPROPERTY()。非UObject的大型对象/需要多态考虑使用智能指针 (TUniquePtrT,TSharedPtrT) 或原始指针如果所有权明确且生命周期简单。小型结构体/POD类型直接作为值成员。引用成员慎用仅用于表示类所依赖的、生命周期更长的外部服务依赖注入且必须在构造函数初始化列表中初始化。函数参数内置类型 (int, float, bool)直接传值。UObject派生类传指针 (T*)。如果函数内不修改对象用const T*。其他所有类型 (FString, TArray, FVector等)如果只读用const T。如果需要修改用T。如果可选可能为空用T*可传nullptr。需要修改指针本身用T*指针的引用。函数返回值新创建的对象非UObject直接返回值利用RVO。返回已存在的对象且保证非空返回const T或T。返回可能不存在的对象返回T*可能为nullptr或TOptionalT。UObject派生类返回T*。遍历容器for (const auto Item : Container)只读遍历无拷贝。for (auto Item : Container)需要修改容器内元素。for (auto* Item : Container)容器内存储的是指针如TArrayAActor*。安全性使用任何UObject指针前用IsValid()检查。不要返回局部变量的引用或地址。明确每个指针的所有权谁创建谁销毁。对于非UObject的动态内存优先使用智能指针而非原始new/delete。理解和*的区别并能在UE4的上下文中正确应用是写出高质量C代码的关键一步。这不仅仅是语法问题更关乎你对程序内存模型、对象生命周期和UE4引擎机制的理解。开始的时候可能会觉得需要刻意选择但随着练习这些选择会变成一种本能你的代码也会因此变得更加清晰、高效和健壮。
返回列表