Curso de C · Módulo 11 de 12

Multi-fichero en C: headers, macros y depuración

Qué aprenderás en este módulo

organizar proyectos en varios ficheros y arreglar programas que "compilan pero fallan".

Incluye 3 ejercicios con pistas y solución y un quiz de 5 preguntas. Tiempo estimado: 80 minutos.

1. El preprocesador

Se ejecuta antes de compilar y trabaja con texto. Todas sus líneas empiezan por #.

#include <stdio.h>        // busca en las carpetas del sistema (librerías estándar)
#include "mates.h"        // busca primero en tu carpeta (tus headers)

#define MAX_ALUMNOS 30            // constante: se sustituye el texto
#define MAX(a, b) (((a) > (b)) ? (a) : (b))   // macro con parámetros

#ifdef DEBUG                      // compilación condicional
    printf("depuracion: x=%d\n", x);
#endif

Compila con depuración activa: gcc -DDEBUG prog.c.

Macros: la trampa del texto

#define CUADRADO(x) x*x
CUADRADO(a+b)      // → a+b*a+b   ¡mal!
#define CUADRADO(x) ((x) * (x))    // parentesis en cada parametro y en el total

Y aún así: CUADRADO(i++) evalúa i++ dos veces. Para funciones simples prefiere static inline.

2. Dividir en ficheros: .h y .c

Fichero Contiene Se incluye / compila
mates.h declaraciones: prototipos, tipos, macros, extern se #include donde se use
mates.c definiciones: el código de las funciones se compila una vez
main.c usa la librería

mates.h

#ifndef MATES_H          // include guard
#define MATES_H

int cuadrado(int x);

#endif

mates.c

#include "mates.h"       // comprueba que firma y definición coinciden
int cuadrado(int x) { return x * x; }

main.c

#include <stdio.h>
#include "mates.h"
int main(void) { printf("%d\n", cuadrado(5)); }

Compilar:

gcc -Wall -Wextra main.c mates.c -o prog

Los include guards

#ifndef X / #define X / #endif evitan que un header se procese dos veces si lo incluyen varios ficheros (daría errores de "redefinición").

Compilar por separado (proyectos grandes)

gcc -c main.c            # → main.o  (solo compila)
gcc -c mates.c           # → mates.o
gcc main.o mates.o -o prog   # enlaza

Así, si solo cambias mates.c, solo recompilas ése. Eso automatiza make:

prog: main.o mates.o
    gcc main.o mates.o -o prog
%.o: %.c
    gcc -Wall -Wextra -c $< -o $@

(Las líneas de comandos empiezan con TAB.)

static y extern

  • static en una función/variable global: privada de ese .c.
  • extern int contador; en el .h + int contador; en un solo .c: variable global compartida (úsalo con moderación).

Tipos opacos (TAD)

En el header: typedef struct Pila Pila; y solo las funciones. El struct Pila {...} completo se define en pila.c. Los usuarios no pueden tocar los campos → puedes cambiar la implementación sin romper nada (lo harás en el módulo 12).

3. Errores típicos de enlace

Mensaje Causa
undefined reference to 'foo' declaraste foo pero no compilaste/enlazaste el .c que la define
multiple definition of 'foo' definiste (no solo declaraste) algo en un .h incluido por varios .c
implicit declaration of function falta #include o prototipo

4. Depuración: el método científico

Cuando un programa compila pero da resultados erróneos:

  1. Reproduce el fallo con el ejemplo más pequeño posible.
  2. Predice a mano qué debería pasar, paso a paso.
  3. Observa: pon printf (a stderr) en puntos clave y compara lo previsto con lo real. El primer punto donde difieren acota el bug.
  4. Una hipótesis cada vez: cambia una cosa, prueba, repite.
  5. Explícaselo a un patito de goma 🦆: dicho en voz alta, el error salta solo.

Herramientas: * Warnings: -Wall -Wextra (¡léelos!). * Sanitizers: -fsanitize=address,undefined -g detecta desbordamientos, use-after-free, UB aritmético. * Depurador gdb: gcc -g prog.c -o prog && gdb ./prog → break main, run, next, step, print x, bt (traza tras un crash), watch x, quit. * assert: #include <assert.h>; assert(n > 0); aborta con el sitio exacto si se rompe una suposición.

Bugs típicos de lógica

Off-by-one en bucles · confundir = y == · división entera · índices desde 0 vs 1 · variable sin inicializar · olvidar el \0.

Prueba esto

  1. Convierte uno de tus ejercicios previos en proyecto de 3 ficheros.
  2. Escribe un Makefile y ejecuta make.
  3. Mete a propósito un a[10] en un array de 5 y compila con -fsanitize=address para ver el informe.

Ejercicios prácticos del módulo 11

Programar se aprende programando: intenta cada ejercicio antes de mirar las pistas, y usa la solución solo para comparar. Marca los que resuelvas y tu progreso se guarda en este navegador.

11.1 Tu primera libreria (.h + .c)
Este ejercicio tiene VARIOS ficheros (carpeta mis_ejercicios/ej11_1/): mates.h -> declaraciones (ya hecho, con include guards) mates.c -> TU TRABAJO: implementa cuadrado, cubo y valor_abs main.c -> usa la libreria (no lo toques) Se compilan juntos: gcc main.c mates.c -o prog (el corrector lo hace por ti).

Entrada de ejemplo

5

Salida esperada

cuadrado=25 cubo=125 abs=5

Tu punto de partida

mates.h

#ifndef MATES_H
#define MATES_H

