Un solo archivo .c está bien hasta que tiene 2.000 líneas. Repartir un programa entre archivos permite compilar cada parte por separado, reutilizarla en otros programas y leerla de forma aislada, pero C no tiene sistema de importación. Lo que tiene es el preprocesador pegando texto, más un enlazador que junta las piezas al final.
Un archivo de cabecera (.h) es el contrato compartido entre esas piezas: le dice a cada archivo fuente qué existe en otro lado, sin contener la implementación.
Declaraciones frente a definiciones
Todo el diseño descansa sobre una distinción.
Una declaración dice esto existe en alguna parte y esta es su forma. No genera código y puede aparecer cuantas veces haga falta:
int add(int a, int b); /* declaracion de funcion (prototipo) */
extern int error_count; /* declaracion de variable */
struct Point { int x, y; }; /* definicion de tipo: se puede repetir por archivo */
Una definición crea la cosa. Debe aparecer exactamente una vez en todo el programa:
int add(int a, int b) { return a + b; } /* definicion de funcion */
int error_count = 0; /* definicion de variable */
Las cabeceras llevan declaraciones. Los archivos fuente llevan definiciones. Invierte eso y el enlazador se queja con "multiple definition of ...", el único mensaje de error que significa de forma fiable que una definición se coló en una cabecera.
Un programa de dos archivos
Aquí está la división útil más pequeña. Una cabecera que declara dos funciones:
/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int max_of(int a, int b);
#endif
El archivo fuente que las implementa; fíjate en que incluye su propia cabecera:
/* math_utils.c */
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int max_of(int a, int b) {
return (a > b) ? a : b;
}
Y el programa que las usa:
/* main.c */
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("add(3, 4) = %d\n", add(3, 4));
printf("max_of(3, 4) = %d\n", max_of(3, 4));
return 0;
}
Compila ambos archivos fuente juntos:
gcc main.c math_utils.c -o app
./app
Hay dos detalles que vale la pena notar. Primero, math_utils.c incluye su propia cabecera, y eso no es redundante: hace que el compilador compruebe que cada definición coincide con su declaración, así que si cambias el prototipo de la cabecera y te olvidas del archivo .c, obtienes un error de inmediato en lugar de un desajuste al enlazar.
Segundo, math_utils.h no está en la línea de comandos de gcc. Las cabeceras nunca se compilan; #include las pega dentro de los archivos .c. Pasarle un .h al compilador produce un archivo de cabecera precompilada suelto y ningún código enlazable.
El mismo programa comprimido en un archivo, para que puedas ejecutarlo aquí:
Las declaraciones que van antes de main son lo que aporta la cabecera en la versión real, que es justamente por lo que los prototipos de funciones y las cabeceras son la misma idea a dos escalas.
Guardianes de inclusión
#include pega texto, y el texto pegado dos veces es texto duplicado. Eso es inofensivo para el prototipo de una función y fatal para un struct:
/* shapes.h SIN guardian */
struct Point { int x, y; };
Si main.c incluye tanto shapes.h como canvas.h, y canvas.h también incluye shapes.h, el compilador ve struct Point definido dos veces en una unidad de traducción y se detiene con "redefinition of 'struct Point'". En un proyecto real estas cadenas se vuelven lo bastante profundas como para que no puedas seguirlas a mano.
La solución es un guardián de inclusión: una macro que registra "esta cabecera ya se pegó".
/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H
struct Point { int x, y; };
struct Point origin_point(void);
#endif /* SHAPES_H */
La primera inclusión encuentra SHAPES_H sin definir, así que el cuerpo se conserva, y define SHAPES_H de paso. Cualquier inclusión posterior en el mismo archivo lo encuentra definido y salta directo al #endif. El nombre de la macro debe ser único en todo el proyecto; NOMBREARCHIVO_H derivado de la ruta es la convención habitual.
La alternativa de una línea la admiten todos los compiladores importantes:
/* shapes.h */
#pragma once
struct Point { int x, y; };
#pragma once no puede sufrir una colisión de nombres ni romperse por una errata en el #endif. Su único inconveniente es que no está en el estándar de C, así que un proyecto que deba compilar en compiladores poco comunes debería preferir la forma #ifndef. Sea cual sea, toda cabecera lleva uno, sin excepciones, incluidas las cabeceras que crees que nadie más va a incluir.
Qué va en una cabecera
Pon en un .h:
- Prototipos de funciones
- Definiciones de
struct,unionyenum - Declaraciones
typedef - Macros pensadas para compartirse
- Declaraciones
externde variables globales compartidas - Los
#includeque esa cabecera necesita para bastarse a sí misma
Mantén fuera de un .h:
- Cuerpos de funciones (salvo que sean
static inlinea propósito) - Definiciones de variables:
int counter;en una cabecera define una variable distinta en cada archivo que la incluya, o un error de enlace, según el compilador #includede cabeceras que la propia cabecera no necesita: eso le empuja esa dependencia a todo el que venga después
Que "se baste a sí misma" merece ser una regla: una cabecera debería compilar si se incluye primero, antes que nada más. Si shapes.h usa size_t, incluye <stddef.h> ella misma en lugar de confiar en que lo haya hecho el archivo que la incluye.
Una cabecera completa y bien formada:
/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H
#include <stddef.h> /* para size_t, usado abajo */
#define MAX_NAME 64
typedef struct {
char name[MAX_NAME];
int quantity;
double price;
} Item;
/* compartida en todo el programa, definida una vez en inventory.c */
extern int item_count;
void inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);
#endif /* INVENTORY_H */
Compartir una global con extern
Una variable global debe estar definida en exactamente un archivo .c y declarada en todos los demás. extern es lo que hace la declaración:
/* inventory.h - declaracion, sin almacenamiento */
extern int item_count;
/* inventory.c - la unica definicion */
#include "inventory.h"
int item_count = 0;
/* main.c - la usa, a traves de la cabecera */
#include <stdio.h>
#include "inventory.h"
int main(void) {
printf("%d articulos\n", item_count);
return 0;
}
Quita el extern de la cabecera y cada archivo que la incluya define su propio item_count, lo que en el mejor caso es un error de enlace por "multiple definition" y en el peor son dos contadores independientes.
La necesidad opuesta es igual de común: una variable o una función auxiliar que debería quedarse privada dentro de un archivo .c. static en el ámbito del archivo hace eso: le da al nombre enlace interno, invisible para el enlazador y por tanto para todos los demás archivos.
/* inventory.c */
static Item storage[256]; /* privado de este archivo */
static int find_slot(const char *name); /* auxiliar privado */
Dos archivos pueden tener cada uno un static int counter; sin colisión. Esta es la versión en C de un miembro privado, y es la opción por defecto a la que deberías recurrir: en la cabecera solo va lo que los otros archivos genuinamente necesitan.
Compilar programas más grandes
Listar cada archivo funciona y es lento, porque cada archivo se recompila cada vez:
gcc main.c inventory.c report.c -o app
La forma que escala compila cada fuente a un archivo objeto y los enlaza:
gcc -c main.c # produce main.o
gcc -c inventory.c # produce inventory.o
gcc -c report.c # produce report.o
gcc main.o inventory.o report.o -o app
Ahora cambiar report.c solo necesita gcc -c report.c y volver a enlazar. Esa es exactamente la contabilidad que automatiza make:
app: main.o inventory.o report.o
gcc main.o inventory.o report.o -o app
%.o: %.c
gcc -Wall -Wextra -c $< -o $@
Si tus cabeceras viven en un subdirectorio, -Iinclude lo añade a la ruta de búsqueda de los ángulos.
Errores y qué significan
Los dos modos de fallo son fáciles de distinguir una vez que sabes qué etapa los produjo.
"undefined reference to 'add'": un error del enlazador. La declaración se encontró, la definición no. O te olvidaste de listar el archivo .c en la línea de comandos, o la función es static, o el nombre está mal escrito en uno de los dos sitios.
"multiple definition of 'item_count'": también un error del enlazador, la imagen especular: una definición terminó en una cabecera, o en dos archivos fuente. Muévela a un solo .c y deja una declaración extern en la cabecera.
"redefinition of 'struct Item'": un error del compilador, que significa que una cabecera se pegó dos veces en un mismo archivo. Agrega el guardián de inclusión.
"implicit declaration of function 'add'": un aviso del compilador (un error en C99 y modos posteriores) que significa que el prototipo nunca se vio. Olvidaste el #include, o la cabecera no la declara.
Una vez que un programa abarca varios archivos, lo siguiente que sueles querer es variar qué se compila según la plataforma o el tipo de compilación, que es la compilación condicional.
Preguntas frecuentes
¿Qué va en un archivo .h y qué va en un archivo .c?
La cabecera lleva declaraciones: prototipos de funciones, definiciones de struct y typedef, enum, macros y declaraciones extern de globales compartidas. El archivo .c lleva definiciones: los cuerpos de las funciones y las variables reales. La regla práctica: una cabecera dice qué existe, un archivo fuente dice qué hace.
¿Qué es un guardián de inclusión y por qué lo necesito?
#include pega texto, así que incluir una cabecera dos veces pega su contenido dos veces, lo que redefine cada struct y typedef que contenga y no compila. Un guardián de inclusión envuelve la cabecera en #ifndef MICABECERA_H / #define MICABECERA_H / #endif, así que la segunda inclusión ve la macro ya definida y se salta el cuerpo.
¿Cómo compilo un programa de C con varios archivos?
Lista cada archivo .c en la línea de comandos: gcc main.c math_utils.c -o app. Nunca pongas ahí un archivo .h: las cabeceras las pega #include, no se compilan por su cuenta. Para proyectos más grandes, compila a archivos objeto (gcc -c main.c) y enlázalos, que es lo que automatiza un Makefile.
¿Uso #pragma once o guardianes con #ifndef?
Ambos funcionan. #pragma once es una sola línea, no puede sufrir colisiones de nombre y lo admiten todos los compiladores importantes, pero no está en el estándar de C. La forma #ifndef/#define/#endif es estándar y funciona en todas partes. Elige una y úsala de forma consistente en un proyecto; para máxima portabilidad, la de #ifndef.