引言

读完本篇文章,你会了解为何UE中C++作为其开发语言,使用的指针,为何各式各样。
你需要对UE有所了解,如果不了解也没关系,也可以看下这篇文章,就当了解一下最复杂的应用的系统指针设计是如何。
可以肉眼可见,类对象存在还是被释放了。

类型

我这边给出的是自己个人对指针种类分类的看法,主要是结合项目使用情况,大致得出下列类型。
graph LR
C{指针}
C --> D[原生C++裸指针]
C --> E[原生C++共享指针]
C --> F[原生C++弱指针]
C --> G[UObject裸指针]
C --> H[UObject带UProperty指针]
C --> Y[UObject弱指针]

工具

  • 将UE中EditorPreference->Show Frame Rate and Memory 打开(√)

[图1]

可以通过观察上图内存变化,肉眼可见对象是否彻底释放。(其实或者看Log,主要是构造函数和析构函数)

  • 自定义FCustomDefinedClass,不继承任何基类,即是纯原生C++类。
//自定义原生C++类

class FCustomDefinedClass
{
public:
FCustomDefinedClass()
{
Arr.AddDefaulted(100*1024*1024); //为了测试便于观察对比,申请内存
UE_LOG(LogTemp, Log, TEXT("FCustomDefinedClass() Start"));
} ~FCustomDefinedClass()
{
Arr.Reset();//为了测试方便,释放内存
UE_LOG(LogTemp, Log, TEXT("~FCustomDefinedClass() Stop"));
} void PrintArr()
{
UE_LOG(LogTemp, Log, TEXT("FCustomDefinedClass PrintArr"));
} TArray<bool> Arr;
}; UCLASS()
class UCustomDefinedObject :public UObject
{ GENERATED_BODY() public: UCustomDefinedObject(const class FObjectInitializer& ObjectInitializer) {
Arr.AddDefaulted(100 * 1024 * 1024); //为了测试便于观察对比,申请内存
UE_LOG(LogTemp, Log, TEXT("UCustomDefinedObject() Start"));
}; ~UCustomDefinedObject()
{
Arr.Reset();//为了测试方便,释放内存
UE_LOG(LogTemp, Log, TEXT("~UCustomDefinedObject() Stop"));
} void PrintArr()
{
UE_LOG(LogTemp, Log, TEXT("UCustomDefinedObject PrintArr"));
} TArray<bool> Arr;
};

构造函数中我们申请100MB的内存,在析构函数中释放这100MB的对象。

在代码中New出一个该类对象,内存就会增大100M,该类被析构,就会释放,于是肉眼可见的对象是否存活,实现了。

  • 强制开启GC指令,控制GC的开启时机可以方便我们快速测验。

    gc.ForceCollectGarbageEveryFrame 1

分析

一步一步来,从最简单的开始分析。

1.原生C++裸指针

其实这个比较简单,我new一个,之后我必须手动释放。代码如下

    //UE中观察引擎内存显示(类似图1)
// Mem:1309MB
FCustomDefinedClass* InCustomDefinedObject = new FCustomDefinedClass();
// Mem:1407MB
delete InCustomDefinedObject;
InCustomDefinedObject = nullptr;
// Mem:1299MB

(大约都是100MB的落差,符合预期,有点误差,可以忽略,FCustomDefinedClass类的作用完成,类对象肉眼可见是否存在实现)

2.原生C++共享指针

上述代码如果不写或者漏调 delete InCustomDefinedObject,观察内存显示,即使我停止(Play)游戏,数目都没有减少,再次Play启动游戏 New该类,再停止Play,会发现内存一直在增加,这就是传说的内存泄漏。 非常严重。我只是没调这个析构,忘记调了(对象那么多,每个都要delete,肯定忘记),可是每个对象都需要手动这么写,也太累了。 于是C++原生的智能指针出现了。

