UEC++ API——FMemory 内存操作篇

1. 内存分配与释放

1.1 分配内存 (FMemory::Malloc)

函数签名:

void* FMemory::Malloc(SIZE_T Count, uint32 Alignment = DEFAULT_ALIGNMENT)

功能描述:
FMemory::Malloc 函数用于分配一块指定大小的内存。这个函数是 Unreal Engine 中最基本的内存分配函数,它替代了标准 C 的 malloc 函数。

参数解释:

  • Count: 你想要分配的内存大小(以字节为单位)
  • Alignment: 内存对齐值,通常使用默认值 DEFAULT_ALIGNMENT。在某些特殊情况下,例如 SIMD 操作,可能需要特定的内存对齐。

返回值:

  • 如果成功,返回一个指向新分配内存的指针
  • 如果失败(例如,内存不足),返回 nullptr(空指针)

示例代码:

// 分配1024字节的内存
void* MemoryPtr = FMemory::Malloc(1024);

// 始终检查内存分配是否成功
if (MemoryPtr)
{
    // 内存分配成功
    UE_LOG(LogTemp, Log, TEXT("成功分配了1024字节的内存"));

    // 这里可以使用MemoryPtr了
    // 例如,可以将它转换为特定类型的指针并使用
    int32* IntArray = static_cast<int32*>(MemoryPtr);
    IntArray[0] = 10; // 设置第一个整数为10

    // 使用完毕后,记得释放内存(我们稍后会讲到)
    FMemory::Free(MemoryPtr);
}
else
{
    // 内存分配失败
    UE_LOG(LogTemp, Error, TEXT("内存分配失败,可能是内存不足"));
}

注意事项:

  1. 总是检查 Malloc 的返回值,确保内存分配成功。
  2. 分配的内存不会自动初始化,里面可能包含随机数据。如果需要初始化内存,可以使用 FMemory::MemzeroFMemory::Memset
  3. 记住分配的内存大小,避免越界访问。
  4. 使用完毕后,一定要调用 FMemory::Free 释放内存,否则会造成内存泄漏。
  5. 在性能敏感的代码中,考虑使用内存池或自定义分配器来减少内存分配的开销。

1.2 释放内存 (FMemory::Free)

函数签名:

void FMemory::Free(void* Ptr)

功能描述:
FMemory::Free 函数用于释放之前通过 FMemory::Malloc 分配的内存。这个函数对应于标准 C 的 free 函数。

参数解释:

  • Ptr: 指向要释放的内存块的指针(就是你之前从 Malloc 得到的那个指针)

返回值:

  • 无返回值

示例代码:

// 首先分配一些内存
void* MemoryPtr = FMemory::Malloc(1024);

if (MemoryPtr)
{
    // 使用内存...
    // 例如:char* CharArray = static_cast<char*>(MemoryPtr);
    // FMemory::Memcpy(CharArray, "Hello, Unreal!", 15);

    // 使用完毕,现在释放内存
    FMemory::Free(MemoryPtr);

    // 良好的编程习惯:释放后将指针置为nullptr
    MemoryPtr = nullptr;

    UE_LOG(LogTemp, Log, TEXT("内存已成功释放"));
}

注意事项:

  1. 只释放通过 FMemory::Malloc 分配的内存。不要使用 FMemory::Free 来释放通过 new 关键字或其他内存分配函数分配的内存。
  2. 不要重复释放同一块内存,这会导致未定义行为,可能引起程序崩溃。
  3. 释放后,最好将指针设为 nullptr,防止后续误用(悬空指针)。
  4. 在类的析构函数中确保释放所有动态分配的内存,防止内存泄漏。
  5. 如果你在使用智能指针(如 TSharedPtr),通常不需要手动调用 Free,因为智能指针会自动管理内存。

1.3 调整内存块大小 (FMemory::Realloc)

函数签名:

void* FMemory::Realloc(void* Original, SIZE_T Count, uint32 Alignment = DEFAULT_ALIGNMENT)

功能描述:
FMemory::Realloc 函数用于改变已分配内存块的大小。它可以增加或减少内存块的大小,相当于标准 C 的 realloc 函数。

参数解释:

  • Original: 指向原内存块的指针(之前由 MallocRealloc 分配的)
  • Count: 新的内存大小(字节数)
  • Alignment: 内存对齐值,通常使用默认值

返回值:

  • 如果成功,返回指向调整大小后内存块的指针(可能与原指针不同)
  • 如果失败,返回 nullptr,原内存块保持不变

示例代码:

// 首先分配1024字节的内存
void* OriginalPtr = FMemory::Malloc(1024);

