You are currently browsing the category archive for the ‘Trucos’ category.

Por qué una simulación numérica

Cuando se hacen predicciones, se interesa por el valor de una variable x en un determinado tiemp t partiendo de una situación inicial. Estas predicciones pueden ser el número de coches que pasan por una calle a una determinada hora o cuando se forma un atasco. Muchas veces no es posible hallar estos valores analíticamente, es decir, no es posible escribir una ecuación x(t) = algo relativamente fácil a calcular.

En esta situación no hay más remedio que utilizar una simulación numérica. La idea fundamental de una simulación en el tiempo es la fácilidad de predicir lo que va a pasar en el próximo instante y que una larga secuencia de instantes me puede llevar a lo que va a succeder en un futuro lejano.

El algoritmo fundamental

Por ejemplo, si delante un semáforo esperan 5 coches y uno se acerca, es previsible que algunos segundos después habrá 6 coches esperando. En cambio, sería muy difícil predicir cuántos coches esperarán dentro de una hora. Sin embargo, podré aprovechar una predicción a corto plazo como estado inicial de una nueva predicción.

  1. Partiendo de una situación inicial, predigo (con fácilidad) lo que ocurrirá en el próximo instante.
  2. Utilizo esta predicción como la situación inicial para la próxima iteración.

Si sólo uso este algoritmo un número de veces suficiente grande, entonces llegaré a predecir a cuántos coches esperarán en una hora.

El modelo de la simulación

Un modelo en la ciencia es una representación simplificada de la realidad, pero que contiene las magnitudes más significativas. Un modelo de una simulación de tráfico de coches podría consistir en un grafo, donde los nodos representan intersecciones y los enlaces las calles. A cada calla se podría asociar una capacidad de coches. Cada coche tendría asociado un camino que no es otra cosa que una secuencia de nodos en el grafo. Si el coche es el primero en su calle, entonces entrará en la próxima si está todavía no está llena. De otro modo espera – y con ello todos los demás coches detrás.

Como vemos, este modelo representa el comportamiento más fundamental durante la hora punta en una ciudad, cuando todo está atascado. Este modelo no tiene en cuenta el tiempo que tarda el coche en trascurrir una calle. Pero este tiempo es despreciable cuando un coche pasa la mayor parte del tiempo esperando detrás otro. Sin embargo, este tiempo, sí, sería significativo cuando hay poco tráfico y el coche pasa la mayor parte a la velocidad máxima permitida.

Simplicidad contra precisión del modelo

Desde luego, la vida real es infinitamente más compleja: algún coche da la vuelta, peatones cruzan la calle, otros obstaculos ralentizan el tráfico. Omitir esto hace nuestra simulación menos precisa, pero más rápida a calcular. Muchas veces la imprecisión no nos afecta. Pedimos a una simulación numérica que nos diga si podemos esperar un atasco en una calle determinada, mientras no nos importa el número del primer coche en el atasco.

Otras veces la imprecisión puede ser significativa: también con nuestro pequeño algoritmo mencionado arriba, será bastante difícil predicir el número de coches exacto que esperan delante de un semáforo dentro de una hora. En un caso favorable, el número será más o menos el real. En el caso de que un sistema entra en lo que se llama caós determinista, la situación puede ser totalmente diferente. Dependiendo de que mi simulación numérica sirve para un proyecto de ingeniería o para uno de física de sistemas complejos, me interesa el primer o el segundo caso.

El significado del resultado

En todo caso es importante cuestionar si los resultados de la simulación se ajustan lo suficientemente bien a la realidad. Un modelo demasiado simple puede acabar en resultados inútiles. Un modelo más completo se ajusta más a la realidad, pero requiere un mayor tiempo de cálculo – a veces demasiado tiempo: un algoritmo que tarda 3 días en calcular la predicción del tiempo para mañana es poco útil.

A veces es incluso imposible encontrar un modelo adecuado. La razón por qué no podemos predicir el tiempo a largo plazo es el efecto mariposa: un minúsculo cambio puede causar un resultado completamente distinto. Una simulación puede dar una pista sobre el comportamiento esperado, pero no suele ser una predicción a ciencia cierta.

En general es preferible utilizar el modelo más simple que dé resultados útiles, ya que cualquier ingrediente adicional al modelo sube el esfuerzo de cálculo más que la precisión de los resultados. Además, un modelo simple es más versátil. Nuestro modelo de predicción de tráfico en una ciudad podría valer perfectamente para el tráfico de maletas en un aeropuerto o el datos en una red de ordenadores.

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

Cuando compilamos patrones de clases (templates) en C++, como los contenedores de la STL, entonces tenemos a veces errores de compilación con mensajes exageradamente largos, que esconden más que aclaran. En este artículo quiero enseñar de como “leer” estos mensajes para fácilmente encontrar el error causante.

