【UE5 C++课程系列笔记】20——共享指针的简单使用
目录
概念
创建共享指针示例
重设共享指针
共享指针内容转移
共享指针和共享引用的转换
判断共享指针的相等性
共享指针访问成员函数
自定义删除器
概念
共享指针(主要以 TSharedPtr
为例),TSharedPtr
基于引用计数机制来工作,旨在解决对象所有权共享以及确保在合适的时候自动释放对象资源的问题。它允许多个 TSharedPtr
实例指向同一个对象,虚幻引擎会在内部记录该对象被引用的次数(即引用计数),每当有新的 TSharedPtr
通过拷贝构造、赋值等方式开始指向这个对象时,引用计数就会增加;而当一个 TSharedPtr
实例结束其生命周期(比如离开作用域、被重新赋值等情况),引用计数会相应地减少,当引用计数最终降为 0
时,所指向的对象就会被自动销毁,释放其占用的内存等资源。
创建共享指针示例
1. 新建一个actor,这里命名为“SmartActor”,在“SmartActor”中定义一个名为“FSmartPtrStruct”的结构体。通过继承 TSharedFromThis<FSmartPtrStruct>
,FSmartPtrStruct
类型的对象可以从自身获取到一个指向自己的 TSharedPtr
共享指针
2. 在“SmartPtrActor”中定义一个方法“InitSPStruct”,用于创建共享指针。
3. 实现“InitSPStruct”如下,主要展示了几种不同方式创建和使用共享指针(TSharedPtr
)的示例,涉及创建空白共享指针、指向新创建对象的共享指针、从共享引用转换而来的共享指针以及创建线程安全的共享指针。
void ASmartPtrActor::InitSPStruct()
{// 创建一个空白的共享指针TSharedPtr<FSmartPtrStruct> EmptyPointer;// 为新对象创建一个共享指针TSharedPtr<FSmartPtrStruct> NewPointer(new FSmartPtrStruct(100));// 从共享引用创建一个共享指针TSharedRef<FSmartPtrStruct> NewReference(new FSmartPtrStruct(200));TSharedPtr<FSmartPtrStruct> PointerFromReference = NewReference;NewReference->aa = 123;// 创建一个线程安全的共享指针TSharedPtr<FSmartPtrStruct, ESPMode::ThreadSafe> NewThreadsafePointer = MakeShared<FSmartPtrStruct, ESPMode::ThreadSafe>(300);
}
4. 编译后创建基于“SmartPtrActor”的蓝图类“BP_SmartPtrActor”
将“BP_SmartPtrActor”拖入视口
5. 打开“BP_SmartPtrActor”,在BeginPlay时调用方法“InitSPStruct”
6. 运行后可以看到输出日志如下
重设共享指针
重设共享指针的目的是释放共享指针所指向的对象资源,使共享指针不再持有对相应对象的引用,以此来管理对象的生命周期以及避免悬空指针等问题,确保内存管理的正确性。通常可以通过使用Reset
方法重置共享指针,或通过赋值 nullptr
重置共享指针。
如下代码所示,通过调用Reset
方法和赋值 nullptr,
使 Pointer1、
Pointer2
变为空指针状态,不再持有对任何对象的引用,方便后续对其进行重新赋值等操作,确保内存管理的有序性和对象生命周期的合理控制。
void ASmartPtrActor::ResetSPStruct()
{TSharedPtr<FSmartPtrStruct> Pointer1(new FSmartPtrStruct(111));TSharedPtr<FSmartPtrStruct> Pointer2(new FSmartPtrStruct(222));Pointer1.Reset();Pointer2 = nullptr;
}
共享指针内容转移
可以使用 MoveTemp
(或 MoveTempIfPossible
)函数将一个共享指针的内容转移到另一个共享指针,转移共享指针(TSharedPtr
)所指向对象的所有权,目的是在不进行额外拷贝(避免不必要的资源复制开销)的情况下,重新分配对象的所有权归属,以优化对象生命周期管理和内存使用效率,尤其适用于一些涉及对象传递、赋值等场景中避免多余的构造和析构操作带来的性能损耗。
如下代码所示,通过 MoveTemp
将Pointer1的内容移至Pointer2,在此之后,Pointer1将引用空指针。然后再通过MoveTempIfPossible
将Pointer2的内容移至Pointer1,在此之后,Pointer2将引用空指针。
void ASmartPtrActor::MoveSPStruct()
{TSharedPtr<FSmartPtrStruct> Pointer1(new FSmartPtrStruct(111));TSharedPtr<FSmartPtrStruct> Pointer2;Pointer2 = MoveTemp(Pointer1); //将Pointer1的内容移至Pointer2,在此之后,Pointer1将引用空指针Pointer1 = MoveTempIfPossible(Pointer2); //将Pointer2的内容移至Pointer1,在此之后,Pointer2将引用空指针
}
运行结果如下,一开始Pointer1有内容,Pointer2无内容,经过第43行代码后Pointer1为空指针,Pointer1的内容移至Pointer2。
经第44行代码后,Pointer2为空指针,Pointer2的内容又移回Pointer1。
共享指针和共享引用的转换
从共享指针转换为共享引用可以使用 ToSharedRef
函数,但这个转换有一个重要前提,那就是共享指针必须是有效的,即它当前指向一个实际存在的对象(也就是其内部所管理对象的引用计数大于 0
)。而共享引用可以直接隐式转换为共享指针。
如下代码所示,展示了共享引用(TSharedRef
)和共享指针(TSharedPtr
)之间相互转换。
第49行代码创建了一个 TSharedRef
类型的共享引用 MySharedReference
,它通过 new
关键字创建一个 FSmartPtrStruct
类型的对象,并将其传递给 TSharedRef
的构造函数,使得 MySharedReference
指向这个新创建的对象。TSharedRef
表示对对象的一种强引用,它在创建时必须绑定一个有效的对象,并且在对象的整个生命周期内,只要有 TSharedRef
指向它,该对象就不会被销毁(通过虚幻引擎内部的引用计数机制来管理,这里对象的引用计数初始为 1
,因为只有这一个共享引用指向它)。
第50行代码实现了从共享引用 MySharedReference
到共享指针 MySharedPointer
的转换。当进行此转换时,会将所指向对象的引用计数加 1
(因为又多了一个共享指针来管理这个对象),现在对象的引用计数变为 2
,MySharedPointer
同样指向 MySharedReference
所指向的那个 FSmartPtrStruct
对象,后续可以通过 MySharedPointer
来访问对象的成员变量、调用成员函数等操作,就像使用普通指针一样,只要对象的引用计数大于 0
,即还有共享指针或共享引用指向它,对象就是有效的。
第51~54行代码首先通过 MySharedPointer.IsValid()
来检查共享指针 MySharedPointer
是否有效,当条件成立后,尝试从共享指针 MySharedPointer
再转换回共享引用 BackReference
。
第55行代码通过调用 MySharedPointer.Reset()
函数对共享指针进行重置操作,将 MySharedPointer
所指向的当前对象释放掉。
第56行代码是一个错误的操作,因为第55行代码已经通过 Reset
函数将 MySharedPointer
重置为空指针了,此时再尝试调用 ToSharedRef
函数从这个空指针转换为共享引用是不合法的,会导致程序出现未定义行为从而引发运行时错误。ToSharedRef
函数要求其调用的共享指针必须是有效的,指向一个实际存在的对象,才能进行转换操作。
判断共享指针的相等性
如下代码所示,主要用于测试两个共享指针(TSharedPtr
)是否指向同一个对象,通过使用重载的 ==
运算符来进行相等性判断。在如下代码中,首先创建了 Pointer1
并通过 MakeShareable(new FSmartPtrStruct())
让它指向一个新创建的 FSmartPtrStruct
类型的对象。然后使用赋值语句 Pointer2 = Pointer1;
,这样 Pointer2
就获得了与 Pointer1
相同的指向,它们都指向了最初由 Pointer1
指向的那个对象。此时,该对象的引用计数会从 1
(仅有 Pointer1
指向时)变为 2
,Pointer1 == Pointer2
的比较结果为 true。
void ASmartPtrActor::EqualSPStruct()
{TSharedPtr<FSmartPtrStruct> Pointer1;TSharedPtr<FSmartPtrStruct> Pointer2;Pointer1 = MakeShareable(new FSmartPtrStruct(100));;Pointer2 = Pointer1;if (Pointer1 == Pointer2){UE_LOG(LogTemp, Warning, TEXT("Pointer1=Pointer2"));}
}
共享指针访问成员函数
这里介绍3种访问成员函数/变量的方式:
1. 通过 ->
运算符访问成员函数;
2. 通过 Get
函数获取原始指针后访问成员函数
3. 通过解引用操作符 *
访问成员函数
解引用操作的本质是获取共享指针所指向对象的实际内存地址对应的内容,也就是通过共享指针找到对应的对象实例,然后可以对该对象进行操作,比如访问成员变量、调用成员函数等。但需要注意的是,在进行解引用操作之前,必须确保共享指针是有效的,即它指向一个实际存在的对象,否则对空的共享指针进行解引用会导致程序出现未定义行为从而导致程序崩溃等严重后果。
除了通过解引用操作访问成员变量外,还可以在确保共享指针有效的条件下,直接通过共享指针来访问所指向对象的成员变量、成员函数等。
如下代码所示,主要展示了通过共享指针(TSharedPtr
)对其所指向对象的成员函数PrintAA
进行访问的几种不同方式。
第74行代码通过 ->
运算符访问成员函数。->
运算符会先对 Pointer
进行解引用操作,找到其所指向的 FSmartPtrStruct
对象,然后调用该对象的 PrintAA
成员函数。
第75行代码通过 Get
函数获取原始指针后访问成员函数。Get
函数是 TSharedPtr
提供的一个成员函数,它的作用是返回共享指针所指向对象的原始指针(也就是类似普通指针的形式)。在这行代码中,Pointer.Get()
会获取到 Pointer
所指向的 FSmartPtrStruct
对象的原始指针,再通过 ->
运算符调用 PrintAA
成员函数。
第76行代码通过解引用操作符 *
访问成员函数。使用 *
运算符对共享指针 Pointer
进行解引用,得到其所指向的 FSmartPtrStruct
对象本身(相当于将共享指针转换为普通对象的引用形式),然后再通过 .
运算符来调用 PrintAA
成员函数。这种方式与使用 ->
运算符的功能是等价的,只是语法形式上有所不同,不过在实际编程中,使用 ->
运算符更加简洁直观,所以通常更推荐使用 Pointer->PrintAA();
的形式。
PrintAA()
如下:
代码执行结果如下,可以看到3种方式都可以成功访问成员函数。
自定义删除器
自定义删除器是一个很有用的特性,它允许你指定在智能指针释放其所管理对象时执行的特定清理逻辑。
自定义删除器的意义:
在如下代码中,使用 TSharedPtr
(共享指针)并为其指定自定义删除器(以 lambda 表达式的形式)的用法。其目的是在共享指针所管理的对象生命周期结束(即引用计数变为 0 时),执行自定义的清理逻辑。
通过 new FSmartPtrStruct(100)
创建了一个 FSmartPtrStruct
类型的对象,这个对象的构造函数会被调用,输出相应的构造日志信息,并且其成员变量 aa
的初始值被设置为 100
。然后将这个新创建的对象传递给 TSharedPtr
的构造函数,使得 NewPointer
共享指针指向这个对象,此时该对象的引用计数初始为 1
,由 NewPointer
通过虚幻引擎内部的引用计数机制来管理其生命周期。同时,在 TSharedPtr
的构造函数中还传入了一个 lambda 表达式作为自定义删除器。这个 lambda 表达式接收一个 FSmartPtrStruct*
类型的指针参数 Obj
,代表了共享指针所管理的对象指针。
执行结果如下,从打印结果可以看到,虽然执行了FSmartPtrStruct的构造函数,却没有执行析构。
“SmartPtrActor”完整代码:
// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SmartPtrActor.generated.h"struct FSmartPtrStruct :public TSharedFromThis<FSmartPtrStruct>
{FSmartPtrStruct(int32 Inaa):aa(Inaa){UE_LOG(LogTemp, Warning, TEXT("FSmartPtrStruct Construction aa = %d"), aa);}~FSmartPtrStruct(){UE_LOG(LogTemp, Warning, TEXT("FSmartPtrStruct Destruction aa = %d"), aa);}void PrintAA() {UE_LOG(LogTemp, Warning, TEXT("aa = %d"), aa);}int32 aa = -1;
};UCLASS()
class STUDY_API ASmartPtrActor : public AActor
{GENERATED_BODY()public: // Sets default values for this actor's propertiesASmartPtrActor();UFUNCTION(BlueprintCallable)void InitSPStruct();UFUNCTION(BlueprintCallable)void ResetSPStruct();UFUNCTION(BlueprintCallable)void MoveSPStruct();UFUNCTION(BlueprintCallable)void ConvertSPStruct();UFUNCTION(BlueprintCallable)void EqualSPStruct();UFUNCTION(BlueprintCallable)void AccessSPStruct();UFUNCTION(BlueprintCallable)void DeleteStruct();protected:// Called when the game starts or when spawnedvirtual void BeginPlay() override;public: // Called every framevirtual void Tick(float DeltaTime) override;};
// Fill out your copyright notice in the Description page of Project Settings.#include "SmartPointer/SmartPtrActor.h"// Sets default values
ASmartPtrActor::ASmartPtrActor()
{// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.PrimaryActorTick.bCanEverTick = true;}void ASmartPtrActor::InitSPStruct()
{// 创建一个空白的共享指针TSharedPtr<FSmartPtrStruct> EmptyPointer;// 为新对象创建一个共享指针TSharedPtr<FSmartPtrStruct> NewPointer(new FSmartPtrStruct(100));// 从共享引用创建一个共享指针TSharedRef<FSmartPtrStruct> NewReference(new FSmartPtrStruct(200));TSharedPtr<FSmartPtrStruct> PointerFromReference = NewReference;NewReference->aa = 123;// 创建一个线程安全的共享指针TSharedPtr<FSmartPtrStruct, ESPMode::ThreadSafe> NewThreadsafePointer = MakeShared<FSmartPtrStruct, ESPMode::ThreadSafe>(300);
}void ASmartPtrActor::ResetSPStruct()
{TSharedPtr<FSmartPtrStruct> Pointer1(new FSmartPtrStruct(111));TSharedPtr<FSmartPtrStruct> Pointer2(new FSmartPtrStruct(222));Pointer1.Reset();Pointer2 = nullptr;
}void ASmartPtrActor::MoveSPStruct()
{TSharedPtr<FSmartPtrStruct> Pointer1(new FSmartPtrStruct(111));TSharedPtr<FSmartPtrStruct> Pointer2;Pointer2 = MoveTemp(Pointer1); //将Pointer1的内容移至Pointer2,在此之后,Pointer1将引用空指针Pointer1 = MoveTempIfPossible(Pointer2); //将Pointer2的内容移至Pointer1,在此之后,Pointer2将引用空指针
}void ASmartPtrActor::ConvertSPStruct()
{TSharedRef<FSmartPtrStruct> MySharedReference(new FSmartPtrStruct(100));TSharedPtr<FSmartPtrStruct> MySharedPointer = MySharedReference;if (MySharedPointer.IsValid()){TSharedRef<FSmartPtrStruct> BackReference = MySharedPointer.ToSharedRef();}/*MySharedPointer.Reset();TSharedRef<FSmartPtrStruct> BackReferenceNULL = MySharedPointer.ToSharedRef();*/
}void ASmartPtrActor::EqualSPStruct()
{TSharedPtr<FSmartPtrStruct> Pointer1;TSharedPtr<FSmartPtrStruct> Pointer2;Pointer1 = MakeShareable(new FSmartPtrStruct(100));;Pointer2 = Pointer1;if (Pointer1 == Pointer2){UE_LOG(LogTemp, Warning, TEXT("Pointer1=Pointer2"));}
}void ASmartPtrActor::AccessSPStruct()
{TSharedPtr<FSmartPtrStruct> Pointer(new FSmartPtrStruct(111));Pointer->PrintAA();Pointer.Get()->PrintAA();(*Pointer).PrintAA();
}void ASmartPtrActor::DeleteStruct()
{TSharedPtr<FSmartPtrStruct> NewPointer(new FSmartPtrStruct(100), [](FSmartPtrStruct* Obj) {UE_LOG(LogTemp, Warning, TEXT("aa = %d"), Obj->aa);});
}// Called when the game starts or when spawned
void ASmartPtrActor::BeginPlay()
{Super::BeginPlay();}// Called every frame
void ASmartPtrActor::Tick(float DeltaTime)
{Super::Tick(DeltaTime);}