Я изучаю C, читая книги о нем. Был список, демонстрирующий некоторые концепции. В данном случае происходит приведение типов. После того, как я скопировал листинг и запустил программу, я получил непонятный мне результат.
int main(){
float value = 65.78;
float *flt_ptr = &value;
char *ch_ptr = (char*) &value;
printf("type-cast (char*) flt_ptr: %c\n", (char*) flt_ptr); // line 1
printf("type-cast (char) *flt_ptr: %c\n", (char) *flt_ptr); // line 2
printf("type-cast (char*) ch_ptr: %c\n", (char*) ch_ptr); // line 3
printf("type-cast (char) *ch_ptr: %c\n", (char) *ch_ptr); // line 4
return 0;
}
В качестве выходных значений я получаю строки:
char
бессмысленны и неизвестны ASCII-таблице65
. Все идет нормально.""
, которая имеет преобразованное значение 92
Когда я конвертирую эти значения в двоичные, первые 8
биты (размер указателя, на который он указывает) 65
идентичны первым 8
битам моего плавающего числа 65.78
. Но первые 8
биты двоичного значения 92
отличаются от плавающего числа. Я не понимаю, какую часть памяти я читаю и почему в этой printf()
-строке я читаю другую часть памяти?
Я думал, что все указатели в этой программе указывают на один и тот же адрес, но размер, на который они указывают, разный. Но преобразованные двоичные файлы должны были быть идентичными, но это не так.Фактически вы наблюдаете неопределенное поведение, поскольку пытаетесь выразить числа с плавающей запятой (или их часть) в виде символов. Число с плавающей запятой в компьютерах не хранится в виде серии символов, например «65,78» (пять символов). Он хранится в виде пары двоичных значений, первая часть которых представляет собой показатель степени (степень 2), а вторая — мантиссу или значение. Тот факт, что одна часть имеет значение 65 или «А», является просто совпадением. Так что все эти печатные утверждения выше совершенно бессмысленны.
char *ch_ptr = (char''') &value;
недопустимо C. Вы имели в виду char *ch_ptr = (char *) &value;
вместо этого?
@OldBoy Скорее, они передают указатели, хотя должны передавать символы. «Строку 1» и «строку 3» необходимо разыменовать.
строки 1 и 3: Указатель — это число, его нельзя интерпретировать как char
, он слишком мал, чтобы содержать это число. Вы пытаетесь отобразить в виде char
только часть указателя. Вы должны использовать %p
для отображения указателя. строка 4: Вы пытаетесь получить доступ к памяти float
как char
. Здесь также осуществляется доступ только к части float
, и это «Неопределённое поведение» (разыменование указателя на не его реальный тип), всякое может случиться.
@OldBoy Строка 2 не случайна. Он извлекает значение с плавающей запятой (65,78), затем преобразует его в целочисленное символьное значение размером в байт (65), которое отображается как A
. Строка 3 — это UB, поскольку данные не обязательно представляют собой байтовую строку с нулевым завершением.
@OldBoy: Битовая строка для двоичного представления с плавающей запятой IEEE-754 имеет три поля, а не два: бит знака, смещенный показатель степени и мантиссу. («Мансисса» — предпочтительный термин; «мантисса» — старый термин для обозначения дробной части логарифма.) Тот факт, что буква «А» напечатана, не является совпадением; преобразование значения float
для 65,78 в char
усекает значение, создавая 65, и при печати этого значения с использованием ASCII печатается «A».
@Dúthomhas Вы правы, но это не то, на что следует когда-либо полагаться.
@EricPostpischil Извините, я знал, что что-то пропустил, и, конечно же, это был знак. И да, случается, что число 65 печатается как A, но на самом деле это не то, что кому-либо следует использовать в своих программах.
@OldBoy В C существуют очень строгие и четко определенные правила приведения типа float к int. (char) *p_float
совершенно допустимы.
(char''') &value;
— это ерунда и недействительный C, поэтому код даже не скомпилируется стандартным компилятором C.
(char*) flt_ptr
и (char*) ch_ptr
бессмысленны, поскольку вы не можете передать указатель на printf, где он ожидает символ, через %c
. Вы вызываете неопределенное поведение, поэтому может произойти все что угодно - странный вывод не обязательно вызван тем, что «адреса памяти в char бессмысленны», а скорее тем, что вы солгали printf
и сказали, что передадите символ, а затем передали что-то совершенно другое.
(char*) ch_ptr
— это нонсенс, поскольку он преобразуется от char*
к char*
.
(char) *ch_ptr
— это нонсенс, поскольку он преобразует char
в char
.
Итак, у вас здесь довольно много концептуальных проблем.
Помимо этого, нам нужно осознать разницу между (char)some_float
и *(char*)&some_float
. Любой из них верен C, но эти два примера делают совершенно разные вещи.
При использовании (char)some_float
char
обрабатывается так же, как любой целочисленный тип, поэтому это практически то же самое, что и запись (int)some_float
. Здесь происходит то, что мы сообщаем компилятору, что нужно переинтерпретировать наше число с плавающей запятой в формат с фиксированной запятой, что достигается путем отбрасывания десятичных знаков. В этом случае у вас получится 65
. Любое целое число 65
, напечатанное с %c
, действительно приведет к 'A'
, учитывая наиболее распространенные таблицы символов (ASCII/UTF8). Вот что вы получили, когда сделали (char) *flt_ptr
.
Но в случае *(char*)&some_float
мы говорим компилятору взять адрес с плавающей запятой и рассматривать то, что там хранится, как массив символов, к которому мы можем получить доступ через указатель на символ. Самый левый *
в моем примере — это разыменование первого байта числа с плавающей запятой. Здесь вы наткнулись на особую особенность C, которая работает только тогда, когда мы переходим от указателя на что угодно к указателю на символ. Цель этого — позволить нам выполнять аппаратное программирование и проверять необработанное двоичное содержимое любого объекта, байт за байтом. Следовательно, тип символа, поскольку символ — это тип C, используемый для представления байта.
В итоге мы получаем необработанное двоичное представление float
, которое будет просто тарабарщиной, если его напечатать как %c
. Потому что необработанный двоичный файл будет IEEE 754 с плавающей запятой со знаковым битом, экспонентой и дробной частью. Как числа с плавающей запятой хранятся в памяти — это отдельная глава.
Теперь это превращается в более сложную тему:
В случае, если мы хотим напечатать двоичное представление float
с printf
, нам лучше использовать unsigned char
, поскольку тип char
может быть или не быть целочисленным типом со знаком в зависимости от компилятора.
Мы можем попробовать этот код:
unsigned char *ch_ptr = (unsigned char*) &value;
for(size_t i=0; i<sizeof(float); i++)
{
printf("%.2X ", ch_ptr[i]);
}
Это дает нам результат 5C 8F 83 42
. 0x5C — это 92, которые вы заметили. Необработанное двоичное представление числа 65,78 можно легко получить на таком сайте, как https://www.h-schmidt.net/FloatConverter/IEEE754.html. Фактическое сохраненное число не является точным на 100%, поскольку мы имеем дело с числами с плавающей запятой. Но мы должны ожидать 42838f5c
при переводе в шестнадцатеричный формат. И это те цифры, которые мы только что получили, но наоборот. Обратно, потому что я использовал компьютер Intel x86, который использует прямой порядок байтов, что означает, что я сначала получаю наименее значимый байт - почему он отображается задом наперед при печати побайтно, начиная с наименьшего адреса.
Следует отметить, что приведение указателя к другому типу в C и последующее разыменование его в большинстве случаев недопустимо. Это сложная тема, в которой вступают в игру такие вещи, как выравнивание и базовая система типов C, а также проблемы, связанные с const
корректностью и т. д.
printf("type-cast (char*) flt_ptr: %c\n", (char*) flt_ptr); // line 1
Это передает char *
за %c
. Для %c
printf
ожидает получить int
, который преобразуется в unsigned char
и печатается. Поведение этого не определено стандартом C. Во многих случаях он печатает младший байт указателя как unsigned char
.
printf("type-cast (char) *flt_ptr: %c\n", (char) *flt_ptr); // line 2
*flt_ptr
получает значение float
, около 65,78 (точно 65,779998779296875, когда для float
используется формат двоичный32 IEEE-754). Затем (char)
преобразует это в char
. Преобразование значения с плавающей запятой в целое число усекает его, получая 65. Когда реализация C использует ASCII, будет напечатана «A».
printf("type-cast (char*) ch_ptr: %c\n", (char*) ch_ptr); // line 3
Как и в строке 1, здесь передается char *
вместо %c
.
printf("type-cast (char) *ch_ptr: %c\n", (char) *ch_ptr); // line 4
Поскольку ch_ptr
установлено так, чтобы указывать на value
(после исправления опечатки в (char''')
на (char *)
, *ch_ptr
извлекает первый байт value
. Это байт кодирования значения float
в биты в памяти. Кодировка, используемая для float
, является бит задействован и обсуждается в других вопросах о переполнении стека. Так получилось, что первый байт имеет значение 92, которое в ASCII обозначает символ обратной косой черты «\», поэтому он печатается.
Эта строка не компилируется, когда я пробую ваш код: char *ch_ptr = (char''') &value; Я получаю «ошибка C2137: пустая символьная константа». Я использую компилятор Visual Studio 2022 C++. Я также получаю несколько других ошибок компиляции.