wandres.dev
MODULAR · Modularidad y linkage

Modularidad, headers y linkage

Cómo se parte un programa en varios archivos: unidades de traducción, la separación header/implementación, y el linkage interno vs externo que controla la visibilidad.

⏱ 14 min

Un programa real no cabe en un archivo. Dividirlo bien —qué va en el header, qué en el .c, qué se exporta y qué se oculta— es lo que hace un código mantenible. Y conecta directamente con el proceso de compilación: cada archivo es una unidad de traducción que el linker luego une.

🎯 Al terminar esta lección sabrás
  • Unidades de traducción y compilación separada.
  • La separación header (.h) / implementación (.c).
  • Linkage interno vs externo.
  • Buenas prácticas de headers.

Unidades de traducción

Cada archivo .c es una unidad de traducción: se compila de forma independiente a un .o (recuerda el nivel 3). El linker luego une todos los .o. Esto permite compilar solo lo que cambió, no todo el programa.

gcc -c mates.c -o mates.o     # compila una unidad
gcc -c main.c  -o main.o      # otra, independiente
gcc mates.o main.o -o programa # el linker las une

Header e implementación

La convención universal: el header (.h) declara la interfaz (qué existe); el .c define la implementación (cómo funciona). Los demás archivos incluyen el header, no el .c.

// mates.h — la interfaz pública
#pragma once
int  sumar(int a, int b);        // declaración
extern int contador_global;       // variable compartida (declarada)

// mates.c — la implementación
#include "mates.h"
int contador_global = 0;          // definición (una sola)
int sumar(int a, int b) { return a + b; }

// main.c — el consumidor
#include "mates.h"                 // ve las declaraciones, no la implementación
int main(void) { return sumar(2, 3); }
El header es un contrato, no código

El error mental de novato es pensar que #include "x.h" “trae el código de x”. No: trae declaraciones — promesas de que esas funciones y variables existen en algún .o. El header es el contrato entre módulos; el .c lo cumple; el linker verifica que cada promesa tiene su definición. Por eso un header debe contener declaraciones (prototipos, tipos, macros) y casi nunca definiciones (código o variables globales), que provocarían “definición múltiple” al incluirse en varios sitios. Entender esta separación es entender cómo se ensambla un programa grande — y por qué el famoso “undefined reference” (nivel 3) significa “prometiste algo en un header que ningún .c definió”.

Linkage: qué se ve desde fuera

Cada símbolo global tiene un linkage que decide su visibilidad entre unidades de traducción:

Declaración Linkage Significado
int x; (global) externo visible desde otros .c
static int x; (global) interno privado de este .c
extern int x; externo “definido en otro .c
función normal externo exportada
función static interno privada del módulo
// utils.c
static int estado = 0;             // privado: ningún otro .c lo ve
static void ayudante(void) { }     // función interna del módulo
int api_publica(void) { return 0; } // exportada
💡
static por defecto en lo privado

La regla de oro de la modularidad en C: marca static todo lo que no forme parte de la API pública de tu módulo — funciones auxiliares, variables de estado interno. Esto tiene tres beneficios: evita colisiones de nombres entre archivos, deja clarísimo qué es la interfaz pública (lo que NO es static), y permite al compilador optimizar mejor (sabe que nadie externo llama a esa función). Un módulo bien hecho expone poco (unas pocas funciones no-static en su header) y oculta mucho (todo lo demás, static). Menos superficie pública = menos acoplamiento = código más fácil de cambiar.

Higiene de headers

🔒

Guard siempre

#pragma once o include guards para evitar inclusión doble.

📦

Incluye lo que usas

Cada archivo incluye los headers de lo que usa, sin depender de inclusiones transitivas.

🪶

Headers ligeros

En el header, declaraciones y tipos; nada de código pesado. Menos dependencias, compilación más rápida.

🙈

Oculta con opacos

Expón tipos opacos (nivel 12) cuando puedas: el header no revela la estructura interna.

⚔️ Parte un programa
  1. Separa un programa en mates.h / mates.c / main.c y compílalo por unidades.
  2. Compila solo un .o tras cambiar un archivo, y reenlaza; observa que no recompilas todo.
  3. Marca static una función auxiliar y comprueba que otro .c no puede llamarla.
  4. Provoca un “undefined reference” declarando algo en el header que ningún .c define, y relaciónalo con el linker.