OOP in C: programación orientada a objetos en C
A diferencia de los lenguajes OOP C++ y Objective-C, no existe programación orientada a objetos en C. Debido al uso generalizado de este lenguaje y a la popularidad de la programación orientada a objetos, existen enfoques para utilizar OOP en C.
OOP in C: ¿es realmente posible?
El lenguaje de programación con C no está pensado para la programación orientada a objetos. Este lenguaje es un excelente ejemplo del estilo de programación estructurada según la programación imperativa Sin embargo, es posible emular enfoques orientados a objetos en C. Este lenguaje de programación contiene todos los componentes necesarios para ello y sirvió, por ejemplo, de base para la programación orientada a objetos en Python.
La OOP te permite definir tus propios “tipos de datos abstractos” (TDA). Un TDA puede considerarse como un conjunto de valores posibles y funciones que operan sobre ellos. Es importante que la interfaz visible externamente y la implementación interna estén desacopladas entre sí. De este modo, tú como usuario puedes confiar en que los objetos se comportan de acuerdo con su descripción.
Los lenguajes orientados a objetos, como Python, Java y C++, utilizan el concepto de “clase” para modelar tipos de datos abstractos. Las clases sirven de plantilla para crear objetos similares; lo que se conoce como instanciación. Intrínsecamente, C no reconoce clases, y estas no pueden modelarse dentro del lenguaje. En cambio, hay varios enfoques para implementar las características de OOP en C.
¿Cómo funciona la programación orientada a objetos en C?
Para entender cómo funciona la programación orientada a objetos en C, primero hay que hacerse la pregunta: ¿qué es exactamente la programación orientada a objetos (OOP)? La programación orientada a objetos es un estilo de programación muy extendido, manifestación del paradigma de programación imperativo. Se puede distinguir la programación orientada a objetos de la programación declarativa y su especialización, la programación funcional.
La idea básica de la programación orientada a objetos es modelar objetos y dejar que interactúen entre sí. El flujo del programa resulta de las interacciones de los objetos y, por tanto, solo se fija en tiempo de ejecución. En esencia, la OOP comprende solo tres propiedades:
- Los objetos encapsulan su estado interno.
- Los objetos reciben mensajes a través de sus métodos.
- Los métodos se asignan dinámicamente en tiempo de ejecución.
Un objeto en un lenguaje de programación orientada a objetos puro como Java es una unidad autocontenida. Comprende una estructura de datos de cualquier complejidad, así como métodos (funciones) que operan sobre ella. El estado interno del objeto, representado en los datos que contiene, solo puede leerse y modificarse usando estos métodos. Para la gestión de la memoria de los objetos se suele utilizar una función del lenguaje llamada “Garbage Collector”.
En C, no es fácil vincular estructuras de datos y funciones a objetos. En su lugar, se teje un sistema manejable de estructuras de datos, definiciones de tipos, punteros y funciones. Como es habitual en C, quien programa es responsable de la correcta asignación y liberación de memoria.
El código C basado en objetos resultante no se parece mucho a lo que se está acostumbrado en los lenguajes OOP, pero funciona. A continuación, se ofrece una visión general de los conceptos centrales de la programación orientada a objetos, junto con su equivalente en C:
Concepto OOP | Correspondencia en C |
---|---|
Clase | Tipo de estructura |
Instancia de clase | Ejemplo de estructura |
Método de instancia | Función que acepta punteros a variables struct |
Variable this/self | Puntero a variable struct |
Instanciación | Asignación y referencia mediante puntero |
Nueva palabra clave | Activar la función malloc |
Modelar objetos como estructuras de datos
Descubre primero cómo puede modelarse en C la estructura de datos de un objeto al estilo de los lenguajes de programación orientada a objetos. C es un lenguaje compacto que funciona con pocas construcciones lingüísticas. Para crear estructuras de datos arbitrariamente complejas, se utilizan los llamados “structs”, cuyo nombre deriva del término “estructura de datos”, o “Data Structure” en inglés.
Una struct-C define una estructura de datos que incluye campos llamados “miembros”. En otros lenguajes, una construcción de este tipo también se denomina “registro”, por lo que bien se puede imaginar una struct como la fila de una tabla de una base de datos: un compuesto de varios campos, posiblemente de distintos tipos.
La sintaxis de una declaración struct en C es muy sencilla:
struct struct_name;
COpcionalmente, también se puede definir la struct especificando los miembros con nombre y tipo. Como ejemplo estándar, se considera un punto en un espacio bidimensional con coordenadas x e y. Se muestra la definición de struct:
struct point {
/*X-coordinate*/
int x;
/*Y-coordinate*/
int y;
};
CEn el código C convencional, esto va seguido de la instanciación de una variable struct. Se crea la variable y se inicializan ambos campos con 0:
struct point origin = {0, 0};
CAsí se pueden leer los valores de los campos y restablecerlos. El acceso a los miembros se realiza mediante la conocida sintaxis origin.x y origin.y también presente en otros lenguajes:
/*Read struct member*/
origin.x == 0
/*Assign struct member*/
origin.y = 42
CSin embargo, esto viola el requisito de encapsulación: solo se puede acceder al estado interno de un objeto mediante métodos definidos a tal efecto. Así que a nuestro planteamiento le sigue faltando algo.
Definir tipos para crear objetos
Como se ha dicho, C no reconoce el concepto de clase. En su lugar, los tipos pueden definirse con la sentencia typedef. Con typedef se da un nuevo nombre a un tipo de datos:
typedef <old-type-name> <new-type-name>
CDe este modo, se puede definir un tipo de punto correspondiente para nuestra struct de puntos:
typedef struct point Point;
CLa combinación de typedef con una definición struct corresponde aproximadamente a una definición de clase en Java:
typedef struct point {
/*X-coordinate*/
int x;
/*Y-coordinate*/
int y;
} Point;
CEn el ejemplo, “point” es el nombre de la struct, mientras que “Point” es el nombre del tipo definido.
Aquí tienes la definición de clase correspondiente en Java:
class Point {
private int x;
private int y;
};
JavaUtilizar typedef nos permite crear una variable Point sin utilizar la palabra clave struct:
Point origin = {0, 0}
/*Instead of*/
struct point origin = {0, 0}
CLo que sigue faltando es la encapsulación del estado interno.
Encapsulación del estado interno
Los objetos mapean su estado interno en su struct de datos. En los lenguajes de programación orientada a objetos, como Java, las palabras clave “private”, “protected”, etc., se utilizan para restringir el acceso a los datos de los objetos. Esto impide el acceso directo desde el exterior y garantiza la separación entre interfaz e implementación.
Para realizar la OOP en C, se utiliza un mecanismo diferente. Se utiliza como interfaz una declaración de avance en el archivo de cabecera y se crea así un “Incomplete type”:
/*In C header file*/
struct point;
/*Incomplete type*/
typedef struct point Point;
CLa implementación de point-struct sigue en un archivo de código fuente C independiente, que incrusta la cabecera mediante la macro include. Este enfoque evita la creación de variables estáticas de tipo Point. Sigue siendo posible utilizar punteros de tipo. Como los objetos son estructuras de datos creadas dinámicamente, se referencian con punteros de todos modos. Los punteros a instancias de struct se corresponden aproximadamente con las referencias a objetos utilizadas en Java.
Sustituye los métodos por funciones
En lenguajes de programación orientada a objetos, como Java y Python, los objetos incluyen, además de sus datos, las funciones que operan sobre ellos. Se llaman métodos o métodos de instancia. Cuando se escribe un código de OOP en C, en lugar de métodos se utilizan funciones que toman un puntero a una instancia de struct:
/*Pointer to `Point` struct*/
Point * point;
CComo C no reconoce clases, no es posible agrupar las funciones pertenecientes a un tipo bajo un nombre común. En su lugar, se proporcionan los nombres de las funciones con un prefijo que contiene el nombre del tipo. Las firmas de función correspondientes se declaran primero en el archivo header de C:
/*In C header file*/
/*Function to move update a point's coordinates*/
void Point_move(Point * point, int new_x, int new_y);
CA continuación, se implementa la función en el archivo de código fuente C:
/*In C source file*/
void Point_move(Point * point, int new_x, int new_y) {
point->x = new_x;
point->y = new_y;
};
CEste enfoque recuerda a los métodos de Python, que son funciones normales que toman self como primer parámetro. Además, el puntero a una instancia de struct se corresponde aproximadamente con la variable this en Java o JavaScript. La diferencia es que cuando se llama a la función C, el puntero se da explícitamente:
/*Call function with pointer argument*/
Point_move(point, 42, 51);
CCon la llamada a función equivalente en Java, el objeto point está disponible dentro del método como una variable this:
// Call instance method from outside of class
point.move(42, 51)
// Call instance method from within class
this.move(42, 51)
JavaPython permite llamar a los métodos como funciones con un argumento self explícito:
# Call instance method from outside or from within class
self.move(42, 51)
# Function call from within class
move(self, 42, 51)
PythonInstanciar objetos
Una característica definitoria de C es la gestión manual de la memoria: los programadores son responsables de asignar memoria a las estructuras de datos. Los lenguajes dinámicos y orientados a objetos, como Java y Python, les liberan de este trabajo. En Java, la palabra clave new se utiliza para instanciar un objeto. En el código, la memoria se asigna automáticamente:
// Create new Point instance
Point point = new Point();
JavaCuando se escribe código OOP en C, se define una función constructora especial para la instanciación. Esto asigna memoria a nuestra instancia struct, la inicializa y le devuelve un puntero:
Point * Point_new(int x, int y) {
/*Allocate memory and cast to pointer type*/
Point *point = (Point*) malloc(sizeof(Point));
/*Initialize members*/
Point_init(point, x, y);
// return pointer
return point;
};
CEn nuestro ejemplo, se desacopla la inicialización de los miembros de la struct de la instanciación. De nuevo, se utiliza una función con el prefijo Point:
void Point_init(Point * point, int x, int y) {
point->x = x;
point->y = y;
};
C¿Cómo se puede reiniciar un proyecto C orientado a objetos?
Reescribir un proyecto existente en C utilizando las técnicas de programación orientada a objetos descritas solo se recomienda en casos excepcionales. Los siguientes enfoques son más sensatos:
- Reescribir el proyecto en un lenguaje similar a C con características de programación orientada a objetos y utilizar la base de código C existente como especificación
- Reescribir partes del proyecto en un lenguaje de programación orientada a objetos y conservar componentes específicos de C
Siempre que la base de código C esté bien escrita, el segundo enfoque debería dar buenos resultados. Es práctica habitual implementar partes del programa críticas para el rendimiento en C y acceder a ellas desde otros lenguajes. Probablemente ningún otro lenguaje sea más adecuado para ello que C. Pero ¿qué lenguajes son adecuados para reconstruir un proyecto C existente utilizando principios de programación orientada a objetos?
Lenguajes orientados a objetos tipo C
Existe una amplia selección de lenguajes tipo C con orientación a objetos incorporada. Probablemente el más conocido sea el C++; sin embargo, este lenguaje es famoso por su complejidad, lo que ha provocado que muchos dejen de usarlo. Debido a la gran similitud de las construcciones básicas del lenguaje, el código C es relativamente fácil de incorporar a C++.
Mucho más ligero que C++ es Objective-C. Este dialecto de C, basado en el lenguaje OOP original Smalltalk, se utilizaba principalmente para programar aplicaciones en Mac y en los primeros sistemas operativos iOS. Más tarde, le siguió el desarrollo del lenguaje propio de Apple, Swift. Las funciones escritas en C pueden invocarse desde ambos lenguajes.
Lenguajes orientados a objetos basados en C
Otros lenguajes de programación OOP que no están relacionados con C en términos de sintaxis también son adecuados para reescribir un proyecto en C. Existen enfoques estándar para incluir código C en Python, Rust y Java.
En Python, los llamados enlaces Python permiten incluir código C. Puede que haya que traducir los tipos de datos de Python a los tipos correspondientes. También existe la C Foreign Function Interface (CFFI), que automatiza hasta cierto punto la traducción de tipos. Rust también admite la llamada a funciones C con poco esfuerzo. La palabra clave externa puede utilizarse para definir una Foreign Function Interface (FFI). Las funciones de Rust que acceden a funciones externas deben declararse como unsafe:
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
Rust