Сериализация double и плавающее с C

Как я могу сериализовать двойные и плавающие числа в C?

У меня есть следующий код для сериализации шортов, целых чисел и символов.

unsigned char * serialize_char(unsigned char *buffer, char value)
{
    buffer[0] = value;
    return buffer + 1;
}

unsigned char * serialize_int(unsigned char *buffer, int value)
{
    buffer[0] = value >> 24;
    buffer[1] = value >> 16;
    buffer[2] = value >> 8;
    buffer[3] = value;
    return buffer + 4;
}

unsigned char * serialize_short(unsigned char *buffer, short value)
{
    buffer[0] = value >> 8;
    buffer[1] = value;
    return buffer + 2;
}

Редактировать:

Я нашел эти функции из этого вопроса

Редактировать 2:

Целью сериализации является отправка данных в сокет UDP и гарантия того, что они могут быть десериализованы на другом компьютере, даже если порядковый номер отличается. Существуют ли другие «лучшие практики» для выполнения этой функции, учитывая, что мне нужно сериализовать целые числа, двойные числа, числа с плавающей запятой, и char *?

12
задан Community 23 May 2017 в 12:26
поделиться

8 ответов

После вашего обновления вы упоминаете, что данные должны передаваться с использованием UDP, и запрашиваете рекомендации. Я настоятельно рекомендую отправлять данные в виде текста, возможно, даже с добавленной разметкой (XML). Отладка ошибок, связанных с порядком следования байтов, в линии передачи - пустая трата времени

Только мои 2 цента на часть вашего вопроса о «передовых методах»

3
ответ дан 2 December 2019 в 03:14
поделиться

Я помню, как впервые увидел приведение, использованное в моем примере ниже, в старом добром исходном коде Quake подпрограммы "rsqrt", содержащем самый крутой комментарий, который я видел в то время (погуглите, вам понравится)

unsigned char * serialize_float(unsigned char *buffer, float value) 
{ 
    unsigned int ivalue = *((unsigned int*)&value); // warning assumes 32-bit "unsigned int"
    buffer[0] = ivalue >> 24;  
    buffer[1] = ivalue >> 16;  
    buffer[2] = ivalue >> 8;  
    buffer[3] = ivalue;  
    return buffer + 4; 
} 

Надеюсь, я правильно понял ваш вопрос (и пример кода). Сообщите мне, было ли это полезно?

12
ответ дан 2 December 2019 в 03:14
поделиться

Вы всегда можете использовать объединения для сериализации:

void serialize_double (unsigned char* buffer, double x) {
    int i;
    union {
        double         d;
        unsigned char  bytes[sizeof(double)];
    } u;

    u.d = x;
    for (i=0; i<sizeof(double); ++i)
        buffer[i] = u.bytes[i];
}

На самом деле это не более надежно, чем простое приведение адреса double к char * , но по крайней мере используя sizeof () во всем коде, вы избегаете проблем, когда тип данных занимает больше / меньше байтов, чем вы думали (это не помогает, если вы перемещаете данные между платформами, которые используют разные размеры для двойной ).

Для чисел с плавающей запятой просто замените все экземпляры double на float . Вы можете создать хитрый макрос для автоматической генерации серии этих функций, по одной для каждого интересующего вас типа данных.

2
ответ дан 2 December 2019 в 03:14
поделиться

Вы можете переносимо сериализовать в IEEE-754 независимо от собственного представления:

