You are currently browsing the tag archive for the ‘C’ tag.

Punteros a funciones son variables que guardan una dirección de memoria: la dirección de una función. Punteros son una especie de referencia. En muchos lenguajes de programación existe el concepto de referencias a funciones que se pueden guardar en una variable. En este artículo hablamos de punteros de funciones como se usan en los lenguajes C y C++. Sin embargo, los conceptos se dejan aplicar a cualquier tipo de referencias a funciones – también a aquella otra que usa C++: los functores.

El caso clásico de usar punteros a funciones es una cola de eventos. Si programas una cola de eventos, entonces no sabes cómo se llamarán las funciones (manejadores de eventos) que se registrarán. Por eso no puedes llamarles por nombre. De hecho, colas de eventos se implementan frecuentemente en una biblioteca cuyo desarrollo es independiente de la programación de las funciones a registrar. Pues, no sabrás los nombres de las funciones y tampoco necesitas saberlos.

Tu cola de eventos suministras una función de registro. Esta función tiene un parámetro cuyo tipo es una función. Es decir, aunque no sabes los nombres de las funciones, sí, obligas a que tengan un tipo conocido: el que defines tú.

Tu cola de eventos tiene una lista con punteros a funciones que tienen este tipo. Por este punto de vista, una cola de evento no es muy diferente a una base de datos, donde el usuario puede guardar (es decir, registrar) datos. La differencia es que a los datos de una cola de eventos se puede llamar como función.

La ventaja de todo esto: Puedes llamar a un número arbitrario de funciones sin necesitar la declaración de estas funciones de antemano.

No obstante, esto lleva también una pequeña desventaja. Herramientas de análisis de código pueden extraer del código, qué función llama a qué otra función. Esto es una inforamción útil cuando quieres saber a qué “clientes” afectas al cambiar una función. No obstante, este análisis no funciona con punteros a funciones, ya que el valor de ellos sólo se sabrá a tiempo de ejecución. Sin embargo, esto no suele ser una restricción importante. Las funciones que se llaman mediante puntero suelen ser pocas y bien claras. Por ejemplo, una función de nombre OnMouseClick es fácil de reconocer como manenjador de eventos. Además, no es buen estilo de llamar a manejadores de eventos por nombre desde otras funciones. Así no lo harías, ¿verdad?

Muchas colas de eventos suelen guardar más datos que el puntero a la función. La razón es que no quieres escribir 20 manejadores para eventos que sólo se diferencian por un parámetro. La función de registro te permite añadir un dato de tipo identificador, que pasa como parámetro al manejador de evento. El manejador de evento es, por lo tanto, un tipo de función que lleva este parámetro. Dentro del manejador puedes leer este dato y saber así, a base de qué registro fue llamado. Así puedes registrar la misma función para varios eventos.

La pregunta es: ¿qué es el tipo más adecuado para este dato identificador? Pues, muchas implementaciones usan un tipo “cualquier cosa”. En Java, esto sería un Object, en C y C++ gusta un puntero de tipo void* con un parámetro adicional con el tamaño del buffer a que el puntero apunta. De esta forma puedes registrar cualquier dato en la cola de eventos, sin que la cola necesita conocer su estructura. El tipo real sabrá el manejador de eventos, que convierte el tipo “cualquier cosa” en el tipo que realmente espera.

// Al registrar el manejador usas un identificador de tipo struct Id
struct Id
{
    int index;
    char subindex;
};
Id mi_id = { 5, 1 };
cola_de_eventos.registra(mi_callback, mi_id);

// Tu manejador de eventos haría esto
void mi_callback(const void*const datos)
{
    // Tu manejador sabe que está por detrás de estos "datos"
    const Id*const id = static_cast<const Id*const>(datos);
    
    // Aquí haría algo con estos datos como
    // switch (id.index) ...
}