Primero debemos entender que el mensaje del compilador intenta mostrar toda la información posible. Según el tipo del problema, diferentes partes del mensaje pueden ser significantes. Por ejemplo, si cambio el allocator estándar de un contenedor por uno diferente, entonces me importará qué dice el mensaje de error sobre este componente. No obstante, en este artículo asumimos un uso básico de contenedores de la STL.

Más concretamente compilamos este trozo de código.

    // Una instancia de un mapa
	std::map mi_mapa;

	// Un iterator a una posición del mapa
	std::map::const_iterator const_itr = mi_mapa.begin();

	// Insertar un nuevo elemento en la posición indicada por el iterador
	mi_mapa.insert(const_itr, std::pair("atributo", "valor"));

Este código falla en la línea mi_mapa.insert.

Descifrando un mensaje de MinGW

Compilando con un compilador MinGW obtenemos el siguiente error para esta línea.

error: no matching function for call to `std::map, std::allocator > >::insert(std::_Rb_tree_const_iterator >&, std::pair)'
C:/opt/Dev-Cpp/include/c++/3.4.2/bits/stl_map.h:360: note: candidates are: std::pair<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator, bool> std::map<_Key, _Tp, _Compare, _Alloc>::insert(const std::pair&) [with _Key = std::string, _Tp = std::string, _Compare = std::less, _Alloc = std::allocator >]
C:/opt/Dev-Cpp/include/c++/3.4.2/bits/stl_map.h:384: note:                 typename std::_Rb_tree<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator std::map<_Key, _Tp, _Compare, _Alloc>::insert(typename std::_Rb_tree<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator, const std::pair&) [with _Key = std::string, _Tp = std::string, _Compare = std::less, _Alloc = std::allocator >]

El mensaje consiste de tres líneas: primero el error de que no hay una función que corresponde a la llamada y luego dos propuestas del compilador que funciones se podría haber intentado llamar. Como nosotros no hemos escrito la función a que llamamos, sabemos que nuestra llamada debe ser mala.

El truco para descifrar este mensaje largo consiste en eliminar sucesivamente los parámetros de los patrónes (template parameters). Nos fijamos en la primera línea lo que está entre las comas altas (‘): std::map, std::allocator > >::insert(std::_Rb_tree_const_iterator >&, std::pair).

Empezamos a eliminar desde dentro de las paréntesis angulares < > todo lo que nosotros no hemos especificado en ningún sitio. Sí, correcto, simplemente lo borramos. Son parámetros por defecto que no nos interesan. Hemos definido una instancia de std::map. En el mensaje de error, estos dos parámetros de tipo string aparecen, pero luego seguido por un std::less que no hemos puesto en ningún sitio. ¡Así fuera! Lo mismo pasa con el std::allocator. Lo borramos. Haciendo esto nuestro mensaje de error queda más corto: std::map::insert(std::_Rb_tree_const_iterator >&, std::pair).

El método insert tiene dos parámetros. Correcto. El último, es realmente un std::pair, pero el primero es un _Rb_tree_const_iterator de que no sabemos de donde procede. Nuestro primer parámetro en un const_iterator. Pues bien, _Rb_tree_const_iterator contiene la palabra const_iterator y será un tipo interno para representar un iterador a constante a un mapa. ¿Pero por qué tree? Aquí nos ayuda saber que los contenedores asociativos (std::set, std::map, std::multimap) están internamente organizado como un árbol – que es un tree en inglés. Si sustituimos la palabra tree por map, entonces el primer parámetro del método insert tendra el tipo std::_Rb_map_const_iterator >. Como nuestro tipo de iterador no tiene parámetros de patrón, simplemente lo borramos y nos queda nada más que std::_Rb_map_const_iterator.

La primera línea del mensaje de error queda entonces en

std::map::insert(std::_Rb_map_const_iterator&, std::pair)

Esto se parece a la línea donde se produjo el error:

mi_mapa.insert(const_itr, std::pair("atributo", "valor"));

Este parecido entre si no es de extrañar, ya que el mensaje de error hace referencia justamente a esta llamada.

 

Ahora nos fijamos en la segunda línea que contiene una propuesta de función y la acortamos de la misma manera.

C:/opt/Dev-Cpp/include/c++/3.4.2/bits/stl_map.h:360: note: candidates are: std::pair<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator, bool> std::map<_Key, _Tp, _Compare, _Alloc>::insert(const std::pair&) [with _Key = std::string, _Tp = std::string, _Compare = std::less, _Alloc = std::allocator >]

Primero eliminamos todo el texto de introducción hasta “candidates are:”. Luego eliminamos los _Compare y _Alloc que nosostros no hemos pedido y nos quedamos con esto:

std::pair<_Key, std::pair, std::_Select1st >>::iterator, bool> std::map<_Key, _Tp>::insert(const std::pair&) [with _Key = std::string, _Tp = std::string]

Recordamos que podemos reemplazar tree por map y renombamos _Rb_tree a _Rb_map. Los parámetros de patrón contiene un conjunto de tipos que no nos interesa, así simplemente lo quitamos y nos queda

std::pair std::map<_Key, _Tp>::insert(const std::pair&) [with _Key = std::string, _Tp = std::string]

Si nos ayuda podemos reemplazar _Key y _Tp por std::string como indica la última parte del mensaje. El nombre de espacio std y la palabra clave typename no nos aportan tampoco información, así los eliminamos también.

pair<_Rb_map::iterator, bool> map::insert(const pair&)

Finalmente vemos claro que la función propuesta es un método insert que tomo un tipo pair como entrada.

 

Hacemos el procedimiento a la tercera línea del mensaje de error.

C:/opt/Dev-Cpp/include/c++/3.4.2/bits/stl_map.h:384: note:                 typename std::_Rb_tree<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator std::map<_Key, _Tp, _Compare, _Alloc>::insert(typename std::_Rb_tree<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator, const std::pair&) [with _Key = std::string, _Tp = std::string, _Compare = std::less, _Alloc = std::allocator >]

Eliminamos los parámetros de patrones innecesarios, reemplazamos tree por map, sustituimos _Key y _Tp por std::string, y eliminamos std y typename. Y con esto llegamos a esta función propuesta:

_Rb_map::iterator std::map<string, string>::insert(_Rb_map::iterator, const pair&)

Esta propuesta ya se parece bastante a nuestra llamada

mi_mapa.insert(const_itr, std::pairstring, std::string>("atributo", "valor"));

Lo que es diferente es el primer parámetro: nosotros usamos un const_iterator mientras la función propuesta requiere un iterator sin const. Si cambiamos la línea

std::mapstring, std::string>::const_iterator const_itr = mi_mapa.begin();

por

std::mapstring, std::string>::iterator ya_no_const_itr = mi_mapa.begin();

entonces compila.

Entender a Microsoft Visual Studio

Como ejercicio puedes usar las mismas técnicas para entender el resultado de compilación del mismo código con Microsoft Visual Studio.

Error	1	error C2664: 'std::_Tree<_Traits>::iterator std::_Tree<_Traits>::insert(std::_Tree<_Traits>::iterator,const std::pair<_Ty1,_Ty2> &)' : no se puede convertir el parámetro 1 de 'std::_Tree<_Traits>::const_iterator' a 'std::_Tree<_Traits>::iterator'

error C2664: 'std::_Tree<_Traits>::iterator std::_Tree<_Traits>::insert(std::_Tree<_Traits>::iterator,const std::pair<_Ty1,_Ty2> &)' : no se puede convertir el parámetro 1 de 'std::_Tree<_Traits>::const_iterator' a 'std::_Tree<_Traits>::iterator'
1>        with
1>        [
1>            _Traits=std::_Tmap_traits,std::allocator>,false>,
1>            _Ty1=const std::string,
1>            _Ty2=std::string
1>        ]
1>        and
1>        [
1>            _Traits=std::_Tmap_traits,std::allocator>,false>
1>        ]
1>        and
1>        [
1>            _Traits=std::_Tmap_traits,std::allocator>,false>
1>        ]
1>        No hay disponible ningún operador de conversión definido por el usuario que pueda realizar esta conversión, o bien no se puede llamar al operador

Conclusión

Al inicio se pueden copiar los mensajes de error largos a un editor de texto y modificar ahí sucesivamente el mensaje hasta sólo queda la parte significativa. Con un poco de práctica, uno consigue hacer estas reducciones en la mente. La mayor parte del mensaje está “contaminado” por parámetros de patrones por defecto, que se necesitan por razones técnicas pero que no se usan como usuario de una clase.

La experiencia enseña también, que la mayoría de los errores son del mismo estilo: mezclar iterator y const_iterator o confundirse en los parámetros o valores de retorno de métodos sobrecargados.

Referencias

Guardar datos de forma segura implica dos necesidades

  1. Recuperar los datos perdidos.
  2. Prevenir que personas no autorizadas puedan acceder a ellos.

El primer punto trata de fallos técnicos o robo, el segundo por la protección de datos mediante contraseñas y encriptación. Ambos dominios son demasiados amplios para ser tratados en detalle aquí, así me limito a dar unas consideraciones a tener en cuenta.

Es casi seguro que alguna vez un dispositivo de almacenamiento deja de funcionar y se pierden todos los datos. Por lo tanto, guardar datos en sólo un sitio es una manera casi segura de perderlos algún día. Por eso se crean redundancias mediante copias de seguridad (back-up en inglés).

La forma más simple y directa es hacer una copia de seguridad en el mismo disco duro. Es una buena manera para guardar cambios pequeños, por ejemplo, para guardar cada hora el documento Word en que estás trabajando actualmente. Conviene mantener varias copias, porque a veces no te enteras que un documento fue contaminado por un virus o dañado de otra forma hasta días después. Por este motivo un sistema de respaldo en tiempo real (RAID) tampoco da toda la seguridad: no copia sólo los documentos en tiempo real, sino también los virus. Sin embargo, es, sin duda, el mejor sistema para protegerse contra fallos de hardware y una necesidad para una base de datos o cualquier otro sistema que nunca debe fallar.

Un back-up en el mismo disco duro no protege contra la pérdida de datos en caso de fallo del disco duro. Por lo tanto, conviene tener un segundo disco duro. Puede estar en la misma máquina o uno externo. Si tu ordenador no sufre una sobretensión que destruye sus componentes, estás a salvo. Más seguro aún es guardar copias de seguridad en un lugar físicamente desconectado del equipo. Basta con desenchufar el disco duro externo o guardar los datos más importante en un pen-drive o un DVD regrabable.

Esto te protege contra una pérdida total del ordenador pero no contra un robo o un incendio en tu casa u oficina. Para prevenir esto puedes guardar datos en la nube, es decir, en algún servidor en Internet. Esto es muy práctico, ya que puedes acceder a estos datos desde cualquier sitio, pero tiene la desventaja que ya no sabes quién más lee y usa estos datos. O igual ya te lo imaginas: empresas que ofrecen publicidad a medida o las autoridades que están constantemente preocupados que algún terrorista todavía no esté contratado o al menos conocido por el servicio secreto de tu país. Una manera de fastidiarles el negocio con tus datos personales es publicarlos en WikiLeaks. Otra es guardarlos en un DVD en la casa de tu abuela. La forma más segura de proteger tus datos sigue siendo no enviarlos por Internet. Y tampoco tiene por qué ser la más lenta. No subestimes el ancho de banda de un camión cargado con DVDs.

Más seriamente, guardar datos importantes fuera de hogar, pero en un sitio de confianza, protege contra males mayores. Igual como guardarías documentos importantes en papel en una caja fuerte en un banco, lo podrías hacer con un DVD con datos que no quieres perder – aunque sean sólo las fotos de tu boda. Y no olvides encriptar los datos si lo ves necesario.

El artículo “Guía definitiva para mantener tus datos seguros en caso de robo o pérdida de tu equipo” por Goyko Alexander Obrenovich Vinces da razones y consejos prácticos sobre elegir contraseñas, sistemas de respaldo e incluso una propuesta de como localizar tu equipo en caso de robo.

Referencias

Este artículo trata de como aprovechar una hoja de cálculo para inicializar tablas en la base de datos.

Dentro de una base de datos guardamos tablas que consideramos “de configuración” ya que no se suelen modificar pero, sí, leerse al menos durante la inicialización del programa. Como estas tablas se modifican pocas veces, es habitual no crear una interfaz gráfica para rellenarlas sino se editan sus datos directamente con una herramienta de administración de la base de datos.

Las tablas de configuración pueden contener datos muy parecidos entre si. Por ejemplo, una tabla de configuración podría definir una red de ordenadores de la siguiente manera:

Ejemplo de una tabla de configuración
Id Nombre Dirección IP
1 Ordenador 1 192.168.0.1
2 Ordenador 2 192.168.0.2
3 Ordenador 3 192.168.0.3
n Ordenador n 192.168.0.n

Como vemos, los datos en cada registro son casi iguales. La parte variable he marcado con una n en la última línea.

Editar una tabla así puede ser pesado cuando se tratan de muchas entradas. Por eso buscamos una manera más sofisticada. Una es crear los comandos INSERT con una hoja de cálculo como Microsoft Excel o Open Office Calc.

Hoja de cálculo para crear comandos INSERT
A B C D
1 Id Nombre Dirección IP Comando SQL
2 1 =”Ordenador ” & A1 =”192.168.0.” & A1 =”insert into mi_tabla values(” & A1 & “, ‘” & B1 & “‘, ‘” & C1 &”‘);”
3 =A1+1 Repite fórmula arriba
4 Repite fórmula arriba

Con “Repite fórmula arriba” me refiero a “copiar hacia abajo” la fórmula de la primera fila.

Con las fórmulas en la hoja de cálculo anterior obtenemos el siguiente resultado.

Hoja de cálculo resultante
A B C D
1 Id Nombre Dirección IP Comando SQL
2 1 Ordenador 1 192.168.0.1 insert into mi_tabla values(1, ‘Ordenador 1’, ‘192.168.0.1’);
3 2 Ordenador 2 192.168.0.2 insert into mi_tabla values(2, ‘Ordenador 2’, ‘192.168.0.2’);
4 3 Ordenador 3 192.168.0.3 insert into mi_tabla values(3, ‘Ordenador 3’, ‘192.168.0.3’);

La última columna podemos copiar y pegar en un fichero de texto y ejecutarlo en el administrador de la base de datos. Así añadimos todas las entradas de golpe sin tener que editar todas las entradas a mano.

insert into mi_tabla values(1, 'Ordenador 1', '192.168.0.1');
insert into mi_tabla values(2, 'Ordenador 2', '192.168.0.2');
insert into mi_tabla values(3, 'Ordenador 3', '192.168.0.3');

Conviene guardar la hoja de cálculo junto con el proyecto para poder fácilmente cambiar la configuración como añadir una nueva fila para un nuevo ordenador o una nueva columna para una característica adicional.

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

Cuando tenemos una tabla dentro de un texto, muchas veces nos parece lo más bonito de colocarla centrada. Sin embargo, hay una diferencia entre navegadores a la hora de colocar una tabla en el centro de la línea. No es posible conseguirlo únicamente con atributos de estilo. Es necesario especificar el atributo align de la tabla.

El siguiente código es una tabla dentro un elemento DIV con alineamiento centrado:

<div style="border: 1pt solid #fbb; text-align: center;">
	Una tabla con una celda
	<table border="1">
		<tr>
			<td>Una celda</td>
		</tr>
	</table>
</div>

Esto aparece en el navegador así:

Una tabla con una celda 

Una celda

Si tienes abierto este artículo en Mozilla Firefox o Opera, entonces la tabla se encuentra alineado a la izquierda. Si lo tienes abierto en Internet Explorer, entonces la tabla se encuentra centrada – aunque no dentro de este artículo (el autor de CSS de WordPress sabrá como). Hay que copiar el código en un fichero HTML vacío para ver este efecto.

Para poder centrar la tabla en los otros navegadores, es imprescindible de usar el atributo align:

<div style="border: 1pt solid #bfb;">
	Otra tabla con una celda
	<table border="1" align="center">
		<tr>
			<td>Una celda</td>
		</tr>
	</table>
</div>

El navegador muestra este código así:

Otra tabla con una celda 

Una celda

Esta representación aparece con el mismo alineamiento en todos los navegadores.

Referencias

La primera tarea del sistema de documentación automatizado doxygen es la creación de un manual de referencia del código del programa. Sin embargo, doxygen ofrece también la funcionalidad de añadir un texto libre. Por la capacidad de doxygen de entender etiquetas de HTML, podemos editar un documento HTML con un editor aparte y luego integrarlo en la documentación generada con doxygen. Esto nos trae la ventaja de poder entregar una documentación completa y actualizada con cada ejecución de doxygen.

Creación de documentación libre

Doxygen conoce varias etiquetas para la creación de documentación libre. En primer lugar están las etiquetas \mainpage, \page y \subpage para la creación y la estructuración de páginas independientes. Dentro de cada página podemos estructurar el texto en apartados con las etiquetas provenientes LaTeX: \section, \subsection, \subsubsection y \paragraph. Conviene guardar la documentación libre en un fichero aparte por no tener correspondencia a ninguna entidad de código en concreto. El sufijo previsto para estos ficheros es dox, es decir, podemos crear una carpeta “doc” o “dox” que incluye todos los ficheros fuente que contienen documentación pero ninguna línea de código ejecutable.

Las etiquetas mencionadas arriba deben encontrarse dentro de un comentario de documentación. Lo más conveniente en este caso es abrir el comentario con /** o /*! en la primera línea y cerrarlo con */ en la última. Doxygen no exige iniciar una línea en medio con *. Ver la documentación de doxygen para un ejemplo.

Lo normal sería escribir la documentación libre en un fichero de texto, pero como ya he mencionado, en este artículo queremos averiguar, si podemos utilizar fuentes escritos en HTML. Dentro de lo que cabe tenemos dos estrategias para usarlas:

  1. Crear un documento HTML que doxygen lee y interpreta como un fichero fuente, es decir, un fichero .dox, y
  2. integrar un documento HTML externo

Ambas maneras tienen ventajas y desventajas que detallaremos a continuación.

Procesar HTML como fichero fuente

Podemos crear un fichero HTML y dejarlo procesar por doxygen como si fuera un fichero fuente. Doxygen entiende la mayoría de las etiquetas de HTML. Las que no entiende son en primer lugar etiquetas de contenido dinámico como OBJECT o EMBED. Esto no es de extrañar ya que doxygen no es un navegador de Internet sino una herramienta de documentación. Lo que duele más que tampoco entiende las etiquetas de cabecera. A contrario de lo que diga la documentación, la versión 1.7.1 que yo probé, no reconoce nada de lo que aparece entre <HEAD> y </HEAD> y tampoco la etiqueta BODY. Estas etiquetas aparecen entonces como cualquier texto normal.

Doxygen tampoco reconoce el comentario de HTML <– –>. Por lo tanto no podríamos esconder la apertura del comentario de documentación /** dentro de un comentario para que no nos aparezca si abrimos el fichero HTML en un navegador. En fin, no es posible crear un documento HTML que queda perfecto tanto en un navegador y en el resultado de doxygen a la vez. Todavía necesitamos un poco de trabajo manual:

Creamos un fichero dox que contiene algo como lo siguiente

/** \page mi_pagina_html Mi página en HTML
<h1>Soy un título</h1>
<p>En esta página podemos hablar de asuntos <em>muy</em> importantes.</p>
*/

Entre la primera y la última línea copiamos todo lo que se encuentra entre las etiquetas <BODY> y </BODY> en el documento HTML. (En este caso sólo he puesto dos líneas.)

La ventaja de esta manera es que doxygen crea una salida homogénea con las otras partes de la documentación. Más interesante aún es que doxygen enlaza los nombres de identificadores con la referencia correspondiente. De esta manera podemos, por ejemplo, crear un manual de usuario de una biblioteca en que aparecen automáticamente las enlaces al manual de referencia de los objetos de que estamos hablando.

Incluir un documento HTML

A veces los documentos HTML no son tan simples para que podamos usarlo directamente con doxygen. Puede haber varias razones. Por ejemplo:

  • El documento HTML se quiere usar tal cual como una página de Internet.
  • El documento usa una hoja de estilo u otros enlaces. En este caso y el anterior no podemos quitar la cabecera.
  • El documento usa contenido dinámico con Javascript.
  • El documento usa contenido empotrado como vídeos o animaciones flash.

En todos estos casos no queremos perder las características del propio HTML. Lo que podemos hacer es crear un pequeño fichero dox con el contenido siguiente:

/** \page mi_otra_pagina_html Mi otra página HTML
\htmlinclude "mi_fichero.html"
(Este documento sólo está disponible en la versión HTML de la documentación.)
*/

Con esta fuente, doxygen crea una página en que incluye el documento HTML “mi_fichero.html”, pero sólo para la versión HTML de la documentación. Desgraciadamente no existe una etiqueta en doxygen, con que podemos escribir algo en todos los formatos menos HTML. Así aparece una página prácticamente vacía en los demás formatos. Sólo se ve la frase “(Este documento sólo está disponible en la versión HTML de la documentación.)“. Esta frase aparece también al final del documento HTML, pero visualmente se impone menos. Existen también comandos exclusivos para otros formatos como \latexonly, pero no podrán sustituir el contenido dinámico de una página HTML, desde luego.

Debemos añadir el directorio, donde guardamos los ficheros HTML a incluir, al EXAMPLE_PATH de la configuración de doxygen. Si no es posible que doxygen no encuentra los ficheros que incluimos.

La ventaja de incluir un documento HTML de esta manera es poder usarlo sin modificación y aprovecharse de toda la potencia que una página WEB ofrece. La desventaja es que doxygen no enlaza nombres de identificadores con el manual de referencia y en los formatos no HTML no aparece ningún su contenido. Sin embargo, en muchos casos esto no supone un problema, ya que no se crea una documentación en otro formato que HTML.

Una aplicación de este truco puede ser la inclusión de un manual de usuario u otro texto libre. Se puede escribir en un editor como Microsoft Word y guardarlo como una página HTML. Luego se incluye esta página mediante \htmlinclude en la documentación de doxygen. Así es posible mantener una documentación completa y actualizada en todo momento.

Una cosa que, por cierto, no funciona es usar el comando \link a un fichero interno para crear un enlace a este documento HTML en la documentación. El enlace, sí, estará en la documentación creada por doxygen, pero no el fichero que enlaza. Esto sólo se puede hacer para documentos con una URL de Internet pública.

Conclusión

Hemos visto dos maneras de incluir documentación adicional al manual de referencia que doxygen crea a partir del código. La capacidad de doxygen de interpretar muchas etiquetas HTML permite incluir documentos escritos en HTML en la documentación. Es una buena opción para documentos no interactivos. Para páginas HTML más complejos se ofrece incluir la página tal cual mediante \htmlinclude, que mantiene toda la potencia de una página interactiva, pero a coste de no poder verlo en los demás formatos que doxygen puede crear.

Referencias

En el artículo “Como organizar ficheros en carpetas” ya he descrito criterios a tener en cuenta a la hora de crear las carpetas de un proyecto informático. En este artículo quiero dar un ejemplo real con comentarios de como se pueden organizar las carpetas en el caso concreto de una aplicación en C++. Aunque se trata de un caso concreto, aporta muchas ideas que se pueden generalizar.

Presento las carpetas en formato de árbol. Es el formato habitual para mostrar una estructura de carpetas, sin embargo aleja mucho los elementos principales de la carpeta raíz que, por lo tanto, se ven más difícilmente en su conjunto. Sin embargo, creo que será suficiente tener este problema en mente.

Los nombres propuestos para cada carpeta aparecen en cursiva. Están en inglés, pero se pueden cambiar según conveniencia. Los nombres de las carpetas usan el guión bajo ‘_’ en lugar de espacios para nombres compuestos por varias palabras. Conviene no usar espacios en las rutas de ficheros, ya que pueden complicar el procesamiento.

Todo lo relacionado con el proyecto se encuentra en una carpeta raíz.

  • cfg: Ficheros de configuración cuyos contenidos dependen de la máquina en que corre el programa. Normalmente sólo habrá uno que contiene información como la dirección IP o el nombre del equipo en la red. Se debería guardar un fichero de configuración por defecto en la carpeta code.
  • code: La carpeta que contiene los ficheros fuentes y es la carpeta raíz para un control de versiones.
    • mi_lib: Cada biblioteca tiene su propia carpeta. El nombre de la carpeta se ajusta lógicamente a lo que hace la biblioteca. Conviene usar un nombre corto que puede usarse como prefijo en los nombres de los ficheros fuentes. También es posible sólo añadir el directorio code a la ruta de inclusiones y incluir los ficheros mediante una ruta relativa como #include "mi_lib/include/mi_cabecera.h". En Java, las carpetas deben tener el nombre del paquete a que corresponden.
      • doc: Ficheros de documentación y tutoriales a compilar junto con la documentación de los ficheros de código por el sistema de documentación automático. La documentación en esta carpeta completa el manual de referencia creado a partir de los ficheros fuente del programa.
      • include: Ficheros de inclusión y otros ficheros públicos.
      • lib: Ficheros “cpp” en C++ u otros ficheros privados. También es habitual llamar esta carpeta src. Yo prefiero no usar este nombre ya que todo dentro de la carpeta code es “source”. De hecho, la carpeta code se podría llamar source.
    • main: Para el programa principal también hay una carpeta. Esta tiene la misma estructura que las demás. En lugar de main se puede usar el nombre del programa como nombre de carpeta. Si tengo varios ejecutables, entonces conviene tener una carpeta main en cuya subcarpeta doc se encuentra la documentación del proyecto y donde se hace referencia a todos los ejecutables y bibliotecas del proyecto – en fin sobre todas las subcarpetas de la carpeta code.
      • cfg: Una carpeta adicional para el programa principal puede ser una carpeta con ficheros patrones para la configuración de la máquina. Estos ficheros se copian a la carpeta cfg y se modifican para la máquina del programador.
    • project: Esta es la carpeta contiene los ficheros creados por el IDE. Cuando añades la carpeta code al control de versiones deberás borrar algunos ficheros no fuentes creados por el IDE. Los nombres de estos ficheros son diferentes para cada IDE.
      • Eclipse: Se usas varios IDE, entonces crea una subcarpeta para cada uno. Eclipse sería un nombre adecuado para el IDE Eclipse. Puede haber otros nombres como NetBeans o VisualStudio. Si no te gustan los entornos gráficos puedes crear una carpeta makefile para ser usado desde una consola.
  • compiled: Todos los ficheros generados. La idea es poder borrar el contenido de esta carpeta por completo para hacer “limpieza”.
    • generated_doc: Documentación generada por sistema de documentación automáticos.
      • html: Esta carpeta crea doxygen para el formato HTML. Habrá más carpetas para otros formatos de documentación como LaTeX.
    • gcc: Una carpeta para cada compilador que se usa. Si sólo uso un compilador, entonces este nivel de carpeta no es necesario.
      • Debug: El compilador de C++ suele crear una carpeta para cada configuración. Una suele llamarse “Debug” y otra “Release”. Puede haber más carpetas para más configuraciones; por ejemplo para enlaces con diferentes bibliotecas.
        • exe: La carpeta de ejecutables contiene todos los ficheros binarios necesarios para utilizar el programa compilado. Puede haber ficheros de configuración por defecto que meramente se necesitan copiar. ¡Incluye esta copia en el makefile! Para una aplicación java, se puede considerar usar el nombre jar en lugar de exe.
        • obj: Ficheros intermedios creado por el compilador y no necesarios para la ejecución del programa. En Java, esta carpeta podría denominarse class.
  • ext: Bibliotecas externas. Cada componente tiene una carpeta propia. Es importante archivar la biblioteca externa junto con el proyecto, ya que una versión más moderna quizá no funcionará con el código propio. Para reducir el uso del disco duro puede ser útil guardar las bibliotecas externas físicamente en una carpeta común y hacerlas aparecer en la carpeta ext mediante un enlace (soft link) del sistema operativo (con el comando ln en Unix.)
  • io: Carpeta principal para ficheros temporales y de entrada y salida del programa. Se entiende que se pueden borrar todos estos ficheros sin perjuicio al funcionamiento del programa. En un entorno real, las subcarpetas de io pueden encontrarse en otros directorios. Esto se debería poder ajustar con los ficheros en la carpeta cfg. Para hacer pruebas sin ensuciar al resto del disco duro, se aprovecha la carpeta io dentro de la carpeta raíz del proyecto. Para archivar un proyecto no haría falta guardar los ficheros dentro de la carpeta io – sólo la estructura de los directorios.
    • in: Ficheros de entrada al sistema. Es la responsabilidad del programa de borrar los ficheros procesados.
    • out: Fichero de salida del sistema.  Es la responsabilidad del programa de no sobrescribir ficheros todavía no procesados. No es su responsabilidad de borrarlos, ya que esto haría el sistema que recibe los ficheros. En un sistema real, la carpeta out sería la carpeta in del sistema siguiente.
    • temp: Ficheros temporales del programa. Es la responsabilidad del programa de borrar los ficheros. Es la responsabilidad del programador que sólo el programa deja sus ficheros ahí y que pueda limpiar la carpeta con un simple “borra todo”.
    • trace: Conviene tener una carpeta especial para trazas.
  • documentation: Documentación que ni es generada ni un código fuente para un sistema de documentación automatizado. Estos son ficheros Word, Excel, PDF, imágenes, emails y cualquier otro editado y procesado por una mente humana.
    • emails: Correos electrónicos con el cliente y otras personas involucradas. No guardes todos los emails aquí. Sólo aquellos que documentan cambios en los demás documentos.
    • manuals: Manuales de usuario o de referencia editado por un ser humano. Aquellos creados por un un sistema de documentación automático se encuentran en la subcarpeta generated_doc de la carpeta compiled.
    • specification: La especificación del proyecto. Para proyectos grandes es importante separar la especificación de otra documentación ya que los ingenieros de prueba usan meramente la especificación para comprobar que un software cumple los requisitos.

Por la longitud del artículo se nota que la creación de un árbol de carpetas para un proyecto no es trivial. Aún así todavía faltan carpetas que pueden ser importantes como una web_doc_root para un proyecto de páginas web. Pero, en fin, es sólo un ejemplo.

Referencias

HTML tiene un elemento SELECT que permite el atributo multiple para poder seleccionar más que una opción a la vez. (Las selecciones mútliples se hacen normalmente pulsando la tecla Control mientras se seleccionan las opciones con el ratón.) El atributo multiple no tiene valor. Si no está, entonces el elemento SELECT sólo permite seleccionar una opción. Este artículo explica como puedo obtener múltiples opciones seleccionadas en un programa PHP.

PHP devuelve los datos enviados desde un formulario en la variable “súperglobal” $_REQUEST. La variable $_REQUEST es una unión de las variables súperglobales $_GET, $_POST y $_COOKIE. Por lo tanto, su contenido no depende si hemos recibido los datos via un “GET” o un “POST”. Es decir, es una buena opción en general.

La variable $_REQUEST es un array asociativo, donde los índices son los valores de los atributos id o name de los elementos de entrada en el formulario HTML enviado. A cada índice se asigna el contenido del elemento correspondiente. En un SELECT simple se devolvería la opción seleccionada si hay.

Por ejemplo, consideramos el siguiente SELECT de selección simple (nótese la ausencia del atributo multiple):

<SELECT id="mi_select">
    <OPTION value="hola mundo">Hola mundo</OPTION>
</SELECT>

Si hubiéramos seleccionado la opción “Hola mundo”, entonces, en PHP, la expressión $_REQUEST['mi_select'] tendría el valor 'hola mundo'. (El valor del atributo value del elemento OPTION.)

Sin embargo, si el elemento SELECT tuviera puesto el atributo multiple, entonces sólo obtendríamos el valor de la última opción seleccionado. Para obtener todas las opciones, el valor de $_REQUEST['mi_select'] debería ser un array. Esto conseguimos cambiando el id del elemento SELECT a mi_select[]. (Esto es el mismo nombre más los corchetes [].)

<SELECT id="mi_select[]" multiple>
    <OPTION value="hola mundo">Hola mundo</OPTION>
</SELECT>

Este pequeño truco causa que $_REQUEST['mi_select'] es un array. Ahora puedo hacer un bucle sobre todas las opciones seleccionadas.

foreach ($_REQUEST['mi_select'] as $option_value)
{
    // Haz algo con el valor de la opción seleccionada $option_value
}

En corto: Asigna un nombre terminado en corchetes “[]” a un elemento SELECT cuando tiene puesto el atributo multiple para poder extraer todas las opciones seleccionadas desde PHP.

Referencias

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

Únete a otros 56 seguidores

Archivos

febrero 2020
L M X J V S D
« Nov    
 12
3456789
10111213141516
17181920212223
242526272829