int fwriteieee754(double x, FILE * fp, int bigendian)
{
    int                     shift;
    unsigned long           sign, exp, hibits, hilong, lowlong;
    double                  fnorm, significand;
    int                     expbits = 11;
    int                     significandbits = 52;

    /* zero (can't handle signed zero) */
    if(x == 0) {
        hilong = 0;
        lowlong = 0;
        goto writedata;
    }
    /* infinity */
    if(x > DBL_MAX) {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        lowlong = 0;
        goto writedata;
    }
    /* -infinity */
    if(x < -DBL_MAX) {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        hilong |= (1 << 31);
        lowlong = 0;
        goto writedata;
    }
    /* NaN - dodgy because many compilers optimise out this test
     * isnan() is C99, POSIX.1 only, use it if you will.
     */
    if(x != x) {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        lowlong = 1234;
        goto writedata;
    }

    /* get the sign */
    if(x < 0) {
        sign = 1;
        fnorm = -x;
    } else {
        sign = 0;
        fnorm = x;
    }

    /* get the normalized form of f and track the exponent */
    shift = 0;
    while(fnorm >= 2.0) {
        fnorm /= 2.0;
        shift++;
    }
    while(fnorm < 1.0) {
        fnorm *= 2.0;
        shift--;
    }

    /* check for denormalized numbers */
    if(shift < -1022) {
        while(shift < -1022) {
            fnorm /= 2.0;
            shift++;
        }
        shift = -1023;
    } else {
        /* take the significant bit off mantissa */
        fnorm = fnorm - 1.0;
    }
    /* calculate the integer form of the significand */
    /* hold it in a  double for now */

    significand = fnorm * ((1LL << significandbits) + 0.5f);

    /* get the biased exponent */
    exp = shift + ((1 << (expbits - 1)) - 1);   /* shift + bias */

    /* put the data into two longs */
    hibits = (long)(significand / 4294967296);  /* 0x100000000 */
    hilong = (sign << 31) | (exp << (31 - expbits)) | hibits;
    lowlong = (unsigned long)(significand - hibits * 4294967296);

 writedata:
    /* write the bytes out to the stream */
    if(bigendian) {
        fputc((hilong >> 24) & 0xFF, fp);
        fputc((hilong >> 16) & 0xFF, fp);
        fputc((hilong >> 8) & 0xFF, fp);
        fputc(hilong & 0xFF, fp);

        fputc((lowlong >> 24) & 0xFF, fp);
        fputc((lowlong >> 16) & 0xFF, fp);
        fputc((lowlong >> 8) & 0xFF, fp);
        fputc(lowlong & 0xFF, fp);
    } else {
        fputc(lowlong & 0xFF, fp);
        fputc((lowlong >> 8) & 0xFF, fp);
        fputc((lowlong >> 16) & 0xFF, fp);
        fputc((lowlong >> 24) & 0xFF, fp);

        fputc(hilong & 0xFF, fp);
        fputc((hilong >> 8) & 0xFF, fp);
        fputc((hilong >> 16) & 0xFF, fp);
        fputc((hilong >> 24) & 0xFF, fp);
    }
    return ferror(fp);
}

На машинах, использующих IEEE-754 (т.е. общий случай ), все вы » Чтобы получить номер, вам понадобится fread () . В противном случае декодируйте байты самостоятельно (знак * 2 ^ (экспонента-127) * 1. мантисса) .

Примечание: при сериализации в системах, где собственное двойное значение более точное, чем двойное значение IEEE, вы можете столкнуться с ошибками разнесения на единицу в младшем бите.

Надеюсь, это поможет.

6
ответ дан 2 December 2019 в 03:14
поделиться

Для начала, вы никогда не должны предполагать, что short , int и т. Д. Имеют одинаковую ширину с обеих сторон. Было бы намного лучше использовать типы uint32_t и т. Д. (Беззнаковые), которые имеют известную ширину с обеих сторон.

Затем, чтобы убедиться, что у вас нет проблем с порядком байтов, существуют макросы / функции ntoh htos и т.д., которые обычно намного эффективнее, чем все, что вы можете сделать самостоятельно. (на оборудовании Intel это, например, всего одна инструкция ассемблера). Таким образом, вам не нужно писать функции преобразования, в основном они уже есть, просто приведите указатель buffer к указателю правильного целочисленного типа.

