Я не понимаю, как данные указатели работают в C

Я изучаю 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;
}

В качестве выходных значений я получаю строки:

  • строки 1 и 3: некоторые из них не интерпретируются, поскольку адреса памяти в char бессмысленны и неизвестны ASCII-таблице
  • строка 2 дает букву «А» с преобразованным целым числом 65. Все идет нормально.
  • строка 4 выводит "", которая имеет преобразованное значение 92 Когда я конвертирую эти значения в двоичные, первые 8 биты (размер указателя, на который он указывает) 65 идентичны первым 8 битам моего плавающего числа 65.78. Но первые 8 биты двоичного значения 92 отличаются от плавающего числа. Я не понимаю, какую часть памяти я читаю и почему в этой printf()-строке я читаю другую часть памяти? Я думал, что все указатели в этой программе указывают на один и тот же адрес, но размер, на который они указывают, разный. Но преобразованные двоичные файлы должны были быть идентичными, но это не так.

Эта строка не компилируется, когда я пробую ваш код: char *ch_ptr = (char''') &value; Я получаю «ошибка C2137: пустая символьная константа». Я использую компилятор Visual Studio 2022 C++. Я также получаю несколько других ошибок компиляции.

dcp 15.08.2024 14:15

Фактически вы наблюдаете неопределенное поведение, поскольку пытаетесь выразить числа с плавающей запятой (или их часть) в виде символов. Число с плавающей запятой в компьютерах не хранится в виде серии символов, например «65,78» (пять символов). Он хранится в виде пары двоичных значений, первая часть которых представляет собой показатель степени (степень 2), а вторая — мантиссу или значение. Тот факт, что одна часть имеет значение 65 или «А», является просто совпадением. Так что все эти печатные утверждения выше совершенно бессмысленны.

OldBoy 15.08.2024 14:27
char *ch_ptr = (char''') &value; недопустимо C. Вы имели в виду char *ch_ptr = (char *) &value; вместо этого?
Tom Karzes 15.08.2024 14:38
Какие параметры компилятора рекомендуются новичкам, изучающим C? Следуйте этому совету, чтобы не тратить время на устранение ошибок, которые компилятор уже обнаружил и сообщил.
Lundin 15.08.2024 14:56

@OldBoy Скорее, они передают указатели, хотя должны передавать символы. «Строку 1» и «строку 3» необходимо разыменовать.

Lundin 15.08.2024 14:58

строки 1 и 3: Указатель — это число, его нельзя интерпретировать как char, он слишком мал, чтобы содержать это число. Вы пытаетесь отобразить в виде char только часть указателя. Вы должны использовать %p для отображения указателя. строка 4: Вы пытаетесь получить доступ к памяти float как char. Здесь также осуществляется доступ только к части float, и это «Неопределённое поведение» (разыменование указателя на не его реальный тип), всякое может случиться.

dalfaB 15.08.2024 15:01

@OldBoy Строка 2 не случайна. Он извлекает значение с плавающей запятой (65,78), затем преобразует его в целочисленное символьное значение размером в байт (65), которое отображается как A. Строка 3 — это UB, поскольку данные не обязательно представляют собой байтовую строку с нулевым завершением.

Dúthomhas 15.08.2024 15:08

@OldBoy: Битовая строка для двоичного представления с плавающей запятой IEEE-754 имеет три поля, а не два: бит знака, смещенный показатель степени и мантиссу. («Мансисса» — предпочтительный термин; «мантисса» — старый термин для обозначения дробной части логарифма.) Тот факт, что буква «А» напечатана, не является совпадением; преобразование значения float для 65,78 в char усекает значение, создавая 65, и при печати этого значения с использованием ASCII печатается «A».

Eric Postpischil 15.08.2024 15:26

@Dúthomhas Вы правы, но это не то, на что следует когда-либо полагаться.

OldBoy 15.08.2024 16:13

@EricPostpischil Извините, я знал, что что-то пропустил, и, конечно же, это был знак. И да, случается, что число 65 печатается как A, но на самом деле это не то, что кому-либо следует использовать в своих программах.

OldBoy 15.08.2024 16:15

@OldBoy В C существуют очень строгие и четко определенные правила приведения типа float к int. (char) *p_float совершенно допустимы.

Dúthomhas 15.08.2024 16:29
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
11
84
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий
  • (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_floatchar обрабатывается так же, как любой целочисленный тип, поэтому это практически то же самое, что и запись (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. Для %cprintf ожидает получить 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 обозначает символ обратной косой черты «\», поэтому он печатается.

Другие вопросы по теме