Ten en cuenta que los datos mencionados arriba se guardan junto con la dirección a la función de antemano. Pues, identifican el registro. No es lo mismo que parámetros que se producen junto con el evento. Por ejemplo, un manejador de OnMouseClick esperaría las coordenadas del ratón, donde tuvo lugar el click. Cómo diferentes eventos pueden producir diferentes datos subyacentes, se encuentra a menudo la idea de usar un tipo “cualquier cosa” también para estos parámetros generados en el evento.

Cuando creas un registro de funciones como una cola de eventos, te deberías preguntar, qué tipo de datos quieres permitir. Un tipo “cualquier cosa” da mucha flexibilidad – también para su abuso. Y no es necessario si sólo quieres permitir un identificador.

Si prefieres usar clases en lugar de punteros, define quién debe hacer la limpieza en la memoria. Pasar un objeto por referencia obliga a guardarlo hasta que ya no se usa. Pasar un objeto por copia deja claro al autor del manejador que se debe hacer una copia antes de terminar el manejador si no quiere perder los datos.

Piensa también que C++ permite functores. Functores son clases que sobrecargan el operador (). Así se puede tratar una instancia de esta clase como función: mi_instancia() llama realmente a mi_instancia.operator(). Functores son instancias de clases y estas se protegen mejor contra un fraude como lo permitirían los punteros. ¡Imagínate que el autor de la cola de eventos es malo y no llama al puntero de función que registraste sino a esta dirección más cuatro bytes!

Como ves, punteros a funciones son muy útiles para determinadas tareas como cola de eventos. Puede tener sentido guardar datos de identificación junto con el puntero y puede ser útil manejar un tipo “cualquier cosa” para pasar datos de tipo arbitrario. No obstante es importante de pensar bien, si es mejor restringir la flexibilidad.

Lectura adicional

La directiva de preprocesador #include se usa en los lenguajes C y C++ para “incluir” las declaraciones de otro fichero en la compilación. Esta directiva no tiene más misterio para proyectos pequeños. En cambio, puede ayudar aprovechar bien esta directiva en proyectos con un gran número de subdirectorios.

El efecto de #include

Cuando el preprocesador encuentra una línea #include "fichero", entonces reemplaza esta línea por el fichero incluido. Así procede con todas las directivas de inclusión – también en aquellas anidadas en los fichero ya a su vez incluidos. Es decir, existe un sólo fichero grande tras la precompilación.

No obstante, esta unión de varios ficheros no tiene lugar físicamente. Lo que sucede es que se interrumpe la compilación del fichero actual, se compila el fichero incluido y, tras compilarlo, se continúa con el primero. Por eso, el compilador puede decirnos, en qué fichero tuvo lugar un error de compilación.

En cambio, conviene tener esta idea del fichero único en mente, porque a veces ayuda a encontrar errores. Uno muy típico es olvidarse el punto y coma tras la declaración de una clase.

En este caso hay una declaración de clase en el fichero incluido:

class MiClase {}

En el segundo puede haber algo así:

#include "mi_clase.h"
MiClase miInstancia;

En este ejemplo, el compilador se quejará de que aquí no se puede definir un nuevo tipo en la línea de MiClase miInstancia aunque esta línea es correcta. El error verdadero es la falta del ; en el fichero incluido. Lo que el compilador realmente ve es

class MiClase {} MiClase miInstancia;

No obstante, el programador no lo ve, porque el código está distribuido sobre dos fichero y el error se produce en el correcto.

La precompilación sólo modifica el código a nivel textual. No entiende del sintaxis del lenguaje. Por eso es posible distribuir el código de forma arbitraria. Por ejemplo, el siguiente ejemplo compilaría.

Fichero incluido

{
	int

Fichero principal

void main(void)
#include "fichero_incluido"
	a;
}

Esto es así porque el compilador ve el conunto

void main(void)
{
	int
	a;
}

La posición del #include

