Introducción
Los punteros son la razón por la que C es tan potente — y también la razón por la que tantos programadores los temen. Un puntero es simplemente una variable que almacena una dirección de memoria. Entenderlos cambia la forma en que piensas sobre cómo funciona un programa.
Son el mecanismo detrás de arrays dinámicos, listas enlazadas, árboles, paso de parámetros por referencia y la gestión manual de memoria. Sin punteros, C sería un lenguaje ordinario.
¿Qué vas a aprender en este lab?
- Qué es un puntero y cómo declararlo
- Operadores
&(dirección) y*(desreferencia) - Relación entre punteros y arrays
- Aritmética de punteros
- Punteros a funciones
- Gestión dinámica de memoria (
malloc,free) - Errores más comunes y cómo evitarlos
Requisitos previos: fundamentos de C (variables, tipos, funciones, arrays). Este es un tema avanzado — tómate tu tiempo.
1. ¿Qué es un puntero?
Cada variable vive en una dirección de memoria. Un puntero almacena esa dirección.
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x; /* p almacena la dirección de x */
printf("Valor de x: %d\n", x); /* 42 */
printf("Dirección de x: %p\n", &x); /* 0x7ffee4b1234c (ejemplo) */
printf("Valor de p (direc.): %p\n", p); /* misma dirección */
printf("Valor apuntado (*p): %d\n", *p); /* 42 */
/* Modificar x a través del puntero */
*p = 100;
printf("x ahora vale: %d\n", x); /* 100 */
return 0;
}
Los dos operadores clave:
| Operador | Nombre | Qué hace |
|---|---|---|
&x |
Dirección-de | Devuelve la dirección de memoria de x |
*p |
Desreferencia | Accede al valor en la dirección que guarda p |
Declaración de punteros
int *pi; /* puntero a int */
double *pd; /* puntero a double */
char *pc; /* puntero a char (también string) */
void *pv; /* puntero genérico — puede apuntar a cualquier tipo */
int **pp; /* puntero a puntero a int */
Siempre inicializa los punteros. Un puntero no inicializado apunta a memoria aleatoria — usarlo es comportamiento indefinido.
int *p = NULL; /* puntero nulo — seguro: claramente "no apunta a nada" */
if (p != NULL) {
*p = 42; /* solo desreferencia si no es NULL */
}
2. Punteros y funciones
El uso más común de punteros es pasar variables por referencia para que una función pueda modificarlas.
#include <stdio.h>
/* Intercambia los valores de dos enteros */
void intercambiar(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
/* Devuelve múltiples valores "de salida" */
void dividir(int num, int den, int *cociente, int *resto) {
*cociente = num / den;
*resto = num % den;
}
int main(void) {
int x = 10, y = 20;
printf("Antes: x=%d, y=%d\n", x, y); /* 10, 20 */
intercambiar(&x, &y);
printf("Después: x=%d, y=%d\n", x, y); /* 20, 10 */
int c, r;
dividir(17, 5, &c, &r);
printf("17 / 5 = %d con resto %d\n", c, r); /* 3 con resto 2 */
return 0;
}
3. Punteros y arrays
En C, el nombre de un array es un puntero a su primer elemento.
int numeros[] = {10, 20, 30, 40, 50};
int *p = numeros; /* equivale a &numeros[0] */
printf("%d\n", *p); /* 10 */
printf("%d\n", numeros[2]); /* 30 */
printf("%d\n", *(p + 2)); /* 30 — aritmética de punteros */
/* Las dos notaciones son equivalentes */
printf("%d\n", p[3]); /* 40 */
printf("%d\n", *(p + 3)); /* 40 */
Aritmética de punteros
Cuando sumas n a un puntero, avanza n × sizeof(tipo) bytes — no n bytes.
int arr[] = {1, 2, 3, 4, 5};
int *inicio = arr;
int *fin = arr + 5; /* apunta al elemento DESPUÉS del último */
for (int *p = inicio; p < fin; p++) {
printf("%d ", *p); /* 1 2 3 4 5 */
}
/* Diferencia entre punteros = número de elementos entre ellos */
printf("\nElementos: %td\n", fin - inicio); /* 5 */
Pasar arrays a funciones
Cuando pasas un array a una función, se convierte en puntero — el tamaño se pierde.
/* Estas dos declaraciones son equivalentes: */
void imprimir(int *arr, int n);
void imprimir(int arr[], int n); /* azúcar sintáctico */
/* Siempre pasa el tamaño como parámetro separado */
void imprimir(int arr[], int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
/* sizeof NO funciona dentro de la función — ya es puntero */
void incorrecto(int arr[]) {
int n = sizeof(arr) / sizeof(arr[0]); /* INCORRECTO: sizeof de puntero */
}
Ejercicio 1
Escribe estas funciones que trabajen con arrays usando aritmética de punteros (sin usar arr[i], solo *(arr + i) o moviendo el puntero):
int suma(int *arr, int n)— suma todos los elementosvoid revertir(int *arr, int n)— invierte el array en su lugarint *buscar(int *arr, int n, int valor)— devuelve puntero al elemento oNULLsi no existe
4. Punteros a strings
Un string en C es un char * que apunta al primer carácter de una secuencia terminada en \0.
char str1[] = "Hola"; /* array en stack — modificable */
char *str2 = "Mundo"; /* puntero a literal — NO modificable en la práctica */
/* Recorrer con puntero */
for (char *p = str1; *p != '\0'; p++) {
putchar(*p);
}
putchar('\n');
/* Implementación de strlen con punteros */
size_t mi_strlen(const char *s) {
const char *p = s;
while (*p) p++; /* avanza hasta el \0 */
return p - s; /* diferencia = longitud */
}
Ejercicio 2
Implementa las siguientes funciones de string usando solo aritmética de punteros:
char *mi_strchr(const char *s, int c)— puntero a la primera ocurrencia decens, oNULLvoid mi_strrev(char *s)— invierte el string en su lugarint mi_atoi(const char *s)— convierte"42"a42(ignora espacios iniciales, maneja signo)
5. Gestión dinámica de memoria
Con arrays estáticos, debes conocer el tamaño en tiempo de compilación. La memoria dinámica te permite allocar en tiempo de ejecución.
#include <stdio.h>
#include <stdlib.h> /* malloc, calloc, realloc, free */
int main(void) {
int n;
printf("¿Cuántos números? ");
scanf("%d", &n);
/* malloc — allocar n enteros sin inicializar */
int *arr = malloc(n * sizeof(int));
if (arr == NULL) {
fprintf(stderr, "Error: sin memoria\n");
return 1;
}
/* Usar el array */
for (int i = 0; i < n; i++) {
arr[i] = i * i; /* cuadrados */
}
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
/* SIEMPRE libera la memoria al terminar */
free(arr);
arr = NULL; /* evita usar un puntero colgante */
return 0;
}
malloc vs calloc vs realloc
/* malloc — allocar sin inicializar (más rápido) */
int *a = malloc(10 * sizeof(int));
/* calloc — allocar e inicializar a 0 */
int *b = calloc(10, sizeof(int));
/* realloc — cambiar el tamaño de un bloque existente */
int *c = malloc(5 * sizeof(int));
c = realloc(c, 10 * sizeof(int)); /* ahora tiene espacio para 10 */
Implementar un vector dinámico
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *datos;
size_t tamanio;
size_t capacidad;
} Vector;
Vector *vector_crear(void) {
Vector *v = malloc(sizeof(Vector));
v->capacidad = 4;
v->tamanio = 0;
v->datos = malloc(v->capacidad * sizeof(int));
return v;
}
void vector_push(Vector *v, int valor) {
if (v->tamanio == v->capacidad) {
v->capacidad *= 2;
v->datos = realloc(v->datos, v->capacidad * sizeof(int));
}
v->datos[v->tamanio++] = valor;
}
int vector_get(const Vector *v, size_t i) {
return v->datos[i];
}
void vector_destruir(Vector *v) {
free(v->datos);
free(v);
}
6. Punteros a funciones
En C, una función también tiene una dirección. Puedes guardarla en un puntero y llamarla dinámicamente.
#include <stdio.h>
int sumar(int a, int b) { return a + b; }
int restar(int a, int b) { return a - b; }
int multiplicar(int a, int b) { return a * b; }
/* Tipo: puntero a función que recibe dos int y devuelve int */
typedef int (*OperacionBinaria)(int, int);
void aplicar(int a, int b, OperacionBinaria op, const char *nombre) {
printf("%s(%d, %d) = %d\n", nombre, a, b, op(a, b));
}
int main(void) {
OperacionBinaria ops[] = { sumar, restar, multiplicar };
const char *nombres[] = { "sumar", "restar", "multiplicar" };
for (int i = 0; i < 3; i++) {
aplicar(10, 3, ops[i], nombres[i]);
}
/* sumar(10, 3) = 13
restar(10, 3) = 7
multiplicar(10, 3) = 30 */
return 0;
}
qsort — ordenamiento genérico con puntero a función
#include <stdlib.h>
int comparar_enteros(const void *a, const void *b) {
return *(int*)a - *(int*)b; /* ascendente */
}
int numeros[] = {5, 2, 8, 1, 9, 3};
int n = sizeof(numeros) / sizeof(numeros[0]);
qsort(numeros, n, sizeof(int), comparar_enteros);
/* numeros ahora es {1, 2, 3, 5, 8, 9} */
Ejercicio 3
Implementa una función map que aplique una función a cada elemento de un array:
void map(int *arr, int n, int (*f)(int));
Y una función filter que copie solo los elementos que cumplan un predicado:
int filter(int *src, int n, int *dst, int (*pred)(int));
/* devuelve cuántos elementos pasaron el filtro */
Úsalas para doblar todos los elementos y para quedarte solo con los pares.
7. Errores Comunes
Error 1 — Segmentation Fault: desreferenciar NULL
int *p = NULL;
*p = 42; /* Segmentation fault — intenta escribir en dirección 0 */
/* Verifica antes de desreferenciar */
if (p != NULL) *p = 42;
Error 2 — Puntero colgante (dangling pointer)
int *p = malloc(sizeof(int));
*p = 42;
free(p);
/* p sigue apuntando a la memoria liberada — usarla es UB */
printf("%d\n", *p); /* comportamiento indefinido */
/* Asigna NULL después de free */
free(p);
p = NULL;
Error 3 — Memory leak (olvidar free)
void procesar(int n) {
int *arr = malloc(n * sizeof(int));
/* ... procesamiento ... */
return; /* falta free(arr) — memoria nunca liberada */
}
/* */
void procesar(int n) {
int *arr = malloc(n * sizeof(int));
/* ... procesamiento ... */
free(arr);
}
Usa valgrind para detectar memory leaks:
gcc -g programa.c -o programa
valgrind --leak-check=full ./programa
Error 4 — Off-by-one en aritmética de punteros
int arr[] = {1, 2, 3};
int *fin = arr + 3; /* apunta al elemento DESPUÉS de arr[2] */
/* Desreferenciar fin es UB */
printf("%d\n", *fin);
/* fin solo se usa como límite de comparación */
for (int *p = arr; p < fin; p++) {
printf("%d ", *p);
}
Resumen
| Concepto | Sintaxis |
|---|---|
| Declarar puntero | int *p; |
| Dirección de variable | &x |
| Desreferenciar | *p |
| Puntero nulo | NULL |
| Puntero a función | int (*f)(int, int) |
| Allocar memoria | malloc(n * sizeof(T)) |
| Liberar memoria | free(p); p = NULL; |
| Avanzar puntero | p++ o p + n |
Próximos pasos
- Estructuras (
struct): crear tus propios tipos y combinarlos con punteros - Listas enlazadas: la primera estructura de datos dinámica con punteros
- Árbol binario de búsqueda: punteros recursivos en estructuras