zig-entendiendo-punteros

Punteros en Zig: tipos, memoria y seguridad explícita

  • 5 min

Un puntero en Zig es un valor que contiene la dirección de memoria de otro valor. Si mencionas la palabra “puntero” en una sala llena de desarrolladores web, verás caras de terror; pero en Zig vamos a tratarlos con bastante más orden que en C.

Un puntero no es más que una dirección de memoria. Un número que dice “el dato está en la casilla 0x1234”.

C y C++ distinguen el tipo del elemento apuntado, pero un T* no expresa si detrás hay un único valor o una secuencia. Si te equivocas con la longitud o la vida útil, puedes acceder a memoria que ya no corresponde al programa.

Zig separa varios tipos de puntero para expresar si apuntan a uno o a varios elementos.

Obtener la dirección (&)

Lo primero es lo primero: ¿Cómo consigo un puntero? Al igual que en C, usamos el operador & (ampersand) delante de una variable.

var numero: u8 = 10;

// 'ptr' es de tipo *u8 (Puntero a un u8)
const ptr = № 

std.debug.print("La dirección es: {*}\n", .{ptr});
Copied!

Aquí ptr sabe exactamente dos cosas:

  1. Dónde está el dato.
  2. Qué tipo de dato es (u8).

Punteros de un elemento (*T)

En Zig, un puntero escrito como *T (ej. *i32, *bool) apunta a EXACTAMENTE UN elemento.

Esto aporta una restricción importante: no existe aritmética de punteros para el tipo *T.

Si tienes un puntero a un entero ptr, en C podrías hacer ptr + 1 para acceder “a lo que sea que esté al lado en la memoria”. En Zig, eso es un Error de Compilación.

var x: i32 = 42;
const ptr: *i32 = &x;

// ERROR: binary operator '+' cannot be applied to type '*i32'
// const siguiente = ptr + 1;
Copied!

Zig te dice: “Oye, me has dicho que esto apunta a UN entero. No tengo garantías de que haya nada válido después. Si quieres un array, usa un Slice”.

Desreferenciar (.*)

Para acceder al valor guardado en esa dirección (lo que en C sería *ptr), en Zig usamos la sintaxis de sufijo .*.

var x: u8 = 10;
const ptr = &x;

ptr.* += 5; // Accedemos al valor y le sumamos 5

std.debug.print("Valor de x: {d}\n", .{x}); // Imprime 15
Copied!

¿Por qué al final? Poner el asterisco al final (variable.*) mejora la legibilidad cuando encadenamos accesos. En lugar de (*(*ptr).campo).algo, en Zig leemos ptr.campo.algo. Mucho más limpio.

const y var en punteros

Aquí es donde la gente se lía. Hay dos niveles de inmutabilidad:

  1. ¿Puedo cambiar a quién apunta el puntero?
  2. ¿Puedo cambiar el valor al que apunto?
var vida: u8 = 100;
var otra_vida: u8 = 80;

// 1. Puntero mutable a datos mutables (*u8)
var p1: *u8 = &vida;
p1.* = 50; // OK
p1 = &otra_vida; // OK

// 2. Puntero a datos CONSTANTES (*const u8)
// Puedo leer la vida, pero no cambiarla a través de este puntero
const p2: *const u8 = &vida;
// p2.* = 0; // ERROR: cannot assign to constant

// 3. Puntero inmutable (const p)
// El puntero siempre apuntará a 'vida', no se puede reasignar.
const p3: *u8 = &vida;
Copied!

La regla general es: si una función recibe un argumento para leerlo (y es un struct grande), pásalo como *const T. Si es para modificarlo, pásalo como *T.

Punteros de muchos elementos ([*]T)

Para interoperar con C o implementar ciertas estructuras de bajo nivel existe el puntero de muchos elementos, escrito [*]T.

Este tipo funciona exactamente igual que un puntero de C.

  • Apunta a un elemento, pero asume que hay más después.
  • Permite aritmética: ptr + 1 es válido.
  • Permite indexado: ptr[5] es válido.
  • No conoce los límites: si el índice excede la región válida, incurres en comportamiento ilegal.
var array = [_]u8{ 10, 20, 30, 40 };

// Casteamos a puntero de muchos elementos
// .ptr nos da el puntero "crudo" del array
const ptr_muchos: [*]u8 = &array; 

// Acceso estilo C (sin seguridad de límites)
const tercer_elemento = ptr_muchos[2]; // 30
const cuarto_elemento = (ptr_muchos + 3).*; // 40
Copied!

Evita usar [*]T cuando puedas conservar la longitud. Su uso principal es interoperabilidad con C o estructuras de datos de muy bajo nivel. Para secuencias con longitud conocida, usa slices ([]T).

Comparación de tipos

Para no perderte nunca, aquí tienes la jerarquía de referencias en Zig:

TipoNombre¿Sabe el tamaño?¿Seguro?AritméticaUso
*TPuntero a un elementoEs 1Comprobaciones limitadasNoReferencias a valores, paso de parámetros.
[]TSlice (.len)Comprueba límites en modos segurosNoSecuencias, cadenas, buffers.
[*]TPuntero de muchos elementosNoNo comprueba límitesInteroperabilidad con C y código de bajo nivel.

Punteros opcionales

Una última cosa importante que diferencia a Zig de C/C++: Los punteros en Zig (*T) nunca pueden ser null.

Una variable de tipo *u8 no puede contener la dirección cero. Eso no garantiza por sí solo que el puntero sea válido: todavía debes respetar el alineamiento, la procedencia y la vida útil de la memoria apuntada.

Cuando una referencia pueda faltar, usamos un puntero opcional: ?*T. Así el posible null queda reflejado en el tipo y debe manejarse antes de desreferenciarlo.