if (OriginalPtr)
{
    UE_LOG(LogTemp, Log, TEXT("初始内存大小: 1024 字节"));

    // 现在将内存块大小调整为2048字节
    void* NewPtr = FMemory::Realloc(OriginalPtr, 2048);

    if (NewPtr)
    {
        // 重新分配成功
        OriginalPtr = NewPtr; // 更新指针,因为地址可能已改变
        UE_LOG(LogTemp, Log, TEXT("内存大小已调整为: 2048 字节"));

        // 使用新的内存块...
        int32* IntArray = static_cast<int32*>(OriginalPtr);
        IntArray[256] = 42; // 现在我们可以安全地访问更多内存

        // 使用完毕后释放
        FMemory::Free(OriginalPtr);
    }
    else
    {
        // 重新分配失败,原内存块仍然有效
        UE_LOG(LogTemp, Error, TEXT("内存重新分配失败"));
        FMemory::Free(OriginalPtr);
    }
}

注意事项:

  1. Realloc 可能会改变内存块的地址,所以要更新所有相关指针。
  2. 如果 Realloc 失败,原内存块保持不变,你需要自己处理这种情况(例如,尝试单独分配新内存并复制数据)。
  3. 增加内存大小时,新增的部分不会被初始化,可能包含随机数据。
  4. 减小内存大小时,多余的数据会被截断。
  5. 频繁调用 Realloc 可能影响性能,考虑一次性分配足够的内存或使用其他数据结构(如 TArray)来管理动态大小的数据。
  6. 在多线程环境中使用 Realloc 时要特别小心,因为它可能改变内存地址,这可能导致其他线程访问无效内存。

1.4 对齐内存分配 (FMemory::Malloc_Aligned)

函数签名:

void* FMemory::Malloc_Aligned(SIZE_T Size, uint32 Alignment)

功能描述:
FMemory::Malloc_Aligned 函数用于分配对齐的内存。这在某些特殊情况下很有用,例如当你需要为 SIMD 操作分配内存时。

参数解释:

  • Size: 要分配的内存大小(字节数)
  • Alignment: 内存对齐值,通常是 2 的幂(如 4, 8, 16, 32, 64 等)

返回值:

  • 如果成功,返回指向分配的对齐内存的指针
  • 如果失败,返回 nullptr

示例代码:

// 分配 1024 字节,16 字节对齐的内存
void* AlignedMemory = FMemory::Malloc_Aligned(1024, 16);

if (AlignedMemory)
{
    UE_LOG(LogTemp, Log, TEXT("成功分配了 1024 字节的 16 字节对齐内存"));

    // 检查内存是否真的对齐了
    if (reinterpret_cast<uintptr_t>(AlignedMemory) % 16 == 0)
    {
        UE_LOG(LogTemp, Log, TEXT("内存正确地 16 字节对齐"));
    }

    // 使用内存...

    // 释放对齐的内存
    FMemory::Free(AlignedMemory);
}
else
{
    UE_LOG(LogTemp, Error, TEXT("对齐内存分配失败"));
}

注意事项:

  1. 对齐值通常应该是 2 的幂。常见的对齐值有 4, 8, 16, 32, 64 等。
  2. 使用对齐内存可能会带来一些内存开销,因为可能需要额外的空间来确保对齐。
  3. 对齐内存对于某些硬件优化很重要,特别是在使用 SIMD 指令时。
  4. 释放对齐内存时,使用普通的 FMemory::Free 函数即可,不需要特殊的释放函数。
  5. 在大多数情况下,使用默认的 FMemory::Malloc 就足够了,只有在确实需要特定对齐的情况下才使用 Malloc_Aligned

这就是 FMemory 类中关于内存分配与释放的主要函数的详细说明。这些函数构成了 Unreal Engine 中内存管理的基础。正确使用这些函数可以帮助您有效地管理游戏或应用程序的内存使用,提高性能并避免内存相关的问题。

2. 内存操作

2.1 内存设置 (FMemory::Memset)

函数签名:

void FMemory::Memset(void* Dest, uint8 Char, SIZE_T Count)

功能描述:
FMemory::Memset 函数用于将一块内存区域设置为特定的值。这个函数类似于标准 C 的 memset 函数。

参数解释:

  • Dest: 目标内存块的指针
  • Char: 要设置的值(作为 8 位无符号整数)
  • Count: 要设置的字节数

返回值:

  • 无返回值

示例代码:

// 分配 100 字节的内存
void* Buffer = FMemory::Malloc(100);