Lo habitual es posicionar las inclusiones al inicio de cada fichero. Esto tiene sentido, porque se suele requerir declaraciones básices antes de declarar clases más complejas. Mi lugar preferido en los ficheros de cabecera es tras el guardián de inclusión múltiple.

// Guardian de inclusión múltiple
#ifndef FICHERO_YA_INCLUIDO
#define FICHERO_YA_INCLUIDO

#include "declaraciones_basicas.h"

#endif

Así se evita que un compilador poco sofisticado abre otra vez el mismo conjunto de ficheros cuando se inluye un fichero de cabecera dos o más veces.

En los ficheros de definición (los .c o .cpp), los guardianes de inclusión múltiple no hacen falta. No obstante, puede haber una restricción importante cuando se usan cabeceras precompiladas. En este caso, todos los ficheros fuente deben incluir primero el mismo fichero de cabecera – que es aquello que define la cabecera precompilada. El compilador de C++ de Borland permite varios ficheros de cabecera para la definición de una cabecera precompilada. Estas inclusiones deben ser lor primeros ficheros incluidos y se deben incluir en el mismo orden.

Puede darse el caso de no poner las inclusiones en el inicio de un fichero. Esto es frecuente en los fichero que se podrían denominar “programados en directivas de precompilación”. Normalmente se trata de ficheros de cabeceras con definiciones muy básicas como ajustes a la plataforma empleada. Por ejemplo, en medio de un fichero “definiciones_basicas.h” puede haber unas líneas

#if PLATAFORMA_ES_LINUX
#include "funcionalidad_gratis.h"
#elif PLATAFORMA_ES_MICROSOFT_WINDOWS
#include "funcionalidad_cara.h"
#elif PLATAFORMA_ES_APPLE
#include "funcionalidad_muy_cara.h"
#else
#error Esta plataforma no está soportada
#endif

La diferencia entre “” y <>

La directiva #include existe en dos versiones. En una se pone el nombre de fichero entre comillas, en la otra entre paréntesis angulares (el signo menor y mayor como “comillas”).

#include "fichero_con_comillas.h"
#include <fichero_entre_menor_y_mayor.h>

La versión con los paréntesis angulares busca los ficheros en todos los directorios que se han especificado en la llamada al compilador – normalmente con la opción “-I”. Estos directorios se suelen rastrear por el fichero incluido en el orden en que aparecen en la línea de comando.

Cuando se incluye un fichero entre comillas, entonces el compilador busca este fichero primero en el mismo directorio que el fichero actualemente compilado y después en los demás directorios. Es decir, la versión con comillas se diferencia de la versión con paréntesis angulares únicamente por buscar primero en el directorio del fichero compilado. Tras no encontrarlo ahí actúa igual.

Esto muchas veces no es ninguna diferencia, ya que se suelen especificar todos los directorios en la línea de comando del compilador. Así no se suele dar el caso que se puede incluir un fichero con comillas pero no con paréntesis angulares.

Más significativo es el comportamiento ante ficheros con el mismo nombre en distintos directorios. En este caso la versión con comillas da preferencia sobre el fichero en el mismo directorio y esto suele ser el mejor acertado. Aunque sea preferible nombrar ficheros de forma única en un proyecto, es posible que no se pueda evitar tener dos ficheros con el mismo nombre cuando se incluyen varias bibliotecas de terceros.

De ahí se puede deducir que es imperativo incluir cabeceras de la misma biblioteca con comillas. De esta forma se puede asegurar que las cabeceras de una biblioteca se incluyan entre si aunque haya otros con el mismo nombre en uno de los directorios especificados en la línea de comandos.

Además, incluir con comillas puede dar al programador un significado adicional: que este fichero está bajo la custodia de mi equipo de desarrollo. Las cabeceras incluidas con paréntesis angulares son de bibliotecas de terceros. Los primeros ficheros puedo modificar si hace falta, los segundos no.

El orden de las inclusiones

