引言

读完本篇文章,你会了解为何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. webrtc编译,不使用内置boringssl,使用openssl的

    前言 在项目开发过程中,会遇到使用https.TLS.DTLS等场景,这些第三方库一般会使用openssl作为加密套件.例如,qt中加密套件就会使用openssl,但是webrtc会默认使用borin ...

  2. 如何在kali Linux上安装VMware Tools

    作用: 1.让虚拟机和本地上的文件可以互传,直接拖动就可以实现转接 2.可在虚拟机上执行本地脚本 3.本地时钟与虚拟机同步 4........... 方法: 1.运行虚拟机 2.在上方菜单栏中点击安装 ...

  3. Sql Server日期转汉字字符串

    以下脚本转至互联网,增加了自己需要的功能并改成了函数的方式 SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO -- ================== ...

  4. day42 6-5 springMVC调度器、ModelAndView、配置thymeleaf模板引擎 & 6-6 thymeleaf语法 & 6-7 springMVC拦截器 & 6-8 设置请求编码过滤器Filter

    springMVC调度器 - DispatcherServlet - SpringMVC框架的入口 定义 DispatcherServlet成为调度器,配置在web.xml文件中,用于拦截匹配的请求. ...

  5. Dev-Cpp下载与安装

    目录 一.介绍 Dev-Cpp 二.下载 Dev-Cpp 1.通过百度网盘下载 2.通过 SourceForge 官网下载 三.安装 Dev-Cpp 写在结尾的话 免责声明 大家好,这里是 main工 ...

  6. 【实时数仓】Day02-DWD、DIM层数据准备:各层职能、行为日志DWD层、业务日志DWD层及分流(Phoenix和HBASE)

    一.需求分析及实现思路 1.分层需求 建立数仓目的:增加数据计算的复用性 可以从半成品继续加工而成 从kafka的ODS层(数据一开始就读到了kafka)读用户行为数据和业务数据,并写回到kafka的 ...

  7. Kubernetes-基于容器云构建devops平台

    1.基于kubernetes devops的整体方案 本文以Kubernetes为基础,为基于java语言研发团队提供一套完整的devops解决方案.在此方案中,开发人员基于eclipse集成开发环境 ...

  8. Ubuntu20.04 Java相关环境(JDK、Mysql、Redis、nacos、influxdb)部署以及运行

    重装了系统,系统版本号为:Ubuntu20.04 1.云平台 登录云平台,选择要重装的服务器,关机.一键重装即可 2.安装jdk 下载jdk-8u341-linux-x64.tar.gz,并复制到服务 ...

  9. 多种方法实现单例模式 pickle模块

    目录 单例模式 类方法@classmethod实现 元类实现 模块实现 装饰器实现 双下call.反射实现 pickle序列化模块 单例模式 比如系统调用打印机,不管你要打印几次,都是调用同一个打印机 ...

  10. 使用 SmartIDE 开发golang项目

    目录 概述 架构 开发视图 快速开始 安装 SmartIDE CLI 环境 启动 创建环境 安装工具 调试 基本调试 Start 命令调试 很荣幸在去年加入到 SmartIDE 产品组,从事开发工作, ...