Puntero a lo easy.
Un puntero es un número (cómo todo dentro de un ordenador) y ese número representa una dirección de memoria.
Por ejemplo:
#include <stdio.h>
int main() {
printf("%d", *(int*)0x61FE1C); }
En el ejemplo anterior directamente le digo a C que me muestre qué hay en la dirección de memoria 61FE1C. ¿Se puede? Sí. Por cierto, ese casting (int*) es necesario para convertir el literal en un puntero. El * que hay delante nos muestra el contenido.
* Un poco más avanzado: En este caso he hecho un programa previo para ver dónde residía la memoria de datos del programa y pudiera apuntar ahí con este ejemplo. Los sistemas operativos actuales y los microprocesadores funcionan por capas. No van a dejar que un programa de usuario pueda leer o escribir fuera de su área designada. Antes sí que se podía, o en sistemas sin protección sí puede hacerse.
Podemos usar una variable para alojar esa dirección de memoria del ejemplo anterior:
#include <stdio.h>
int main() {
int *p = (int*)0x61FE1C;
}
Aquí asignamos el literal a una variable puntero. Las variables puntero se denotan por anteponer el asterisco (*) delante del nombre durante su definición o declaración. Para ver la definición y declaración por separado sería así:
#include <stdio.h>
int main() {
int *p;
p = (int*)0x61FE1C;
}
Nótese que cuándo se hace la definición (se le da valor) no se usa el asterisco (*), pues éste tiene la función, en sus posteriores usos, de conseguir el valor guardado en esa posición de memoria, que en este caso es la posición 0x61FE1C.
Pero cómo hemos dicho el conjunto sistema operativo/microprocesador no te deja apuntar a zonas arbitrarias de la memoria, el sistema te la tiene que dar y ahí es cuando entra C que es quien negocia con el sistema para conseguir memoria libre; nosotros no debemos preocuparnos por nada de eso (afortunadamente). Por tanto cuando haces
int n;
C se encarga de reclamar una zona de memoria donde cabe un entero y el sistema se la otorga. ¿Cuál? La verdad es que no nos importa. Ídem para
int *n = malloc(sizeof(int));
Hace lo mismo: pide al sistema una zona de memoria dónde quepa un entero y éste, si tiene, nos la da.
* Un poco más avanzado: la memoria de un programa en C está dividida en sectores que son la memoria de programa, la memoria de la pila y la memoria del montón (heap). Hay más. La pila es donde caen las variables normales como int, char, double, arrays, structs, etcétera y es C el encargado de manejarlas. En el montón van a parar los malloc, calloc y toda esa família; C no entra ahí y es el propio programador el encargado de comprobar que se ha tomado memoria, de liberarla para futuros usos demás menesteres de administración.
Lo normal de un puntero es recibir la dirección de memoria de otra variable:
int n;
int *p = &n
El ampersand (&) es un operador que (en este caso) dice 'La dirección de'.
También es muy importante de los punteros el tipo de dato al que apuntan. Has visto en todos estos ejemplos cómo siempre han sido a entero (int *), pero pueden ser de cualquier tipo de dato: char *, double *, struct mi_estructura *, etcétera. Esto es debido a que los diferentes tipos de datos ocupan tamaños en bytes diferentes en memoria y el compilador necesita saber cuánto ocupa para recuperarlo o para guardar algo en él. Por ejemplo un char suele ser de 1 byte y un entero de 4 bytes. Si los punteros no especificaran el tamaño con su tipo podríamos guardar un número muy grande en una posición dónde se guarda un char machacando datos vecinos: intentaríamos guardar 4 bytes dónde sólo cabe 1.
Y bueno, esto es lo básico de los punteros. Hay más cosas: punteros como argumentos en funciones y porqué usarlos; similitudes y diferencias entre punteros y arrays; punteros a funciones (sí, también se pueden apuntar a funciones); punteros a punteros (por loco que parezca muchas veces son necesarios). Pero eso ya para más adelante. Ahora intenta comprender sus bases.