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
- Un artículo sobre trucos avanzados para la programación orientado a objetos (en inglés)
- Un libro en formato PDF sobre programación orientada a objetos con ANSI-C (en inglés)
- Paradigmas de programación en Wikipedia
- Programación orientado a objetos
- Programación estructurada
- El manual de referencia de GObject dentro de la biblioteca de GNOME es un ejemplo de un biblioteca C programado orientado a objetos.
- La página de X Windows
8 comentarios
Comments feed for this article
25 May 2014 a 03:44
No where man
¡Wow! muy bueno el truco. Me ahorraste 1 mes de estudio para hacer eso. Mil gracias.
25 May 2014 a 20:26
trucosinformaticos
De nada y gracias a ti por el piropo. Me alegro que te haya servido, porque para esto está.
26 febrero 2015 a 17:37
Luis Ángel
Es decir que para poder usar métodos de un objeto ( o simularlo) es necesario pasarle siempre la instancia ?
Si bien lo haces con esta función:
void hazAlgo(MiClase *const miInstancia)
{
miInstancia->miDato = 5;
}
Si yo quisiera hacer otra funcion sería muy similar:
void hazAlgo2(MiClase *const miInstancia , int multi)
{
miInstancia->miDato = (5 * multi);
}
27 febrero 2015 a 21:10
trucosinformaticos
Sí, es correcto lo que dices.
Tu función aplicas a datos. El puntero dice, donde están estos datos. Este puntero existe también en C++ – el puntero
this
. La diferencia es que en C++ no hace falta escribirlo. Cuando escribes el nombre de una instancia, el compilador aplica este punterothis
.Pon atención, si usas funciones como
miInstancia->(*miFuncion)(algo)
. Para poder hacer esto, debes definir un miembromiFuncion
en la estructura correspondiente. Este miembro tiene el tipo de una función y lo debes inicializar antes de usarlo.27 febrero 2015 a 22:02
Luis Ángel
Interesante! Eso que dices entonces es trabajar con punteros a funciones ? Porque de ser como entiendo, entonces lo haré. Puesto que como talvez te diste cuenta, lo que quiero es simular OOP en C.
Con tu ayuda y https://stackoverflow.com/questions/840501/how-do-function-pointers-in-c-work
Seguramente me estoy complicando para una simple tarea pero como uno viene acostumbrado a programar orientado a objetos quiero tratar de mantener la misma lógica. Aunque C no se de tan fácilmente para tales objetivos, me suena interesante poder hacerlo y por lo tanto me veo tentado.
3 marzo 2015 a 19:08
trucosinformaticos
Probarlo seguramente es una experiencia. Pero tendrás que pensar bien como lo haces. Los métodos normales tienen enlace estático, es decir, inicializas los punteros a las funciones antes de ejecutar el main. Los métodos virtuales se inicializan a tiempo de creación de una instancia con a base de una tabla de métodos virtuales, que se inicializa a tiempo de compilación (así lo hace el compilador de C++ con el vftable (vf = virtual function).
Puede ser que esto pide demasiado al usuario de tus clases. No lo sé. Lo que, sí, sé, es que todas las bibliotecas de C pasan la instancia como parámetro – como el servidor X, que toma una referencia a una ventana. A lo mejor es lo que resultó más práctico. Aún así sigue siendo orientado a objetos.
Te deseo suerte con tu proyecto.
14 agosto 2016 a 09:44
Orly
Muy bueno el tip, pero creo también lo haces con apuntadores o creo estoy mal. Es genial C
15 agosto 2016 a 13:54
trucosinformaticos
En C++ existe el apuntador
this
que apunta a una instancia de una clase. Como este no existe en C, se debe manejarlo manualmente. Es este la base de programación orientado a objetos en C: todos los métodos tienen un apuntador como parámetro que representa el apuntadorthis
.