Недавно я узнал об указателях приведения типов. Итак, я поигрался и сделал этот плохой код:
#include <stdio.h>
int main()
{
int* a;
char b[]= "Hye";
a = (int*)&b;
*a='B';
printf("%s\n", b);
return 0;
}
Этот код дает выход «B».
После дальнейших экспериментов я написал этот код:
#include <stdio.h>
int main()
{
int* a;
char b[]= "Hye";
a = (int*)&b;
*a='B';
b[1]='y';
b[2]='e';
printf("%s\n", b);
return 0;
}
Вывод: «Пока».
Я не могу понять, что делает оператор *a='B';
и почему переопределение второго и третьего элементов массива меняет выходные данные?
Я уже использовал такие вещи, как GPT-4o, искал в Google такие вещи, как указатель приведения типов, затем читал статьи и смотрел связанные с ним видео, чтобы получить ответ на свой вопрос. Но я не получил ни одного. И вот я здесь.
Ага! Я (извините за поздний ответ. Я здесь всего лишь нуб, поэтому мне буквально пришлось гуглить, что означает «little endian»)
В этом коде много неопределенного поведения. Некоторые реализации могут привести к нарушению сегментации или, конечно же, дать результаты, отличные от того, что вы видите. Во-первых, нет никакой гарантии, что ваш массив char
приемлемо выровнен для указателя int
.
Почти во всех системах int
— это четыре байта, а char
— один байт. С помощью *a='B';
компилятор преобразует символ 'B'
в int
и присваивает его четырем байтам, на которые указывает a
. В зависимости от вашей системы порядок байтов ваш массив станет либо { 'B', 0, 0, 0 }
, либо { 0, 0, 0, 'B' }
. Вы, начинающие книги, должны были научить вас, что нулевой терминатор строки равен 0
.
@pmg Спасибо за ответ. Теперь, когда я думаю об этом, я определенно понимаю, что B будет неявно преобразовано в его значение ASCII. Но я до сих пор не могу понять, как целочисленное значение вызовет такое странное поведение.
@Someprogrammerdude Ну, спасибо! Я получил ответ. Теперь я понял, что это был глупый вопрос.
Не играйте с указателями в «реальном коде» :-)
@pmg Спасибо за совет! Я буду помнить об этом. Я просто хотел убедиться, что понимаю указатели приведения типов.
Попробуйте *a = 6650178;
в первом примере :-)
@pmg вау! Это печатает «Пока». Но как это именно работает? (Я заметил, что 66 — это значение ASCII для B. Но как «50178» выводит «ye». Я не думаю, что «50178» вместе или по отдельности представляет символы ASCII «y», «e»)
Это 66
неудачное совпадение, не обращайте на него внимания, оно никак не влияет на происходящую здесь работу! Попробуйте разобраться в этом сами; Дам подсказку: думайте базово 256
.
Еще одна подсказка: десятичное число 6650178 — это шестнадцатеричное число 00657942. И ваша машина имеет прямой порядок байтов.
поднимите его на ступеньку выше: измените это 6650178
на число, которое заставит вашу программу печатать :-)
@pmg возможно *a=2698554; дам :-)
Отлично, @RajeshPaul, поздравляю!
Также спасибо @PaulLynch за то, что помог мне разобраться🙇
Спасибо @pmg за помощь! Думаю, я никогда не пойму, как вы можете знать так много и при этом быть такими скромными🙇
Просто важное замечание, которое не упоминалось во всех наших играх: важно, чтобы массив b
имел 4 или более байтов ("Bye"
равен 4
байтам из-за нулевого терминатора строки).
@RajeshPaul Приятно это выяснить. Простой «трюк» pmg — это хороший опыт обучения — хорошее предложение.
@pmg Спасибо! Я понял, что выделенная память массива переполняется. Но в этом случае откуда printf получает нулевой символ, обозначающий конец печати? (Записывается ли из-за переполнения нулевой символ в адрес памяти, соседний с массивом, и оттуда он считывает нулевой символ?)
Число 2698554
— это 58 + 45*256 + 41*256*256 + 0*256*256*256
. 58 дает :
, 45 дает -
, 41 дает )
, а 0 дает нулевой терминатор строки, всего 4
байт. Вот почему важно, чтобы стартовая строка была достаточно длинной!
выделенная память массива НЕ переполняется
@pmg Я вижу, что массив имеет размер 4 байта. Так что он не переполняется. Извините за мою ошибку
int* a
и char b[]
— это два разных типа, обычно вы нарушаете строгое правило псевдонимов, которое делает поведение этого шаблона доступа неопределенным (6.5, 7).
Как отмечает @TomKarzes, вы можете столкнуться с проблемами, если ваша платформа требует int
выравнивания, скажем, по 4 байтам, тогда как char b[]
, возможно, потребуется выравнивание только по 1 байту. Если вы используете такую платформу, ваша программа может выйти из строя из-за «ошибки шины» или по другим причинам.
Стандарт C гарантирует только то, что int
имеет длину не менее 16 бит. На 64-битной платформе обычно это 4 байта, поэтому *a='B';
вероятно записывает в массив 4 байта b
. На платформе с прямым порядком байтов (amd64) первая программа выведет «B» {'B', 0, 0, 0}
, а вторая программа — «Пока» {'B'¸'y', 'e' 0}
, тогда как обе программы будут печатать «» на платформе с прямым порядком байтов {0, 0, 0, 'B'}
и {'0, 'y', 'e', 'B'}
соответственно.
У вас тоже возникнут проблемы, если вы sizeof b < sizeof(int)
. sizeof(char)
определяется как 1
. stdint.h
дает вам доступ int
к типам известного размера.
Что касается «поскольку b
является массивом символов, я думаю, вы в порядке (6.5, 7)»: Нет. *a=
'B';` обращается к объекту эффективного типа char [4]
(b
) или объектам эффективного типа char
(элементам b
). C 2018 6.5 7 говорит, что доступ к нему должен быть возможен только с использованием lvalue совместимого типа, квалифицированной версии совместимого типа, соответствующего знакового или беззнакового типа, квалифицированной версии соответствующего знакового или беззнакового типа, агрегата или объединения, содержащего один предыдущего или типа символа. Но используемое lvalue имеет тип int
, который не является ни одним из них.
Re «Если вы работаете на такой платформе, ваша программа может выйти из строя из-за «ошибки шины»»: она также может выйти из строя по другим причинам.
Re: «Стандарт C гарантирует только то, что sizeof(int)
>= 2»: Стандарт C гарантирует int
16 бит. sizeof
измеряет размер в единицах char
объектов, а стандарт не требует, чтобы int
было как минимум два char
. char
может быть шире восьми бит. sizeof (int) может быть один.
@EricPostpischil Спасибо. Переработано, как было предложено. Я думаю, что ваш второй комментарий носит педантичный характер, поскольку, как я уже говорил, возможно, произошел сбой из-за ошибки шины, и подразумевались другие способы.
@AllanWind - 6.5 7, наверное, мой самый любимый вариант того, как НЕ объяснять что-то в стандарте. «... lvalue совместимого типа, квалифицированная версия совместимого типа, соответствующий знаковый или беззнаковый тип, квалифицированная версия соответствующего знакового или беззнакового типа...» не может быть переварено за один раз. сидя.
@DavidC.Rankin Как новичок, я с тобой согласен! Мне пришлось прочитать много статей, чтобы понять, о чем он говорит.
@DavidC.Rankin Да, простым смертным трудно понять (включая меня; с другой стороны, авторы компиляторов не простые смертные). Мне больше всего нравится то, что мы называем это строгим псевдонимом, хотя термин «псевдоним» встречается только в сноске, а в индексном списке «псевдоним» для этой страницы. Вы даже не можете найти этот термин.
@RajeshPaul - не расстраивайтесь, этот раздел стандарта (в широком смысле называемый «Правилом строгого псевдонимов») также был предметом многочисленных споров и дискуссий среди старожилов. Суть правила заключается в том, чтобы не обращаться к вещам в памяти через указатели другого типа. Вся эта тарабарщина — попытка объяснить исключения из того, когда это допустимо. (существуют законные варианты использования исключений, например, для функций, которые возвращают типы void
, не имеющие «типа», например memcpy()
и т. д.) Если вы всегда используете правильный указатель типа, вы обычно не столкнетесь с этой проблемой.
«Этот код дает результат «B». Итак, я делаю вывод, что вы используете компьютер с прямым порядком байтов :-) Подсказка: «B» (на компьютере с прямым порядком байтов, ASCII) — это последовательность из 4 байтов
66
,0
,0
,0
.