Programación En C: Enumeraciones, Bit-Fields y Uniones

Este artículo es parte de una serie – Cómo programar cualquier cosa: C Programación

Prefacio

Hasta ahora hemos cubierto tipos de datos básicos, tipos de datos agregados, como estructuras, matrices, punteros, etc. Hay tres tipos de datos, que denomino misceláneos, para tratar en C y estos son Enumeración, Campos de bits y Uniones. Estos últimos de estos tres son algo esotéricos y no necesariamente se usan mucho, excepto cuando se trata de hardware o la implementación de compiladores y demás. Las enumeraciones son básicamente una forma de especificar un tipo de datos que asigna claves dadas a números enteros o constantes. Un campo de bits le permite almacenar varios identificadores diferentes como un mapa en los bits de uno o más bytes, pero debido a su naturaleza puede ser específico del compilador de C. Los sindicatos son tipos que le permiten acceder al mismo espacio en la memoria bajo los auspicios de diferentes tipos de datos, por lo que, por ejemplo, puede acceder a la misma dirección en la memoria como una matriz de caracteres de dos elementos o una breve int.

Enumeraciones

Las enumeraciones son conceptos bastante simples. Una enumeración es una secuencia de constantes enteras donde a cada entero se le asigna un nombre. Es decir, podría usar el nombre “uno” para 1, “deux” para 2, “tres veces” para 3, etc. No se limitan a un tipo de datos, lo que significa que una variable puede ser del tipo del enumeración para aprovechar la asignación de los nombres de la enumeración, pero también puede usar los nombres definidos en la enumeración para representar sus valores enteros correspondientes. Las enumeraciones se definen como estructuras, pero con sus propias idiosincrasias. Un fragmento de código puede ayudar a ilustrar:

En este ejemplo de código, he definido una enumeración de todos los meses del año. Cada mes es un identificador que representa un valor entero. De hecho, cada nombre representa un valor entero 1 mayor que el anterior. En este ejemplo enero = 0, februray = 1, marzo = 3, etc. En el fragmento de código he mostrado cómo puede hacer que una variable se limite a los valores encontrados en la enumeración usando enum como tipo de datos, pero también puede use los nombres de enumeración “fuera” de su alcance de tipo de datos. En la última instrucción en este ejemplo, “imprime” el valor de may a la salida estándar (la pantalla). Todavía no hemos cubierto este tipo de funcionalidad, pero puede ver que puedo usar may out de myMonth.

En este ejemplo, los nombres de los meses se asignan a los números enteros correspondientes contados desde el inicio de 0. Es posible especificar los valores de un enumerador asignando el nombre a ese valor dentro de la declaración enum. Todos los identificadores que vienen después de ese nombre agregan uno al valor dado. Un ejemplo de esto sigue:

En esta enumeración, primero sería igual a 0 y el segundo sería 1. Sin embargo, cuando llegamos a la tercera lo configuramos para que iguale el valor de 300. Esto altera la definición de cuarto y quinto, que ahora respectivamente son iguales a 301 y 302. Tenga en cuenta que solo puedes asignar enteros a los valores enum.

Del fragmento de código anterior puede ver que el formato genérico de una declaración enum es el siguiente:

Las enumeraciones son útiles cuando desea una lista de identificadores que no interfieren entre sí en cuanto al valor. Supongamos que quiere referirse a una gran cantidad de nombres que podrían enumerarse en otra parte, no como sus valores, sino por sus identificadores. Los enum servirían bien para este propósito. Un ejemplo podría ser todos los códigos de operación de un compilador. Mientras escribes el compilador, puedes referirte a cada código de operación usando un nombre legible para los humanos, pero para la computadora sería un valor entre muchos. Podríamos hacer lo mismo con las variables y constantes globales, pero las enumeraciones son una solución mucho más elegante, ya que se pueden empaquetar en un tipo de datos.

Las enumeraciones no son cadenas

Hay una advertencia importante cuando se trata de enumeradores. El programador debe recordar que una enumeración es simplemente un identificador para un número. En el contexto del programa en ejecución o compilado, el identificador no tiene importancia. Esto significa que no puede esperar recuperar los identificadores en las operaciones del programa. Por ejemplo, después de nuestro enumerador de meses, lo siguiente no funcionaría:

En el código myMonth se le asigna el código entero que april representa en la enumeración. myMonth en realidad no es igual al identificador abril. Asignar myMonth como si fuera una cadena es un tipo no coincidente. Todos los enumeradores son enteros. Para asignar un valor de enumeración a una contraparte de cadena, tendría que crear una instrucción de conmutación (no hemos cubierto aún las instrucciones de conmutación), que probaría la enumeración para ver si era un valor particular y luego generaría una cadena. , o podría crear una tabla de búsqueda.