MakeShareable<FCustomDefinedClass> InCustomShareObject = MakeShareable<FCustomDefinedClass>(new FCustomDefinedClass());
InCustomShareObject = nullptr;

再次观察内存情况,内存可以正常释放。

  • InCustomShareObject置为nullPtr
  • InCustomShareObject置为nullPtr变量超出作用域
  • 本质就是没有引用计数了,会立刻自动执行析构函数,释放占有的内存。

关于共享指针的原理,可以参考:手把手带你实现一个智能指针

3.原生C++弱指针

使用共享指针的主要原因是避免手动管理指针释放资源。但是,在某些情况下共享指针不能实现预期的行为:

一种情况是循环引用。如果两个对象使用共享指针相互引用,并且不存在对这些对象的其他引用,若要释放这些对象及其关联的资源,则共享指针不会释放数据,因为每个对象的引用计数仍为1。在这种情况下,可能想使用普通的指针,但是这样做需要手动管理相关资源的释放。

另一种情况是当明确想要共享但不拥有对象。这种情况下引用的生存期超过了它所引用的对象的生命周期。如果使用共享指针则其将永远不会释放对象。如果使用普通指针则可能出现指针所引用的对象不再有效,这会带来访问已释放数据的风险。

对于这两种情况都可以使用弱指针指针处理。弱指针是共享指针的辅助类,弱指针需要共享指针才能创建。

上述我们知道共享指针是如果有引用计数,就不会被释放,那么如果我只是想用一个对象,但是又不想对他造成影响,就是不想影响他的计数,不想影响他的生命周期。换而言之就是共享指针那边该干嘛就干嘛,我这边WeakPtr这边不影响他。只是说他那边没了,我这边也要没了,他那边还在,我这边就还在。

于是弱指针就来了。

void ATestObjectActorManager::TestCallGenerate()
{
const TSharedPtr<FCustomDefinedClass> WeakSharePtr = MakeShareable<FCustomDefinedClass>(new FCustomDefinedClass());
InCustomWeakObject = WeakSharePtr;
}
//WeakSharePtr 在这个函数执行完,因为是临时变量,会被干掉,引用计数为0,释放内存了。 void ATestObjectActorManager::TestCallDestory()
{
if (InCustomWeakObject.IsValid()) //执行到这的时候InCustomWeakObject已经invalid了,为false了。
{
// ....
}
}

(共享指针&弱指针用法,都需要IsValid来预先判断)

4.UObject裸指针

终于到了UE这边了,因为UE考虑到C++的指针释放内存啥的是个麻烦的事,C++原生虽然有自己的智能指针,但是作为游戏,有一些觉得C++原生做的不好的(具体我也不知道哪里不好)。自己搞的,才是适合自己的,适合游戏的,于是UE 让UObject(组成UE世界的最小单元)就附带了垃圾回收的功能

案例一

void ATestObjectActorManager::TestCallGenerate()
{
UCustomDefinedObject* TempDefinedObj = NewObject<UCustomDefinedObject>();
}

该函数执行完,因为是临时变量,做得事跟上述共享指针类似得事,引用计数为0,但是观察内存情况,尝试执行3次,每次都在不断增长1

0MB内存,涨了300MB

我们这个时候在输入强制GC指令:gc.ForceCollectGarbageEveryFrame 1

之后会发现上涨得300MB都被释放了。

void ATestObjectActorManager::TestCallGenerate()
{
TempDefinedObj = NewObject<UCustomDefinedObject>();
}

因为没有UProperty,执行GC,该因为没有引用,所以被释放且指针没有置nullPtr,就是传说“野指针”了

小结:继承自UObject得裸指针在没有引用计数后,可能算是“泄漏”,但是只要有UE得垃圾回收机制执行,这些所谓“泄漏”得内存还是会被释放。

5.UObject带UProperty指针

因为有UPROPERTY,引用关系计算了,

void ATestObjectActorManager::TestCallGenerate()
{
TempDefinedObj = NewObject<UCustomDefinedObject>();
}

