此文章为下面文章的整理,感谢前辈
智能指针的介绍
UE 中的 UObject 本身就可以帮我们做回收,做释放的。我们可以看到 UE4 中所有的类都是继承自UObject,这样的话当我们引擎被关闭的时候,它会帮我们自动释放这些资源,或者是我们手动去释放掉它,这样能保证能彻底删掉它。
UE4 对原生对象(即不继承 UObject 的类)不提供垃圾回收,需要手动进行清理。
原生对象创建和销毁的方式有:malloc / free
和 new / delete
。new 与 malloc 的区别在于,new 在分配内存完成之后会调用构造函数。
我们会遇到这样的问题:
- 创建一个类之后,如果忘记把它销毁,则它占的内存可能永远都无法被释放掉。
- 创建一个类之后,有其它的指针指向了它,结果不小心把它释放了,其它指针依然保留了原始的信息,导致我们在访问这个类的时候而产生野指针奔溃。
为了防止这些问题的出现,智能指针就出现了,它就是用来管理这些非继承 UObject 的类。
智能指针
UE4 提供共享指针库来管理内存,它是 C++11 智能指针的自定义实现,虚拟智能指针库和C++智能指针在功能和效率方面都有一些共同的特点。
智能指针优点
- 可以像常规的 C++ 指针那样复制,解引用,比较共享指针等
- 防止内存泄漏:共享引用不存在时,智能指针(弱指针除外)会自动删除对象。
- 弱引用:弱指针会中断引用循环并阻止悬挂指针。
- 包含了可以通过多线程安全地进行访问的“线程安全”版本
- 可以创建任何类型的对象的共享指针
- 支持针“const”、前置声明的不完全类型、类型转换等
- 等
为什么使用 UE4 的智能指针库?
std::shared_ptr
不是在所有的平台都能用的- 可以和其它虚幻容器及类型无缝协作
- 更好的控制平台特性,包括线程处理和优化
- 等
可以查看详细的官方文档。
如何使用智能指针?
智能指针的主要类型分为:TSharedPtr, TSharedRef, TWeakPtr, TUniquePtr。
先声明一个测试类
class TestA
{
public:
int32 a;
float b;
};
TSharedPtr 共享指针
TSharedPtr 为共享指针。
创建(MakeShareable
)/初始化/重置(Reset
)
用法如下:
void TestSharedPtr()
{
//声明
TSharedPtr<TestA> MyTestA;
//分配内存
MyTestA = MakeShareable(new TestA());
//先判读智能指针是否有效
if (MyTestA.IsValid() || MyTestA.Get())
{
//访问
int32 a = MyTestA->a;
//复制指针
TSharedPtr<TestA> MyTesta = MyTestA;
//获取共享指针引用计数
int32 Count = MyTestA.GetSharedReferenceCount();
//销毁对象
MyTesta.Reset();
}
};
TSharedPtr 不能指向 UObject。如果想要指向UObject,可以使用 TWeakObjectPtr
。TSharedPtr 可以对 FStructures 使用。
TSharedPtr 可以在 .h 中定义,并且可以置为空(= nullptr
)。
TShareRef 不允许在 .h 中定义的,并且一直有值,下面会介绍。
TSharedPtr 的 .Get()
方法将 TSharedPtr 转换成 C++ 原生指针。在访问 TSharedPtr 时,需要先通过 .IsValid()
或者 .Get()
判断共享指针是否有效。如果指针是无效的,调用重载的操作符 ->
去获取值时,直接会导致断言(check)失败,将会导致奔溃。
FORCEINLINE
ObjectType* operator->() const
{
// 可以看到这里的check
check( IsValid() );
return Object;
}
注意:MyTestA.IsValid()
中 “.” 是访问共享指针的成员,而 MyTestA->a
中 “->” 是访问这个指针指向的对象中的成员。
Reset()
用于重置共享指针。
TSharePtr 和 C++11 的 shareptr 一样,内部都是基于引用计数的。所以可以通过GetSharedReferenceCount()
获取到当前的引用计数
复制
复制共享指针
// 复制共享指针
TSharedPtr<SimpleObject> simObjectPtr_copy = simObjectPtr;
UE_LOG(LogTemp, Warning, TEXT("引用计数: simObjectPtr[%d], simObjectPtr_copy[%d],"),
simObjectPtr.GetSharedReferenceCount(), simObjectPtr_copy.GetSharedReferenceCount());
输出:
引用计数: simObjectPtr[2], simObjectPtr_copy[2]
转移
转移可以通过 MoveTemp
或 MoveTempIfPossible
。
// 转移共享指针
TSharedPtr<SimpleObject> simObjectPtr_MoveTemp = MoveTemp(simObjectPtr_copy); // 另 MoveTempIfPossible()
UE_LOG(LogTemp, Warning, TEXT("引用计数: simObjectPtr[%d], simObjectPtr_copy[%d], simObjectPtr_MoveTemp[%d]"),
simObjectPtr.GetSharedReferenceCount(), simObjectPtr_copy.GetSharedReferenceCount(), simObjectPtr_MoveTemp.GetSharedReferenceCount()
输出:
引用计数: simObjectPtr[2], simObjectPtr_copy[0], simObjectPtr_MoveTemp[2]
条件判断 / 对比 / 解引用与访问
->
运算符Get()
函数IsValid()
函数==
和!=
运算符
if (simObjectPtr) // 条件判断
{
simObjectPtr->ExeFun(); // 解引用
}
if (simObjectPtr.Get() != nullptr) // 条件判断
{
simObjectPtr.Get()->ExeFun(); //解引用
}
if (simObjectPtr.IsValid()) // 条件判断
{
(*simObjectPtr).ExeFun(); // 解引用
}
if (simObjectPtr == simObjectPtr_copy) // 对比
{
UE_LOG(LogTemp, Warning, TEXT("simObjectPtr_copy == simObjectPtr"));
}
TSharedRef 共享引用
TSharedRef 为共享引用,共享引用不可为空,所以它没有 IsValid
方法,因为它一直是有效的。TSharedRef 不可用于 UObject 对象。
创建/初始化
MakeShareable()
/ MakeShared<T>()
函数
void TestSharedRef()
{
// 创建共享引用
TSharedRef<SimpleObject> objRef(new SimpleObject());
TSharedRef<SimpleObject> objRef2 = MakeShareable(new SimpleObject());
TSharedRef<SimpleObject> objRef3 = MakeShared<SimpleObject>();
//访问:
int32 a = objRef->a;//方法一
int32 b = (*objRef).a;//方法二
//销毁对象
objRef.Reset();
};
注意:
- 与共享指针类似,固定引用非空对象,使用时候直接用->即可,因为一直有有效值;
- ShareRef 不允许在 .h 中定义的,会崩溃;
- 不能 =nullptr 这么写;
- 也可以通过
GetSharedReferenceCount()
获取引用技术。
TSharedPtr 和 TSharedRef 之间的相互转换
TSharedRef 转为 TSharedPtr,支持隐式转换,直接赋值即可。
TSharedPtr 转为 TSharedRef,通过 ToSharedRef()
进行转换。
void ATestSharedRefAndPtr()
{
//创建普通指针
TestA* MyTestC = new TestA();
//创建共享指针
TSharedPtr<TestA> MyTestA;
//创建共享引用
TSharedRef<TestA> MyTestB(new TestA());
//共享引用转换为共享指针,支持隐式转换
MyTestA = MyTestB;
//普通的指针转换为共享指针
MyTestA = MakeShareable(MyTestC);
//共享指针转换为共享引用,共享指针不能为空
MyTestB = MyTestA.ToSharedRef();
};
TWeakPtr(弱指针)
TWeakPtr 为弱指针,保持对一个对象的弱引用,与 TSharedPtr 相比,它不参与引用计数,也就不会阻止对象的销毁。当 TWeakPtr 指向的类销毁时,那么这个弱指针(TWeakPtr)也会为null,所以,使用时需要判断有效性。
TWeakPtr 打破 TSharedPtr 共享指针带来的循环引用问题。
创建/初始化/重置
- 通过 TSharedPtr 创建
- 通过 TSharedRef 创建
- 运算符 = 赋值
- IsValid() 函数判断有效性
- Reset() 或 nullptr 重置
TWeakPtr 的创建必须基于一个 TSharedPtr 或者 TSharedRef。
// 强指针创建弱指针
TSharedPtr<SimpleObject> ObjPtr = MakeShared<SimpleObject>();
TWeakPtr<SimpleObject> ObjWeakPtr(ObjPtr);
UE_LOG(LogTemp, Warning, TEXT("step1 引用计数:ObjPtr[%d]"), ObjPtr.GetSharedReferenceCount());
// 强引用创建弱指针
TSharedRef<SimpleObject> objRef = MakeShareable(new SimpleObject());
TWeakPtr<SimpleObject> ObjWeakPtr2(objRef);
TWeakPtr<SimpleObject> ObjWeakPtr_Copy = ObjWeakPtr;
UE_LOG(LogTemp, Warning, TEXT("step2 引用计数:ObjPtr[%d]"), ObjPtr.GetSharedReferenceCount());
// 判断有效性
if (ObjWeakPtr.IsValid())
{
TSharedPtr<SimpleObject> ObjPtr2 = ObjWeakPtr.Pin();
ObjPtr2->ExeFun();
}
// 清空强指针
ObjPtr.Reset();
UE_LOG(LogTemp, Warning, TEXT("step3 引用计数:ObjPtr[%d]"), ObjPtr.GetSharedReferenceCount());
TSharedPtr<SimpleObject> ObjPtr2 = ObjWeakPtr.Pin();
// 判断有效性
if (!ObjPtr2)
{
UE_LOG(LogTemp, Warning, TEXT("弱指针已空"));
}
// 重置
ObjWeakPtr.Reset();
ObjWeakPtr_Copy = nullptr;
输出:
step1 引用计数:ObjPtr[1]
step2 引用计数:ObjPtr[1]
step3 引用计数:ObjPtr[0]
弱指针已空
转换
TWeakPtr 的 Pin()
函数可以将 WeakPtr 转成 TSharedPtr。
- Pin() 函数转成 TSharedPtr ,再解引用访问对象
void MyWeakPtrToSharePtr()
{
TSharedPtr<TestA> ptr = MakeShareable(new TestA());
//通过SharePtr初始化TWeakPtr
TWeakPtr<TestA> WeakPtr = ptr;
//利用 WeakPtr的.Pin() 函数将 WeakPtr 转成SharedPtr
TSharedPtr<TestA> WeakPtrToSharePtrObj = WeakPtr.Pin();
if (WeakPtrToSharePtrObj.IsValid())
{
//tdo smth...
}
}
TWeakPtr 无法直接转换为 TSharedRef,需要先将 WeakPtr 转成 SharedPtr,再将 SharedPtr 转 SharedRef。
void MyWeakPtrToShareRef()
{
TSharedPtr<TestA> ptr = MakeShareable(new TestA());
//通过SharePtr初始化TWeakPtr
TWeakPtr<TestA> WeakPtr = ptr;
//利用WeakPtr的.Pin()函数将WeakPtr转成SharedPtr
TSharedPtr<TestA> WeakPtrToSharePtrObj = WeakPtr.Pin();
if (WeakPtrToSharePtrObj.IsValid())
{
//利用TSharedPtr的ToSharedRef()函数将TSharedPtr转换成TSharedRef
TSharedRef<TestA> ref = WeakPtrToSharePtrObj.ToSharedRef();
}
}
注意:TWeakPtr 一定是从 TSharedPtr 或者 TSharedRef 过来的。
TUniquePtr 唯一指针
TUniquePtr 指向的对象只能被唯一指向,因而 Unique 指针不能赋值给其它指针。
不要为共享指针或共享引用的对象创建唯一指针。
创建/初始化/判断/解引用/重置
- MakeUnique()
- IsValid()
- -> 运算符
- Get() 函数
- Release() 释放并返回指针
- Reset() 或 nullptr 重置
// 创建唯一指针
TUniquePtr<SimpleObject> ObjUniquePtr = MakeUnique<SimpleObject>();
UE_LOG(LogTemp, Warning, TEXT("Validity: ObjUniquePtr[%d]"), ObjUniquePtr.IsValid());
// 判断有效性
if (ObjUniquePtr.IsValid())
{
ObjUniquePtr->ExeFun(); // 解引用
}
// 释放指针,移交
TUniquePtr<SimpleObject> ObjUniquePtr2(ObjUniquePtr.Release());
UE_LOG(LogTemp, Warning, TEXT("Validity: ObjUniquePtr[%d], ObjUniquePtr2[%d]"), ObjUniquePtr.IsValid(), ObjUniquePtr2.IsValid());
// 重置
ObjUniquePtr.Reset();
ObjUniquePtr2 = nullptr;
基类与派生类的智能转换
先声明两个类
class Father
{
public:
int32 a = 10;
};
class Child :public Father
{
public:
int32 b = 20;
};
共享指针:基类与派生类的转换
派生类转换为基类
派生类转基类为隐式转换。
//声明基类共享指针_father
TSharedPtr<Father>_father;
//创建派生类共享指针_Child
TSharedPtr<Child>_child = MakeShareable(new Child);
//派生类转换为基类,支持隐式转换
_father = _child;
基类转换为派生类
通过 StaticCastSharedRef 进行转换。
//基类转换为派生类
TSharedPtr<Child>_child = StaticCastSharedPtr<Child>(_father);
//_child调用派生类的成员
if (_child.IsValid())
{
_child->b;
}
常量基类转换为派生类
常量基类转换为派生类,即将 Const TSharePtr<基类>
转换为 TSharePtr<派生类>
,有两种方式:
方法一:
- 先通过
ConstCastSharedRef
转换城非 Const 的基类 - 再通过
StaticCastSharedRef
转换
//创建派生类转为基类的共享指针,支持隐式转换
const TSharedPtr<Father> const_father = MakeShareable(new Child);
//先将基类指针转换为指向基类的指针
TSharedPtr<Father> const_father_a = ConstCastSharedPtr<Father>(const_father);
//然后基类转换为派生类
TSharedPtr<Child> _child_a = StaticCastSharedPtr<Child>(const_father_a);
//调用派生类成员
if (_child_a.IsValid())
{
_child_a->b;
}
方法二:直接通过 StaticCastSharedRef 转换
TSharedPtr<Child> _child_a = StaticCastSharedRef<Child>(const_father);
if (_child_a.IsValid())
{
_child_a->b;
}
注意:不能通过 ConstCastSharedPtr 直接转成派生类.
//错误写法, 不能通过 ConstCastSharedPtr 直接转成派生类.
TSharedPtr<Child> ptrcast = ConstCastSharedPtr<Child>(const_father);
共享引用:基类与派生类的转换
用法和共享指针一样,只需修改以下即可
- TsharedPtr 改为 TSharedRef,
- StaticCastSharedPtr 改为 StaticCastSharedRef,
- ConstStaticCastSharedPtr 改为 ConstStaticCastSharedRef
TSharedFromThis
继承了 TSharedFromThis 其实里面会多一个弱引用指针(WeakPtr),存了一个对象,同时继承了 AsShared 方法,来获取这个类的共享引用,方便我们直接做转换。
将一个类继承自
TSharedFromThis
之后,那么这个类就会知道自己是属于哪一个共享指针了。
TSharedFromThis 使用场景
TSharedFromThis 哪些场合适合使用?
- 比如我们要写一个单例管理类,可以将数据类继承自 TSharedFromThis;
- 需要转换成原生 C++ 类,又想在某一处将原生 C++ 类再次转换成 TSharedPtr 的需求;
- TSharedFromThis 是支持线程安全的。
用法
先声明一个类 ClassBase 继承自 TSharedFromThis<ClassBase>
:
class ClassBase : public TSharedFromThis<ClassBase>
{
public:
int32 nBase;
};
class ClassTop : public ClassBase
{
public:
void MyFunc() {}
};
TSharedFromThis 的使用:
- AsShared() 将裸指针转智共享引用,可再隐式转为共享指针
- SharedThis(this) 会返回具备"this"类型的TSharedRef
不要在构造函数中调用 AsShared 或 Shared,共享引用此时并未初始化,将导致崩溃或断言。
void MySharedFromThisTest()
{
TSharedPtr<ClassTop> ptr = MakeShareable(new ClassTop());
if (ptr.IsValid())
{
//我们通过.Get()将TSharedPtr转换成C++ 原生指针
ClassTop* pOriginPtr = ptr.Get();
//现在我们想将pOriginPtr这个原生C++指针怎么转回智能指针呢?
{
//错误的用法
//TSharedPtr<ClassTop> ptr = MakeShareable(pOriginPtr);
//错误的用法, AsShared()不能用派生类接
//TSharedPtr<ClassTop> ptr22 = pOriginPtr->AsShared();
}
{
//正确的用法, AsShared()不能用基类接(基类指的是你继承自TSharedFromThis的那个类)
TSharedPtr<ClassBase> ptr2 = pOriginPtr->AsShared();
TSharedPtr<ClassTop> ptr3 = StaticCastSharedPtr<ClassTop>(ptr2);
if (ptr3.IsValid())
{
ptr3->MyFunc();
}
}
}
}
注意
- 错误的用法:通过 Get() 将 TSharedPtr 转换成 C++ 原生指针,在创建 TSharedPtr。
TSharedPtr<ClassTop> ptr = MakeShareable(pOriginPtr);
- 错误的用法(AsShared() 不能用派生类接)
TSharedPtr<ClassTop> ptr22 = pOriginPtr->AsShared();
- 正确的用法,AsShared() 需要用基类接(基类指的是指直接继承自TSharedFromThis的那个类)
TSharedPtr<ClassBase> ptr2 = pOriginPtr->AsShared();
TSharedPtr<ClassTop> ptr3 = StaticCastSharedPtr<ClassTop>(ptr2);
if (ptr3.IsValid())
{
ptr3->MyFunc();
}
SharedThis
示例:
// 基类
class BaseClass :public TSharedFromThis<BaseClass>
{
public:
BaseClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
virtual ~BaseClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
virtual void ExeFun() {
TSharedRef<BaseClass> ThisAsSharedRef = AsShared();
}
};
// 派生类
class ChildClass :public BaseClass
{
public:
ChildClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
virtual ~ChildClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
virtual void ExeFun() override{
// AsShared()返回 TSharedRef<BaseClass>, 因而编译不通过
// TSharedRef<ChildClass> AsSharedRef = AsShared();
TSharedRef<ChildClass> AsSharedRef = SharedThis(this);
}
};
TSharedPtr<BaseClass> BaseClassPtr = MakeShared<BaseClass>();
UE_LOG(LogTemp, Warning, TEXT("引用计数:BaseClassPtr[%d]"), BaseClassPtr.GetSharedReferenceCount());
BaseClass* tempPtr = BaseClassPtr.Get();
TSharedPtr<BaseClass> BaseClassPtr_Shared = tempPtr->AsShared();
UE_LOG(LogTemp, Warning, TEXT("引用计数:BaseClassPtr[%d], BaseClassPtr_Shared[%d]"),
BaseClassPtr.GetSharedReferenceCount(),
BaseClassPtr_Shared.GetSharedReferenceCount());
// 使用下面语句运行,程序死机
// TSharedPtr<BaseClass> BaseClassPtr_New = MakeShareable(tempPtr);
输出:
引用计数:BaseClassPtr[1]
引用计数:BaseClassPtr[2], BaseClassPtr_Shared[2]
注意事项
- 避免将数据作为 TSharedRef 或 TSharedPtr 参数传到函数,此操作将因取消引用和引用计数而产生开销。相反,建议将引用对象作为
const &
进行传递。 - 可将共享指针向前声明为不完整类型。
- 共享指针与虚幻对象(UObject及其衍生类)不兼容。引擎具有UObject管理的单独内存管理系统,两个系统未互相重叠。
- 避免智能指针和原生指针混用,可能会遇到内存释放等问题。
- 切记不能用 UPROPERTY 这些反射修饰符来修饰这些智能指针变量
- 切记 TSharedRef 变量是不允许定义在头文件里面作为成员变量出现的
- C++ 的原生 cast 方法(static_cast、dynamic_cast、const_cast、reinterpret_cast)不能用于 UE4 智能指针的转换,请使用 ConstCastSharedRef、ConstCastSharedPtr、StaticCastSharedPtr、StaticCastSharedRef。
- 在UE4里面还是使用UE4的智能指针,就别用C++11的了,因为UE的智能指针能和UE的代码比如一些容器能够方便的使用。
- UE的智能指针要比C++占得字节要大
- 这些智能指针都是支持线程安全的,有Fast和Safe两种模式。默认是Fast,也就是非线程安全。可以按需要标记成Safe模式。