Для float вы, вероятно, можете предположить, что они 32-битные и имеют одинаковое представление с обеих сторон. Поэтому я думаю, что хорошей стратегией было бы использовать приведение указателя к uint32_t * , а затем использовать ту же стратегию, что и выше.

Если вы думаете, что у вас могут быть разные представления float , вам придется разделить на мантиссу и показатель степени. Возможно, вы могли бы использовать для этого frexpf .

1
ответ дан 2 December 2019 в 03:14
поделиться

Переносимый способ: используйте frexp для сериализации (преобразование в целочисленную мантиссу и экспоненту) и ldexp для десериализации.

Простой способ: предположим, что в 2010 году любая машина, которая вам небезразлична, использует IEEE float, объявите объединение с элементом float и элементом uint32_t и используйте свой код целочисленной сериализации для сериализуйте поплавок.

Путь ненавистников двоичных файлов: сериализовать все как текст, включая числа с плавающей запятой. Используйте описатель формата printf «% a» , чтобы получить шестнадцатеричное число с плавающей запятой, которое всегда выражается точно (при условии, что вы не ограничиваете точность чем-то вроде «%. 4a» ) и не подвержены ошибкам округления. Вы можете прочитать их с помощью strtod или любой из функций семейства scanf .

13
ответ дан 2 December 2019 в 03:14
поделиться

Это упаковывает значение с плавающей запятой в пару int и long long , которые вы затем можете сериализовать с другими вашими функциями. Функция unpack () используется для десериализации.

Пара чисел представляет собой показатель степени и дробную часть числа соответственно.

#define FRAC_MAX 9223372036854775807LL /* 2**63 - 1 */

struct dbl_packed
{
    int exp;
    long long frac;
};

void pack(double x, struct dbl_packed *r)
{
    double xf = fabs(frexp(x, &r->exp)) - 0.5;

    if (xf < 0.0)
    {
        r->frac = 0;
        return;
    }

    r->frac = 1 + (long long)(xf * 2.0 * (FRAC_MAX - 1));

    if (x < 0.0)
        r->frac = -r->frac;
}

double unpack(const struct dbl_packed *p)
{
    double xf, x;

    if (p->frac == 0)
        return 0.0;

    xf = ((double)(llabs(p->frac) - 1) / (FRAC_MAX - 1)) / 2.0;

    x = ldexp(xf + 0.5, p->exp);

    if (p->frac < 0)
        x = -x;

    return x;
}
8
ответ дан 2 December 2019 в 03:14
поделиться

Что касается узкого вопроса о float , обратите внимание, что вы, вероятно, в конечном итоге предполагаете, что оба конца провода используют одно и то же представление для плавающей точки. Сегодня это может быть безопасно, учитывая повсеместное использование IEEE-754, но обратите внимание, что некоторые современные DSP (я считаю, что черные плавники) используют другое представление. Раньше было по крайней мере столько же представлений для чисел с плавающей запятой, сколько производителей оборудования и библиотек, так что это была большая проблема.

Даже при том же представлении он может храниться в другом порядке байтов. Это потребует определения порядка байтов в сети и корректировки кода на каждом конце. На практике будет работать либо приведение к типу указателя, либо объединение. Оба вызывают поведение, определяемое реализацией, но пока вы проверяете и тестируете, это не имеет большого значения.

Тем не менее, текст часто является вашим другом для передачи чисел с плавающей запятой между платформами.Уловка состоит в том, чтобы не использовать слишком много символов, которые действительно необходимы для его обратного преобразования.

В общем, я бы порекомендовал серьезно подумать об использовании такой библиотеки, как XDR , которая является надежной, существует уже некоторое время и притирается ко всем острым уголкам и крайние случаи.

Если вы настаиваете на использовании собственного, позаботьтесь о тонких вопросах, например о том, является ли int 16-битным, 32-битным или даже 64-битным в дополнение к представлению float и двойной .

3
ответ дан 2 December 2019 в 03:14
поделиться
Другие вопросы по тегам:

Похожие вопросы: