UE 智能指针的介绍

此文章为下面文章的整理,感谢前辈

智能指针的介绍

UE 中的 UObject 本身就可以帮我们做回收,做释放的。我们可以看到 UE4 中所有的类都是继承自UObject,这样的话当我们引擎被关闭的时候,它会帮我们自动释放这些资源,或者是我们手动去释放掉它,这样能保证能彻底删掉它。

UE4 对原生对象(即不继承 UObject 的类)不提供垃圾回收,需要手动进行清理。

原生对象创建和销毁的方式有:malloc / freenew / delete。new 与 malloc 的区别在于,new 在分配内存完成之后会调用构造函数。

我们会遇到这样的问题:

  1. 创建一个类之后,如果忘记把它销毁,则它占的内存可能永远都无法被释放掉。
  2. 创建一个类之后,有其它的指针指向了它,结果不小心把它释放了,其它指针依然保留了原始的信息,导致我们在访问这个类的时候而产生野指针奔溃。

为了防止这些问题的出现,智能指针就出现了,它就是用来管理这些非继承 UObject 的类。

智能指针

UE4 提供共享指针库来管理内存,它是 C++11 智能指针的自定义实现,虚拟智能指针库和C++智能指针在功能和效率方面都有一些共同的特点。

智能指针优点

  1. 可以像常规的 C++ 指针那样复制,解引用,比较共享指针等
  2. 防止内存泄漏:共享引用不存在时,智能指针(弱指针除外)会自动删除对象。
  3. 弱引用:弱指针会中断引用循环并阻止悬挂指针。
  4. 包含了可以通过多线程安全地进行访问的“线程安全”版本
  5. 可以创建任何类型的对象的共享指针
  6. 支持针“const”、前置声明的不完全类型、类型转换等

为什么使用 UE4 的智能指针库?

  1. std::shared_ptr不是在所有的平台都能用的
  2. 可以和其它虚幻容器及类型无缝协作
  3. 更好的控制平台特性,包括线程处理和优化

可以查看详细的官方文档

如何使用智能指针?

智能指针的主要类型分为: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]

转移

转移可以通过 MoveTempMoveTempIfPossible

// 转移共享指针
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();
};

注意:

  1. 与共享指针类似,固定引用非空对象,使用时候直接用->即可,因为一直有有效值;
  2. ShareRef 不允许在 .h 中定义的,会崩溃;
  3. 不能 =nullptr 这么写;
  4. 也可以通过 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<派生类>,有两种方式:

方法一:

  1. 先通过 ConstCastSharedRef 转换城非 Const 的基类
  2. 再通过 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 哪些场合适合使用?

  1. 比如我们要写一个单例管理类,可以将数据类继承自 TSharedFromThis;
  2. 需要转换成原生 C++ 类,又想在某一处将原生 C++ 类再次转换成 TSharedPtr 的需求;
  3. 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();
            }
        }
    }
}

注意

  1. 错误的用法:通过 Get() 将 TSharedPtr 转换成 C++ 原生指针,在创建 TSharedPtr。
TSharedPtr<ClassTop> ptr = MakeShareable(pOriginPtr);
  1. 错误的用法(AsShared() 不能用派生类接)
TSharedPtr<ClassTop> ptr22 = pOriginPtr->AsShared();
  1. 正确的用法,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模式。

猜你喜欢

转载自blog.csdn.net/fuyoufang1/article/details/126178795