Массивы и многоуказатели

Углубляемся в массивы фиксированного размера и завершённые указатели для дружбы с C.

Sentinel-завершённый указатель [*:0]T — указатель, после последнего полезного элемента которого стоит маркер-ограничитель (для строк это 0); так Zig представляет C-строки.

Массив в Zig — это последовательность фиксированной, известной на этапе компиляции длины. Его тип записывается как [N]T, и длина — часть типа. Это отличает массив от среза, у которого длина известна только в рантайме.

Массив фиксированного размера

const std = @import("std");

pub fn main() void {
    const a = [_]i32{ 1, 2, 3, 4 }; // [_] — длина выводится: [4]i32
    var sum: i32 = 0;
    for (a) |x| sum += x;
    std.debug.print("сумма={d} длина={d}\n", .{ sum, a.len });
}

Вывод:

сумма=10 длина=4

Синтаксис [_]i32 просит компилятор вывести длину из числа элементов. Можно указать длину явно: [4]i32. Поскольку длина — часть типа, массивы [3]i32 и [4]i32 — разные типы, и перепутать их компилятор не даст.

Массив на стеке против среза

Массив [N]T хранит данные прямо в себе (на стеке или внутри структуры). Срез []T только ссылается на чужие данные. Поэтому функции обычно принимают срез: он работает с массивом любой длины, не копируя его.

fn sumSlice(items: []const i32) i32 {
    var s: i32 = 0;
    for (items) |x| s += x;
    return s;
}
// вызов: sumSlice(&a) — массив автоматически приводится к срезу

Sentinel-завершённые типы для C

C-строки завершаются нулевым байтом. Zig выражает это через sentinel: [*:0]const u8 — указатель на байты, после которых стоит 0. Строковые литералы Zig на самом деле имеют тип *const [N:0]u8 — массив с нулём-ограничителем, что позволяет передавать их в C-функции напрямую.

const name: [*:0]const u8 = "Zig"; // готово к передаче в C
// в C это был бы просто const char*

Как работает под капотом

Sentinel — это гарантия компилятора, что в памяти после данных лежит маркер. Для строк это позволяет C-функциям вроде strlen найти конец, считая байты до нуля. При этом Zig-срез строки знает длину и так, поэтому внутри Zig работа со строками не зависит от завершающего нуля — он нужен лишь на границе с C.

Частые ошибки

Первая ошибка — считать массив и срез одним и тем же: у массива длина в типе, у среза в рантайме. Вторая — забыть, что строковый литерал sentinel-завершён, и удивляться его типу. Третья — пытаться индексировать массив за его границы: в safe-сборке это паника, как и со срезом.

Итог

  • Массив [N]T хранит данные в себе; длина — часть типа.
  • [_]T{...} просит вывести длину из числа элементов.
  • Функции обычно принимают срез []const T — массив приводится к нему автоматически.
  • [*:0]const u8 — sentinel-завершённый указатель, представление C-строк.
Проверьте себя
1. Где хранится длина у массива [N]T в Zig?
AВ рантайме как у среза
BЭто часть типа, известная на этапе компиляции
CВ отдельной переменной
DДлина не хранится
2. Зачем нужен sentinel-завершённый тип [*:0]const u8?
AДля ускорения циклов
BДля представления C-строк с завершающим нулём
CДля срезов произвольной длины
DДля опциональных значений