*const T y *mut T: los punteros sin promesas
La referencia lleva cosida una promesa que el compilador vigila: validez, lifetime, alineación, aliasing. El puntero crudo es esa misma dirección despojada de toda promesa. Qué son *const T y *mut T, por qué crearlos es seguro pero dereferenciarlos exige unsafe, y qué distingue const de mut cuando ninguna de las dos garantiza nada.
Hasta ahora, todo puntero que has manejado en Rust venía con un guardaespaldas. La referencia &T o &mut T lleva cosida una promesa que el compilador vigila sin descanso: apunta a memoria válida, respeta un lifetime, está alineada y obedece las reglas de aliasing. El puntero crudo es esa misma dirección de memoria despojada de toda promesa. *const T y *mut T son números con un poco de historia —una dirección y su procedencia— y nada más. No garantizan apuntar a algo vivo, ni a algo de tu tipo, ni siquiera a algo. Son la puerta que Rust deja entornada hacia el modelo de memoria plano de la máquina, y cruzarla es el gesto primitivo sobre el que descansa todo lo que la biblioteca estándar hace por debajo.
- Definir qué es un puntero crudo y qué garantías abandona frente a una referencia.
- Distinguir
*const Tde*mut Tcomo declaración de intención, no como candado de inmutabilidad. - Comprender por qué crearlos es seguro pero dereferenciarlos exige
unsafe. - Reconocer los punteros gordos cuando
Tno tiene un tamaño conocido en compilación.
Una dirección despojada de garantías
Una referencia &T es, en la máquina, exactamente una dirección; pero en el sistema de tipos es una dirección más cuatro promesas que el compilador demuestra por ti: que no es nula, que está alineada, que apunta a un valor inicializado del tipo correcto durante un lifetime concreto, y que respeta el aliasing (una &mut es única; las & comparten sin mutar). El puntero crudo conserva la dirección y tira las cuatro promesas por la ventana.
let x = 42_i32;
let r: &i32 = &x; // referencia: el compilador vela por ella
let p: *const i32 = &x; // puntero crudo: coercion desde la referencia
let nulo: *const i32 = std::ptr::null(); // crear un nulo: seguro
let colgante = 0x1234 as *const i32; // una direccion inventada: seguro
let valor = unsafe { *p }; // dereferenciar: aqui firmas tu el contrato
La asimetría es deliberada y define el nivel entero. Crear un puntero crudo no toca memoria: solo calcula o copia una dirección, y por eso es una operación segura, incluso si el puntero es nulo o disparatado. Dereferenciarlo —leer o escribir en lo que apunta— sí toca memoria, y ahí el compilador se aparta: no tiene forma de demostrar que la dirección es válida, así que te obliga a envolver la operación en unsafe, tu firma jurando que has verificado a mano lo que él ya no puede. Un puntero crudo es además Copy (duplicarlo es copiar una palabra), no arrastra ningún Drop, y —crucialmente— no es Send ni Sync: por eso los tipos que los contienen deben reafirmar esas capacidades a mano.
Solo dirección y procedencia
Ni lifetime, ni validez, ni no-nulidad. Puede colgar, ser nulo, estar desalineado y solaparse con cualquier otro puntero sin que el tipo se queje.
Crear es seguro
Calcular una dirección no lee ni escribe memoria. null(), &x as *const T o un entero casteado son expresiones seguras.
Dereferenciar es unsafe
Leer o escribir a través de él toca memoria real. El unsafe es tu promesa de que la dirección cumple todas las invariantes.
Ni Send ni Sync
Un puntero crudo no se puede compartir ni mover entre hilos de forma automática. Quien lo encapsula reafirma esas cotas bajo su propia responsabilidad.
En C, todo puntero es crudo y toda dereferencia es implícitamente un acto de fe: no hay marca que separe el acceso demostrado del apostado. Rust ofrece exactamente el mismo poder —*const T es moralmente el const T* de C— pero traza la línea que C nunca dibujó. Fuera de unsafe, el compilador demuestra; dentro, demuestras tú. No es menos potente: es igual de potente con la responsabilidad hecha visible.
const frente a mut: intención, no candado
Con las referencias, &T y &mut T son universos separados que el compilador hace cumplir: por una &T jamás escribes, y una &mut T es exclusiva. Con los punteros crudos, la distinción entre *const T y *mut T se degrada a mera documentación de intención. No hay candado: puedes convertir libremente entre ambas con .cast_mut(), .cast_const() o un simple as.
let mut n = 10_i32;
let pc: *const i32 = &n;
let pm: *mut i32 = pc.cast_mut(); // const a mut, sin ceremonia
unsafe { *pm = 20; } // el tipo lo permite; la validez la juras tu
Que el sistema de tipos lo permita no significa que sea sano. El permiso real para escribir no lo concede el mut del puntero, sino la procedencia: la asignación de la que el puntero deriva. Si pm procede en última instancia de una &i32 compartida o de un static inmutable, escribir a través de él es comportamiento indefinido aunque el compilador te haya dejado teclearlo. El mut del puntero es una sombra difusa de las reglas del borrow checker, no su reencarnación.
Castear un *const T a *mut T y escribir es legal para el compilador de tipos, pero solo es sano si la memoria subyacente era realmente mutable: procede de una &mut, de un let mut, o vive tras un UnsafeCell. Escribir sobre lo que originalmente fue un &T o un static no marcado como mutable es UB. El puntero olvida su origen; tú no debes.
Punteros gordos: cuando T no cabe en una palabra
Un puntero crudo a un tipo de tamaño conocido (i32, un struct corriente) es una sola palabra máquina. Pero cuando T no tiene tamaño en compilación —un slice [U] o un objeto de trait dyn Trait— el puntero se vuelve gordo: transporta la dirección y una segunda palabra de metadatos, la longitud para un slice o el puntero a la vtable para un objeto de trait.
use std::mem::size_of;
assert_eq!(size_of::<*const i32>(), size_of::<usize>()); // fino: 1 palabra
assert_eq!(size_of::<*const [i32]>(), 2 * size_of::<usize>()); // gordo: dato y longitud
let v = [1, 2, 3];
let gordo: *const [i32] = std::ptr::slice_from_raw_parts(v.as_ptr(), 3);
assert_eq!(unsafe { (*gordo).len() }, 3); // la longitud viaja en el puntero
Esta dualidad importará cuando construyas estructuras de datos: slice_from_raw_parts y su gemela mutable te dejan fabricar la vista gorda desde una dirección fina más una longitud, sin pasar por una referencia.
El nulo, y por qué crear basura es legal
std::ptr::null() y std::ptr::null_mut() fabrican el puntero nulo, y un entero cualquiera casteado con as fabrica un puntero a una dirección arbitraria. Ambas cosas compilan sin unsafe, y esto desconcierta a quien viene creyendo que “puntero peligroso” y “operación peligrosa” son sinónimos.
let nulo: *mut u8 = std::ptr::null_mut();
let inventado = 0xDEAD_BEEF_usize as *const u32; // legal de construir
assert!(nulo.is_null());
assert!(!inventado.is_null());
No hay peligro en tener una dirección disparatada; el peligro nace al creerla válida y dereferenciarla. Por eso Rust separa con nitidez los dos gestos: construir es un cálculo puro sin consecuencias observables, y solo el acceso —el unsafe— afirma algo sobre el mundo. Guarda esta idea, porque reaparecerá con fuerza: Option<NonNull<T>> aprovecha precisamente la imposibilidad del patrón de bits nulo para representar la ausencia sin gastar ni un byte de más.
flowchart TD M[Direccion de memoria] --> R[Referencia ref T o ref mut T] M --> C[Puntero crudo const T o mut T] R --> RG[El compilador prueba validez lifetime alineacion y aliasing] C --> CG[Cero garantias solo direccion y procedencia] RG --> S[Usarla es seguro] CG --> U[Dereferenciarlo exige unsafe] style R fill:#a6e3a1,color:#11111b style C fill:#f38ba8,color:#11111b style S fill:#89b4fa,color:#11111b style U fill:#cba6f7,color:#11111b
Todo el edificio de seguridad de Rust —ownership, borrowing, lifetimes— es una demostración: el compilador prueba, antes de ejecutar, que ningún acceso a memoria es inválido. Pero esa demostración necesita un cimiento indemostrable, un punto donde alguien diga “confía en mí, esto es cierto”, porque el hardware por debajo no conoce referencias ni lifetimes: solo conoce direcciones planas y bytes. El puntero crudo es ese cimiento hecho tipo. Cuando escribes *const T, estás bajando un peldaño por debajo de la lógica del borrow checker hasta el nivel donde la memoria es lo que siempre fue en C: un array gigante de bytes que puedes indexar con un número. La diferencia con C no es que Rust te lo prohíba —te lo ofrece sin rodeos—, sino que traza una frontera nítida. Del lado seguro, el compilador carga con la prueba; del lado unsafe, la cargas tú, y el bloque unsafe es la firma legible de ese traspaso de responsabilidad. Por eso crear un puntero es seguro y dereferenciarlo no: calcular una dirección no afirma nada sobre el mundo, pero leerla afirma que ahí vive un valor válido de tu tipo, y esa afirmación, cuando el compilador no puede verificarla, tienes que respaldarla tú. Que const y mut se degraden a intención, que el puntero olvide su procedencia, que sea Copy y no Send: todo son síntomas de lo mismo. Has salido del jardín vallado donde el lenguaje piensa por ti y has entrado en el territorio donde el lenguaje se limita a ejecutar lo que afirmas. No es un lugar peligroso por descuido de Rust; es el lugar exacto donde toda la seguridad de arriba tiene que apoyarse en algo, y ese algo eres tú, sosteniendo invariantes que ninguna prueba automática puede sostener.
Un puntero crudo es una dirección más su procedencia, sin lifetime ni validez garantizados. *const T y *mut T se crean en código seguro —incluso nulos o disparatados—, porque calcular una dirección no toca memoria; solo dereferenciarlos exige unsafe, tu firma de que la dirección cumple las invariantes. La distinción const/mut es intención, no candado: se castea libremente y el permiso real de escritura lo concede la procedencia, no el tipo del puntero. Son Copy, no son Send ni Sync, y se vuelven gordos —dato más metadato— cuando T no tiene tamaño conocido.
- Crea un
*const i32a partir de unlet, un*const i32nulo conptr::null, y un*const i32desde un entero casteado. Comprueba que compilan sinunsafe. - Añade la línea que dereferencia cada uno y observa qué exige el compilador; explica por qué solo esa línea necesita
unsafe. - Toma un
*const i32y conviértelo a*mut i32con.cast_mut(); escribe a través de él cuando el origen es unlet muty razona por qué sería UB si el origen fuera un&i32. - Con
size_ofdemuestra que*const i32ocupa una palabra y*const [i32]dos; explica qué guarda la segunda. - Construye un
*const [i32]conslice_from_raw_partsa partir de un array y su longitud, y lee sulensin haberlo dereferenciado del todo.