A di­fe­re­n­cia de los lenguajes OOP C++ y Objective-C, no existe pro­gra­ma­ción orientada a objetos en C. Debido al uso ge­ne­ra­li­za­do de este lenguaje y a la po­pu­la­ri­dad de la pro­gra­ma­ción orientada a objetos, existen enfoques para utilizar OOP en C.

OOP in C: ¿es realmente posible?

El lenguaje de pro­gra­ma­ción con C no está pensado para la pro­gra­ma­ción orientada a objetos. Este lenguaje es un excelente ejemplo del estilo de pro­gra­ma­ción es­tru­c­tu­ra­da según la pro­gra­ma­ción im­pe­ra­ti­va Sin embargo, es posible emular enfoques orie­n­ta­dos a objetos en C. Este lenguaje de pro­gra­ma­ción contiene todos los co­m­po­ne­n­tes ne­ce­sa­rios para ello y sirvió, por ejemplo, de base para la pro­gra­ma­ción orientada a objetos en Python.

La OOP te permite definir tus propios “tipos de datos ab­s­tra­c­tos” (TDA). Un TDA puede co­n­si­de­rar­se como un conjunto de valores posibles y funciones que operan sobre ellos. Es im­po­r­ta­n­te que la interfaz visible ex­te­r­na­me­n­te y la im­ple­me­n­ta­ción interna estén des­aco­pla­das entre sí. De este modo, tú como usuario puedes confiar en que los objetos se comportan de acuerdo con su de­s­cri­p­ción.

Los lenguajes orie­n­ta­dos a objetos, como Python, Java y C++, utilizan el concepto de “clase” para modelar tipos de datos ab­s­tra­c­tos. Las clases sirven de plantilla para crear objetos similares; lo que se conoce como in­s­ta­n­cia­ción. In­trí­n­se­ca­me­n­te, C no reconoce clases, y estas no pueden modelarse dentro del lenguaje. En cambio, hay varios enfoques para im­ple­me­n­tar las ca­ra­c­te­rí­s­ti­cas de OOP en C.

¿Cómo funciona la pro­gra­ma­ción orientada a objetos en C?

Para entender cómo funciona la pro­gra­ma­ción orientada a objetos en C, primero hay que hacerse la pregunta: ¿qué es exac­ta­me­n­te la pro­gra­ma­ción orientada a objetos (OOP)? La pro­gra­ma­ción orientada a objetos es un estilo de pro­gra­ma­ción muy extendido, ma­ni­fe­s­ta­ción del paradigma de pro­gra­ma­ción im­pe­ra­ti­vo. Se puede di­s­ti­n­guir la pro­gra­ma­ción orientada a objetos de la pro­gra­ma­ción de­cla­ra­ti­va y su es­pe­cia­li­za­ción, la pro­gra­ma­ción funcional.

La idea básica de la pro­gra­ma­ción orientada a objetos es modelar objetos y dejar que in­ter­ac­túen entre sí. El flujo del programa resulta de las in­ter­ac­cio­nes de los objetos y, por tanto, solo se fija en tiempo de ejecución. En esencia, la OOP comprende solo tres pro­pie­da­des:

  1. Los objetos en­ca­p­su­lan su estado interno.
  2. Los objetos reciben mensajes a través de sus métodos.
  3. Los métodos se asignan di­ná­mi­ca­me­n­te en tiempo de ejecución.

Un objeto en un lenguaje de pro­gra­ma­ción orientada a objetos puro como Java es una unidad au­to­co­n­te­ni­da. Comprende una es­tru­c­tu­ra de datos de cualquier co­m­ple­ji­dad, así como métodos (funciones) que operan sobre ella. El estado interno del objeto, re­pre­se­n­ta­do en los datos que contiene, solo puede leerse y mo­di­fi­car­se 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 es­tru­c­tu­ras de datos y funciones a objetos. En su lugar, se teje un sistema manejable de es­tru­c­tu­ras de datos, de­fi­ni­cio­nes de tipos, punteros y funciones. Como es habitual en C, quien programa es re­s­po­n­sa­ble de la correcta asi­g­na­ción y li­be­ra­ción de memoria.

El código C basado en objetos re­su­l­ta­n­te no se parece mucho a lo que se está aco­s­tu­m­bra­do en los lenguajes OOP, pero funciona. A co­n­ti­nua­ción, se ofrece una visión general de los conceptos centrales de la pro­gra­ma­ción orientada a objetos, junto con su equi­va­le­n­te en C:

Concepto OOP Co­rre­s­po­n­de­n­cia en C
Clase Tipo de es­tru­c­tu­ra
Instancia de clase Ejemplo de es­tru­c­tu­ra
Método de instancia Función que acepta punteros a variables struct
Variable this/self Puntero a variable struct
In­s­ta­n­cia­ción Asi­g­na­ción y re­fe­re­n­cia mediante puntero
Nueva palabra clave Activar la función malloc

