Подтвердить что ты не робот

Зачем бросать указатель, а затем разыгрывать?

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

void ExamineFloat(float fValue)
{
    printf("%08lx\n", *(unsigned long *)&fValue);
}

Зачем брать адрес fValue, отбрасывать беззнаковый длинный указатель, а затем разыгрывать? Не все ли это просто эквивалентны прямой трансляции в unsigned long?

printf("%08lx\n", (unsigned long)fValue);

Я попробовал это, и ответ не тот, так запутался.

4b9b3361

Ответ 1

(unsigned long)fValue

Это преобразует значение float в значение unsigned long в соответствии с "обычными арифметическими преобразованиями".

*(unsigned long *)&fValue

Цель состоит в том, чтобы взять адрес, по которому хранится fValue, притворяться, что на этом адресе нет float, но unsigned long, а затем читать это unsigned long. Цель состоит в том, чтобы изучить бит-шаблон, который используется для хранения float в памяти.

Как показано, это приводит к поведению undefined.

Причина. Вы не можете получить доступ к объекту с помощью указателя на тип, который не является "совместимым" с типом объекта. "Совместимые" типы - это, например, (unsigned) char и каждый другой тип или структуры, которые имеют одни и те же начальные элементы (говоря о C здесь). См. §6.5/7 N1570 для подробного (C11) списка (обратите внимание, что мое использование "совместимых" отличается - более широкое - чем в ссылочном тексте.)

Решение. Передайте unsigned char * доступ к отдельным байтам объекта и соберите unsigned long из них:

unsigned long pattern = 0;
unsigned char * access = (unsigned char *)&fValue;
for (size_t i = 0; i < sizeof(float); ++i) {
  pattern |= *access;
  pattern <<= CHAR_BIT;
  ++access;
}

Обратите внимание, что (как указывал @CodesInChaos) вышеупомянутое рассматривает значение с плавающей запятой как хранимое с его самым значительным байтом сначала ( "большой конец" ). Если ваша система использует другой порядок байтов для значений с плавающей запятой, вам нужно будет приспособиться к этому (или изменить порядок байтов выше unsigned long, что более практично для вас).

Ответ 2

Значения с плавающей запятой имеют представления памяти: например, байты могут представлять значение с плавающей запятой, используя IEEE 754.

Первое выражение *(unsigned long *)&fValue будет интерпретировать эти байты, как если бы это было представление значения unsigned long. Фактически в стандарте C это приводит к поведению undefined (в соответствии с так называемым "строгим правилом псевдонимов" ). На практике есть такие вопросы, как утверждение, которое должно быть принято во внимание.

Второе выражение (unsigned long)fValue соответствует стандарту C. Он имеет точный смысл:

C11 (n1570), § 6.3.1.4 Реальные плавающие и целые

Когда конечное значение реального плавающего типа преобразуется в целочисленный тип, отличный от _Bool, дробная часть отбрасывается (т.е. значение усекается к нулю). Если значение целой части не может быть представлено целым типом, поведение undefined.

Ответ 3

*(unsigned long *)&fValue не является эквивалентом прямого приведения к unsigned long.

Преобразование в (unsigned long)fValue преобразует значение fValue в unsigned long, используя обычные правила преобразования значения float в значение unsigned long. Представление этого значения в unsigned long (например, в терминах битов) может сильно отличаться от того, как это же значение представлено в float.

Преобразование *(unsigned long *)&fValue формально имеет поведение undefined. Он интерпретирует память, занятую fValue, как если бы она была unsigned long. Практически (т.е. Это часто случается, хотя поведение undefined), это часто дает значение, отличное от fValue.

Ответ 4

Typecasting в C выполняет преобразование типов и преобразование значений. С плавающей точкой → беззнаковое длинное преобразование усекает дробную часть числа с плавающей запятой и ограничивает значение до возможного диапазона беззнакового длинного. Преобразование из одного типа указателя в другое не требует изменения в значении, поэтому использование указателя-указателя - это способ сохранить одно и то же представление в памяти при изменении типа, связанного с этим представлением.

В этом случае это способ вывода двоичного представления значения с плавающей запятой.

Ответ 5

Как уже отмечалось другими, приведение указателя на тип не char к указателю на другой тип не char, а затем разыменование - это поведение undefined.

То, что printf("%08lx\n", *(unsigned long *)&fValue) вызывает поведение undefined, не обязательно означает, что запуск программы, которая пытается выполнить такую ​​пародию, приведет к стиранию жесткого диска или выведению носовых демонов из одного носа (два признака поведения undefined). На компьютере, в котором sizeof(unsigned long)==sizeof(float) и на котором оба типа имеют одинаковые требования к выравниванию, printf почти наверняка выполнит то, что он ожидает, чтобы напечатать шестнадцатеричное представление рассматриваемого значения с плавающей запятой.

Это не должно удивлять. Стандарт C открыто предлагает реализации для расширения языка. Многие из этих расширений находятся в областях, которые, строго говоря, относятся к undefined. Например, функция POSIX dlsym возвращает void*, но эта функция обычно используется для поиска адреса функции, а не глобальной переменной, Это означает, что указатель void, возвращаемый dlsym, должен быть переведен в указатель функции, а затем разыменован для вызова функции. Это, очевидно, поведение undefined, но тем не менее работает на любой совместимой с POSIX платформе. Это не будет работать на машине архитектуры Гарварда, на которой указатели на функции имеют разные размеры, чем указатели на данные.

Аналогично, приведение указателя на float к указателю на целое число без знака, а затем разыменование происходит для почти любого компьютера с почти любым компилятором, в котором требования к размеру и выравниванию этого беззнакового целого являются такими же, как и a float.

Тем не менее, использование unsigned long может привести к неприятностям. На моем компьютере unsigned long имеет длину 64 бит и имеет требования к выравниванию 64 бит. Это несовместимо с поплавком. Было бы лучше использовать uint32_t - на моем компьютере, то есть.


Взлом профсоюза - это один из способов избежать этого беспорядка:

typedef struct {
    float fval;
    uint32_t ival;
} float_uint32_t;

Присвоение float_uint32_t.fval и доступ к `` float_uint32_t.ival` было undefined. Это больше не относится к C. Нет компилятора, что я знаю об ударах носовых демонов для взлома. Это не было UB на С++. Это было незаконно. До С++ 11 совместимый компилятор С++ должен был жаловаться на совместимость.


Любой лучший способ избежать этого беспорядка - использовать формат %a, который был частью стандарта C с 1999 года:

printf ("%a\n", fValue);

Это просто, легко, переносимо, и нет никаких шансов на поведение undefined. Это печатает шестнадцатеричное/двоичное представление рассматриваемого значения с плавающей запятой двойной точности. Поскольку printf является архаичной функцией, все аргументы float преобразуются в double до вызова printf. Это преобразование должно быть точным для версии стандарта C на 1999 год. Точное значение можно получить с помощью вызова scanf или его сестер.