Cuando diseñas un módulo en C, estás definiendo un contrato. El archivo de cabecera (.h) es la promesa que le haces al resto del sistema: “puedes usar estas funciones y estos tipos, y te garantizo que existirán al compilar”. Por otro lado, el archivo de implementación (.c) es donde cumples esa promesa, ocultando los detalles de cómo se realizan las tareas.
Esta separación no es una cuestión de estética, sino de arquitectura y de cómo funciona el enlazador (linker). El compilador procesa cada archivo .c de forma independiente, creando lo que llamamos unidades de traducción (TU). El enlazador es quien une todas esas piezas. Si intentas definir algo en un header que no sea una declaración (como una función con cuerpo o una variable con inicialización), estarás dando una definición completa en cada archivo que incluya ese header. Esto provocará una violación de la Regla de Definición Única (ODR), y el enlazador se detendrá con un error de “multiple definition”, ya que no sabrá con cuál de todas las copias idénticas debe quedarse.
Para evitar esto, debemos ser quirúrgicos. En un header solo deben residir declaraciones: prototipos de funciones, definiciones de tipos (struct, union, enum), macros de constantes, y declaraciones extern para variables globales. Si necesitas que una función pequeña esté en el header para que sea rápida, debes usar static inline. Esto le dice al compilador que la función tiene vinculación interna (internal linkage); es decir, cada unidad de traducción tendrá su propia copia privada de la función, evitando el conflicto en el enlazador.
¿Cuándo usar esta separación? Siempre. Si estás escribiendo más de un archivo .c, la separación es obligatoria para mantener la escalabilidad y reducir los tiempos de compilación. Si fallas en esto, te encontrarás con dependencias circulares imposibles de resolver o con tiempos de compilación que crecen exponencialmente.
/*
* Ejemplo: Simulación de un módulo de sensor.
* Para que este código sea compilable como un único archivo (como pide el estándar
* de este ejercicio), hemos simulado la separación de archivos dentro de uno mismo.
* En un proyecto real, el contenido de SENSOR_H iría en sensor.h y el de SENSOR_C en sensor.c.
*/
#include <stdio.h>
// --- [SIMULACIÓN DE SENSOR.H] ---
// Este bloque representa lo que un usuario vería en "sensor.h"
// Usamos una forward declaration para evitar incluir estructuras pesadas
// si el usuario solo necesita un puntero a ella.
typedef struct SensorData SensorData;
// Declaración de una variable global: le decimos al compilador que
// "existe", pero la definición real está en otro lado.
extern int g_sensor_count;
// Prototipos de funciones: la interfaz pura.
void sensor_init(void);
void sensor_print_status(const SensorData *data);
// Una función inline para lógica trivial. Se define en el header
// para que el compilador pueda insertarla directamente (inlining)
// en cada unidad de traducción sin causar duplicados en el linker.
static inline int is_valid_reading(int value) {
return value >= 0 && value < 1024;
}
// --------------------------------
// --- [SIMULACIÓN DE SENSOR.C] ---
// Este bloque representa lo que estaría en "sensor.c"
struct SensorData {
int last_value;
int id;
};
// La definición real de la variable global (solo puede haber una).
int g_sensor_count = 0;
// Implementación de las funciones.
void sensor_init(void) {
g_sensor_count = 1;
printf("[Sensor] Sistema inicializado.\n");
}
void sensor_print_status(const SensorData *data) {
if (data != NULL) {
printf("[Sensor] ID: %d, Último valor: %d\n", data->id, data->last_value);
}
}
// --------------------------------
// --- [PROGRAMA PRINCIPAL] ---
// Este sería el "main.c" que consume el módulo.
int main(void) {
// Inicializamos el módulo
sensor_init();
// Creamos una instancia de la estructura.
// Aunque no conocemos su definición completa aquí (por la forward declaration),
// podemos usarla si tenemos el objeto completo.
SensorData my_sensor = { .last_value = 512, .id = 101 };
// Usamos la función inline desde el "header"
int lectura = 500;
if (is_valid_reading(lectura)) {
my_sensor.last_value = lectura;
}
// Usamos la interfaz pública
sensor_print_status(&my_sensor);
printf("Sensores activos: %d\n", g_sensor_count);
return 0;
}
Desglose del diseño
En el ejemplo, observa cómo SensorData se maneja con una forward declaration (typedef struct SensorData SensorData;) en la parte superior. Esto es una técnica de optimización crucial: si un archivo .c solo necesita pasar un puntero a SensorData a una función, no necesita conocer la estructura interna de la variable; solo necesita saber que es un tipo de dato válido. Esto rompe la dependencia de incluir cabeceras pesadas y acelera la compilación.
La variable g_sensor_count utiliza la palabra clave extern. Esto le indica al compilador: “No reserves espacio para esto ahora, solo confía en que el enlazador encontrará su definición más tarde”. Si hubiéramos escrito int g_sensor_count = 0; en el header, cada archivo que incluyera ese header intentaría reservar su propio espacio para esa variable, provocando el error de duplicidad.
La función is_valid_reading es static inline. Al ser static, su ámbito se limita a la unidad de traducción actual. Aunque se incluya en diez archivos .c distintos, el enlazador no verá un símbolo global conflictivo, sino diez implementaciones locales, permitiendo al compilador optimizar el código al sustituir la llamada por el cuerpo de la función directamente.
El error frecuente
El error más clásico en proyectos que crecen es definir una variable global directamente en un header:
// sensor.h int error_count = 0; // ¡ERROR! Definición de variable en un header.
Si incluyes sensor.h en main.c y en driver.c, el compilador generará dos objetos (main.o y driver.o) que contienen ambos una variable llamada error_count. Cuando el enlazador intente unirlos, se encontrará con dos símbolos con el mismo nombre y lanzará un error de multiple definition.
Para corregirlo, la variable debe definirse en un .c y declararse como extern en el .h. Si usas herramientas como AddressSanitizer o Valgrind, estas no detectarán esto (porque es un error de enlazado, no de memoria), pero un simple gcc -Wall -Wextra o el propio enlazador te detendrán inmediatamente.
N° 91