El orden de las directivas #include no importa cuando todos los identificadores del programa son únicos. No obstante, a veces no lo son y conviene generar el error “este identificador ya existe” en nuestro código y no en el código de una biblioteca estándar.

Esto se consigue incluyendo primero las caberas de terceros. Si aparece un error de identificador doble, entonces aparece en la segunda definición – que es la nuestra – y ahí podemos cambiar el nombre del objeto sin problema.

En proyectos realmente grandes puede haber varias bibliotecas de distintas dependencias. Por la misma razón de generar los errores de identificadores dobles en el código más fácilmente modificable, conviene incluir las bibliotecas más básicas primero. Dentro del mismo nivel podemos ordenar los ficheros incluidos de forma alfabética. Esto ayuda a encontrar inclusiones que faltan o sobran.

El siguiente código muestra una secuencia de inclusiones para un fichero “definicion_de_mi_clase.cpp”.

// Primero se debe incluir la cabecera de precompilación
#include "cabecera_de_precompilacion.h"

// Segundo, incluir la cabecera correspondiente
// a este fichero de implementación.
// Esto deja más claro, que es la
// clase que se implementa aquí.
#include "definicion_de_mi_clase.h"

// A continuación inclusiones de la biblioteca estándar.
// Se usan paréntesis angulares.
#include <vector>

// Inclusiones de otras bibliotecas de terceros
#include <wx.h>
#include <gl.h>

// Inclusiones de subbibliotecas básicas de
// mi proyecto con comillas
#include "mis_definiciones_basicas.h"

// Luego las demás inclusiones de mi proyecto
#include "clases_auxiliares.h"
#include "más_definiciones_para_mi_clase.h"

Usar rutas relativas

Una forma de evitar nombres de fichero dobles es incluir ficheros con rutas relativas.

#include "definiciones/tipos_básicos.h"
#include "funcionalidad/tipos_básicos.h"

La desventaja de esta forma es, que uno debe saber, en qué directorio se encuentra cada cabecera. No obstante, esto suele ser un problema menor. Sin ruta relativa, uno debería poner un prefijo a cada nombre de fichero para evitar nombres dobles. Estos prefijos son típicamente los nombres de los directorios. Es decir, todos los ficheros en el directorio “definiciones” tienen se llaman “definiciones_xxx”. Al final debo saber el nombre de directorio de todas formas.

Los programadores de C++ han copiado de Java la idea de estructurar los directorios como los espacios de nombre. Así, el uso de la clase NombreDeEspacio::Subespacio::MiClase requiere la inclusión del fichero “nombre_de_espacio/subespacio/mi_clase.h”.

El uso de rutas relativas en las inclusiones puede mejorar bastante el orden y reducir la configuración del compilador. Así basta incluir un solo directorio para obtener acceso a todos los componentes de la biblioteca boost, por ejemplo. En cambio, rutas relativas hacen más complicado recolocar ficheros a otros directorios. Y esto puede pasar a menudo en fases tempranos de un proyecto.

Optimizar la velocidad de compilación

La inclusión de un sólo fichero normalmente no afecta mucho al tiempo, que el compilador requiere para la compilación de un fichero. Pero esto se cambia si el fichero incluido incluye más ficheros que a su vez incluyen aún más ficheros. Gracias a tantas inclusiones anidadas, una sola inclusión, sí, puede cambiar el tiempo de compilación drásticamente.

Una forma de mejorar la velocidad es utilizar cabeceras precompiladas. Estas cabeceras deben estar incluidas por todos los ficheros de definición y, por eso, debe ser una decisión temprana en un proyecto de utilizarlas o no – al menos si uno quiere evitar añadir esta inclusión en muchos ficheros.

Otra forma es evitar la inclusión de ficheros no necesarios. Normalmente una inclusión adicional no perjudica al resultado de la compilación. Simplemente añade declaraciones que finalmente no se usan. Pero la compilación de estas declaraciones cuesta un tiempo que nos queremos ahorrar.

