引言

读完本篇文章,你会了解为何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. Centos7.6分区、格式化、自动挂载磁盘

    个人名片: 对人间的热爱与歌颂,可抵岁月冗长 Github‍:念舒_C.ying CSDN主页️:念舒_C.ying 个人博客 :念舒_C.ying 目录 1. 添加硬盘 2. 执行fdisk -l ...

  2. 关于python实现与体重秤蓝牙ble通信研究(Linux)

    前言 前几天买一个带蓝牙的体重秤,功能就是可以通过手机app连接,然后每一次称重都会记录下来,然后进行一些计算(体脂等),但是我不想用手机来操作,我习惯用电脑,就想写一个软件来与体重秤通信,记录我的每 ...

  3. 多表查询、Navicat软件、PyMySQL模块

    目录 多表查询.Navicat软件.PyMySQL模块 一.多表查询的两种方法 1.准备工作 2.第一种:连表操作 3.第二种:子查询 总结与结论: 二.多表查询练习题 1.课堂多表查询练习题 2.以 ...

  4. Python异步协程(asyncio详解)

    续上篇讲解yield from博客,上篇链接:https://www.cnblogs.com/Red-Sun/p/16889182.html PS:本博客是个人笔记分享,不需要扫码加群或必须关注什么的 ...

  5. question(The hierarchy of the type MyServlet is inconsistent)——解决tomcat重新添加依赖

    在保存代码时突然出现类似于tomcat不能正常使用的警告弹窗,在eclipse中删除tomcat依赖之后,按照以下步骤先在eclipse添加依赖 1.eclipse项目中添加tomcat依赖 wind ...

  6. mysql-DuplicateUpdate和java的threadpool的"死锁"

    大家千万不要被文章的标题给迷惑了,他两在本篇文章是没有关系的, 今天给大家讲讲最近2个有意思的issue,分享一下我学到的 mysql DuplicateUpdate的用法要注意的点 java的thr ...

  7. 当我们说大数据Hadoop,究竟在说什么?

    前言 提到大数据,大抵逃不过两个问题,一个是海量的数据该如何存储,另外一个就是那么多数据该如何进行查询计算呢.好在这些问题前人都有了解决方案,而Hadoop就是其中的佼佼者,是目前市面上最流行的一个大 ...

  8. angr_ctf——从0学习angr(三):Hook与路径爆炸

    路径爆炸 之前说过,angr在处理分支时,采取统统收集的策略,因此每当遇见一个分支,angr的路径数量就会乘2,这是一种指数增长,也就是所说的路径爆炸. 以下是路径爆炸的一个例子: char buff ...

  9. 微服务组件-----Spring Cloud Alibaba 注册中心Nacos的CP架构Raft协议分析

    前言 本篇幅是继  注册中心Nacos源码分析 的下半部分. 意义 [1]虽说大部分我们采用注册中心的时候考虑的都是AP架构,为什么呢?因为性能相对于CP架构来说更高,需要等待的时间更少[相对于CP架 ...

  10. vue-router路由之路-极简教程

    01.什么是前端路由? 前端路由的一个大背景就是当下流行的单页应用SPA,一些主流的前端框架,如vue.react.angular都属于SPA,那什么是SPA呢? 1.1.SPA SPA(single ...