Para entender cómo se construye un programa en C, debes entender que el proceso no es un bloque monolítico, sino un diálogo entre dos herramientas: el compilador y el enlazador (linker).
Una declaración es una promesa que le haces al compilador. Le estás diciendo: “Confía en mí, existe un objeto llamado x que es de tipo int, aunque ahora mismo no sepas dónde está o cuánto mide”. La declaración le proporciona al compilador el nombre y el tipo para que pueda verificar que el uso que le das en el código sea semánticamente correcto (por ejemplo, que no intentes sumar una cadena a un entero).
Una definición, en cambio, es el cumplimiento de esa promesa. Es el momento en que le dices al compilador: “Aquí está el objeto; reserva el espacio de memoria necesario para él” o “Aquí está la implementación real de esta función”.
Este mecanismo existe para permitir la compilación separada. Si el compilador necesitara ver todo el código de todas las librerías cada vez que compilas un archivo .c, los tiempos de espera serían infinitos. Gracias a las declaraciones, puedes compilar un archivo usando solo las “promesas” (prototipos en archivos .h) y dejar que el enlazador, en el último paso, conecte todas las definiciones reales.
Debes usar declaraciones cada vez que necesites usar un tipo, variable o función que no esté definida en el mismo archivo donde la estás invocando, como en las dependencias circulares entre estructuras. Si intentas definir lo mismo dos veces (por ejemplo, definir la misma variable global en dos archivos distintos que luego quieres unir), romperás la Regla de una Única Definición (One Definition Rule o ODR). Esto resultará en un error de enlazado de tipo multiple definition o, en el peor de los casos, el programa compilará pero se comportará de forma errática porque el enlazador elegirá una definición al azar, ignorando la otra de manera silenciosa.
#include <stdio.h>
/* 1. DECLARACIÓN (Forward Declaration de un tipo)
Le decimos al compilador que existe un 'struct Sensor'.
No sabemos qué contiene, pero sabemos que el nombre es 'struct Sensor'.
Esto permite que funciones que usan punteros a este struct se declaren
antes de que el struct sea definido por completo. */
struct Sensor;
/* 2. DECLARACIÓN (Prototipo de función)
Le informamos al compilador que existe una función llamada 'leer_valor'.
Sabe que recibe un puntero a 'struct Sensor' y devuelve un 'int'.
El compilador puede verificar que los tipos coincidan en el 'main'. */
int leer_valor(struct Sensor *s);
/* 3. DECLARACIÓN (Variable externa)
'extern' indica que la variable 'id_sistema' existe, pero su memoria
no está reservada aquí. Se definirá en otro lugar (en este caso, más abajo). */
extern int id_sistema;
/* 4. DEFINICIÓN (Definición de tipo)
Aquí finalmente definimos qué hay dentro de 'struct Sensor'.
Ahora el compilador ya sabe que ocupa un tamaño determinado en memoria. */
struct Sensor {
int id;
float temperatura;
};
/* 5. DEFINICIÓN (Definición de variable)
Aquí se reserva el espacio en la sección de datos del binario. */
int id_sistema = 1024;
/* 6. DEFINICIÓN (Definición de función)
Aquí es donde reside el cuerpo de la función. */
int leer_valor(struct Sensor *s) {
/* El compilador ya sabe qué es 's->temperatura' gracias a la definición anterior. */
return (int)s->temperatura;
}
int main(void) {
/* Definición de una variable local de tipo 'struct Sensor' */
struct Sensor termometro = {1, 26.5f};
/* Usamos la variable externa 'id_sistema' y la función declarada */
printf("Sistema ID: %d\n", id_sistema);
printf("Lectura: %d\n", leer_valor(&termometro));
return 0;
}
Desglose del ejemplo
Fíjate en cómo el flujo de información permite que el main sea capaz de compilar correctamente:
- En la línea
int leer_valor(struct Sensor *s);, estamos usando una declaración de función. Si intentáramos llamar aleer_valor(&termometro)en elmainsin haber escrito esa línea (o una similar), el compilador daría un error porque no sabría si el argumento es un puntero o un entero. - La variable
id_sistemaes el ejemplo perfecto de la distinción. En la líneaextern int id_sistema;le pedimos permiso al compilador para usarla. Pero es en la líneaint id_sistema = 1024;donde realmente se asigna un espacio en la memoria de nuestro programa. - La forward declaration de
struct Sensores una técnica vital en sistemas complejos. Nos permite definir estructuras que se apuntan entre sí (circularmente) sin que el compilador se pierda, ya que un puntero tiene siempre un tamaño fijo, independientemente de lo que contenga la estructura a la que apunta.
El error frecuente
Un error clásico de principiante ocurre cuando intentas “compartir” variables globales incluyendo archivos .c en lugar de .h, o definiendo variables directamente en un archivo de cabecera (.h).
// archivo_compartido.h int contador_global; // ERROR: Esto es una DEFINICIÓN
Si incluyes archivo_compartido.h en main.c y también en log.c, cuando el enlazador intente unir ambos archivos, encontrará que contador_global ha sido definida dos veces. El compilador no te avisará; el error aparecerá en la fase de enlace (linking error).
La forma correcta para un archivo de cabecera es usar la palabra clave extern, convirtiéndola en una declaración:
// archivo_compartido.h extern int contador_global; // CORRECTO: Es solo una promesa
Si utilizas herramientas como AddressSanitizer o Valgrind, no verás errores de este tipo, ya que esto no es un error de memoria (como un buffer overflow), sino un error de estructura del programa que impide la creación del ejecutable.
N° 11