Para proyectos grandes puede convenir no incluir definiciones de tipos que sólo aparecen como punteros o referencias en los ficheros de cabecera. Esto tipos se pueden introducir con una declaración forward. Sólo en el fichero de definición se incluye entonces la cabecera que define este tipo.

Como ejemplo damos una clase, que guarda un puntero a otra clase. En el fichero de cabecera tenemos

// Declaración forward
class OtraClase;

// Declaración de la clase principal
class UnaClase
{
	OtraClase* dame_un_puntero_a_otra_clase();
};

En el fichero de definición tenemos

#include "una_clase.h"
#include "otra_clase.h"

OtraClase* UnaClase::dame_un_puntero_a_otra_clase()
{
	static OtraClase otraInstancia;
	return &otraInstancia;
}

Cabe señalar que una declaración forward debe tener lugar dentro del nombre de espacio correspondiente.

// Esto no es correcto
class NombreDeEspacio::Subespacio::Clase;

// Se debe hacer la declaración forward así
namespace NombreDeEspacio
{
	namespace Subespacio
	{
		class Clase;
	}
}

Conclusión

Como hemos comprobado, hay bastante que se puede tener en cuenta sobre la directiva #include, cuando se trabaja en proyectos grandes.

  • La inclusión genera un sólo fichero grande. Tener esto en mente cuando se produce un error de compilación en la primera línea aparamente correcta tras un #include.
  • Conviene incluir ficheros del propio proyecto con comillas y las cabeceras de terceros con paréntesis angulares para marcar la diferencia entre “propio” y “estándar”.
  • Ordenando las propias inclusiones al final ayuda a corregir errores de doble definición de identificadores.
  • Rutas relativas son una buena opción para proyectos con una clara estructura de directorios.
  • Usar cabeceras precompiladas y evitar incluir ficheros innecesarios – también aprovechando declaraciones forward – puede acelerar el proceso de compilación.

Lectura adicional

Introducción

Con funcioncitas me refiero a funciones de una o dos líneas, muchas veces con un cálculo muy simple, pero con un valor semántico muy potente.

Escribir una función adicional fracasa a menudo en una cierta pereza, porque hay que buscar un hueco en el fichero fuente, escribirla, en C y C++ hay que añadirlo en la cabecera, eventualmente serás capaz de escribir algún comentario de qué esta función debe hacer y todo esto para una línea de código bastante trivial. Pero esto se puede ver también de otra forma: escribes el código una vez, lo modificas diez veces y lo lees cien veces. Si tardas 99 segundos (1 minuto y 39 segundos) para escribir una función que te ahorra un segundo en la lectura, todavía ahorras tiempo.

El valor de las funciones pequeñas está en su claridad semántica, que mejora mucho la legibilidad del código – sobre todo cuando se trata de una combinación de varias funciones pequeñas. Para ilustrar esto, presento algunos ejemplos.

Copiar con punteros

Si p y q son dos punteros, entonces ¿qué hace el código de C siguiente?

while (*p++ = *q++) {}

Lo que hace es copiar el valor a que q apunta a la memoria referenciada por p. A continuación pasa ambos punteros al próximo y elemento y sigue copiando hasta que haya copiado un cero. (El valor de una asignación es el valor asignado, el cero se interpreta como un false y el bucle while termina.)

Tenemos una expresión booleana basada en cualquier tipo arbitrario que el compilador sepa amablemente interpretar como false o true y que, además, modifica las variables dentro de la expresión. A parte de tener un buen ejemplo de un uso sofisticado pero poco intuitivo del lenguaje de programación C, ¿para qué se podría utilizar un bucle así?

Pues, para copiar un string. En la biblioteca estándar de C hay una definición de strcpy similar a esta (la versión estándar devuelve un char*):

void strcpy(char *p, const char *q)
{
    while (*p++ = *q++) {}
}