这个时候使用ForceGC指令,内存是不会变化的。

这个时候我给所在对象使用MarkPendingKill,则内存被释放掉。

加了的话,如果所引用的UObject被MarkPendingKill,则该Uobject也会被强制回收。

小结:加了UProperty,算这个UObject指针加入计数了,不然就会被当作没有计数被释放且野指针。

6.UObject弱指针

我们前面已经说过了原生C++ 有共享指针,弱指针。当然UE这边有自己的智能指针Uibject,但是没有弱指针,对于继承于UObject的指针,可以使用UObject的弱指针使用方式。

    UCustomDefinedObject* InObject = NewObject<UCustomDefinedObject>();
TWeakObjectPtr<UObject> ObjectWithWeak(InObject);

也是跟上述原生的C++弱指针的使用方式类似。这里因为UObject的指针本身就自带共享功能,所以这边直接赋值即可。

总结

来源:

C++里有原生指针,可是真的太麻烦,太危险,不好使,所以出了共享指针,自动帮你管理释放,但是共享指针因为计数原理,还有一些副作用弊端,还有需求就是只是单纯的想使用并不想计入引用,于是出了弱指针。在游戏,就是UE这边因为性能等的综合考虑弄了自己的一套自动管理释放对象的系统,就是UObject系统,还有专门针对UObject对象使用的弱指针。

应用:

首先想直接使用原生C++裸指针,肯定是不建议的, 太危险,因为忘记delete后果非常严重。

如果你的类不是继承自UObject,不需要UObject提供的反射等其他复杂功能,真的很简单的类对象的话,那么就使用原生C++的共享指针存储,如果在其他地方需要对共享指针有个引用,但是又不想影响其计数,就使用弱指针。

对于继承自UObject的指针,非常不推荐裸指针的方式,就是不加UPROPERTY, 一定要加UPROPERTY,如果不想加的话,那么使用弱指针的方式即可。

相关推荐参考

