10.内存分配和资源管理

了解分配失败、初始化、范围和释放。

Wave Foundation

什么时候需要动态存储空间?

固定大小的数组在其类型中包含其长度。当数据量仅在运行时已知时(例如文件大小或输入长度),请使用动态内存。当您不再需要每个分配时,将其释放。

在本章中,您将管理一个小型分配,调整其大小,然后使用Buffer。传递指针与转移所有权不同。按照示例操作时,确定每个函数拥有哪些资源。

分配、检查、使用、释放

import("std::mem::alloc")::{
    mem_alloc_zeroed, mem_free
};

fun main() -> i32 {
    var size: i64 = 4;
    var data: ptr<u8> = mem_alloc_zeroed(size);

    if (data == null) {
        println("allocation failed");
        return 1;
    }

    deref data[0] = 42;
    println("{} {}", deref data[0], deref data[1]);

    var status: i64 = mem_free(data, size);

    if (status < 0) {
        return 2;
    }

    return 0;
}

执行结果:

42 0

该程序有四个步骤:请求 4 个字节,检查null,仅访问有效范围,并释放分配。成功后,mem_alloc_zeroed将内存初始化为零,因此即使程序尚未写入第二个字节也是零。

不要假设mem_alloc返回的内存的任何初始内容。在读取每个区域之前对其进行初始化。零或负分配大小返回null。正大小分配也可能无法获取内存。

尺寸单位

内存分配 API 的大小参数以字节为单位。要分配十个整数,请将元素大小乘以元素数量。检查这个乘法没有溢出。

import("std::mem::alloc")::{
    mem_alloc, mem_free
};
import("std::mem::layout")::{
    size_of
};
import("std::mem::ops")::{
    mem_size_mul_checked
};

fun main() -> i32 {
    var count: i64 = 3;
    var bytes: i64 = 0;
    var item_size: i64 = size_of<i32>() as i64;

    if (mem_size_mul_checked(count, item_size, &bytes) < 0) {
        return 1;
    }

    var raw: ptr<u8> = mem_alloc(bytes);

    if (raw == null) {
        return 2;
    }

    var values: ptr<i32> = raw as ptr<i32>;

    for (var index: i64 = 0; index < count; index += 1) {
        deref values[index] = (index + 1) as i32;
    }

    println("{} {} {}", deref values[0], deref values[1], deref values[2]);

    if (mem_free(raw, bytes) < 0) {
        return 3;
    }

    return 0;
}

执行结果:

1 2 3

count 是元素计数; bytes 是字节数。指针算术以i32为单位移动,但释放分配需要其原始大小(以字节为单位)。 size_of 使用目标类型布局,使与元素类型的关系明确。

对于一般非常大的类型,将size_of的结果更改为i64时,还必须考虑转换范围。这里我们使用i32,它的大小已知。

即使在故障路径上也能进行清理

如果分配后另一个操作失败,请在提前返回之前释放内存。所有权表有助于识别您可能会错过的清理路径。

步骤 拥有的资源 如果你失败了
分配前 无 立即归还
分配成功后 data 和原始尺寸 data 发布后返回
重新分配成功后 新地址和新尺寸 新地址发布
释放后 无 不要使用旧地址

覆盖指针变量并丢失原始地址也会丢失释放分配所需的信息。这会导致内存泄漏。相反,通过两个所有者释放相同的分配会导致双重释放。

重新分配以增加规模

import("std::mem::alloc")::{
    mem_alloc, mem_realloc, mem_free
};

fun main() -> i32 {
    var data: ptr<u8> = mem_alloc(4);

    if (data == null) {
        return 1;
    }

    deref data[0] = 7;
    var next: ptr<u8> = mem_realloc(data, 4, 8);

    if (next == null) {
        mem_free(data, 4);
        return 2;
    }

    data = next;
    deref data[4] = 9;
    println("{} {}", deref data[0], deref data[4]);

    if (mem_free(data, 8) < 0) {
        return 3;
    }

    return 0;
}

执行结果:

7 9

首先将结果存储在 next 中。如果较大块的分配失败,原始数据仍然有效并且仍然可以被释放。成功后,旧的分配将被释放,并且必须使用新的地址。在读取新添加的区域之前先对其进行初始化。

本例中的新尺寸为正值。 new_size=0 的请求会尝试释放现有分配并返回 null。因此,null结果并不总是意味着旧的分配仍然有效。当需要检查释放是否成功时,直接调用mem_free即可。

何时重新检查借用的指针

data 保存内部指针并在重新分配后使用它是错误的。这是因为新的data的地址可能不同。如果需要内部位置,可以存储offset代替地址,成功后根据新的data重新计算。

释放或重新分配内存也会影响借用它的代码。检查其他操作是否仍在使用该内存。传递给异步操作的缓冲区必须保持有效,直到操作完成。

字节列表包含Buffer

在手动重新分配时管理长度频繁变化的字节列表需要处理len和cap、扩展失败和大小计算。 std 的Buffer 捆绑了这些操作。

import("std::buffer::alloc")::{
    Buffer, buffer_init, buffer_free
};
import("std::buffer::write")::{
    buffer_append_str
};

fun main() -> i32 {
    var message: Buffer;

    if (buffer_init(&message, 0) < 0) {
        return 1;
    }

    if (buffer_append_str(&message, "Hello") < 0) {
        buffer_free(&message);
        return 2;
    }

    if (buffer_append_str(&message, ", Wave") < 0) {
        buffer_free(&message);
        return 3;
    }

    println("bytes={}", message.len);

    if (buffer_free(&message) < 0) {
        return 4;
    }

    return 0;
}

执行结果:

bytes=11

len 是正在使用的字节数; cap 是分配的容量。必要时附加数据会增加分配。使用Buffer并不能免除调用者释放它的责任。

buffer_append_str 不会将 NUL 添加到字符串末尾。因此,message.data不应该直接输出为str。使用 I/O 函数输出字节,该函数获取长度,或显式构造字符串表示形式。

练习与解题思路

将字节0到9一一加到Buffer并得到和。每次追加失败时必须释放它,并且仅在len范围内执行读取。完整解和边界失效可以按照Buffer 使用方法中的示例进行检查。

尝试在代码中标记分配、重新分配和取消分配调用。对于每个成功的分配,您必须能够描述谁拥有它以及哪条路径释放它。