Programación avanzada en C
En C se programa sabiendo dónde están las cosas. El puntero no es una rareza del lenguaje: es la dirección de memoria, y entenderlo es la diferencia entre pelearse con el compilador y manejar el hardware.
01Toda variable tiene una dirección
Una variable es un nombre para un lugar de la memoria. Ese lugar tiene una dirección, y C permite trabajar con ella directamente, algo que la mayoría de los lenguajes esconde.
| Operador | Se lee | Qué devuelve |
|---|---|---|
&x | dirección de x | Dónde está guardada x |
*p | contenido de p | El valor que hay en la dirección que guarda p |
int *p; | p es puntero a int | Declara una variable que guarda direcciones |
Ocupa lugar en memoria, tiene su propia dirección y se le puede asignar un valor. Lo único especial
es que ese valor es una dirección, y que el tipo dice cuánto mide lo apuntado: eso es lo que hace
que p++ avance 4 bytes en un int* y 1 byte en un char*.
Recorré el programa con el deslizador y mirá qué pasa en la memoria en cada línea.
02Arreglos, cadenas y punteros
En C el nombre de un arreglo es la dirección de su primer elemento. Por eso estas cuatro expresiones significan lo mismo:
v[i] ≡ *(v + i) ≡ *(i + v) ≡ i[v]
La última es una curiosidad que conviene no usar, pero muestra hasta qué punto los corchetes son sólo una notación cómoda para la aritmética de punteros.
C no verifica los límites. Escribir en v[10] de un arreglo de 5 compila sin
protestar y pisa lo que haya al lado: otra variable, la dirección de retorno de la función, cualquier
cosa. Esa es la causa de la mitad de las fallas de seguridad de la historia del software, y por eso hay
que pasar siempre el tamaño junto con el puntero.
Una cadena es un arreglo de char terminado en el byte cero. Ese cero no es
decorativo: es lo único que le dice a strlen o a printf dónde termina el
texto.
03Pasar por valor y por referencia
C pasa siempre los argumentos por valor: la función recibe una copia. Si hay que modificar algo de quien llama, se le pasa su dirección:
void duplicar(int *x) { *x = *x * 2; }
…
duplicar(&a); /* ahora a vale el doble */
Los arreglos parecen una excepción, pero no lo son: lo que se pasa es la dirección del primer
elemento, así que la función puede modificar el contenido. Por eso void f(int v[]) y
void f(int *v) son exactamente lo mismo para el compilador.
También se puede guardar la dirección de una función y llamarla a través de ella. Con eso se arman tablas de despacho, callbacks y máquinas de estados, y es el mecanismo que usan los vectores de interrupción de un microcontrolador.
04Memoria dinámica
| Zona | Quién la maneja | Vive hasta |
|---|---|---|
| Variables globales y estáticas | El compilador | Que termine el programa |
| Pila (variables locales) | Automático | Que retorne la función |
| Montículo (malloc) | El programador | Que alguien llame a free |
int *v = malloc(n * sizeof(int));
if (v == NULL) { /* no había memoria */ }
…
free(v);
v = NULL;
Las tres reglas que evitan casi todos los problemas: verificar siempre que malloc no devolvió NULL, liberar exactamente una vez lo que se reservó, y poner el puntero en NULL después de liberarlo para que un uso posterior falle de inmediato en lugar de corromper memoria en silencio.
- Pérdida de memoria: reservar y no liberar. El programa crece hasta que el sistema lo mata.
- Puntero colgante: usar memoria ya liberada.
- Doble liberación: llamar dos veces a free con el mismo puntero.
- Puntero no inicializado: usar un puntero que nunca recibió una dirección válida.
En un equipo embebido que corre meses sin reiniciarse, una pérdida de memoria de pocos bytes por hora termina en una falla a las semanas. Por eso muchos proyectos embebidos directamente prohíben malloc después del arranque.
05Modularizar
Un programa serio no vive en un solo archivo. El criterio es separar interfaz de implementación:
sensor.h: qué se puede usar —prototipos, tipos y constantes—, protegido contra inclusión múltiple con#ifndef SENSOR_H.sensor.c: cómo está hecho. Lo que no se declara en el encabezado se marcastaticy queda privado del archivo.- Cada
.cse compila por separado y el enlazador junta los objetos.
Esa organización es la que permite reemplazar el módulo de un sensor por otro sin tocar el resto, y es la base de las bibliotecas que se usan en la construcción del software.
06En el laboratorio
Escribir un programa que imprima con %p las direcciones de una variable global, una
local, una reservada con malloc y una constante de texto. Ordenarlas y deducir el mapa de memoria del
proceso.
Escribirlas usando sólo punteros, sin corchetes. Comparar con las de la biblioteca estándar y medir tiempos con cadenas largas.
Compilar un programa con un malloc sin free y correrlo bajo valgrind. Interpretar el
informe, corregir y volver a verificar que no queda memoria perdida.
07Errores frecuentes
- Confundir
*pconp. Uno es el contenido; el otro, la dirección. - Devolver la dirección de una variable local. Al retornar la función, esa memoria ya no es válida.
- Olvidar el byte cero al construir una cadena a mano.
- Reservar con el tamaño equivocado:
malloc(n)en vez demalloc(n * sizeof(int)). - Comparar cadenas con
==, que compara direcciones y no contenido: hay que usarstrcmp. - Usar
sizeofsobre un puntero esperando el tamaño del arreglo: da el tamaño del puntero.
08Autoevaluación
Si int v[5] empieza en 0x1000, ¿qué dirección es v + 2?
0x1008: la aritmética de punteros avanza de a sizeof(int), es decir
4 bytes por elemento.
¿Qué diferencia hay entre char s[] = "hola" y char *s = "hola"?
El primero es un arreglo de 5 bytes modificable, copia del texto. El segundo es un puntero a una constante de sólo lectura: escribir en ella es comportamiento indefinido.
¿Por qué void f(int x) no puede modificar la variable de quien la llama?
Porque recibe una copia. Hay que pasar la dirección: void f(int *x).
¿Qué pasa si se llama dos veces a free con el mismo puntero?
Comportamiento indefinido: en general corrompe las estructuras internas del
asignador. Poner el puntero en NULL después de liberarlo lo evita, porque free(NULL) es
seguro.
¿Cuánto vale sizeof(v) dentro de void f(int v[])?
El tamaño de un puntero —4 u 8 bytes—, no el del arreglo. El tamaño hay que pasarlo como parámetro aparte.
¿Para qué sirve declarar una función static en un archivo .c?
Para que sea privada de ese archivo y no aparezca en el enlazado: evita choques de nombres y aclara qué es interfaz y qué es implementación.
09Para ampliar
- Brian W. Kernighan y Dennis M. Ritchie. El lenguaje de programación C. 2.ª ed., Pearson, 1991. El capítulo 5, sobre punteros y arreglos, sigue siendo la mejor explicación escrita del tema.
- Randal E. Bryant y David R. O'Hallaron. Computer Systems: A Programmer's Perspective. 3.ª ed., Pearson, 2015. Qué pasa realmente en la memoria cuando se ejecuta un programa en C.
- Peter van der Linden. Expert C Programming: Deep C Secrets. Prentice Hall, 1994. Las trampas del lenguaje explicadas con humor y con casos reales.
- Herbert Schildt. C: manual de referencia. 4.ª ed., McGraw-Hill, 2001. Referencia rápida de biblioteca estándar, útil para tener al lado del teclado.