Modelar objetos como es­tru­c­tu­ras de datos

Descubre primero cómo puede modelarse en C la es­tru­c­tu­ra de datos de un objeto al estilo de los lenguajes de pro­gra­ma­ción orientada a objetos. C es un lenguaje compacto que funciona con pocas co­n­s­tru­c­cio­nes li­n­güí­s­ti­cas. Para crear es­tru­c­tu­ras de datos ar­bi­tra­ria­me­n­te complejas, se utilizan los llamados “structs”, cuyo nombre deriva del término “es­tru­c­tu­ra de datos”, o “Data Structure” en inglés.

Una struct-C define una es­tru­c­tu­ra de datos que incluye campos llamados “miembros”. En otros lenguajes, una co­n­s­tru­c­ció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, po­si­ble­me­n­te de distintos tipos.

La sintaxis de una de­cla­ra­ción struct en C es muy sencilla:

struct struct_name;
C

Op­cio­na­l­me­n­te, también se puede definir la struct es­pe­ci­fi­ca­n­do los miembros con nombre y tipo. Como ejemplo estándar, se considera un punto en un espacio bi­di­me­n­sio­nal con coor­de­na­das x e y. Se muestra la de­fi­ni­ción de struct:

struct point {
    /*X-coordinate*/
    int x;
    /*Y-coordinate*/
    int y;
};
C

En el código C co­n­ve­n­cio­nal, esto va seguido de la in­s­ta­n­cia­ción de una variable struct. Se crea la variable y se ini­cia­li­zan ambos campos con 0:

struct point origin = {0, 0};
C

Así se pueden leer los valores de los campos y re­s­ta­ble­ce­r­los. 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
C

Sin embargo, esto viola el requisito de en­ca­p­su­la­ción: solo se puede acceder al estado interno de un objeto mediante métodos definidos a tal efecto. Así que a nuestro pla­n­tea­mie­n­to 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>
C

De este modo, se puede definir un tipo de punto co­rre­s­po­n­die­n­te para nuestra struct de puntos:

typedef struct point Point;
C

La co­m­bi­na­ción de typedef con una de­fi­ni­ción struct co­rre­s­po­n­de apro­xi­ma­da­me­n­te a una de­fi­ni­ción de clase en Java:

typedef struct point {
    /*X-coordinate*/
    int x;
    /*Y-coordinate*/
    int y;
} Point;
C
Nota

En el ejemplo, “point” es el nombre de la struct, mientras que “Point” es el nombre del tipo definido.

Aquí tienes la de­fi­ni­ción de clase co­rre­s­po­n­die­n­te en Java:

class Point {
    private int x;
    private int y;
};
Java

Utilizar typedef nos permite crear una variable Point sin utilizar la palabra clave struct:

Point origin = {0, 0}
/*Instead of*/
struct point origin = {0, 0}
C

Lo que sigue faltando es la en­ca­p­su­la­ción del estado interno.

En­ca­p­su­la­ción del estado interno

Los objetos mapean su estado interno en su struct de datos. En los lenguajes de pro­gra­ma­ción orientada a objetos, como Java, las palabras clave “private”, “protected”, etc., se utilizan para re­s­tri­n­gir el acceso a los datos de los objetos. Esto impide el acceso directo desde el exterior y garantiza la se­pa­ra­ción entre interfaz e im­ple­me­n­ta­ción.

Para realizar la OOP en C, se utiliza un mecanismo diferente. Se utiliza como interfaz una de­cla­ra­ción de avance en el archivo de cabecera y se crea así un “In­co­m­ple­te type”:

/*In C header file*/
struct point;
/*Incomplete type*/
typedef struct point Point;
C

La im­ple­me­n­ta­ción de point-struct sigue en un archivo de código fuente C in­de­pe­n­die­n­te, 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 es­tru­c­tu­ras de datos creadas di­ná­mi­ca­me­n­te, se re­fe­re­n­cian con punteros de todos modos. Los punteros a in­s­ta­n­cias de struct se co­rre­s­po­n­den apro­xi­ma­da­me­n­te con las re­fe­re­n­cias a objetos uti­li­za­das en Java.

Sustituye los métodos por funciones

En lenguajes de pro­gra­ma­ció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;
C

Como C no reconoce clases, no es posible agrupar las funciones pe­r­te­ne­cie­n­tes a un tipo bajo un nombre común. En su lugar, se pro­po­r­cio­nan los nombres de las funciones con un prefijo que contiene el nombre del tipo. Las firmas de función co­rre­s­po­n­die­n­tes 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);
C