if (Buffer)
{
    // 将整个缓冲区设置为 0
    FMemory::Memset(Buffer, 0, 100);
    UE_LOG(LogTemp, Log, TEXT("已将 100 字节内存初始化为 0"));

    // 将缓冲区的前 10 个字节设置为 0xFF
    FMemory::Memset(Buffer, 0xFF, 10);
    UE_LOG(LogTemp, Log, TEXT("已将前 10 字节设置为 0xFF"));

    // 验证结果
    uint8* ByteBuffer = static_cast<uint8*>(Buffer);
    if (ByteBuffer[0] == 0xFF && ByteBuffer[9] == 0xFF && ByteBuffer[10] == 0)
    {
        UE_LOG(LogTemp, Log, TEXT("内存设置正确"));
    }

    // 使用完毕后释放内存
    FMemory::Free(Buffer);
}

注意事项:

  1. Memset 通常用于初始化内存或将内存重置为已知状态。
  2. 虽然 Char 参数是 uint8 类型,但它会被重复应用到每个字节。这意味着你不能用它来一次性设置多字节值(比如 int32)。
  3. 小心不要越界:确保 Count 不超过分配的内存大小。
  4. 对于需要设置为 0 的内存,可以考虑使用专门的 FMemory::Memzero 函数,它可能在某些平台上有优化。
  5. 在处理非 POD(Plain Old Data)类型时要小心使用 Memset,因为它可能会破坏对象的内部结构。

2.2 内存复制 (FMemory::Memcpy)

函数签名:

void FMemory::Memcpy(void* Dest, const void* Src, SIZE_T Count)

功能描述:
FMemory::Memcpy 函数用于将一块内存的内容复制到另一块内存。这个函数类似于标准 C 的 memcpy 函数。

参数解释:

  • Dest: 目标内存块的指针
  • Src: 源内存块的指针
  • Count: 要复制的字节数

返回值:

  • 无返回值

示例代码:

// 源数据
const char* SourceString = "Hello, Unreal Engine!";
SIZE_T StringLength = FPlatformString::Strlen(SourceString) + 1; // +1 for null terminator

// 分配目标内存
void* Destination = FMemory::Malloc(StringLength);

if (Destination)
{
    // 复制内存
    FMemory::Memcpy(Destination, SourceString, StringLength);

    // 验证复制结果
    if (FPlatformString::Strcmp(static_cast<const char*>(Destination), SourceString) == 0)
    {
        UE_LOG(LogTemp, Log, TEXT("内存复制成功: %s"), UTF8_TO_TCHAR(static_cast<const char*>(Destination)));
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("内存复制失败"));
    }

    // 释放内存
    FMemory::Free(Destination);
}

注意事项:

  1. 确保目标内存块至少与要复制的数据一样大,否则可能导致缓冲区溢出。
  2. Memcpy 不检查源和目标内存是否重叠。如果内存区域可能重叠,应使用 FMemory::Memmove
  3. 对于非 POD 类型,直接使用 Memcpy 可能会导致问题,因为它不会调用构造函数或赋值运算符。
  4. 在多线程环境中使用 Memcpy 时要注意同步,以避免数据竞争。
  5. 对于大量数据的复制,可能需要考虑更高效的方法,如 DMA 传输或流式处理。

2.3 内存移动 (FMemory::Memmove)

函数签名:

void FMemory::Memmove(void* Dest, const void* Src, SIZE_T Count)

功能描述:
FMemory::Memmove 函数用于将一块内存的内容移动到另一块内存,即使源和目标内存区域重叠也能正确处理。这个函数类似于标准 C 的 memmove 函数。

参数解释:

  • Dest: 目标内存块的指针
  • Src: 源内存块的指针
  • Count: 要移动的字节数

返回值:

  • 无返回值

示例代码:

// 分配一块内存并初始化
const int32 BufferSize = 10;
int32* Buffer = static_cast<int32*>(FMemory::Malloc(BufferSize * sizeof(int32)));

if (Buffer)
{
    // 初始化缓冲区
    for (int32 i = 0; i < BufferSize; ++i)
    {
        Buffer[i] = i;
    }

    // 打印初始状态
    FString InitialState = TEXT("初始状态: ");
    for (int32 i = 0; i < BufferSize; ++i)
    {
        InitialState += FString::Printf(TEXT("%d "), Buffer[i]);
    }
    UE_LOG(LogTemp, Log, TEXT("%s"), *InitialState);

    // 使用 Memmove 将后半部分移动到前面,覆盖一部分数据
    FMemory::Memmove(Buffer, Buffer + 5, 5 * sizeof(int32));

    // 打印移动后的状态
    FString FinalState = TEXT("移动后状态: ");
    for (int32 i = 0; i < BufferSize; ++i)
    {
        FinalState += FString::Printf(TEXT("%d "), Buffer[i]);
    }
    UE_LOG(LogTemp, Log, TEXT("%s"), *FinalState);

    // 释放内存
    FMemory::Free(Buffer);
}

