photo-1414509902153-26bed16bc962_red.jpg

En ocasiones, en C, sobre todo, una misma variable numérica (un entero, un short, o incluso un char) encierra en sus bits más valores que nos pueden resultar interesantes por separado. Sobre todo, por optimizar la memoria, en lugar de reservar dos variables de tipo entero de 32bits para almacenar valores pequeños, queramos empaquetar en el mismo valor entero los dos valores y luego extraerlos cuando los necesitemos. Esto lo podemos ver en sistemas empotrados en los que estamos muy limitados en memoria.

De la misma forma, si estamos trabajando con grandes bases de datos, con miles de registros (o millones), podremos ahorrar varios megabytes de espacio si reunimos varios valores pequeños en un mismo valor más grande.

En este caso, nos vamos a centrar en una variable de tipo unsigned short (normalmente, 16bits), aunque podremos verificar el tamaño en nuestro sistema con sizeof(). Dicha variable de tipo short, queremos descomponerla en dos variables de tipo char a las que llamaremos parte alta a la parte más significativa de la palabra (es decir, a la que más peso tiene), y parte baja a la que menos peso tiene.

Por ejemplo, el número 43679 podemos representarlo en binario como 10101010 10011111, la parte alta sería 10101010 y la baja 10011111. ¿ Cómo podemos extraer dichos valores en C ?

Nota, hecho en Little Endian

Algunos de estos ejemplos dependerán del sistema que use nuestra plataforma para representar los números de más de un byte. Estos ejemplos están probados en un sistema little endian, con un procesador Intel, puede que si usas otro tipo de CPU, los resultados no sean los mismos, ya que los números grandes de más de un byte, se almacenarán de forma diferente.

Como destino, variables unsigned char

Como el tamaño de cada una de las partes es 8bits, vamos a almacenar los valores resultantes en un unsigned char, de esta forma, nos quitaremos muchos problemas, ya que la parte baja la podemos sacar directamente, y en este caso, la parte alta la calcularemos desplazando los bits del número original a la derecha (operador >>):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
  /* Un número grande, en binario sería 10101010 10011111 */
  /* Como vamos a separar los dos bytes, queremos algo que sea
   fácil de ver. Por un lado sacaremo la parte alta 10101010 y por
   otro la parte baja 10011111*/
  unsigned short numero=43679;
  unsigned char partealta = numero >> 8;
  unsigned char partebaja = numero;

  printf ("Numero: %d\n", numero);
  printf ("Parte alta: %d\n", partealta);
  printf ("Parte baja: %d\n", partebaja);

  return 0;
}

Como destino, variables tipo unsigned char

Pero claro, esto de hacer partebaja = numero, no nos valdrá si el tamaño de la variable resultante es mayor al número de bits que tenemos. Ahora mismo, al tener la variable resultante partebaja el número de bits exactos que queremos extraer se van a copiar tal cual desde número y como partebaja es más pequeña que número sólo se copiará una parte.

 

En otro caso, si en la variable caben más bits de los necesarios, necesitamos transformar el número, dos operaciones que me vienen a la cabeza son:

  • Desplazar número primero a la derecha y luego a la izquierda: al desplazar número a la derecha, la parte alta se perderá, y al desplazar a la izquierda de nuevo, en la parte alta habrá ceros. Con este método tenemos que tener muy en cuenta los tamaños. Por ejemplo:
    1
      unsigned short partebaja = (unsigned short)(numero << 8) >> 8;
  • Utilizar una máscara para extraer el número, en este caso la máscara sería 00000000 11111111, es decir, 255 en decimal. Con esto queremos que sólo se copien los valores que están a uno, por lo que ignoraríamos la parte alta y sólo nos quedaríamos con la baja:
    1
      unsigned short partebaja = numero & 255;

    Al igual que 255 podemos escribir 0xFF, es decir, su valor hexadecimal. Pero claro, si eso lo consideráis un poco feo al escribir un número a pelo, aunque sea un valor como este, siempre podéis decirle a C que lo calcule. Es decir, lléname un short de unos y luego desplázalo a la derecha, con lo que nos quedaría la máscara deseada:

    1
      unsigned short partebaja = numero & (unsigned short)(~0U) >> 8;

    O incluso podemos llenar directamente un unsigned char de unos y listo, no tenemos que desplazar nada:

    1
      unsigned short partebaja = numero & (unsigned char)~0U;

Un inciso para ver los valores en binario

