Зачем бросать указатель, а затем разыгрывать?
Я проходил этот пример, который имеет функцию, выводящую шестнадцатеричный битовый шаблон для представления произвольного поплавка.
void ExamineFloat(float fValue)
{
printf("%08lx\n", *(unsigned long *)&fValue);
}
Зачем брать адрес fValue, отбрасывать беззнаковый длинный указатель, а затем разыгрывать? Не все ли это просто эквивалентны прямой трансляции в unsigned long?
printf("%08lx\n", (unsigned long)fValue);
Я попробовал это, и ответ не тот, так запутался.
Ответы
Ответ 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
или его сестер.