diff --git a/src/ch06.md b/src/ch06.md index 4378f30..715a487 100644 --- a/src/ch06.md +++ b/src/ch06.md @@ -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` diff --git a/src/ch07.md b/src/ch07.md new file mode 100644 index 0000000..aca7d61 --- /dev/null +++ b/src/ch07.md @@ -0,0 +1,3 @@ + +# Обобщённые струкутры данных + diff --git a/src/ch08.md b/src/ch08.md new file mode 100644 index 0000000..9157d5f --- /dev/null +++ b/src/ch08.md @@ -0,0 +1,3 @@ + +# Программируем на языке Zig +