long long cuadrado(int x);
long long cubo(int x);
int valor_abs(int x);

#endif

mates.c

#include "mates.h"

long long cuadrado(int x) {
    // TODO
    return 0;
}

long long cubo(int x) {
    // TODO
    return 0;
}

int valor_abs(int x) {
    // TODO
    return 0;
}

main.c

#include <stdio.h>
#include "mates.h"

int main(void) {
    int n;
    scanf("%d", &n);
    printf("cuadrado=%lld cubo=%lld abs=%d\n", cuadrado(n), cubo(n), valor_abs(n));
    return 0;
}

Cópialo en tu editor, complétalo, compílalo (gcc -Wall -Wextra programa.c -o programa) y prueba la entrada de ejemplo. Para que se corrija solo con varios casos de prueba, descarga el curso.

Pista 1

mates.c incluye su propio header ("mates.h", con comillas) para que el compilador compruebe que las firmas coinciden.

Pista 2

Haz el cast a long long ANTES de multiplicar para evitar desbordar el int.

Ver solución (inténtalo antes)

mates.c

#include "mates.h"

long long cuadrado(int x) {
    return (long long)x * x;
}

long long cubo(int x) {
    return (long long)x * x * x;
}

int valor_abs(int x) {
    return x < 0 ? -x : x;
}
11.2 Macros sin trampas
Escribe en macros.h las macros MAX(a,b), MIN(a,b) y CUADRADO(x) de modo que funcionen correctamente incluso en expresiones como 2 * MAX(a, b) y CUADRADO(a + b). La regla de oro: PARENTESIS alrededor de cada parametro y de la expresion completa.

Entrada de ejemplo

3 5

Salida esperada

max=5 min=3
doble_max=10 doble_min=6
cuadrado=64

Tu punto de partida

macros.h

#ifndef MACROS_H
#define MACROS_H

// TODO: #define MAX(a, b) ...
// TODO: #define MIN(a, b) ...
// TODO: #define CUADRADO(x) ...

#endif

main.c

#include <stdio.h>
#include "macros.h"

int main(void) {
    int a, b;
    scanf("%d %d", &a, &b);
    printf("max=%d min=%d\n", MAX(a, b), MIN(a, b));
    printf("doble_max=%d doble_min=%d\n", 2 * MAX(a, b), 2 * MIN(a, b));
    printf("cuadrado=%d\n", CUADRADO(a + b));
    return 0;
}

Cópialo en tu editor, complétalo, compílalo (gcc -Wall -Wextra programa.c -o programa) y prueba la entrada de ejemplo. Para que se corrija solo con varios casos de prueba, descarga el curso.

Pista 1

Una macro es SUSTITUCION DE TEXTO: CUADRADO(a+b) con x*x se convierte en a+b*a+b.

Pista 2

Parentesis en los parametros: ((x) * (x)); y en la expresion completa para que 2 * MAX(...) no se rompa.

Ver solución (inténtalo antes)

macros.h

#ifndef MACROS_H
#define MACROS_H

#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define CUADRADO(x) ((x) * (x))

#endif
11.3 Depura: caza los bugs logicos
Este programa COMPILA, pero da resultados incorrectos. Lee n y n enteros e imprime: positivos=<cuantos son estrictamente > 0> suma_neg=<suma de los negativos> pos_max=<posicion del mayor, empezando a contar en 1 (primera aparicion)> Hay 3 bugs. Aprende el metodo: pon printf de depuracion, razona con un ejemplo pequeno a mano y compara con lo que imprime. (Los printf extra de depuracion los borras al terminar.) Ejemplo: entrada "6 / 0 5 -3 8 -1 9" -> positivos=3 suma_neg=-4 pos_max=6

Entrada de ejemplo

6
0 5 -3 8 -1 9

Salida esperada

positivos=3 suma_neg=-4 pos_max=6

Tu punto de partida

#include <stdio.h>

int main(void) {
    int v[100], n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &v[i]);

    int positivos = 0, suma_neg = 0, pos_max = 0;
    for (int i = 0; i < n - 1; i++) {
        if (v[i] >= 0) positivos++;
        if (v[i] < 0) suma_neg += v[i];
        if (v[i] > v[pos_max]) pos_max = i;
    }
    printf("positivos=%d suma_neg=%d pos_max=%d\n", positivos, suma_neg, pos_max);
    return 0;
}

Cópialo en tu editor, complétalo, compílalo (gcc -Wall -Wextra programa.c -o programa) y prueba la entrada de ejemplo. Para que se corrija solo con varios casos de prueba, descarga el curso.

Pista 1

Bug 1: el bucle principal se deja un elemento sin procesar (mira la condicion).

Pista 2

Bug 2: el enunciado dice 'estrictamente mayor que 0'; el 0 no es positivo.

Pista 3

Bug 3: los indices en C empiezan en 0, pero la salida pide posiciones desde 1.

Ver solución (inténtalo antes)
#include <stdio.h>

int main(void) {
    int v[100], n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &v[i]);

    int positivos = 0, suma_neg = 0, pos_max = 0;
    for (int i = 0; i < n; i++) {
        if (v[i] > 0) positivos++;
        if (v[i] < 0) suma_neg += v[i];
        if (v[i] > v[pos_max]) pos_max = i;
    }
    printf("positivos=%d suma_neg=%d pos_max=%d\n", positivos, suma_neg, pos_max + 1);
    return 0;
}

Quiz del módulo 11

5 preguntas para comprobar que has fijado los conceptos. Las opciones se barajan cada vez.