Указатели и срезы

Разбираем три вида указателей Zig и понимаем, чем срез лучше C-массива.

Срез (slice) — это указатель на начало последовательности плюс её длина; благодаря длине срез знает свои границы, и выход за них ловится, в отличие от голого указателя C.

В C массив при передаче в функцию «разваливается» в указатель и теряет длину — отсюда классические переполнения буфера. Zig решает это, разделяя понятия на несколько чётких типов указателей, и главный из них — срез.

Одиночный указатель

var x: i32 = 10;
const p: *i32 = &x;  // указатель на одно значение i32
p.* = 20;            // разыменование через .*
// теперь x == 20

Тип *i32 — указатель ровно на одно значение. Разыменование пишется как p.* — постфиксная звёздочка, в отличие от префиксной в C. Адрес берут через &.

Многоуказатель

// [*]T — указатель на неизвестное число элементов
// (аналог голого T* в C, длина неизвестна)
const raw: [*]const u8 = some_c_buffer;

Тип [*]T называется многоуказателем: он указывает на много элементов, но не знает их количества. Это прямой аналог указателя из C, и его используют в основном при взаимодействии с C-кодом. Сам по себе он небезопасен — границы неизвестны.

Срез — безопасный массив с длиной

const std = @import("std");

pub fn main() void {
    const numbers = [_]i32{ 5, 10, 15, 20 };
    const slice: []const i32 = numbers[1..3]; // элементы 10 и 15
    std.debug.print("len={d} first={d}\n", .{ slice.len, slice[0] });
}

Вывод:

len=2 first=10

Тип []T — срез: пара «указатель + длина». Поле slice.len всегда доступно, а доступ slice[i] в safe-сборке проверяется на выход за границу. Срез получают из массива через массив[начало..конец]. Именно срезы — рабочая лошадка Zig для передачи последовательностей в функции.

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

Физически срез — это структура из двух полей: указатель на первый элемент и количество элементов (usize). Когда вы пишете slice[i] в safe-режиме, компилятор вставляет проверку i < slice.len и роняет программу при нарушении вместо чтения чужой памяти. В ReleaseFast проверка убирается ради скорости. Срез строк — это просто []const u8: последовательность байтов с длиной.

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

Первая — путать *T (один элемент) и [*]T (много элементов): по одиночному указателю нельзя индексироваться. Вторая — использовать p* вместо p.* для разыменования. Третья — забыть, что срез хранит длину, и вручную таскать её отдельной переменной, как в C: это лишнее.

Итог

  • *T — указатель на одно значение, разыменование через p.*.
  • [*]T — многоуказатель без длины, аналог C-указателя, небезопасен.
  • []T — срез: указатель плюс длина, с проверкой границ в safe-сборке.
  • Срезы — основной способ передавать последовательности в функции.
Проверьте себя
1. Чем срез []T отличается от многоуказателя [*]T?
AНичем, это синонимы
BСрез хранит длину и проверяет границы, многоуказатель длины не знает
CМногоуказатель безопаснее
DСрез нельзя передать в функцию
2. Как разыменовать указатель p в Zig?
A*p
Bp.*
C&p
Dderef(p)