A co­n­ti­nua­ción, se im­ple­me­n­ta 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;
};
C

Este 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 co­rre­s­po­n­de apro­xi­ma­da­me­n­te con la variable this en Java o Ja­va­S­cri­pt. La di­fe­re­n­cia es que cuando se llama a la función C, el puntero se da ex­plí­ci­ta­me­n­te:

/*Call function with pointer argument*/
Point_move(point, 42, 51);
C

Con la llamada a función equi­va­le­n­te en Java, el objeto point está di­s­po­ni­ble 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)
Java

Python 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)
Python

In­s­ta­n­ciar objetos

Una ca­ra­c­te­rí­s­ti­ca de­fi­ni­to­ria de C es la gestión manual de la memoria: los pro­gra­ma­do­res son re­s­po­n­sa­bles de asignar memoria a las es­tru­c­tu­ras de datos. Los lenguajes dinámicos y orie­n­ta­dos a objetos, como Java y Python, les liberan de este trabajo. En Java, la palabra clave new se utiliza para in­s­ta­n­ciar un objeto. En el código, la memoria se asigna au­to­má­ti­ca­me­n­te:

// Create new Point instance
Point point = new Point();
Java

Cuando se escribe código OOP en C, se define una función co­n­s­tru­c­to­ra especial para la in­s­ta­n­cia­ción. Esto asigna memoria a nuestra instancia struct, la ini­cia­li­za 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;
};
C

En nuestro ejemplo, se desacopla la ini­cia­li­za­ción de los miembros de la struct de la in­s­ta­n­cia­ció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?

Re­es­cri­bir un proyecto existente en C uti­li­za­n­do las técnicas de pro­gra­ma­ción orientada a objetos descritas solo se re­co­mie­n­da en casos ex­ce­p­cio­na­les. Los si­guie­n­tes enfoques son más sensatos:

  1. Re­es­cri­bir el proyecto en un lenguaje similar a C con ca­ra­c­te­rí­s­ti­cas de pro­gra­ma­ción orientada a objetos y utilizar la base de código C existente como es­pe­ci­fi­ca­ción
  2. Re­es­cri­bir partes del proyecto en un lenguaje de pro­gra­ma­ción orientada a objetos y conservar co­m­po­ne­n­tes es­pe­cí­fi­cos de C

Siempre que la base de código C esté bien escrita, el segundo enfoque debería dar buenos re­su­l­ta­dos. Es práctica habitual im­ple­me­n­tar partes del programa críticas para el re­n­di­mie­n­to en C y acceder a ellas desde otros lenguajes. Pro­ba­ble­me­n­te ningún otro lenguaje sea más adecuado para ello que C. Pero ¿qué lenguajes son adecuados para re­co­n­s­truir un proyecto C existente uti­li­za­n­do pri­n­ci­pios de pro­gra­ma­ción orientada a objetos?

Lenguajes orie­n­ta­dos a objetos tipo C

Existe una amplia selección de lenguajes tipo C con orie­n­ta­ción a objetos in­co­r­po­ra­da. Pro­ba­ble­me­n­te el más conocido sea el C++; sin embargo, este lenguaje es famoso por su co­m­ple­ji­dad, lo que ha provocado que muchos dejen de usarlo. Debido a la gran similitud de las co­n­s­tru­c­cio­nes básicas del lenguaje, el código C es re­la­ti­va­me­n­te fácil de in­co­r­po­rar a C++.

Mucho más ligero que C++ es Objective-C. Este dialecto de C, basado en el lenguaje OOP original Smalltalk, se utilizaba pri­n­ci­pa­l­me­n­te para programar apli­ca­cio­nes en Mac y en los primeros sistemas ope­ra­ti­vos iOS. Más tarde, le siguió el de­sa­rro­llo del lenguaje propio de Apple, Swift. Las funciones escritas en C pueden invocarse desde ambos lenguajes.

Lenguajes orie­n­ta­dos a objetos basados en C

Otros lenguajes de pro­gra­ma­ción OOP que no están re­la­cio­na­dos con C en términos de sintaxis también son adecuados para re­es­cri­bir 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 co­rre­s­po­n­die­n­tes. También existe la C Foreign Function Interface (CFFI), que au­to­ma­ti­za hasta cierto punto la tra­du­c­ción de tipos. Rust también admite la llamada a funciones C con poco esfuerzo. La palabra clave externa puede uti­li­zar­se para definir una Foreign Function Interface (FFI). Las funciones de Rust que acceden a funciones externas deben de­cla­rar­se como unsafe:

extern "C" {
    fn abs(input: i32) -> i32;
}
fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Rust
Ir al menú principal