Está muy bien ver los valores con números enteros, pero podemos ayudarnos de un pequeño código que publiqué hace unos días para visualizar el valor en base2 de un número. El tema quedaría así:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdlib.h>
#include <stdio.h>

#define typetobin(type) const char* type##_a_binario(type num)  \
  {                             \
  return getBits(sizeof(type), &num);               \
  }

#define extypetobin(mod, type) const char* mod##type##_a_binario(mod type num)  \
  {                             \
  return getBits(sizeof(type), &num);               \
  }

const char* getBits(size_t const size, void* ptr);

typetobin(float);
typetobin(double);
typetobin(char);
typetobin(short);
extypetobin(unsigned, short);
typetobin(int);

int main(int argc, char *argv[])
{
  /* Un número grande, en binario sería 10101010 10011111 */
  /* Como vamos a separar los dos bytes, queremos algo que sea
   fácil de ver. Por un lado sacaremo la parte alta 10101010 y por
   otro la parte baja 10011111*/
  unsigned short numero=43679;
  unsigned short partealta = numero >> 8;
  unsigned short partebaja = numero & (unsigned char)~0U;

  printf ("Numero: %d (%s)\n", numero, unsignedshort_a_binario(numero));
  printf ("Parte alta: %d (%s)\n", partealta, unsignedshort_a_binario(partealta));
  printf ("Parte baja: %d (%s)\n", partebaja, unsignedshort_a_binario(partebaja));

  return 0;
}

const char* getBits(size_t const size, void* ptr)
{
    unsigned char *b = (unsigned char*) ptr;
    unsigned char byte;
    int i, j;
    char *resultado = (char*)malloc(100);
    char *res = resultado;

    for (i=size-1;i>=0;i--)
    {
        for (j=7;j>=0;j--)
        {
            byte = b[i] & (1<<j);
            byte >>= j;
        *res='0'+byte;
        res++;
        }
    *res++=' ';
    }

    *res='\0';
    return res-size*9;
}

Usando punteros

Puede que este método sea más fácil si tenemos manejo con punteros. En este caso, tendremos lo siguiente:

1
2
3
  unsigned short numero=43679;
  unsigned char *partealta = (unsigned char*)&numero+1;
  unsigned char *partebaja = (unsigned char*)&numero;

aquí podríamos controlar el caso en el que compilemos en un big-endian cambiando el nombre de las variables *partealta y *partebaja.

Dejo también el ejemplo para copiar y pegar, y ver los valores en binario:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdlib.h>
#include <stdio.h>

#define typetobin(type) const char* type##_a_binario(type num)  \
  {                             \
  return getBits(sizeof(type), &num);               \
  }

#define extypetobin(mod, type) const char* mod##type##_a_binario(mod type num)  \
  {                             \
  return getBits(sizeof(type), &num);               \
  }

const char* getBits(size_t const size, void* ptr);

typetobin(float);
typetobin(double);
typetobin(char);
typetobin(short);
extypetobin(unsigned, short);
typetobin(int);

int main(int argc, char *argv[])
{
  /* Un número grande, en binario sería 10101010 10011111 */
  /* Como vamos a separar los dos bytes, queremos algo que sea
   fácil de ver. Por un lado sacaremo la parte alta 10101010 y por
   otro la parte baja 10011111*/
  unsigned short numero=43679;
  unsigned char *partealta = (unsigned char*)&numero+1;
  unsigned char *partebaja = (unsigned char*)&numero;

  printf ("Numero: %d (%s)\n", numero, unsignedshort_a_binario(numero));
  printf ("Parte alta: %d (%s)\n", *partealta, char_a_binario(*partealta));
  printf ("Parte baja: %d (%s)\n", *partebaja, char_a_binario(*partebaja));

  return 0;
}

const char* getBits(size_t const size, void* ptr)
{
    unsigned char *b = (unsigned char*) ptr;
    unsigned char byte;
    int i, j;
    char *resultado = (char*)malloc(100);
    char *res = resultado;

    for (i=size-1;i>=0;i--)
    {
        for (j=7;j>=0;j--)
        {
            byte = b[i] & (1<<j);
            byte >>= j;
        *res='0'+byte;
        res++;
        }
    *res++=' ';
    }

    *res='\0';
    return res-size*9;
}

 

Foto: Jay Wennington (Unsplash)

Fuente: poesiabinaria

¿Quién está en línea?

Hay 6682 invitados y ningún miembro en línea