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 — количество элементов; байты — это количество байтов. Арифметика указателей перемещается в единицах 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
Сначала сохраните результат в следующем. Если выделение большего блока не удалось, исходные данные остаются действительными и их все равно можно освободить. В случае успеха старое выделение освобождается и необходимо использовать новый адрес. Инициализируйте вновь добавленный регион перед его чтением.
Новый размер в этом примере является положительным. Вместо этого запрос с new_size=0 пытается освободить существующее выделение и возвращает null. Таким образом, результат null не всегда означает, что старое распределение все еще действительно. Вызовите mem_free напрямую, когда вам нужно проверить, удалось ли освободиться.
Когда перепроверять заимствованные указатели
data Неправильно сохранять внутренний указатель и использовать его после перераспределения. Это связано с тем, что адрес нового data может быть другим. Если вам нужно внутреннее местоположение, вы можете сохранить offset вместо адреса и выполнить перерасчет на основе нового data после успеха.
Освобождение или перераспределение памяти также влияет на код, который ее заимствовал. Проверьте, использует ли эта память другая операция. Буфер, переданный асинхронной операции, должен оставаться действительным до завершения операции.
Список байтов содержит Buffer.
Управление списком байтов с часто меняющейся длиной при перераспределении вручную требует обработки как len, так и cap, ошибок расширения и вычислений размера. Buffer из std объединяет эти операции.
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 Как использовать.
Попробуйте пометить в своем коде вызовы выделения, перераспределения и освобождения. Для каждого успешного распределения вы должны иметь возможность описать, кто им владеет и какой путь его освобождает.