On branch main

modified:   src/ch06.md
new file:   src/ch07.md
new file:   src/ch08.md
This commit is contained in:
zed
2023-11-16 00:08:20 +03:00
parent 689555b3a9
commit e512a9dd38
3 changed files with 117 additions and 0 deletions

View File

@@ -1,10 +1,121 @@
# Динамическая память и аллокаторы
Всё, счем имели дело до сих пор, требовало знаний размеров всех сущностей
во время компиляции. Массивы всегда имеют размер, известный во время
компиляции (фактически, длина массива является частью типа). Все наши
строки были строковыми литералами, длина которых тоже понятна по
исходному тексту.
Далее, две стратегии управления памятью (глобальные данные и стек),
которые мы видели, хоть и являются простыми и эффективными, всё же
ограничивают наши возможности. Обе они никак не могут помочь в управлении
данными, которые могут менять размер во время исполнения и, кроме того,
не обладают необходимой гибкостью в плане времён жизни - глобальные
данные живут всё время работы программы (и при этом не могут быть
модифицированы), переменные в стеке живут, пока работает функция, который
принадлежит этот стековый кадр.
Эта главв содержит 2 темы. Первая это общий обзор нашей третьей области
памяти, кучи. А вот вторая более интересна - в языке Zig имеется вполне
ясный, но тем не менее уникальный подход к управлению динамической
памятью. Даже если Вы так или иначе умеете работать с кучей
(например, при помощи `malloc` и `free` из стандартной библиотеки языка C),
Вам всё равно следует проштудировать эту главу, поскольку в Zig
есть свои особенности.
## Динамическая память (heap, "куча")
Куча это наша третья область памяти в нашем распоряжении.
В сравнении с первыми двумя это немного "дикий запад": дозволено всё.
Именно, мы можем создавать сущности в куче с размером,
известным только во время исполнения программы и при этом
имеем полный контроль над временем жизни этих сущностей.
Стек вызовов это замечательная штука ввиду простоты и предсказуемости
способа, которым он управляет данными (создание/удаление стековых
кадров). С другой стороны, эта простота одновременно является
недостатком: времена жизни переменных в стеке жёстко привязяны ко времени
жизни самого стекового кадра, в котором они "живут". Куча это полная
противоположность этому. Для объектов в куче нет никакого встроенного
механизма контроля времени жизни, поэтому эти объекты могут существовать
столь долго или, наоборот, столь коротко, сколько мы захотим. И эти
преимущества, опять же, палка о двух концах: если мы не освободим память,
никто её за нас не освободит.
Давайте посмотрим на следующий пример:
```zig
const std = @import("std");
pub fn main() !void {
// we'll be talking about allocators shortly
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// ** The next two lines are the important ones **
var arr = try allocator.alloc(usize, try getRandomCount());
defer allocator.free(arr);
for (0..arr.len) |i| {
arr[i] = i;
}
std.debug.print("{any}\n", .{arr});
}
fn getRandomCount() !u8 {
var seed: u64 = undefined;
try std.os.getrandom(std.mem.asBytes(&seed));
var random = std.rand.DefaultPrng.init(seed);
return random.random().uintAtMost(u8, 5) + 5;
}
```
Вскоре мы рассмотрим аллокаторы более подробно, а пока нам нужно знать
только то, что `allocator` в этом примере это `std.mem.Allocator`. Мы
используем два его метода, `alloc` и `free`. Из того, что мы вызываем
`allocator.alloc` при помощи `try`, следует, что этот метод может
завершиться с ошибкой. На настоящий момент возможна только одна ошибка,
`OutOfMemory`. Параметры этого метода и возвращаемое значение, в
принципе, говорят нам о том, как он работает: он требует тип (`T`) и
количество элементов, при успешном завершении возвращает полный срез
выделенного в куче массива. Выделение памяти происходит во время
выполнения, иначе никак, поскольку длина становится известной только во
время исполнения.
Общее правило при динамическом выделении памяти - каждый вызов `alloc`,
как правило, имеет соответствующий вызов `free`. Первый выделяет память,
второй освобождает. Однако, не дайте этому примеру ограничить Ваше
воображение. Да, этот паттерн, `try alloc` и сразу следом `defer free`
действительно часто испольузется, и не зря: такое размещение инструкций
является относительно надёжной защитой от ошибок в виде того, что мы
"забыли" освободить. Но так же часто используется совершенно иная схема:
выделение памяти делается в одном месте, а освобождение совершенно в
другом (например, в другой функции, в другом методе или даже в другой
нити). Как мы уже отметили, нет никаких автоматических механизмов
управления временем жизни объектов в куче, всё в нашей власти.
Представьте себе веб-сервер с постадийной обработкой запроса, причём
каждую стадию отрабатывает выделенная для этого нить - тогда память может
быть выделена на стадии получения HTTP-запроса, а освождена где-то на
конечной стадии, например, при записи в журнал, в котором фиксируются все
запросы.
## `defer` и `errdefer`
В рассматриваемом примере мы ввели в обиход незнакомую нам до этого новую
особенность языка Zig, `defer`. Этот механизм, согласно его названию,
откладывает исполнение обозначенного кода (который сам по себе может быть
блоком) до того момента, когда закончится выполняться блок (включая явный
`return`), в котором находится `defer`. Такое отложенное выполнение вовсе
не связано конкретно с аллокаторами и управлением памятью, это более
общий механизм, Вы можете использовать его для выполнения абсолютно
произвольного кода.
`defer` в Zig подобен таковому в `Go`, с одним важным отличием:
в Zig отложенные инструкции будут выполнены по завершении
*блока*, где они заданы, а в Go они будут выполнены в конце *функции*.
## Повторное освобождение и утечки памяти
## `create` и `destroy`

3
src/ch07.md Normal file
View File

@@ -0,0 +1,3 @@
# Обобщённые струкутры данных

3
src/ch08.md Normal file
View File

@@ -0,0 +1,3 @@
# Программируем на языке Zig