Ahora la pregunta: ¿qué te parece más comprensible? ¿Una línea strcpy(p, q) o un bucle while (*p++ = *q++) {}? Técnicamente no trae ninguna ventaja llamar a la función strcpy, pero semánticamente, es decir, el significado del código es mucho más claro para el programador.

Funciones pequeñas son a menudo parte del estándar. La clase String en C# tiene un método IsNullOrEmpty. La expresión

if (!String.IsNullOrEmpty(miString))

reemplaza a

if (miString != null && miString.Length > 0)

Es lo mismo, pero el método IsNullOrEmpty deja la intención más obvia.

Acotar rangos

Un ejemplo simple. Mira el reloj cuánto tardas en contestar a la pregunta. ¿Qué representa c?

c = a < b ? a : b;

Y ahora los mismo otra vez. ¿Qué representa c?

c = min(a, b);

Esto era fácil, pero ¿qué tal con algunos niveles de anidamiento?

c = c < a ? a : (c > b ? b : c);

Esto limita c al rango [a, b]. Si no estás tan firme en el uso del operador ? : podrías escribir la línea anterior con if.

if (c < a)
{
    c = a;
}
else
{
    if (c > b)
    {
	    c = b;
	}
}

Un poco más amigable sería usar las funciones pequeñas min y max:

c = max(a, min(b, c));

Mi opción preferida sería una función

c = limitToRange(a, c, b);

donde el parámetro en medio será acotado al intervalo [a, b].

Puedes optar por hacer una pequeña función que ocupa una línea o la construción con el if anidado arriba que ocupa once líneas por cada variable que quieres acotar. Y puede haber muchas: si c es un índice de un array, entonces podrías tener la necesidad de acotarlo cada vez que accedes al array.

Algo parecido vale si quieres generar un error al recibir un índice fuera de rango. La manera directa sería escribir algo así

if (index < start || index >= end)
{
    std:ostringstream mensaje;
	mensaje << "Error de índice index = "
	        << index
			<< ". Fuera del rango ("
			<< start
			<< ", "
			<< end
			<< ").";
    throw std::exception(mensaje.str());
}

Pero igual vale la pena hasta escribir una clase de excepción propia.

if (indexOutOfRange(start, index, end))
{
    throw IndexException(start, index, end);
}

Conclusión

Funciones pequeñas aumentan considerablemente la legibilidad del código. Ayudan a reducir el número de líneas dedicadas a comprobar la calidad de los datos y dejan a consecuencia más espacio a la parte principal de una función. Para no perder velocidad en la ejecución, C++ ofrece definir estas pequeñas funciones inline.

Muchas veces las funciones pequeñas son tan generales que se pueden usar en todos los proyectos de programación. Una buena gestión debería procurar que se mantiene una biblioteca que todo el mundo en la empresa pueda usar. Como son funciones muy básicas, conviene también pensarse bien sus nombres y el espacio de nombres en que se encuentran.

Si no eres gerente sino un simple programador, entonces te recomiendo que guardes estas funciones en un fichero personal. Así puedes copiar y pegarlas en cualquier proyecto en que participas. De hecho, mantener una biblioteca personal puede facilitar tu trabajo.

Referencias

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

Introducción

Igual como la palabra clave const, el modificador static trae confusiones porque se puede utilizar en varios contextos en C y C++. Podemos declarar estáticas funciones y métodos. Variables pueden estar estáticas dentro de funciones, métodos, clases y a nivel global, es decir, por todas partes.

Variables estáticas locales

Lo más común es declarar una variable local estática:

void mi_funcion_o_metodo(void)
{
    static int var_estatica = 4;
}

El valor de la variable var_estatica no se pierde aunque ya no se encuentre dentro del alcance, es decir, cuando el programa haya salido de la función en que la variable fue definida. Esto diferencia una variable estática de una variable no estática, cuyo valor se pierde al final de la función.

