En C, la encapsulación no se logra mediante clases o modificadores private o public como en C++. En su lugar, utilizamos el enlace interno (internal linkage) y la declaración anticipada (forward declaration) para controlar qué partes del código pueden ver qué datos o funciones.
Para entender esto, debemos hablar de la unidad de traducción, que es cada archivo .c compilado individualmente junto con sus cabeceras. El modificador static aplicado a una función o variable limita su visibilidad exclusivamente a esa unidad de traducción; es decir, el enlazador (linker) no verá esos símbolos al intentar unir los archivos, evitando colisiones de nombres y permitiendo que existan funciones “privadas”.
Por otro lado, los tipos opacos (opaque types) son la herramienta definitiva para ocultar la implementación. Consiste en declarar un struct en el archivo .h mediante una declaración anticipada (por ejemplo, typedef struct Sensor Sensor;) sin definir sus campos. Al hacer esto, el usuario del módulo sabe que el tipo Sensor existe, pero el compilador no conoce su tamaño ni su estructura. Esto obliga a que toda manipulación del objeto se haga a través de punteros (Sensor*) y funciones específicas. La gran ventaja es la estabilidad de la ABI (Application Binary Interface): puedes añadir o cambiar campos en el struct dentro del .c sin que el código del cliente necesite ser recompilado, ya que el tamaño del puntero no cambia.
Sin embargo, esto introduce una restricción: el cliente no puede declarar variables de tipo Sensor directamente en el stack (ej. Sensor s;), porque el compilador no sabe cuánto espacio reservar. Solo puede trabajar con punteros, lo que generalmente implica el uso de memoria dinámica (malloc).
Finalmente, para que todo esto funcione sin errores de enlazado, debemos respetar la regla de una definición (ODR, One Definition Rule). En C, cada objeto (variable global) o función no inline debe tener exactamente una definición en todo el programa. Si defines una variable en un cabecera sin usar static o extern, y ese cabecera se incluye en dos archivos .c distintos, el enlazador encontrará dos definiciones para el mismo símbolo y lanzará un error.
Para implementar un módulo profesional, se requiere el uso de punteros para mantener el tipo opaco y static para proteger la lógica interna.
#include <stdio.h>
#include <stdlib.h>
/* --- MÓDULO SENSOR (Simulado como si fuera un .h) --- */
// Declaración anticipada: El cliente sabe que 'Sensor' existe,
// pero no sabe qué contiene. Esto es un TIPO OPACO.
typedef struct Sensor Sensor;
// Interfaz pública: Solo punteros a Sensor.
Sensor* sensor_create(int id);
void sensor_update(Sensor* s, float valor);
void sensor_print(const Sensor* s);
void sensor_destroy(Sensor* s);
/* --- IMPLEMENTACIÓN DEL MÓDULO (Simulado como si fuera un .c) --- */
// La definición real del struct está aquí, oculta del cliente.
struct Sensor {
int id;
float valor;
float limite_seguridad;
};
// 'static' limita esta variable a esta unidad de traducción.
// Es una variable "privada" del módulo.
static int total_sensores_activos = 0;
Sensor* sensor_create(int id) {
Sensor* s = malloc(sizeof(Sensor));
if (!s) {
return NULL;
}
s->id = id;
s->valor = 0.0f;
s->limite_seguridad = 100.0f;
total_sensores_activos++;
return s;
}
void sensor_update(Sensor* s, float valor) {
if (s) {
s->valor = valor;
if (s->valor > s->limite_seguridad) {
printf("[LOG] Alerta: Sensor %d excedió límite!\n", s->id);
}
}
}
void sensor_print(const Sensor* s) {
if (s) {
printf("ID: %d | Valor: %.2f\n", s->id, s->valor);
}
}
void sensor_destroy(Sensor* s) {
if (s) {
free(s);
total_sensores_activos--;
}
}
/* --- CLIENTE (main.c) --- */
int main(void) {
// El cliente puede crear el sensor porque el módulo provee la fábrica.
Sensor* mi_sensor = sensor_create(101);
if (!mi_sensor) {
return 1;
}
// El cliente interactúa mediante la interfaz pública.
sensor_update(mi_sensor, 45.5f);
sensor_print(mi_sensor);
// Prueba de límite
sensor_update(mi_sensor, 105.0f);
// ERROR DE COMPILACIÓN:
// Si intentamos hacer esto, el compilador fallará porque
// no conoce el contenido de 'struct Sensor' en esta unidad.
// mi_sensor->id = 500;
// Limpieza
sensor_destroy(mi_sensor);
// Nota: No podemos acceder a 'total_sensores_activos' aquí;
// es una variable estática y privada del módulo.
return 0;
}
Análisis del código
- Encapsulación mediante tipos opacos: En
main, intentamos acceder ami_sensor->id. Esto fallaría en un proyecto real dividido en archivos, porque el compilador, al procesarmain.c, solo ve la declaracióntypedef struct Sensor Sensor;y desconoce el tamaño de la estructura. Por tanto, el cliente está obligado a usar la interfaz de funciones (sensor_update,sensor_print). - Gestión de memoria: Debido a que el cliente no conoce el tamaño de
struct Sensor, la funciónsensor_createdebe realizar la asignación de memoria conmalloc(sizeof(Sensor)). El cliente solo recibe un puntero, cuyo tamaño es fijo (típicamente 8 bytes en sistemas de 64 bits), independientemente de si la estructura interna crece o se reduce. - Visibilidad interna: La variable
total_sensores_activosestá marcada comostatic. Esto significa que si tuvieras otro archivo.cen el mismo proyecto que intentara acceder a ella, el enlazador diría que el símbolo no existe. Esto evita colisiones si otro módulo también tuviera una variable llamada igual. - Ciclo de vida: Como el objeto se creó dinámicamente para mantener la opacidad, es responsabilidad del cliente llamar a
sensor_destroypara liberar la memoria, evitando fugas (memory leaks).
El error frecuente
Un error clásico que rompe la regla de una definición (ODR) es declarar y definir una variable en un cabecera para que sea “global”:
// sensor.h int sensor_contador = 0; // ¡ERROR! Definición de variable en cabecera.
Si incluyes sensor.h en archivo_a.c y en archivo_b.c, el compilador generará una instancia de sensor_contador en cada objeto. Al intentar enlazarlos, el linker encontrará dos símbolos con el mismo nombre y detendrá el proceso con un error de “multiple definition”.
La solución correcta:
Si necesitas una variable global accesible desde múltiples archivos, usa extern en el cabecera para declararla y defínela en un único archivo .c:
// sensor.h extern int sensor_contador; // Declaración: "existe en algún lugar" // sensor.c int sensor_contador = 0; // Definición: "aquí es donde reside la memoria"
Si la variable solo debe ser usada internamente en ese módulo, usa static en el .c. Si quieres que sea una constante compartida, usa extern const o un #define.
N° 92