Type punning представляет изменение типа переменной без изменения ее представления в памяти. На русском языке нет адекватных переводов, но приблизительно можно перевести как "двусмысленность" или "двойственность" типов/типизации (значение одного типа можно принять за значение другого типа). Например, если необходимо отправлять 32-битные значения float, используя протокол связи, который может отправлять только один байт за раз, то придется сначала необходимо преобразовать число float в тип, которым можно манипулировать побайтно. Обычно это означает, что в программном обеспечении его следует рассматривать как целочисленный тип. Поскольку приведение типов меняет используемое представление, необходимо использовать type punning.
Есть два широко используемых метода type punning: использование указателей и использование объединений (union). В кратце рассмотрим оба метода. Но следует сразу отметить, что применяемые типы должны иметь совместимое выравнивание/размер в памяти.
Применение указателей заключается в преобразование указателя на исходный объект к указателю на целевой тип с последующим разыменованием. Например, преобразование из набора беззнаковых байтов в 32-разрядное целое число со знаком:
int32_t int32_from_bytes(uint8_t* buffer) { // Преобразуем указатель на байты в указатель на int32 и разыменовываем return *((int32_t*) buffer); }
Рассмотрим на примере:
#include <stdio.h> #include <stdint.h> // Преобразуем набор байтов в 32-разрядное целое число со знаком int32_t int32_from_bytes(uint8_t* buffer) { // Преобразуем указатель на байты в указатель на int32 и разыменовываем return *((int32_t*) buffer); } //Преобразуем 32-разрядное целое число со знаком в набора байтов void int32_to_bytes(int32_t value, uint8_t* buffer) { // первый элемент массива байтов - первый (самый младший) байт числа int32 buffer[0] = (uint8_t)value; buffer[1] = (uint8_t)(value >> 8); // второй байт числа int32 buffer[2] = (uint8_t)(value >> 16); // третий байт числа int32 buffer[3] = (uint8_t)(value >> 24); // четвертый байт числа int32 } int main(void){ uint32_t number = 0x05040302; uint8_t bytes[4]; // преобразование из int32_t в uint8_t int32_to_bytes(number, bytes); // для проверки выводим на консоль for(int i=0; i< 4; i++){ printf("bytes[%d]: %d\n", i, bytes[i]); } // обратное преобразование из uint8_t в int32_t uint32_t val = int32_from_bytes(bytes); printf("Restored number: %#x\n", val); return 0; }
В функции int32_to_bytes()
преобразуем число типа int32_t
в набор байтов - четырех значений uint8_t
. Для этого применяем операции сдвига:
void int32_to_bytes(int32_t value, uint8_t* buffer) { // первый элемент массива байтов - первый (самый младший) байт числа int32 buffer[0] = (uint8_t)value; buffer[1] = (uint8_t)(value >> 8); // второй байт числа int32 buffer[2] = (uint8_t)(value >> 16); // третий байт числа int32 buffer[3] = (uint8_t)(value >> 24); // четвертый байт числа int32 }
После этого параметр buffer будет содержать все байты числа value из первого параметра.
В другой функции - int32_from_bytes
применяем type punning, обратно преобразуя набор из байтов в число int32_t
:
int32_t int32_from_bytes(uint8_t* buffer) { // Преобразуем указатель на байты в указатель на int32 и разыменовываем return *((int32_t*) buffer); }
То есть выражение (int32_t*) buffer
получает указатель типа (int32_t*)
, который затем разыменовывается с помощью выражения *((int32_t*) buffer)
В функции main выполняем преобразование из числа int32_t в набор байтов и обратно. Консольный вывод программы:
c:\C>gcc -Wall -pedantic app.c -o app & app bytes[0]: 2 bytes[1]: 3 bytes[2]: 4 bytes[3]: 5 Restored number: 0x5040302 c:\C>
При использовании объединений нам надо определить union, который содержит значения обоих типов. Например:
union { uint8_t bytes[4]; float number; } type_swap;
Здесь объединение type_swap имеет размер в 4 байта. При этом на этих 4 байтах может располагаться либо 4 значения uint8_t
, либо одно значение float
.
Рассмотрим на примере:
#include <stdio.h> #include <stdint.h> #include <string.h> // Считываем 32-разрядное целое число со знаком из набора байтов void float_to_bytes(float value, uint8_t* buffer) { // применяем библиотечную функцию memcpy memcpy(buffer, &value, sizeof(float)); } // получаем значение float из массива байт float float_from_bytes(uint8_t * buffer) { union { uint8_t bytes[4]; float number; } type_swap; type_swap.bytes[0] = buffer[0]; type_swap.bytes[1] = buffer[1]; type_swap.bytes[2] = buffer[2]; type_swap.bytes[3] = buffer[3]; return type_swap.number; } int main(void){ float num = 3.1415; uint8_t bytes[sizeof(float)]; // преобразование из float в uint8_t float_to_bytes(num, bytes); // обратное преобразование из uint8_t во float float val = float_from_bytes(bytes); printf("Restored number: %f\n", val); return 0; }
Здесь функция float_to_bytes()
принимает значение float и набор байтов и с помощью библиотечной функции memcpy
копирует байты из float в массив uint8_t
void float_to_bytes(float value, uint8_t* buffer) { // применяем библиотечную функцию memcpy memcpy(buffer, &value, sizeof(float)); }
Функция float_from_bytes()
выполняет восстановления из массива байт, который передается в качестве параметра, в число float
:
float float_from_bytes(uint8_t* buffer) { union { uint8_t bytes[4]; float number; } type_swap; type_swap.bytes[0] = buffer[0]; type_swap.bytes[1] = buffer[1]; type_swap.bytes[2] = buffer[2]; type_swap.bytes[3] = buffer[3]; return type_swap.number; }
Для восстановления внутри функции определяем объединение type_swap и побайтно копируем переданные байты в массив bytes. И в конце эти же байты возвращаем как число float. В функции main выполняем тестирование фукций. Консольный вывод программы:
c:\C>gcc -Wall -pedantic app.c -o app & app Restored number: 3.141500 c:\C>