Debido a esta diferencia, las variables estaticas no se guardan en el stack sino en una memoria especial para variables estáticas. Aunque no sean accesibles durante toda la ejecución del programa, las variables estáticas existen durante toda la ejecución del programa.

Igual como en el caso de las variables convencionales, se llama al constructor de las variables estáticas cuando se construyen. Esto succede una vez antes de llamar a la función main. Igualmente se llama una vez al destructor después de salir del programa. Recuerda que las variables estáticas existen durante toda la ejecución del programa. Por eso deben inicalizarse antes de entrar y después de salir de la función principal.

No hay diferencia entre variables estáticas en funciones y métodos de clases. De hecho, una variable estática se puede definir dentro de cualquier bloque delimitado por {}.

Miembros estáticos de una clase

Variables miembros de una clase también pueden ser estáticas. Una variable estática de una clase es un dato común a todas las instancias de esta clase y accesible en todos los métodos. Una variable miembro estática es incluso accesible sin instancia alguna.

En el código siguiente definimos una clase con una variable miembro estática:

class MiClase
{
public:
    static int mi_variable;
};

Esta variable se inicializa así:

int MiClase::mi_variable = 5;

Si la queremos usar sin instancia, entonces usamos el nombre cualificado:

void haz_algo(void)
{
    MiClase::miVariable = 3;
}

Lógicamente, la variable miembro debe ser pública para que la podamos usar fuera de los métodos de la clase. (Dentro de los métodos de la clase no hace falta poner el nombre de la clase delante.)

Las variables miembros estáticas existen también durante toda la ejecución del programa. Su diferencia con las variables estáticas definidas en una función o método está en su alcance: Las variables miembros están al alcance de todos los métodos de una clase y no sólo de uno.

Constantes estáticas

Variables estáticas se pueden declarar también const. Una constante estática es una constante universal del programa, pero de alcance limitado. Es una buena práctica utilizar constantes estáticas en lugar de macros #define ya que las constantes llevan un tipo: un 5 puede ser un int, un char o un float pero un dato

const static int var = 5;

siempre es un int.

Métodos estáticos

Métodos de clases también pueden ser estáticas. Declarar un método estático es una promesa que este método sólo utiliza miembros estáticos de la clase. Este tipo de método no conoce el puntero this, por lo cual no se puede hacer directamente referencia a métodos y variables no estátitocs de la clase.

class MiClase
{
public:
    // Una variable estática
    static int una_variable_estatica;

    // Una variable no estática
    int una_variable_no_estatica;

    // Un método estático
    static void haz_algo(void)
    {
        // Se pueden usar variables estáticas
        una_variable_estatica = 5;

        // Error: no se pueden usar variables que
        // requieren un puntero this
        una_variable_no_estatica = 5;  // Error de compilación
    }

    // Un método normal (no estático)
    void haz_otra_cosa(void)
    {
        // Se pueden usar variables y métodos estáticos
        // también en métodos no estáticos
        una_variable_estatica = 5;
        haz_algo();

        // Una variable de instancia en un método de instancia
        una_variable_no_estatica = 5;  // Correcto
    }
};

Es posible llamar a métodos estáticos de una clase sin tener una instancia de ella utilizando el nombre cualificado.

void una_funcion_cualquiera(void)
{
    // Correcto: Se puede llamar a un método no estático
    // si instancia
    MiClase::haz_algo();

    // Sin embargo, también se le puede llamar con instancia
    MiClase mi_instancia;
    mi_instancia.haz_algo();

    // No es posible llamar a un método convencional
    // sin instancia
    MiClase::haz_otra_cosa();  // Error de compilación
}

Métodos estáticos son similares a funciones globales. Sólo llevan el nombre de la clase como prefijo. (Un nombre de espacio no se usa de forma diferente que una clase con todos los miembros públicos y estáticos.)