Una tabla de búsqueda consistiría en una serie de cadenas, que es una matriz de cadenas, que imitaría a los identificadores en la enumeración. Una tabla de consulta para nuestra enumeración de meses podría ser similar a la siguiente:

Esto funciona porque ningún identificador en el enumerador se inicializó a algo especial, como 100. Si hubiera una inicialización especial, este tipo de búsqueda de tabla tendría dificultades para funcionar y se tendrían que hacer adaptaciones especiales. Si quisiéramos obtener la versión de cadena de un valor de enumerador en este escenario, entonces, después de nuestro enumarador de meses anteriores, escribiríamos:

Uniónes

A veces querrá referirse a la misma pieza de memoria que contiene datos de dos maneras diferentes. En algunos casos, desea trabajar con él como si fuera un flotador, pero en otros casos desea usarlo como si fuera una matriz de caracteres, accediendo a los mismos datos exactos en la memoria byte por byte. Quizás harías esto para alterar y manipular el formato de precisión del doble, o redondearlo. O tal vez quiera una forma de descomponer rápidamente un flotador en bytes, luego puede escribir en un archivo rápidamente. A veces, los compiladores usan las uniones como valores intermedios, lo que permite que una unión actúe como muchos tipos diferentes de estructuras de datos. Estos ejemplos son donde usarías una unión.

Una unión es precisamente lo que acabamos de describir. Vamos a configurar nuestro flotador que es interpretable como cuatro caracteres a continuación:

Como puede ver, su construcción se parece mucho a la estructura. En este ejemplo, el float f y el char ch [4] tomarían el mismo espacio. Como una estructura, si deseamos acceder a los elementos de una unión, usaríamos el operador punto (.) O el operador flecha (->) dependiendo de si era un puntero o ahora. Esto se volverá más claro en el código aquí, configuremos una variable de unión y luego accedamos a los diversos bytes de datos usando el operador de punto (.):

Cuando se declara una variable de unión, el compilador crea espacio en la memoria igual al miembro más grande de la unión dada, en este caso, el flotante. Si tuviéramos un miembro de una unión de 24 bytes de tamaño, digamos una matriz de caracteres, cualquier tamaño inferior a ese tamaño se asignaría a esa asignación de memoria. Dicho esto, la huella de memoria de cualquier variable de unión dada es igual a la declaración de datos más grande de la unión. Las uniones son útiles cuando desea utilizar el mismo espacio de memoria como, por ejemplo, una matriz de entradas cortas, pero luego necesita hacer un trabajo más cuidadoso byte por byte, para que pueda acceder a él como una serie de caracteres en su lugar.

Bit-Fields

Como C es un lenguaje de nivel medio, algunas de sus características están orientadas a la configuración exacta de bits y la recuperación de bits. Estudiaremos más a fondo los operadores bit a bit en nuestros artículos sobre expresiones C, pero por ahora nos centraremos en un tipo de estructura / estructura integrada que permita a los programadores acceder a bits específicos en datos. Esto se denomina campo de bits, y su soporte es algo diferente dependiendo de su entorno informático y compilador. Sin embargo, dicho esto, los campos de bits pueden ser útiles cuando se quiere hacer referencia a un bit en particular de un dato por nombre. Muchas veces, cuando se trabaja con hardware, es posible que nos devuelva información compactada, en la que diferentes piezas se codifican en uno o varios bits de un byte. O tal vez desea una forma práctica de acceder a cada bit de un byte dado porque ha configurado lo que se conoce como indicadores, valores de activación o desactivación, que ha compactado en un byte en lugar de mantener una variable para cada estado.

Los campos de bits deben declararse dentro de las estructuras (ver nuestro artículo anterior) o los sindicatos, no pueden existir por sí mismos. Debido a esto, cubriremos la forma genérica básica de un campo de bit, er, campo de la siguiente manera:

Los campos de bits solo pueden tener tres tipos básicos (o cuatro si está usando C99). Los tipos permitidos son int, signed y unsigned (y _Bool si usa C99). Para dar sentido a cómo podrían utilizarse los campos de bits, imagínense si tuviéramos una pieza de hardware que devolviera el siguiente byte de información, cada bit significa algo. Para tomar prestado de Wikipedia, supongamos que tenemos un registro de estado de un procesador 6502 (8 bits). Cada vez que obtenga el estado de este registro de estado, obtendrá la siguiente información de cada bit:

  • Bit 7. Negative flag
  • Bit 6. Overflow flag
  • Bit 5. Unused
  • Bit 4. Break flag
  • Bit 3. Decimal flag
  • Bit 2. Interrupt-disable flag
  • Bit 1. Carry flag
  • Bit 0. Zero flag

