mem:分配、重新分配和布局
描述大小(以字节为单位)、分配失败、重新分配边界和释放责任。
Wave Foundation
分配和解除分配
std::mem::alloc
mem_alloc(size: i64) -> ptr<u8>
mem_alloc_zeroed(size: i64) -> ptr<u8>
mem_free(p: ptr<u8>, size: i64) -> i64
mem_realloc(old_ptr: ptr<u8>, old_size: i64, new_size: i64) -> ptr<u8>
大小以字节为单位。大小为 0 或更小的分配会返回 null。如果正大小的分配也失败,则可能是null。不要假设mem_alloc的初始内容,但如果需要零初始化,请使用mem_alloc_zeroed。
调用者拥有每个成功的分配,并且在释放它时必须传递其原始大小。 mem_free(null, size) 返回 0。非 null 指针与非正大小配对是错误的。切勿在释放后访问或释放分配。
重新分配时的分类
| 请求 | 行动 |
|---|---|
| 新尺寸积极且成功 | min(old_size, new_size) 复制字节并释放前一个字节 |
| 正大小的新分配失败 | null 返回,维持现有分配 |
old_ptr == null,正新尺寸 |
行为就像一个新任务 |
new_size == 0 |
尝试释放有效的先前分配并返回 null |
| old_size=0 表示负大小或现有指针 | null 返回 |
重新分配到大小零的null结果并不能确定释放是否成功。如果您需要其状态,请直接致电mem_free。增加分配后,请自行初始化新添加的区域。
保留现有指针的示例
下面是新尺寸为正值的情况。将其保存为main.wave并运行。
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 grown: ptr<u8> = mem_realloc(data, 4, 8);
if (grown == null) {
mem_free(data, 4);
return 2;
}
data = grown;
println("{}", deref data[0]);
if (mem_free(data, 8) < 0) {
return 3;
}
return 0;
}
执行结果:
7
如果在确认失败之前用data = mem_realloc(...)覆盖的话,可能会丢失现有的地址。如果重新分配成功,则旧地址和指向它的指针将不被使用。
目标类型的大小和对齐方式
std::mem::layout
size_of<T>() -> u64
align_of<T>() -> u64
这两个值都是编译目标的布局,而不是运行它的计算机的布局。 size_of 包括尾部填充,并且不生成或评估值。当将元素数量乘以其大小时,我们检查是否溢出。您可以使用std::mem::ops的mem_size_mul_checked和mem_size_add_checked。
mem_copy 用于复制不重叠的范围,mem_move 用于复制可能重叠的范围。两者都无法仅根据指针确定实际分配长度,因此调用者必须保证边界。可以使用 Buffer 管理不同大小的字节列表。
布局查询示例
将其保存为main.wave并运行。文档中涉及的目标i32的大小和对齐方式均为4字节,因此输出4 4。通过目标检查其他类型的值,尤其是结构体和指针。
import("std::mem::layout")::{
size_of, align_of
};
fun main() {
println("{} {}", size_of<i32>(), align_of<i32>());
}
检查尺寸计算是否溢出
在将 count * element_size 传递给赋值函数之前,您需要检查它是否有效。如果你用溢出值分配一个小空间并写入与原始数量一样多的内容,就会出界。
import("std::mem::ops")::{mem_size_add_checked, mem_size_mul_checked};
fun main() {
var result: i64 = 99;
if (mem_size_mul_checked(3, 4, &result) == 0) {
println("bytes={}", result);
}
if (mem_size_add_checked(9223372036854775807, 1, &result) < 0) {
println("overflow rejected");
}
}
执行结果:
bytes=12
overflow rejected
失败结果不会写入分配大小。仅通过常规算术计算并查看结果是否为负,无法检测所有溢出。要计算需要检查的尺寸,请从一开始就使用 checked 函数。
对于重叠副本,mem_move
当向后移动同一数组的一部分时,输入和输出区域会重叠。不要将重叠范围传递给mem_copy,请使用mem_move。
import("std::mem::ops")::{mem_move};
fun main() {
var data: array<u8, 5> = [1, 2, 3, 4, 5];
mem_move(&data[1], &data[0], 4);
for (var index: i32 = 0; index < 5; index += 1) {
println("{}", data[index]);
}
}
执行结果:
1
1
2
3
4
原来的前四个字节向右移动一个位置。手动向前复制它们可能会读取已被覆盖的值,从而意外地生成所有值。重叠感知 API 会为您处理复制方向。
编写一个传递所有权的函数
对于返回内存的函数,最好在成功时提供返回地址以及释放内存所需的大小。如果调用者必须猜测大小,则很容易出现错误释放。如果函数返回借用地址,则调用者不应释放该地址,并描述原始地址的生命周期。
跨越函数边界,您应该能够跟踪allocator → owner → deallocation。指针变量的名称或类型不会自动确定所有权。