Hola andreumic,
¡Qué problema más bonito éste del que hablas! Realmente me parece muy interesante, de modo que trataré de contribuir un poco de ayuda, en cuanto me sea posible.
Personalmente, este tipo de temas me entusiasman mucho, de modo que puede que me extienda un poco sobre cosas relacionadas con el tema... espero no hacer una disertación muy aburrida :). Parte de la información que voy a mencionar posiblemente ya la conozcas, pero me parece importante incluirla, aunque fuese al menos por un sentido discreto de completitud (jeje, parece que me hubieran regalado un diccionario en Navidad...).
Ciertamente es una buena idea diseñar un sistema coherente y eficaz para almacenar datos en tus aplicaciones. Bien podría decirse que en la actualidad, con el advenimiento de elementos de
hardware más poderosos, sofisticados y económicos (considerablemente más económicos que hace unos años), el problema de almacenar datos, desperdiciando unos cuantos bits en el proceso, no es gran cosa. Pero, por una parte, difícilmente podría alguien objetar al diseño de soluciones más eficaces, cuando el costo adicional en términos de desarrollo se limita a un poco de planeación y sentido común.
Por otra parte, no se trata sólo del aprovechamiento del espacio en disco, también se trata de la organización. Creo que muchos programadores podrían estar de acuerdo conmigo si digo que, usualmente, cuando se trabaja con formatos de almacenamiento de datos, resulta mucho más importante (a veces en varios órdenes de magnitud) que el formato mismo esté diseñado de forma coherente y flexible, que otras cosas como su nivel de optimización orientado al tamaño de almacenamiento, o rapidez de procesamiento.
Es decir, cuando se manipula información sobre archivos mediante algún formato particular, suele resultar más importante la capacidad de este formato para describir información, que su capacidad de ahorrar espacio en disco, u otras propiedades colaterales que se concentran más en problemas específicos de implementación.
De hecho, al llegar un formato dado a cumplir con su cometido, cuando de
describir información eficazmente se trata, otras propiedades secundarias como su capacidad de almacenar información en forma concisa, suelen venir por añadidura. Por ejemplo, en la actualidad es posible apreciar los efectos que conlleva el uso de un formato dado como el XML, el cual, dígase lo que se diga, es un formato increíblemente poderoso para especificar y almacenar meta-información (información sobre otra información). Concretamente, es posible apreciar en forma general los beneficios de este tipo de solución echándole un vistazo a cierto tipo de aplicaciones como, por ejemplo, los procesadores de texto.
En la actualidad existen procesadores de texto que usan como formato nativo un dialecto de XML (o SGML), y al compararlos con formatos que usan otras aplicaciones (formatos usalmente privados e intrincados), resulta evidente la superioridad en campos como el espacio de almacenamiento y rapidez de procesamiento de los formatos más flexibles y que se concentron en los datos mismos, no en cosas secundarios como la ofuscación de la información para conservar la privacidad del formato... Pero la superioridad técnica del XML no está en la disponibilidad de analizadores sintácticos (o
parsers) eficaces, ya que éstas son solo propiedades emergentes. El valor real del XML está en su capacidad de describir información.
Vaya, ya llevo varios párrafos, y ni siquiera me he referido a los bits. En conclusión, para continuar con la parte divertida (el código), mi recomendación inicial es que no escatimes esfuerzos en la planeación de tus formatos de almacenamiento de información. Un poco de planeación puede representar una diferencia significativa a largo plazo, cuando tus programas sean más sofisticados y tus necesidades como desarrollador evolucionen.
Ahora sí, hablemos de bits,
C, fopen(), etc. El tipo de cosas que seguramente esperabas desde un comienzo.
Antes que nada, algunos recursos y construcciones del lenguaje que me parece que son muy útiles a la hora de trabajar con bits son:
(nota: voy a referirme a C, ya que es mi lenguaje de preferencia (al menos sobre la alternativa de C++))
* Las enumeraciones. Los símbolos declarados con `enum' suelen ser extremadamente útiles. En realidad, a veces pienso que `enum' es un recurso del lenguaje altamente subestimado. Claro está que muchos programadores suelen cubrir la necesidad que `enum' está diseñada para suplir mediante macros usando la directiva `define', por ejemplo:
Código:
/* Conjunto de banderas particular creadas para manipular bits */
#define PROPIEDAD_UNO 1
#define PROPIEDAD_DOS 2
#define PROPIEDAD_TRES 4
#define PROPIEDAD_CUATRO 8
Al usar enumeraciones (tal y como el Señor manda), la solución resulta natural, y contamos con la ventaja adicional de poder crear un tipo de dato especial, en lugar de limitarnos a `int':
Código:
/* Forma bonita de definir los simbolos para el manejo de banderas. De
* este modo, los simbolos PROPIEDAD_* estan `atados' a un tipo de
* datos particular */
typedef enum _MiDato MiDato;
enum _MiDato
{
PROPIEDAD_UNO = 1,
PROPIEDAD_DOS = 2,
PROPIEDAD_TRES = 4,
PROPIEDAD_CUATRO = 8
};
* Los campos de bits. Otro recurso, extremadamente útil en ciertos casos particulares, es la definición de campos de bits en estructuras de C:
Código:
struct paquete {
unsigned int bandera1:1;
unsigned int bandera2:1;
unsigned int corto:4;
unsigned int raro:9;
};
En el anterior ejemplo se definen dos campos de 1 bit de tamaño cada uno, un campo de 4 bits, y otro un poco curioso de 9 bits. Esta capacidad de definir explícitamente el número de bits de un campo en una estructura es muy útil cuando se trabaja sobre registros que están diseñados en una forma particular, en donde algunas campos no cuentan necesariamente con tamaños que correspondan a los tamaños de los tipos de datos nativos en C.
Lamentablemente, esta estratecia suele contar con restricciones dadas por el compilador de C que se utilice, y la arquitectura de la máquina en donde se trabaja.
* Y finalmente, por supuesto, están los operadores de bits de C: & , |, ^, ~, << y >>. En su orden, los operadores implementan las funciones: and, or, xor, complemento, translado de bits a izquierda, translado de bits a derecha.
Los operadores de bits son otro recurso sub-aprovechado con frecuencia. De hecho, no es raro ver listados de código que en ocasiones realizan operaciones complicadas e ineficientes (recurriendo incluso a veces a realizar transformaciones entre valores enteros y reales), cuando en efecto cosas como un simple translado de bits podría lograr el resultado deseado. Algunos lo mencionan como anécdotas graciosas, pero se trata de un problema real del que a veces se padece a la hora de programar.
Bien, pasemos ahora a un ejemplo concreto, en donde implementemos el tipo de cosas que incluyes en tu enunciado (particularmente, la lectura/escritura de bits en archivos). Como sabrás, el acceso a disco se realiza mediante paquetes de bytes (u
octetos, como se les llama en España). Estos bytes constituyen la unidad más pequeña de operación cuando se desea "conversar" con los sistemas de archivo actuales. Esto quiere decir que a la hora de hacer manipulaciones particulares de bits, debe tenerse en cuenta este detalle: todas las operaciones, en últimas, deben expresarse en paqueticos de a ocho. Por fortuna, esto no es muy problemático. Después de todo, hemos crecido en la cultura de los bytes, ¿no es así?
Pero suficiente charla. Pasemos al ejercicio (por fin...).
(continúa)