Cada variable de tu programa vive en algún lugar de la memoria, y cada "algún lugar" tiene un número: su dirección. Un puntero es simplemente una variable que guarda uno de esos números. Esa es toda la idea. Todo lo demás en esta página es sintaxis y consecuencias.
La razón por la que los punteros importan tanto en C es que C le entrega a cada función una copia de sus argumentos. Una función a la que le dan el número 7 puede cambiar su copia cuanto quiera; la variable de quien llamó no se mueve. Dale en cambio una dirección y podrá alcanzar el original y cambiarlo. Los arrays, las cadenas, la memoria dinámica y cualquier estructura de datos que llegues a construir en C descansan sobre esa única capacidad.
Memoria, direcciones y por qué todo tiene una
Cuando escribes int score = 42;, el compilador reserva unos cuantos bytes de memoria y les da un nombre que puedes usar. Esos bytes también tienen una dirección numérica. Piensa en la memoria como una calle larguísima de casas numeradas: score es la familia que vive en la casa número 0x7ffd4c2a; el nombre es para ti, el número es para la máquina.
El operador & pide ese número:
Ejecútalo. Las tres direcciones son distintas porque las tres variables ocupan bytes distintos, y cambiarán de una ejecución a otra: eso es normal, y es por lo que nunca escribes una dirección a mano. Fíjate en la conversión (void *): %p está especificado para tomar un void *, y pasar cualquier otro tipo de puntero es técnicamente comportamiento indefinido. Haz de esa conversión un hábito.
Que las direcciones sean impredecibles no es un problema, porque casi nunca te importa cuál es una dirección. Te importa tenerla.
Declarar un puntero
La declaración de un puntero nombra el tipo al que apunta:
int *p; // p puede guardar la direccion de un int
double *q; // q puede guardar la direccion de un double
char *name; // name puede guardar la direccion de un char
Lee int *p; como "*p es un int": desreferenciar p te da un int. Esa lectura sobrevive a cualquier declaración complicada que C te lance después.
Dónde pones el * es una cuestión de estilo que el compilador ignora: int* p, int *p e int * p declaran lo mismo. La mayoría del código C escribe int *p, y hay una razón práctica:
int* a, b; // a es un puntero a int; b es un int CORRIENTE, no un puntero
int *a, *b; // ambos son punteros: el * se asocia al nombre, no al tipo
El * se pega al declarador, no al tipo. Escribirlo junto al nombre mantiene eso visible.
& y *: los dos operadores
Dos operadores hacen todo el trabajo, y son exactamente opuestos.
&x: dirección de. Produce la dirección donde vivex.*p: desreferencia. Va a la dirección que hay enpy te da el objeto que vive ahí.
Las dos últimas líneas son el sentido entero de los punteros. No se le asignó nada a age directamente y sin embargo age cambió, porque *p = 31 significa "guarda 31 en la dirección que tiene p", y esa dirección es la de age.
Aquí está la imagen en memoria:
p age
+------------+ +----------+
| 0x7ffd1c40 | --------> | 31 |
+------------+ +----------+
en 0x7ffd1c38 en 0x7ffd1c40
p -> la direccion 0x7ffd1c40
*p -> el int guardado ahi, 31
&age -> 0x7ffd1c40, la misma direccion que tiene p
&p -> 0x7ffd1c38, donde vive p mismo
Fíjate en la última línea: el puntero es a su vez una variable en algún lugar de la memoria, así que también tiene su propia dirección. Eso no es una pregunta capciosa, es solo coherencia, y es como funcionan los punteros a punteros (int **).
Como & y * se deshacen mutuamente, *&age es simplemente age, y &*p es simplemente p.
Por qué importan los tipos de puntero
Un puntero guarda una dirección, y una dirección es solo un número, entonces ¿por qué el compilador insiste en saber si apunta a un int o a un double? Por dos razones:
- Cuántos bytes leer. Desreferenciar un
int *lee 4 bytes (típicamente); desreferenciar undouble *lee 8. Sin el tipo,*pno significaría nada. - Cómo interpretar esos bytes. El mismo patrón de bits es un número como
inty uno completamente distinto comofloat.
El tamaño del puntero en sí, en cambio, no depende de a qué apunte: una dirección es una dirección.
En una máquina de 64 bits los tres punteros ocupan 8 bytes, mientras que aquello a lo que apuntan ocupa 4, 8 y 1. El tipo apuntado es lo que hace que *p y la aritmética de punteros signifiquen algo.
Asignar entre tipos de puntero distintos es un error del que el compilador se quejará, y con razón: pi = &d; te prepararía para leer 4 bytes de un double y llamar entero al resultado.
La motivación canónica: swap()
Aquí está la función a la que recurre todo curso de C, porque falla exactamente de la forma que explica los punteros.
La función reporta que los intercambió, y main reporta que no pasó nada. broken_swap recibió copias de x e y. Intercambió sus copias a la perfección, y esas copias dejaron de existir cuando la función retornó.
Entrégale las direcciones en su lugar:
Ahora a y b siguen siendo copias —copias de dos direcciones— pero la copia de una dirección apunta a la mismísima variable a la que apuntaba el original. *a es x, desde donde sea que se llame la función.
Este es el patrón detrás de cada función de C que modifica los datos de quien la llama, y detrás de los parámetros de funciones en general: C no tiene paso por referencia, así que pasas un puntero y lo desreferencias. También es por lo que scanf necesita un &:
int n;
scanf("%d", &n); // scanf debe poder escribir en n, asi que necesita su direccion
Devolver más de un valor
Una función de C devuelve un valor. Los punteros son la forma de sortearlo: pasa direcciones para los resultados extra.
El valor de retorno lleva el estado y los punteros llevan los resultados. Esa división está por todas partes en la propia biblioteca estándar de C y en las API de los sistemas operativos.
Punteros a punteros
Como un puntero es una variable, puedes tomar su dirección, y el tipo de esa dirección es "puntero a puntero a int":
pp p value
+------+ +------+ +------+
| &p | ----> | &val | -----> | 8 |
+------+ +------+ +------+
Te encontrarás con ** de verdad cuando una función necesite cambiar un puntero que tiene quien la llama —por ejemplo, un asignador que apunta el puntero de quien llama a memoria fresca— y en el char *argv[] de los argumentos de la línea de comandos.
Cuatro errores que conviene conocer antes de cometerlos
Usar un puntero que nunca se apuntó a ningún lado. Un puntero sin inicializar guarda los bytes que ya estuvieran ahí. Desreferenciarlo lee o escribe una dirección aleatoria.
int *p; // p guarda basura
*p = 10; // comportamiento indefinido, probablemente un fallo
Inicializa todos los punteros, con una dirección real o con NULL. Mira punteros nulos para la disciplina que lo rodea.
Confundir *p con p. p = 5; pone el puntero en la dirección 5 (un disparate); *p = 5; guarda 5 donde apunta. El compilador avisará del primero, y los avisos merecen leerse.
Olvidar el & en scanf. scanf("%d", n) pasa el valor de n como si fuera una dirección. Compila con un aviso y luego escribe en la memoria que nombre ese número.
Devolver la dirección de una variable local. La local desaparece en el momento en que la función retorna; la dirección es un recibo de una propiedad demolida.
int *bad(void) {
int local = 42;
return &local; // quien llama recibe un puntero colgante
}
Si necesitas memoria que sobreviva a la llamada, resérvala: para eso está la memoria dinámica.
A dónde ir después
Los punteros no se quedan en "guardar una dirección". Sumarle a un puntero lo hace avanzar por la memoria en unidades de su tipo, que es el tema de la aritmética de punteros; y en C el nombre de un array decae a un puntero a su primer elemento, que es por lo que punteros y arrays son en la práctica el mismo tema una vez que conoces ambos. Lee esos dos a continuación y el resto de C deja de parecer magia.
Preguntas frecuentes
¿Qué es un puntero en C?
Un puntero es una variable cuyo valor es una dirección de memoria: la ubicación de algún otro objeto en memoria. int *p = &age; hace que p guarde la dirección de age, y *p lee o escribe entonces la variable age a través de esa dirección.
¿Cuál es la diferencia entre * y & en C?
&x es el operador de dirección: produce la dirección donde vive x. *p es el operador de desreferencia: va a la dirección guardada en p y te da el objeto que hay ahí. Se deshacen mutuamente, así que *&x es simplemente x.
¿Por qué el * aparece dos veces, en la declaración y al usar el puntero?
Son dos usos distintos del mismo símbolo. En int *p; el * es parte del tipo y dice "p es un puntero a int". En *p = 5; el * es el operador de desreferencia y dice "guarda 5 en la dirección que tiene p". Leer la declaración como "*p es un int" hace que ambos tengan sentido.
¿Para qué necesito punteros en C?
C pasa todos los argumentos por valor, así que una función nunca puede cambiar la variable de quien llama a menos que le entreguen la dirección. Los punteros también te dejan recorrer arrays con eficiencia, devolver más de un resultado, construir estructuras enlazadas y usar memoria reservada en tiempo de ejecución con malloc.