Métodos estáticos pueden sobrecargar los nombres métodos no estático. De hecho es una práctica bastante común – sobre todo para métodos recursivos. Como ejemplo podemos considerar una clase String, en que un método convencional afecta a la instancia de una clase, mientras el método estático a un parámetro.

class String
{
public:

    // Crea una instancia con todas las letras del parámetro 
    // convertidas a minúsculas.
    static String a_minusculas(const String& instancia_de_string);

    // Convierte todas las letras de ESTA instancia a minúsculas
    void a_minusculas(void)
    {
        *this = a_minusculas(*this);
    }
};

Enlace interno

Finalmente es posible declarar estáticos funciones y variables globales, es decir, fuera de una función. Una variable a nivel global existe durante toda la ejecución del programa sea estática o no. Y cualquier función tiene acceso a una variable global. Por eso static tiene otro significado fuera de un bloque {}: especifica que el identificador tiene enlace interno.

Enlace interno quiere decir, que el linker no puede usar este nombre para enlazarlo con otro módulo. Es un nombre privado en el fichero de objeto (.o o .obj). El contrario es un enlace externo: El fichero objeto expone este nombre y su tipo al linker que puede usar esta información para enlazarlo con una referencia en otro módulo.

El modificador static en una función o variable suprime el enlace externo. Se puede forzar un enlace externo con el modificador extern. Por defecto, funciones tienen un enlace externo y variables interno. Por eso no es necesario declarar una función extern y una variable static, pero mejora la semántica del código.

Conclusión

Hemos visto que la palabra clave static se usa para declarar variables de un alcance limitado pero con una vida durante toda la ejecución del programa. Métodos estáticos se limitan a acceder a variables miembros estáticos de la clase. Finalmente se usa el modificador static para forzar un enlace interno.

Como última nota cabe mencionar, que el uso de variables estáticas puede complicar la creación de procesos multi-hilo, ya que estos datos son comunes a todos los hilos.

Referencias

Ya hace tiempo que el lenguaje de programación Java ha destronado a C++ como el lenguaje más popular. No extraña que recién titulados carecen de conocimientos profundos sobre C++ y están mal vistos por veteranos que carecen de conocimientos en Java.

Érase una vez que estos veteranos eran los recién titulados y se enfrentaron con problemas similares aunque con lenguajes de programación distintos. Antes del paradigma de orientado a objetos se disfrutó de un lenguaje bien estructurado – donde lo nuevo era el bien: El artículo Real Programmers Don’t Use Pascal fue escrito cuando Java se llamaba Pascal y C++ Fortran. No te dejes escapar el enlace a la historia de Mel dentro de este artículo para los tiempos pre-informáticos cuando el lenguaje Fortran era el Java y el C++ de entonces no tenía ni nombre.

Aunque C++ ya no es el lenguaje más popular, su ráiz, el lenguaje C, está todavía presente en el fondo de todas las plataformas. Un artículo clásico sobre como una broma se hizo seria es Creators Admit Unix, C Hoax.

Aunque a primera vista puede parecer algo para teorías académicas, se comprueba que los lenguajes esotéricos de programación son algo para gente creativa con el tiempo suficiente para escribir programas en Brainfuck, dibujarlas (!) en Piet o hacerlos comprensibles para orang utans en Ouk. También en la forma de desarrollar un tema clásico, los programadores no necesitan quedarse fuera en los premios Nobel de la literatura, aunque sea para decir algo tan simple como hola mundo.

Y finalmente una explicación, porque ya no podemos seleccionar la zona temporal en Microsoft Windows con un mapa del mundo. Pues, es posible que esto incluso ha evitado conflictos fronterizos armados.

Escribe tu dirección de correo electrónico para suscribirte a este blog, y recibir notificaciones de nuevos mensajes por correo.

Únete a otros 46 seguidores

Archivos

mayo 2017
L M X J V S D
« Ene    
1234567
891011121314
15161718192021
22232425262728
293031