引言

读完本篇文章,你会了解为何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. IOS AND Android 配置Fiddler环境

    下载:http://rj.baidu.com/soft/detail/10963.html?ald 运行Fiddler点击Tools: 选择设置选项:   1.     选择HTTPS新选项卡. 2. ...

  2. 【接口测试】Postman(一)--接口测试知识准备

    1.0 前言 ​ 应用程序编程接口(Application Programming Interface, API)是这些年来最流行的技术之一,强大的Web应用程序和领先的移动应用程序都离不开后端强大的 ...

  3. 【大数据-课程】高途-天翼云侯圣文-Day2:离线数仓搭建分解

    一.内容介绍 昨日福利:大数据反杀熟 今日:数据看板 离线分析及DW数据仓库 明日:实时计算框架及全流程 一.数仓定义及演进史 1.概念 生活中解答 2.数据仓库的理解 对比商品仓库 3.数仓分层内容 ...

  4. before-after-hook钩子函数

    before-after-hook 最近看别人的代码,接触到一个插件,before-after-hook,百度搜一圈也没有看到什么地方有教程,看这个字面意思是一个hook,和axios里面的拦截器,v ...

  5. VS2019发布至远程IIS部署流程

    服务器部署 传统的开发将项目发布至本地桌面之后,复制至站点目录或通过FTP上传站点目录,有点小麻烦,通过开发工具VS2019本身集成的功能,可以一步到发布到远程IIS站点. 条件: VS系列发工具,例 ...

  6. 【FAQ】申请Health Kit权限的常见问题及解答

    华为运动健康服务(HUAWEI Health Kit)提供原子化数据开放,用户数据被授权获取后,应用可通过接口访问运动健康数据,对相关数据进行增.删.改.查等操作.这篇文章汇总了申请开通Health ...

  7. easygui的简单使用——实现猜字谜小游戏

    游戏:随机生成个谜底,每轮有3次机会,猜对了结束本轮游戏,猜错了提示猜大了还是猜小了,并显示剩余次数,3次用完后本轮字谜游戏结束,可重新开始猜字谜,也可结束游戏 # 使用 easygui 实现猜字谜游 ...

  8. pyCharm中下载包的速度慢的解决方案

    1.解决方案 使用阿里镜像 2.具体步骤 1.在项目里面新建一个xxx.py文件 2.然后将下面的代码复制进xxx.py文件 import os ini = "[global]\nindex ...

  9. 就聊聊不少小IT公司的技术总监

    本文想告诉大家如下两个观点. 1 很多IT小公司的技术总监,论能力其实也就是相当于大公司的高级程序员. 2 程序员在职业发展过程中,绝对应该优先考虑进大厂或好公司.如果仅仅停留在小公司,由于小公司可能 ...

  10. 痞子衡嵌入式:MCUBootUtility v4.0发布,开始支持MCX啦

    -- 痞子衡维护的 NXP-MCUBootUtility 工具距离上一个大版本(v3.5.0)发布过去 9 个月了,这一次痞子衡为大家带来了版本升级 v4.0.0,这个版本主要有两个重要更新需要跟大家 ...