容器字符串与内存
自动关联目录:容器字符串与内存
这一页讲的是"每天写代码都在用、但代价不清楚"的那批类型:TArray、TMap、TSet、FString、FName、FText,以及它们背后的分配器。
一句话定位:UE 不用 STL 不是因为造轮子癖,而是因为需要统一的内存统计、可预测的分配行为,以及跨平台的确定性。代价是这套容器有自己的规则,照 STL 的直觉用会踩坑。
三种字符串
| 类型 | 本质 | 用途 | 代价 |
|---|---|---|---|
FName | 全局字符串表里的索引 | 标识、Key、Tag、资源名 | 创建要查表(有锁),但比较与复制极快 |
FString | 动态字符数组(TArray<TCHAR>) | 拼接、编辑、IO | 每次操作可能分配 |
FText | 本地化文本(含命名空间与键) | 所有显示给玩家的文本 | 构造与查表最贵 |
判断口诀:
- 要显示 →
FText - 要拼接/解析/打印日志 →
FString - 要比较/当 Key/反复传递 →
FName
FName N = FName(TEXT("Head")); // 查表一次
FString S = N.ToString(); // 转回字符串,有分配
FText T = FText::FromString(S); // 转本地化文本在循环里做 FString → FName 或反过来,是隐藏的性能杀手。热路径上应该统一用 FName,只在边界转换一次。
常见误用
| 误用 | 问题 |
|---|---|
UI 上用 FString | 无法本地化,且每帧重建 |
用 FString 做字典 Key | 比较是逐字符的,FName 只比索引 |
拼字符串用 + 循环 | 每次都重新分配,用 FString::Printf 或 Append |
FName 用 TEXT() 之外的变量频繁构造 | 查表加锁,多线程下更明显 |
| 日志里格式化复杂字符串 | 即使日志级别关闭也会先拼出来 |
容器
| 容器 | 底层 | 特点 |
|---|---|---|
TArray<T> | 连续内存 | 默认首选;随机访问快,中间插入慢 |
TMap<K,V> | 哈希表 | 查找 O(1),无序 |
TSet<T> | 哈希表 | 去重集合 |
TSortedMap | 有序 | 需要有序遍历时用 |
TQueue / TDeque | 队列/双端 | 见 实现双端队列 |
TStaticArray / TInlineAllocator | 栈上/内联 | 小数组避免堆分配 |
TArray 的关键行为
TArray<int32> A;
A.Reserve(1024); // 预留容量,避免多次扩容
A.Add(1);
A.Emplace(2); // 原地构造,少一次拷贝
A.SetNum(10); // 改大小(可能构造/析构元素)
A.SetNumUninitialized(10); // 不初始化,仅用于 POD,更快
A.Reset(); // 清空但保留已分配内存
A.Empty(); // 清空并释放内存
A.Shrink(); // 释放多余容量| 操作 | 复杂度 | 注意 |
|---|---|---|
Add / Emplace | 均摊 O(1) | 扩容会整体搬移 |
中间 Insert / Remove | O(n) | 会搬移后续元素 |
RemoveAtSwap | O(1) | 不保序,性能敏感且不在意顺序时用 |
Sort | O(n log n) | 用 TArray::Sort 而非 std::sort 以走 UE 分配器 |
Find | O(n) | 频繁查找应换 TMap |
Reserve 是被低估的一行代码:在明确知道数量级时预留,能消掉一整条 realloc 链。
TMap / TSet
TMap<FName, int32> Counts;
Counts.Reserve(256);
Counts.FindOrAdd(TEXT("Coin")) += 1;
if (int32* Ptr = Counts.Find(TEXT("Coin"))) { /* ... */ }| 注意 | 说明 |
|---|---|
自定义 Key 要实现 GetTypeHash | 否则编译失败 |
Find 返回指针 | 没找到是 nullptr,不要先 Contains 再 Find(查两次) |
| 遍历时删除 | 用迭代器 RemoveCurrent,不要 Remove 后继续用旧索引 |
TMultiMap | 一键多值 |
for (auto It = Counts.CreateIterator(); It; ++It)
{
if (It->Value <= 0) { It.RemoveCurrent(); }
}智能指针
| 类型 | 对象 | 用途 |
|---|---|---|
TSharedPtr / TWeakPtr | 非 UObject | 引用计数,UE 自己的实现(带调试信息) |
TUniquePtr | 非 UObject | 独占所有权 |
TWeakObjectPtr | UObject | GC 安全弱引用(见 GC) |
TSoftObjectPtr | UObject 资产 | 不强制加载 |
TStrongObjectPtr | UObject | 强引用保活 |
UObject 不要放进 TSharedPtr——两套生命周期机制打架,会出现"引用计数认为活着但 GC 已经回收"的野指针。
分配器
| 分配器 | 用途 |
|---|---|
FMemory::Malloc/Free/Realloc | 走引擎分配器,会计入内存统计 |
FMemory::Memcpy/Memzero | 平台优化版本 |
TInlineAllocator<N> | 容器小容量时内联在对象里,超过才堆分配 |
TFixedAllocator<N> | 固定容量,超出即崩 |
TMemoryImageAllocator | 免反序列化布局 |
FMallocAnsi / FMallocBinned2/3 | 底层实现,-ansimalloc 可切换 |
// 小数组常驻在栈/内联区,避免堆分配
TArray<int32, TInlineAllocator<8>> SmallNums;为什么不用 std::vector:UE 容器能被 FMemory 统计、能被内存分析工具(MemPro、LLM)识别、行为在各平台一致。混用 STL 的结果是内存报告对不上账。
内存统计与排查
| 命令 | 作用 |
|---|---|
stat memory | 总览,各分类占用 |
memreport -full | 生成完整内存报告 |
LLM(Low Level Memory tracker) | 逐系统追踪,需启动时开启 -llm |
stat streaming | 资源流式加载状态 |
排查路线:先 memreport 看是哪一类(Texture / Mesh / UObject / 自定义),再针对该类深入。具体工具见 Profile。
常见坑
| 坑 | 说明 |
|---|---|
用 FString 做高频比较的 Key | 应换 FName |
循环里 + 拼字符串 | 多次分配,用 Append / Printf |
忘记 Reserve | 大量小 realloc |
遍历容器时按索引 Remove | 索引错位,漏删或越界 |
UObject 放进 TSharedPtr | 双重生命周期管理,野指针 |
混用 std:: 容器 | 内存统计对不上,分析工具看不到 |
TArray 存大对象并按值传递 | 触发深拷贝,用引用或 TArrayView |
用 Contains + Find | 查两遍,直接 Find 判空即可 |
返回 TArray 而不保留容量 | 反复分配;复用成员数组 + Reset |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/b4135534.html
更新于:2026年10月10日