Archivos .c y .h: unidades de traducción y su rol

En C, un programa no es solo un bloque de código; es un conjunto de piezas interconectadas. Para entender cómo se organizan, primero debemos entender la unidad de traducción. Cuando el compilador trabaja, no lee todo tu proyecto de golpe; procesa un archivo .c a la vez. Esta unidad de traducción es el contenido de tu archivo .c una vez que el preprocesador ha terminado su trabajo (es decir, después de haber copiado el contenido de los archivos #include y haber expandido las macros #define).

Para gestionar la complejidad, dividimos el código en dos tipos de archivos: las cabeceras (.h) y los archivos de fuente (.c).

Las cabeceras funcionan como un contrato o un “menú” de un restaurante. En ellas colocamos las declaraciones: le decimos al compilador que algo existe (por ejemplo, una función), qué parámetros recibe y qué devuelve, pero no explicamos cómo lo hace. También se usan para definir tipos de datos (struct, typedef) y macros. Si usas #include <stdio.h>, el preprocesador busca en las rutas estándar del sistema; si usas #include "mi_archivo.h", busca primero en la carpeta de tu proyecto.

Los archivos .c contienen la definición, que es la implementación real de ese contrato. Aquí es donde reside la lógica de las funciones y donde se asigna espacio en memoria para las variables.

Este diseño permite la compilación separada. El compilador transforma cada unidad de traducción en un archivo objeto (.o), que es código máquina, pero incompleto porque no sabe dónde están las funciones de otros archivos. Es entonces cuando entra el enlazador (linker), que toma todos los archivos .o y los une, resolviendo las direcciones de memoria para que las llamadas a funciones funcionen correctamente.

Este modelo es esencial cuando trabajas en proyectos grandes. Si cambias una línea en un archivo .c, solo necesitas recompilar ese archivo y volver a enlazar, en lugar de recompilar todo el sistema. Si intentas incluir un archivo .c dentro de otro, el enlazador encontrará la misma función dos veces y lanzará un error de “símbolo duplicado”.

/*
 * Para que este ejemplo sea profesional y modular, en un proyecto real
 * deberías dividir el código en tres archivos distintos.
 * A continuación, simulamos esa estructura en un solo archivo para 
 * que puedas compilarlo directamente con:
 * gcc -std=c11 -Wall -Wextra -Wpedantic -o ejemplo ejemplo.c
 */

#include <stdio.h>

// --- SIMULACIÓN DE math_utils.h (Cabecera) ---
// En un archivo real, esto iría en math_utils.h.
// Solo contiene declaraciones (promesas).
int sumar(int a, int b);
void imprimir_mensaje(const char *mensaje);

// --- SIMULACIÓN DE math_utils.c (Implementación) ---
// En un archivo real, esto iría en math_utils.c.
// Contiene las definiciones (la lógica real).

int sumar(int a, int b) {
    return a + b;
}

void imprimir_mensaje(const char *mensaje) {
    printf("Log: %s\n", mensaje);
}

// --- main.c (Archivo principal) ---
int main() {
    int x = 10;
    int y = 20;

    imprimir_mensaje("Iniciando operación de suma");
    
    int resultado = sumar(x, y);
    
    printf("El resultado de %d + %d es: %d\n", x, y, resultado);
    
    imprimir_mensaje("Operación finalizada");

    return 0;
}

Desglose del ejemplo

En el código anterior, hemos simulado la división de responsabilidades.

  1. Declaraciones: Aunque todo está en un solo archivo para facilitar la compilación con un solo comando, las funciones sumar e imprimir_mensaje están declaradas antes de ser usadas en main. Esto es lo que haría un archivo .h. El compilador ve la declaración y dice: “No sé cómo funciona sumar, pero sé que recibe dos int y devuelve un int, así que confío en que el enlazador encontrará la definición más tarde”.
  2. Implementación: Las funciones tienen su cuerpo (el código entre {...}) definido en la parte inferior. En un proyecto real, estas estarían en un archivo .c independiente.
  3. Uso en main: La función main actúa como el consumidor. Llama a sumar sin conocer su implementación interna, solo su firma (nombre y tipos).

Si hubiéramos puesto el cuerpo de sumar dentro de un archivo .h y ese .h se incluyera en dos archivos .c diferentes, el enlazador encontraría dos implementaciones de sumar y fallaría.

El error frecuente

Un error clásico para quienes empiezan es intentar definir funciones en los archivos de cabecera.

// --- math_utils.h ---
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// ERROR: Esto es una DEFINICIÓN, no una declaración.
int cuadrado(int x) {
    return x * x;
}

#endif

Si incluyes math_utils.h en main.c y también en test.c, el preprocesador copiará el código de cuadrado en ambos archivos. Al intentar compilar y enlazar, el enlazador se encontrará con dos funciones llamadas cuadrado y lanzará un error de “multiple definition of cuadrado.

Si realmente necesitas definir una función en un header para que sea visible en todos los archivos, debes usar la palabra clave static (para que la función sea privada a cada archivo objeto) o inline [disponible desde C99], que le indica al compilador cómo optimizar la inserción del código.

Si mantienes las definiciones en sus archivos .c correspondientes, el enlazador podrá hacer su trabajo sin conflictos.

10

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio