栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

UE 智能指针的介绍

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

UE 智能指针的介绍

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

  • 虚幻4:智能指针基础
  • UE4 智能指针使用介绍
  • 【UE4 C++ 基础知识】<15> 智能指针 TSharedPtr、UniquePtr、TWeakPtr、TSharedRef
智能指针的介绍

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

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

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

我们会遇到这样的问题:

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

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

智能指针

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

智能指针优点

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

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

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

可以查看详细的官方文档。

如何使用智能指针?

智能指针的主要类型分为:TSharedPtr, TSharedRef, TWeakPtr, TUniquePtr。

先声明一个测试类

class TestA
{
public:
    int32 a;
    float b;
};
TSharedPtr 共享指针

TSharedPtr 为共享指针。

创建(MakeShareable)/初始化/重置(Reset)

用法如下:

void TestSharedPtr()
{
    //声明
    TSharedPtr MyTestA;
    //分配内存
    MyTestA = MakeShareable(new TestA());
    //先判读智能指针是否有效
    if (MyTestA.IsValid() || MyTestA.Get())
    {
        //访问
        int32 a = MyTestA->a;
        //复制指针
        TSharedPtr 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 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 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() 函数

void TestSharedRef()
{
    // 创建共享引用
    TSharedRef objRef(new SimpleObject());
    TSharedRef objRef2 = MakeShareable(new SimpleObject());
    TSharedRef objRef3 = MakeShared();

    //访问:
    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 MyTestA;
    //创建共享引用
    TSharedRef 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 ObjPtr = MakeShared();
TWeakPtr ObjWeakPtr(ObjPtr);
UE_LOG(LogTemp, Warning, TEXT("step1 引用计数:ObjPtr[%d]"), ObjPtr.GetSharedReferenceCount());

// 强引用创建弱指针
TSharedRef objRef = MakeShareable(new SimpleObject());
TWeakPtr ObjWeakPtr2(objRef);

TWeakPtr ObjWeakPtr_Copy = ObjWeakPtr;
UE_LOG(LogTemp, Warning, TEXT("step2 引用计数:ObjPtr[%d]"), ObjPtr.GetSharedReferenceCount());

// 判断有效性
if (ObjWeakPtr.IsValid())
{
	TSharedPtr ObjPtr2 = ObjWeakPtr.Pin();
	ObjPtr2->ExeFun();
}

// 清空强指针
ObjPtr.Reset();
UE_LOG(LogTemp, Warning, TEXT("step3 引用计数:ObjPtr[%d]"), ObjPtr.GetSharedReferenceCount());

TSharedPtr 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 ptr = MakeShareable(new TestA());
    //通过SharePtr初始化TWeakPtr
    TWeakPtr WeakPtr = ptr;

    //利用 WeakPtr的.Pin() 函数将 WeakPtr 转成SharedPtr
    TSharedPtr WeakPtrToSharePtrObj = WeakPtr.Pin();
    if (WeakPtrToSharePtrObj.IsValid())
    {
        //tdo smth...
    }
}

TWeakPtr 无法直接转换为 TSharedRef,需要先将 WeakPtr 转成 SharedPtr,再将 SharedPtr 转 SharedRef。

void MyWeakPtrToShareRef()
{
    TSharedPtr ptr = MakeShareable(new TestA());
    //通过SharePtr初始化TWeakPtr
    TWeakPtr WeakPtr = ptr;

    //利用WeakPtr的.Pin()函数将WeakPtr转成SharedPtr
    TSharedPtr WeakPtrToSharePtrObj = WeakPtr.Pin();
    if (WeakPtrToSharePtrObj.IsValid())
    {
        //利用TSharedPtr的ToSharedRef()函数将TSharedPtr转换成TSharedRef
        TSharedRef ref = WeakPtrToSharePtrObj.ToSharedRef();
    }
}

注意:TWeakPtr 一定是从 TSharedPtr 或者 TSharedRef 过来的。

TUniquePtr 唯一指针

TUniquePtr 指向的对象只能被唯一指向,因而 Unique 指针不能赋值给其它指针。

不要为共享指针或共享引用的对象创建唯一指针。

创建/初始化/判断/解引用/重置
  • MakeUnique()
  • IsValid()
  • -> 运算符
  • Get() 函数
  • Release() 释放并返回指针
  • Reset() 或 nullptr 重置
// 创建唯一指针
TUniquePtr ObjUniquePtr = MakeUnique();
UE_LOG(LogTemp, Warning, TEXT("Validity: ObjUniquePtr[%d]"), ObjUniquePtr.IsValid());

// 判断有效性
if (ObjUniquePtr.IsValid()) 
{
    ObjUniquePtr->ExeFun(); // 解引用
}

// 释放指针,移交
TUniquePtr 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;
//创建派生类共享指针_Child
TSharedPtr_child = MakeShareable(new Child);
//派生类转换为基类,支持隐式转换
_father = _child;
基类转换为派生类

通过 StaticCastSharedRef 进行转换。

//基类转换为派生类
TSharedPtr_child = StaticCastSharedPtr(_father);
//_child调用派生类的成员
if (_child.IsValid())
{
    _child->b;
}
常量基类转换为派生类

常量基类转换为派生类,即将 Const TSharePtr<基类> 转换为 TSharePtr<派生类>,有两种方式:

方法一:

  1. 先通过 ConstCastSharedRef 转换城非 Const 的基类
  2. 再通过 StaticCastSharedRef 转换
//创建派生类转为基类的共享指针,支持隐式转换
const TSharedPtr const_father = MakeShareable(new Child);
//先将基类指针转换为指向基类的指针
TSharedPtr const_father_a = ConstCastSharedPtr(const_father);
//然后基类转换为派生类
TSharedPtr _child_a = StaticCastSharedPtr(const_father_a);
//调用派生类成员
if (_child_a.IsValid())
{
    _child_a->b;
}

方法二:直接通过 StaticCastSharedRef 转换

TSharedPtr _child_a = StaticCastSharedRef(const_father);
if (_child_a.IsValid())
{
    _child_a->b;
}

注意:不能通过 ConstCastSharedPtr 直接转成派生类.

//错误写法, 不能通过 ConstCastSharedPtr 直接转成派生类. 		
TSharedPtr ptrcast = ConstCastSharedPtr(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:

class ClassBase : public TSharedFromThis
{
public:
    int32 nBase;
};

class ClassTop : public ClassBase
{
public:
    void MyFunc() {}
};

TSharedFromThis 的使用:

  • AsShared() 将裸指针转智共享引用,可再隐式转为共享指针
  • SharedThis(this) 会返回具备"this"类型的TSharedRef

不要在构造函数中调用 AsShared 或 Shared,共享引用此时并未初始化,将导致崩溃或断言。

void MySharedFromThisTest()
{
    TSharedPtr ptr = MakeShareable(new ClassTop());
    if (ptr.IsValid())
    {
        //我们通过.Get()将TSharedPtr转换成C++ 原生指针
        ClassTop* pOriginPtr = ptr.Get();

        //现在我们想将pOriginPtr这个原生C++指针怎么转回智能指针呢?
        {
            //错误的用法
            //TSharedPtr ptr = MakeShareable(pOriginPtr);

            //错误的用法, AsShared()不能用派生类接
            //TSharedPtr ptr22 = pOriginPtr->AsShared();
        }
        {
            //正确的用法, AsShared()不能用基类接(基类指的是你继承自TSharedFromThis的那个类)
            TSharedPtr ptr2 = pOriginPtr->AsShared();
            TSharedPtr ptr3 = StaticCastSharedPtr(ptr2);
            if (ptr3.IsValid())
            {
                    ptr3->MyFunc();
            }
        }
    }
}

注意

  1. 错误的用法:通过 Get() 将 TSharedPtr 转换成 C++ 原生指针,在创建 TSharedPtr。
TSharedPtr ptr = MakeShareable(pOriginPtr);
  1. 错误的用法(AsShared() 不能用派生类接)
TSharedPtr ptr22 = pOriginPtr->AsShared();
  1. 正确的用法,AsShared() 需要用基类接(基类指的是指直接继承自TSharedFromThis的那个类)
TSharedPtr ptr2 = pOriginPtr->AsShared();
TSharedPtr ptr3 = StaticCastSharedPtr(ptr2);
if (ptr3.IsValid())
{
    ptr3->MyFunc();
}

SharedThis 示例:

// 基类
class BaseClass :public TSharedFromThis
{
public:
	BaseClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
	virtual ~BaseClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
	virtual void ExeFun() { 
		TSharedRef 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, 因而编译不通过
		// TSharedRef AsSharedRef = AsShared(); 

		TSharedRef AsSharedRef = SharedThis(this);
	}
};

TSharedPtr BaseClassPtr = MakeShared();
UE_LOG(LogTemp, Warning, TEXT("引用计数:BaseClassPtr[%d]"), BaseClassPtr.GetSharedReferenceCount());

BaseClass* tempPtr = BaseClassPtr.Get();
TSharedPtr BaseClassPtr_Shared = tempPtr->AsShared();
UE_LOG(LogTemp, Warning, TEXT("引用计数:BaseClassPtr[%d], BaseClassPtr_Shared[%d]"), 
	BaseClassPtr.GetSharedReferenceCount(), 
    BaseClassPtr_Shared.GetSharedReferenceCount());

// 使用下面语句运行,程序死机
// TSharedPtr 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模式。
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1033338.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号