Unreal 各种指针类型是怎么回事的更多相关文章

  1. 对于C语言复杂指针类型的分析

    转载自:http://www.slyar.com/blog/complicated-point-type.html int p; p是一个普通的整型变量. int *p; 1.p与*结合,说明p是一个 ...

  2. C++指针类型识别正确姿势

    指针是C和C++中编程最复杂也是最有技巧的部分,但对于新手来说,指针无疑是最致命的,让很多人望而退步.不过很多事情都是从陌生开始,然后渐渐熟悉起来的,就像交朋友一样,得花点时间去培养感情才行.不过指针 ...

  3. C语言指针类型

    1:只要是指针类型,不管是几级指针[带几个*],其宽度都是4字节 2:任何数据类型[包括自己定义的结构体]前面都能加*号,表示该数据类型的一个指针 3:由于是386处理器,其数据处理的宽度都是四个字节 ...

  4. 《精通C#》自定义类型转化-扩展方法-匿名类型-指针类型(11.3-11.6)

    1.类型转化在C#中有很多,常用的是int类型转string等,这些都有微软给我们定义好的,我们需要的时候直接调用就是了,这是值类型中的转化,有时候我们还会需要类类型(包括结构struct)的转化,还 ...

  5. 编程范式 epesode7,8 stack存放指针类型and heap,register

    这一节从后往前写. ____stack and heap ___stack由 汇编语言操控管理,数据先入后出. 栈是存放局部变量,函数调用子函数时,该函数在栈中占用的空间会增大,用于存放子函数的局部变 ...

  6. Swift中对C语言接口缓存的使用以及数组、字符串转为指针类型的方法

    由于Swift编程语言属于上层编程语言,而Swift中由于为了低层的高性能计算接口,所以往往需要C语言中的指针类型,由此,在Swift编程语言刚诞生的时候就有了UnsafePointer与Unsafe ...

  7. C语言 数组类型与数组指针类型

    //数组类型与数组指针类型 #include<stdio.h> #include<stdlib.h> #include<string.h> void main(){ ...

  8. C语言 详解多级指针与指针类型的关系

    //V推论①:指针变量的步长只与‘指针变量的值’的类型有关(指针的值的类型 == 指针指向数据的类型) //指针类型跟指针的值有关,指针是占据4个字节大小的内存空间,但是指针的类型却是各不相同的 // ...

  9. (七)C语言中的void 和void 指针类型

    许多初学者对C中的void 和void 的指针类型不是很了解.因此常常在使用上出现一些错误,本文将告诉大家关于void 和void 指针类型的使用方法及技巧. 1.首先,我们来说说void 的含义: ...

  10. 指针类型(C# 编程指南)

    原文地址:https://msdn.microsoft.com/zh-cn/library/y31yhkeb.aspx 在不安全的上下文中,类型可以是指针类型.值类型或引用类型. 指针类型声明采用下列 ...

随机推荐

  1. C#使用Task在Winform建立控件上的提示等待窗口,实现局部等待加载,不影响主线程(二)

    效果图: 源码:(处理了亿点点细节) 链接:https://pan.baidu.com/s/18S1IgQBOyXgeGvhnU3nrKQ?pwd=jpq9提取码:jpq9 作者:兮去博客出处:htt ...

  2. Axios +Vue + themeleay

    1.pom.xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId& ...

  3. Nginx 安装篇-1.19.9版本源码安装

    系统环境:CentOS 8.5 64位 [开始安装](此步骤引自网络教程) https://www.cnblogs.com/torchstar/p/16027538.html 教程比较详细,一步步操作 ...

  4. 【每日一题】【二分mid&贪心】2022年2月8日-NC163 最长上升子序列(一)

    1.描述给定一个长度为 n 的数组 arr,求它的最长严格上升子序列的长度.所谓子序列,指一个数组删掉一些数(也可以不删)之后,形成的新数组.例如 [1,5,3,7,3] 数组,其子序列有:[1,3, ...

  5. 【大数据面试】【框架】Hive:架构、计算引擎、比较、内外部表、by、函数、优化、数据倾斜、动静态分区

    一.组成 1.架构 源数据原本是存在dubby数据库,存在MySQL可以支持多个客户端 客户端.数据存储(HDFS).MR计算引擎 2.计算引擎的选择 MR引擎:基于磁盘,计算时间长,但一定能算出结果 ...

  6. ChatGPT 会开源吗?

    最近,我被一款叫做 ChatGPT 的人工智能(AI)聊天机器人,刷屏了.网上有人说它是搜索引擎杀手,还有人说它将取代程序员... 最后,我还是没扛住铺天盖地的赞美,跑去注册了个账号,抱着调侃&quo ...

  7. Burpsuite2022.1详细图文安装教程(含工具链接)

    ​ 应用概述: Burp Suite 是用于攻击web 应用程序的集成平台,包含了许多工具.Burp Suite为这些工具设计了许多接口,以加快攻击应用程序的过程.所有工具都共享一个请求,并能处理对应 ...

  8. 时隔3个月,Uber 再遭数据泄露...

    在今年9月,Uber 就发生过一起数据泄露事件,尽管黑客并无意发动大规模攻击或以此来获取巨额利益,但其成功获取对 Uber 所有敏感服务的完全管理员访问权限仍令人后怕.而在上周,名为"Ube ...

  9. VUE 使用md5对用户登录密码进行加密传输

    VUE 使用md5对用户登录密码进行加密传输到数据库 前言 第一步 npm下载js-md5依赖包 第二步 引入js-md5 直接在需要使用md5加密的页面引入 全局挂载,将js-md5添加到vue原型 ...

  10. 3、swagger-ui导出word接口文档

    参考 1.修改swagger2word项目的 application.yml 文件的 swagger.url 为Swagger Json资源的url地址(网址+端口): 例:swagger.url: ...