Introducción

A la primera vista uno diría que no es posible programar orientado a objetos con el lenguaje de programación C porque no tiene clases. Esto es parcialmente correcto, ya que clases son un componente importante para programar orientado a objetos. El lenguaje C, en cambio, fue diseñado para la programación estructurada. Sin embargo, el lenguaje C permite definir objetos.

Programar orientado a objetos es un paradigma, es decir, una forma de diseñar código. Hay lenguajes como C++ o Java que están diseñados para este paradigma, pero no se programa orientado a objetos meramente por utilizar a uno de estos lenguajes. Por ejemplo, si todos los miembros de una clase son públicos y estáticos, entonces equivalen a variables y funciones globales con el prefijo «NombreDeClase::». (Prefijos pueden reemplazar espacio de nombres.)

En cambio, se puede aplicar el paradigma de orientado a objetos a lenguajes de programación no diseñados para esto, aunque será lógicamente con complicaciones. Como resolver estas complicaciones para el lenguaje de programación C es el tema de este artículo.

Definición de clases

Los datos de una clase se pueden guardar en una estructura. De hecho, en C++ un struct no es otra cosa que una clase con todos los miembros públicos. Como C no permite métodos, usamos funciones globales. El equivalente de C++

class MiClase
{
public:
	int miDato;
	void hazAlgo(void)
	{
	    miDato = 5;
	}
};

en C sería

struct MiClase
{
	int miDato;
}

void hazAlgo(MiClase *const miInstancia)
{
    miInstancia->miDato = 5;
}

Nótese que el primer parámetro de la función en C tiene el mismo tipo que el puntero this en C++.

El uso de estas clases sería en C++

MiClase miInstancia;
miInstancia->hazAlgo();

y en C

MiClase miInstancia;
MiClase_hazAlgo(&miInstancia);

Mediante punteros a funciones y macros es posible que se puede construir el aspecto de llamadas a métodos de C++ como en

miInstancia->miMetodo();

Sin embargo, no lo vamos a discutir en este artículo. Se trata meramente de un aspecto estético que, en mi opinión, más confunde que aclara. El lenguaje C no es C++ y programación orientado a objetos no deja de serlo sólo por ordenar de otra forma los componentes de una llamada a una función.

Estructuras opacas

Es posible declarar y utilizar un tipo de puntero a una estructura sin definir la estructura misma. Con este mecanismo se pueden crear estructuras opacas.

En el fichero de cabecera (el .h) defino un tipo de puntero a una estructura

typedef struct MiStruct* MiClase;

La definición de la estructura estará dentro de un fichero de implementación (*.c).

struct MiStruct
{
    int miDato;
};

El interfaz en el fichero de cabecera define un puntero, que es un tipo de un tamaño conocido. Por eso el compilador puede compilar un programa que usa un puntero aunque el tamaño del elemento a que apunta es desconocido. El usuario de MiClase accede a la biblioteca meramente con el puntero.

Este encapsulamiento de datos es incluso mejor que el de C++ ya se define la estructura de datos en el fichero de definición. Este fichero no tiene por qué ser visible para el usuario de la estructura opaca, ya que se puede meramente publicar los ficheros binarios correspondientes. En otras palabras, se puede esconder por completo la estructura de datos. Esto no es posible con una clase en C++, ya que la declaración de la clase en el fichero de cabecera debe contenedor todos los campos de datos.

Herencia

No existe un mecanismo de clases derivadas en C, sin embargo esto se puede simular. El método de una clase/estructura, que hereda de otra, puede convertir el puntero de su estructura a una estructura de la clase base y llamar con ello los métodos de esta. Esto corresponde a una herencia privada. Para esto hace falta que la primera parte de la estructura derivada contiene en el inicio los mismos campos como la estructura base.

Para hacer una herencia pública, el usuario debe convertir el puntero en su llamada.

MiClaseDerivada miInstancia;
MiClaseBase_HazAlgo((MiClaseBase) miInstancia);

Obviamente esta construcción es aún menos práctica ya que es el usuario debe pensar si llama a una función de la clase base o de la clase derivada.

La herencia complica más en el lenguaje C que ayuda. Por esto vale la pena tener en cuenta una alternativa: usar una clase en lugar de heredar de ella.

Identificadores en lugar de punteros

Una manera potencialmente más potente para la programación orientado a objetos es abstraer de los punteros y referenciar objetos nada más que por un identificador numérico. Cada método de una clase debe primero comprobar si el identificador numérico corresponde a su clase correspondiente. Esto es más complejo pero una buena alternativa cuando se quieren manejar muchas clases diferentes en C. De hecho, muchos lenguajes modernos como Java, C# o PHP devuelven identificadores y no punteros a los objetos, lo cual les permite reordenador los objetos en la memoria si hace falta. Sólo el gestor de objetos sabe bajo qué dirección de memoria se encuentra qué objeto.

Un camino intermedio y buen compromiso es exportar un tipo de identificador diferente para cada clase. Entonces la clase gestiona nada más que sus propias instancias.

Resumen

Hemos visto que se pueden crear «clases» y «objetos» con el lenguaje de programación C. Referenciar objetos por punteros a estructuras opacas o mediante identificadores es una manera potente para el encapsular datos.

Por no ser un lenguaje diseñado para la programación orientado a objetos, el uso de este parádigma a veces no queda muy elegante en el código y tampoco es posible aprovechar la potencia de la herencia sin trucos adicionales.

No obstante, la programación orientado a objetos es posible con el lenguaje C y debería tenerse en cuenta a la hora de realizar proyectos grandes. De hecho hay unas bibliotecas que se han creado de esta forma como el servidor X o la biblioteca GObject.

Referencias