天天看点

【UE4_C++】 <3> 内存管理,智能指针以及调试

本章包含以下内容:
  • 非托管内存-malloc()/free()的使用
  • 非托管内存-new/delete的使用
  • 托管内存-NewObject<>和ConstructObject<>的使用
  • 托管内存-释放内存
  • 托管内存-使用智能指针(TSharePtr,TWeakPtr,TAutoPtr)来跟踪一个object
  • 使用TScopedPoint 来跟踪一个object
  • UE的垃圾回收,以及UPROPERTY()
  • 断点与单步执行代码
  • 查找错误和使用调用堆栈

一、非托管内存-malloc()/free()的使用

添加代码到GameMode中:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

并且在关卡蓝图中调用:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

运行结果如下:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

可见非托管内存的申请和释放方法需要自己来处理

我们使用malloc()来申请内存,使用完毕后用free()来释放

需要特别说明的是调用free()之后,指针会断开与原来内存地址的连接,

i=0 则是将指针指向一个空引用,这是为了清除掉i指针,让i指针不会去引用到任何有效内存片段

二、非托管内存-new/delete的使用

首先说一下malloc与new的区别:

  • new是运算符malloc是库函数
  • new和delete底层还是由malloc和free实现的,区别在于new和delete在生成复杂数据类型时会执行两步
    1. 分配内存和调用构造函数。
    2. 调用析构函数和销毁内存。
  • malloc申请内存没有类型为(void*)需要根据使用需求进行强制转换,new不需要。
  • new并不是每次都会调用构造函数,对于简单类型eg.int是不需要的

验证一下:

添加代码:

将Object类添加进GameMode中:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

添加方法:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

在关卡蓝图调用方法:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

运行结果如下:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

三、托管内存-NewObject<>和ConstructObject<>的使用

前面说到C++中非托管内存的申请和释放使用new ,delete,malloc和free 来进行。

需要注意的是在系统中创建对象分配内存使用完毕之后不要忘记释放,否则则会出现一些没有使用的内存块并且越来越多,就会引发内存泄漏而在UE4中自动使用托管内存,要在引擎中使用的对象的任何分配大部分都使用NewObject() 或SpawnActor()函数来完成

托管内存:防止遗忘释放内存,在程序中,动态索引条目内容:

<indexentry content="managed memory:allocating, ConstructObject used">
           

分配了引用该对象的指针的数量。 如果没有指向该对象的指针,则会立即自动删除该对象,或者在下一次运行垃圾收集器时标记该对象以便删除。

前面有提过在C++中实例化Object,这边取当时的代码:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

这里,UAction: : StaticClass ()为 UAction 对象获得一个基本的 UClass * 。 Newobject 的第一个参数是 GetTransientPackage () ,它只检索游戏的 transient 包。 Ue4中的一个包(UPackage)只是一个数据集合。 这里我们使用 Transient 包来存储堆分配的数据。 您还可以使用 Blueprints 中的 UPROPERTY () TSubclassOf aactor 来选择 UClass 实例。

第三个参数(可选)是指示内存管理系统如何处理 UObject 的参数组合。RF_* flags的详细信息可参考:

https://docs.unrealengine.com/latest/INT/Programming/UnrealArchitecture/Objects/Creation/index.html#objectflags

还有一个非常类似于 NewObject 的函数名为 ConstructObject。 Constructobject 在构造中提供了更多的参数,如果需要初始化某些属性,它很有用。 否则,NewObject 就足以完成功能。

四、托管内存-释放内存

前面说过托管内存可以自动处理释放内存。当不再有对 UObject 实例的引用时,将对 UObject 实例进行引用计数和垃圾回收。 其实使用 ConstructObject 或 NewObject 在 UObject 类派生上分配的内存也可以通过调用 UObject: : ConditionalBeginDestroy ()成员函数手动取消分配(在引用计数降到0之前)。

操作方法前面也有说过,直接调用即可:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

Conditionalbegindestroy ()命令开始执行内存释放过程,调用 BeginDestroy ()和 FinishDestroy ()可重复的函数。

五、托管内存-使用智能指针(TSharePtr,TWeakPtr,TAutoPtr)来跟踪一个object

我们如果担心忘记所创建的标准 c++ 对象的删除调用,可以使用智能指针来防止内存泄漏。 Tsharedptr 是一个非常有用的 c + + 类,它可以让任何定制的 c + + 对象引用被计数(除了已经被引用计数的 UObject 导数)。 还提供了一个替代类 TWeakPtr,用于指向一个引用计数对象,该对象具有无法阻止删除的奇怪属性(因此称为弱属性) :

但是UObject以及他的派生类,也就是任何使用NewObject或ConstructObject创建的类,不能使用TSharePtr.