Por lo tanto, podría crear una estructura usando campos de bits para corresponder a este registro de estado. Esto me permitiría especificar un bit en particular en el registro con un nombre legible para el ser humano:

Notará una de las definiciones peculiares de un campo de bit debajo del campo overflowFlag. Esto se debe a que ese bit no se utiliza, y podemos instruir al compilador para que ‘omita’ esa cantidad de bits. Por qué darle un nombre de identificador si nunca vamos a usarlo, así que sin un nombre de identificador simplemente nos movemos sobre él.

Para acceder al valor de un campo de bits podemos acceder a él como cualquier otro campo de estructura, mediante el uso del operador punto (.) Y flecha (->), si se trata de un puntero, respectivamente. En realidad, no solo podemos recuperar campos de bits, sino que también podemos asignarlos. Aceptan o entienden literales enteros hasta la cantidad de bits que los componen. Por ejemplo, un campo de bits de 3 bits puede calcular hasta 7, mientras que un campo de bits de 4 bits puede calcular hasta 15.

Es posible mezclar campos de bit con elementos de estructura normal. Desde nuestro artículo de estructura, ampliaremos nuestro ejemplo. En ese artículo, construí una estructura de empleados que contenía diversa información sobre los empleados. Supongamos que quisiera hacer un seguimiento para ver si actualmente estaban trabajando o suspendidos, y además si eran empleados o salario por hora. Podría tomar dos bytes completamente nuevos con esa información, asignando cada uno a un char. Sin embargo, con bit-fields puedo almacenar esos dos “flags” de bit en un byte:

NOTA: ¡Los campos de bits tienen restricciones! No se pueden ordenar, y no se puede obtener la dirección de un campo de bits. Esto se debe a que son más pequeños que la unidad direccionable más pequeña: un byte. Además, dependiendo de la máquina que el programa se está ejecutando en los campos, puede ejecutarse de derecha a izquierda o de izquierda a derecha. Por lo tanto, si escribió la información en un archivo, no puede estar seguro si lo lee en una máquina diferente si los datos se duplican de manera genuina. Por lo tanto, cuando utiliza campos de bits, puede estar introduciendo dependencias específicas de la máquina en la ejecución de su programa.

Conclusión

Las estructuras, como se estudió en el artículo anterior, son muy útiles, pero no son el único tipo de datos avanzados en la ciudad. Las enumeraciones nos permiten especificar identificadores para valores enteros sin tener que especificar una larga lista de variables o constantes globales. Por ejemplo, en este artículo creamos una enumeración que numeraba todos los meses del año. También cubrimos los sindicatos, que son un poco más esotéricos, pero useul de todos modos. A veces necesitamos acceder a una matriz de bytes largos como bytes de caracteres individuales, ¡con una unión puede hacer eso! Por último, nos redondeamos con campos de bits: una programación C incorporada que nos permite identificar y trabajar con bits específicos por nombre. En todos estos tipos de datos, incluidas las estructuras, la clave aquí es que estamos configurando maneras de identificar piezas de memoria muy específicas por nombre. Si no pudiéramos acortar algunos de estos arreglos a nombres de identificadores singulares, nuestro código se repetiría una y otra vez a medida que continuáramos abordando las mismas piezas una y otra vez.

Con los tipos de datos básicos y avanzados a nuestro alcance, ahora podemos comenzar a ver qué hacer con todos estos datos. El primer paso en ese camino es una estadía a través de las expresiones y los operadores que los componen. Cubriremos operadores aritméticos, operadores bit a bit, operadores lógicos y relacionales, y operadores de programa (como me refiero a ellos). Espero poder iluminar algo de tipos de datos avanzados en el lenguaje C. ¡Gracias por leer!

Este artículo es parte de una serie – Cómo programar cualquier cosa: Programación C

Si usted aprecia este artículo usted puede ser que considere el apoyar de mi Patreon.

Pero si un compromiso mensual es un poco más, lo entiendo, podría considerar comprarme un café.

photo credit: Kyle McDonald small-enumeration via photopin (license)

También te podría gustar...

Deja un comentario

A %d blogueros les gusta esto: