Я изучаю C, и частично для того, чтобы привыкнуть к указателям, а частично для того, чтобы узнать о плавающих числах IEEE 754, я попробовал следующее:
#include <stdio.h>
int main() {
float f1 = -12.78;
const char *p = (char *)&f1;
for(int i=0; i < sizeof(f1); i++) {
printf("i: %i and p: %p and byte is %#x\n",i,p,*p);
p++;
}
}
Я не получаю никаких предупреждений или ошибок (в отличие от большинства версий, которые я пробовал раньше!), Но получаю странные результаты:
i: 0 and p: 0x16ce13368 and byte is 0xffffffe1
i: 1 and p: 0x16ce13369 and byte is 0x7a
i: 2 and p: 0x16ce1336a and byte is 0x4c
i: 3 and p: 0x16ce1336b and byte is 0xffffffc1
Мое (ограниченное) понимание IEEE 754 заключается в том, что -12,78 действительно представляет собой e1 7a 4c c1 в байтах, но почему первый байт отображается как ff ff ff e1 (и аналогично для последнего байта)?
Есть лучший способ сделать это?
Я также пробовал использовать массивы символов, но далеко не продвинулся. Я также пытался использовать void *p для доступа к памяти, содержащей число с плавающей запятой, но тоже не смог заставить это работать.
Рассматривайте его не как указатель на символ, а как целое число. Затем вы можете извлечь части числа путем сдвига битов. Слева у вас есть знак (1 бит), смещенная экспонента (8 бит) и мантисса (23 бита).
Изменение двух экземпляров char на unsigned char устранило проблему. Спасибо. Является ли это вообще хорошим способом «проверить» байты произвольных структур и типов? Кроме того, есть ли способ сделать это, используя беззнаковый char[], а не указатель? (Или это глупая идея?)
В вашей реализации C char — это 8-битный целочисленный тип со знаком, поэтому любые значения байтов в диапазоне от 0x80 до 0xff будут обрабатываться как маленькие отрицательные целые числа в диапазоне от -128 до -1. При расширении аргументов по умолчанию для переменных аргументов printf эти 8-битные значения char будут расширены по знаку до значений int (которые в вашей реализации C являются 32-битными). Спецификатор %#x printf будет обрабатывать эти значения int как unsigned int.
Ах. Что объясняет его. Сейчас я смутно помню, как читал в документации по printf о преобразовании в unsigned int перед отображением. Спасибо
При разыменовании указателя вы не ограничены доступом к объекту непосредственно в месте расположения указателя. Вы можете использовать арифметику указателей или индексацию массива для доступа к близлежащим объектам. Например, вместо увеличения p в цикле вы можете заменить *p в аргументе printf на *(p + i) или p[i], чтобы получить доступ к байту по смещению i от указателя. (Есть ограничения, нельзя получить доступ за пределы объекта или массива.)
Спасибо, Ян Эбботт. Это именно то, что я пытался выяснить (но мне не пришлось работать в своих экспериментах)
@OldBoy: «Рассматривать это как целое число» сложно, не нарушая строгого правила псевдонимов. Типы символов имеют исключение из этого правила.
Чтобы обойти строгое правило псевдонимов, нужно использовать memcpy() для копирования байтов в целое число. И в любом случае вы должны убедиться, что используете достаточно большой целочисленный тип.
@NateEldredge Я не думаю, что строгое правило псевдонимов уместно в таком фрагменте кода.
@OldBoy: Если бы p было int *, как вы предположили в своем предыдущем комментарии, то строгое правило псевдонимов было бы нарушено, что вызвало бы неопределенное поведение.
Обратите внимание, что при форматировании чисел размера char уместно использовать соответствующий модификатор ширины hh. В данном случае это будет %#hhx. Вы также должны сопоставить знаковость: %x, %u и %o ожидают значения беззнакового типа, тогда как %d и %i ожидают значения знакового типа. На практике иногда удается получить правильный только один из этих атрибутов, особенно для вывода, но на это не следует полагаться.
Это приятно знать. Есть ли хорошая шпаргалка для printf? (Я нашел несколько, но они не показались мне такими уж хорошими.) Я думал, что таблицы в стандартной документации будет достаточно, но мне не хватало таких вещей, как приведенные выше (особенно модификаторы длины).
@AndreasWenzel Это не то, что я предлагал. Я предложил рассматривать переменную с плавающей запятой как целое число с помощью простого приведения, чтобы получить доступ к трем наборам битов.





Поскольку char подписывается и повышается до int при использовании в printf. Именно по этой причине вы видите эти отрицательные целые числа, напечатанные в шестнадцатеричном виде.
Используйте беззнаковый тип и правильный формат.
int main() {
float f1 = -12.78;
const unsigned char *p = (const unsigned char *)&f1;
for(int i=0; i < sizeof(f1); i++) {
printf("i: %i and p: %p and byte is %hhx\n", i, (void *)p,*p);
p++;
}
}
char подписан на конкретном компиляторе и флагах ОП. В других настройках он может быть беззнаковым, поскольку char, что довольно уникально, имеет знаковость, определяемую реализацией, и отличается как от signed char, так и от unsigned char.
Беззнаковый символ @ShadowRanger всегда будет работать. нет.
Я согласен, что unsigned char работает. Я говорю, что ваше первое предложение можно прочитать как подразумевающее, что char всегда подписано, хотя это не так.
Используйте беззнаковый тип, чтобы избежать расширения знака для отрицательных чисел.