注意事项:

  1. Memmove 能够正确处理源和目标内存重叠的情况,这是它与 Memcpy 的主要区别。
  2. 虽然 Memmove 可以处理重叠内存,但如果确定内存不重叠,使用 Memcpy 可能会更快。
  3. Memcpy 一样,Memmove 不适合直接用于非 POD 类型的对象。
  4. 在进行大规模内存移动时,考虑性能影响,可能需要寻找更优化的数据结构或算法。
  5. 在多线程环境中使用时,要确保proper同步以避免数据竞争。

2.4 内存比较 (FMemory::Memcmp)

函数签名:

int32 FMemory::Memcmp(const void* Buf1, const void* Buf2, SIZE_T Count)

功能描述:
FMemory::Memcmp 函数用于比较两块内存区域的内容。这个函数类似于标准 C 的 memcmp 函数。

参数解释:

  • Buf1: 第一个内存块的指针
  • Buf2: 第二个内存块的指针
  • Count: 要比较的字节数

返回值:

  • 如果两块内存完全相同,返回 0
  • 如果 Buf1 小于 Buf2,返回小于 0 的值
  • 如果 Buf1 大于 Buf2,返回大于 0 的值

示例代码:

// 创建两个测试缓冲区
const char* Buffer1 = "Hello, Unreal!";
const char* Buffer2 = "Hello, World!";
SIZE_T BufferLength = FPlatformString::Strlen(Buffer1) + 1; // +1 for null terminator

// 比较整个字符串
int32 CompareResult = FMemory::Memcmp(Buffer1, Buffer2, BufferLength);

if (CompareResult == 0)
{
    UE_LOG(LogTemp, Log, TEXT("缓冲区完全相同"));
}
else if (CompareResult < 0)
{
    UE_LOG(LogTemp, Log, TEXT("Buffer1 小于 Buffer2"));
}
else
{
    UE_LOG(LogTemp, Log, TEXT("Buffer1 大于 Buffer2"));
}

// 比较前 5 个字符
CompareResult = FMemory::Memcmp(Buffer1, Buffer2, 5);

if (CompareResult == 0)
{
    UE_LOG(LogTemp, Log, TEXT("前 5 个字符相同"));
}
else
{
    UE_LOG(LogTemp, Log, TEXT("前 5 个字符不同"));
}

注意事项:

  1. Memcmp 进行的是字节级别的比较,对于非 POD 类型可能不会得到预期的结果。
  2. 比较的结果取决于字节的二进制表示,可能受到字节序(大端序或小端序)的影响。
  3. 对于字符串比较,考虑使用专门的字符串比较函数(如 FPlatformString::Strcmp),它们能更好地处理字符编码问题。
  4. 在比较浮点数时要特别小心,因为浮点数的二进制表示可能因舍入误差而略有不同。
  5. 确保比较的字节数不超过任何一个缓冲区的实际大小,否则可能导致缓冲区溢出。

2.5 内存清零 (FMemory::Memzero)

函数签名:

void FMemory::Memzero(void* Dest, SIZE_T Count)

功能描述:
FMemory::Memzero 函数用于将一块内存区域的所有字节设置为零。这是一个专门用于清零内存的优化版本,可能比使用 Memset 设置为零更高效。

参数解释:

  • Dest: 要清零的内存块的指针
  • Count: 要清零的字节数

返回值:

  • 无返回值

示例代码:

// 分配一块内存
const int32 BufferSize = 1024;
void* Buffer = FMemory::Malloc(BufferSize);

if (Buffer)
{
    // 将内存清零
    FMemory::Memzero(Buffer, BufferSize);

    // 验证内存是否已清零
    bool bIsZero = true;
    uint8* ByteBuffer = static_cast<uint8*>(Buffer);
    for (int32 i = 0; i < BufferSize; ++i)
    {
        if (ByteBuffer[i] != 0)
        {
            bIsZero = false;
            break;
        }
    }

    if (bIsZero)
    {
        UE_LOG(LogTemp, Log, TEXT("内存已成功清零"));
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("内存清零失败"));
    }

    // 释放内存
    FMemory::Free(Buffer);
}