如果不想使用原始指针并手动跟踪删除不使用 UObject 派生的 c + + 代码,那么这段代码是使用智能指针的好选择,比如 TSharedPtr,TSharedRef 等等。 当使用一个动态分配的对象(使用 new 关键字创建)时,可以将其包装在一个引用计数的指针中,以便自动取消分配。 智能指针的不同类型决定了智能指针的行为和删除调用时间。 具体如下:

TSharedPtr: 线程安全(前提是您提供了 ESPMode: : ThreadSafe 作为模板的第二个参数)引用计数指针类型,指示共享对象。 当没有更多对共享对象的引用时,共享对象将被取消分配。

Tautotr: 一个非线程安全的共享指针。

添加代码:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试
【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

弱指针和共享指针之间有一些区别。 当引用计数降到0时,弱指针不具备将对象保存在内存中的能力。

使用弱指针(在原始指针上)的优点是,当手动删除弱指针下面的对象时(使用 ConditionalBeginDestroy ()) ,弱指针的引用变为 NULL 引用。 可以通过检查以下格式的语句来检查指针下面的资源是否仍然正确分配:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

共享指针是线程安全的。 这意味着可以在单独的线程上安全地操作底层对象。

需要注意的是:不能将 TSharedRef 与 UObjects 或 UObject 导数一起使用; 只能在自定义的 c + + 类中使用它们。 Fstructures 可以使用 TSharedPtr、 TSharedRef 和 TWeakPtr 类封装原始指针。

如果要使用智能指针指向对象,则必须使用 TWeakObjectPointer 或 UPROPERTY ()。

如果不需要 TSharedPtr 的线程安全保证,则可以使用 tautotr。 当对某个对象的引用数目减少到0时,该对象将自动删除。

六、使用TScopedPoint 来跟踪一个object

作用域指针是在其声明的块的末尾自动删除的指针。 回想一下,作用域只是代码中变量处于活动状态的一部分。 范围将持续到出现第一个结束大括号}。

例如,在下面的块中,我们有两个作用域。 外部作用域声明一个整型变量 x (对于整个外部块有效) ,而内部作用域声明一个整型变量 y (对于内部块有效,在声明它的行之后) :

{
    int x;
    {
        int y;
    } // scope of y ends
} // scope of x ends
           

当引用计数对象(有超出作用域的危险)在使用期间被保留时,作用域指针非常有用。

要声明一个作用域指针,我们可以使用以下语法:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

由于版本更新,目前的最新API已经改为TUniquePtr。旧版的语法是蓝色框中的注释代码。

Tscopedpointer 变量类型自动向指向的变量添加引用计数。 这可以防止至少在作用域指针的生命周期内取消基础对象的分配。

七、UE4的垃圾回收,以及UPROPERTU()

当有一个对象(比如 TArray)作为 UCLASS ()的 UPROPERTY ()成员时,需要将该成员声明为 UPROPERTY ()(即使不会在 Blueprints 中编辑它) ; 否则,TArray 将无法正确分配。

添加代码

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

必须将 TArray 成员列为 UPROPERTY () ,才能正确地对其进行引用计数。 如果不这样做,则会在代码中出现意外的内存错误类型错误。

Uproperty ()声明告诉 UE4 TArray 必须受到正确的内存管理。 如果没有 UPROPERTY ()声明,TArray 将无法正常工作。

另外,当内存满了,并且希望释放其中的一些内存时,可以强制进行垃圾收集。 很少需要这样做,但是可以在具有非常大的纹理(或一组纹理)的情况下这样做,这些纹理是引用计数的,并且需要清除。

只需对所有需要从内存中取消分配的 UObjects 调用 ConditionalBeginDestroy () ,或者将它们的引用计数设置为0。

强制垃圾回收可使用以下处理方式:

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

版本:在UE4.18 版本之后调用方式使用第二个。

八、断点与单步执行代码

操作步骤:

  • 将模式设置为DebugGame
  • 选择代码按下F9设置断点
  • 运行程序
  • 击中断点时按F10继续下一个断点如此重复
  • 如果击中断点时按下F11则会基于断点处不断单步执行,每按一次执行一步

九、查找错误和使用调用堆栈

当我们按下F5使用调试模式开启编辑器时,如过运行过程中遇到错误,就会出现断点,这是一个查找错误的方法

调用堆栈是已执行的函数调用的列表。 当 bug 发生时,发生 bug 的那一行将被列在 callstack 的顶部

【UE4_C++】 &lt;3&gt; 内存管理,智能指针以及调试

另外我们可以借助C + + 分析器来进行性能的检测。

C + + 分析器对于查找需要大量处理时间的代码段非常有用。 使用分析器可以帮助我们找到优化过程中需要关注的代码部分。 如果怀疑某个区域的代码运行缓慢,那么如果它没有在分析器中突出显示,那么实际上可以确认它并不缓慢。

C++分析器的使用方法可参考以下微软官方文档:

https://docs.microsoft.com/en-us/visualstudio/profiling/?view=vs-2017

继续阅读