注意事项:

  1. Memzero 通常用于快速初始化新分配的内存或重置数据结构。
  2. 对于需要安全清除敏感数据的场景,可能需要使用更复杂的方法,因为简单的零填充可能被恢复。
  3. 在某些平台上,Memzero 可能会使用特殊的指令或优化来提高性能。
  4. 对于大块内存,Memzero 可能比 Memset(Ptr, 0, Size) 更快,但对于小块内存,性能差异可能不明显。
  5. 在清零包含虚函数表指针的类实例时要小心,因为这可能会破坏对象的多态性。

这些内存操作函数构成了 Unreal Engine 中底层内存管理的核心。正确使用这些函数可以帮助您有效地操作内存,提高性能并避免常见的内存相关错误。在下一部分中,我们将探讨 FMemory 类提供的一些更高级的内存管理功能。

3. 高级内存管理功能

3.1 计算内存使用量 (FMemory::GetAllocSize)

函数签名:

FMemory::GetAllocSize(void* Original)

功能描述: FMemory::GetAllocSize 函数用于获取之前通过 FMemory::MallocFMemory::Realloc 分配的内存块的大小。

参数解释:

  • Original: 指向已分配内存块的指针

返回值:

  • 返回分配的内存块的大小(以字节为单位)
  • 如果指针无效或未通过 FMemory 分配,可能返回 0 或未定义的值

示例代码:

// 分配一块内存
void* MemoryBlock = FMemory::Malloc(1024);

if (MemoryBlock)
{
// 获取分配的内存大小
SIZE_T AllocatedSize = FMemory::GetAllocSize(MemoryBlock);

UE_LOG(LogTemp, Log, TEXT("分配的内存大小: %llu 字节"), AllocatedSize);

// 使用内存...

// 释放内存
FMemory::Free(MemoryBlock);
}

注意事项:

  • 这个函数对于调试和内存使用优化非常有用。
  • 不同的内存分配器可能会有一些内存开销,所以返回的大小可能比请求的大小稍大。
  • 在释放内存后不要调用这个函数,因为这可能导致未定义行为。

3.2 内存统计 (FMemory::UpdateStats)

函数签名:

void FMemory::UpdateStats()

功能描述: FMemory::UpdateStats 函数用于更新内存使用的统计信息。这个函数通常在每一帧或固定时间间隔调用,以保持内存统计的最新状态。

参数解释:

  • 无参数

返回值:

  • 无返回值

示例代码:

cppCopy// 在游戏主循环或定时器中调用
void AMyGameMode::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // 更新内存统计
    FMemory::UpdateStats();

    // 获取并记录当前内存使用情况
    FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats();
    UE_LOG(LogTemp, Log, TEXT("当前使用的物理内存: %llu MB"), MemoryStats.UsedPhysical / (1024 * 1024));
    UE_LOG(LogTemp, Log, TEXT("当前使用的虚拟内存: %llu MB"), MemoryStats.UsedVirtual / (1024 * 1024));
}

注意事项:

  • 频繁调用这个函数可能会对性能产生影响,所以要根据实际需求来决定调用频率。
  • 统计信息的精确度可能因平台而异。
  • 这个函数主要用于调试和性能分析,不应该在发布版本中频繁调用。

3.3 内存泄漏检测 (FMemory::EnableMemoryTracking)

函数签名:

void FMemory::EnableMemoryTracking(bool bEnable)

功能描述: FMemory::EnableMemoryTracking 函数用于启用或禁用内存跟踪功能。启用后,可以帮助检测内存泄漏和其他内存相关问题。

参数解释:

  • bEnable: 布尔值,true 表示启用内存跟踪,false 表示禁用

返回值:

  • 无返回值

示例代码:

// 在游戏启动时启用内存跟踪
void AMyGameMode::BeginPlay()
{
Super::BeginPlay();

// 启用内存跟踪
FMemory::EnableMemoryTracking(true);

UE_LOG(LogTemp, Log, TEXT("内存跟踪已启用"));
}

// 在游戏结束时检查内存泄漏
void AMyGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);

// 禁用内存跟踪
FMemory::EnableMemoryTracking(false);

// 检查内存泄漏
FMemory::DumpMemoryStats();

UE_LOG(LogTemp, Log, TEXT("内存跟踪已禁用,并已转储内存统计信息"));
}

注意事项:

  • 内存跟踪可能会降低性能,所以通常只在调试版本中使用。
  • 启用内存跟踪后,可以使用其他工具(如UE内存分析器)来分析内存使用情况。
  • 在发布版本中,确保禁用内存跟踪以获得最佳性能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注