Почему этот код дает вывод C++Sucks
? Какая концепция стоит за этим?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
Проверьте это здесь .
skcuS++C
.
Почему этот код дает вывод C++Sucks
? Какая концепция стоит за этим?
#include <stdio.h>
double m[] = {7709179928849219.0, 771};
int main() {
m[1]--?m[0]*=2,main():printf((char*)m);
}
Проверьте это здесь .
skcuS++C
.
Ответы:
Число 7709179928849219.0
имеет следующее двоичное представление как 64-битное double
:
01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------
+
показывает положение знака; ^
показателя степени и -
мантиссы (то есть значения без показателя степени).
Поскольку представление использует двоичную экспоненту и мантиссу, удвоение числа увеличивает экспоненту на единицу. Ваша программа делает это точно 771 раз, поэтому показатель степени, который начинается с 1075 (десятичное представление 10000110011
), в конце становится 1075 + 771 = 1846; двоичное представление 1846 11100110110
. Результирующий шаблон выглядит так:
01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'
Этот шаблон соответствует строке, которую вы видите напечатанной, только назад. В то же время второй элемент массива становится нулевым, обеспечивая нулевой терминатор, что делает строку пригодной для передачи в printf()
.
7709179928849219
значение и вернул двоичное представление.
Более читаемая версия:
double m[2] = {7709179928849219.0, 771};
// m[0] = 7709179928849219.0;
// m[1] = 771;
int main()
{
if (m[1]-- != 0)
{
m[0] *= 2;
main();
}
else
{
printf((char*) m);
}
}
Он рекурсивно звонит main()
771 раз.
В самом начале m[0] = 7709179928849219.0
, который стоит за C++Suc;C
. При каждом вызове m[0]
удваивается, чтобы «починить» последние две буквы. В последнем вызове m[0]
содержит символьное представление ASCII C++Sucks
и m[1]
содержит только нули, поэтому у него есть нулевой терминатор для C++Sucks
строки. Все по предположению, что m[0]
хранится на 8 байтах, поэтому каждый символ занимает 1 байт.
Без рекурсии и незаконного main()
вызова это будет выглядеть так:
double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
m[0] *= 2;
}
printf((char*) m);
Отказ от ответственности: Этот ответ был опубликован в оригинальной форме вопроса, в которой упоминается только C ++ и заголовок C ++. Преобразование вопроса в чистый C было сделано сообществом, без участия первоначального автора.
Формально говоря, невозможно рассуждать об этой программе, потому что она плохо сформирована (то есть, это не законный C ++). Это нарушает C ++ 11 [basic.start.main] p3:
Функция main не должна использоваться внутри программы.
Помимо этого, он опирается на тот факт, что на типичном потребительском компьютере double
длина a составляет 8 байтов и использует определенное хорошо известное внутреннее представление. Начальные значения массива вычисляются таким образом, чтобы при выполнении «алгоритма» конечное значение первого double
было таким, чтобы внутренним представлением (8 байтов) были коды ASCII из 8 символов C++Sucks
. Тогда второй элемент в массиве 0.0
, первый байт которого находится 0
во внутреннем представлении, делает эту строку допустимой в стиле C. Затем он отправляется на вывод с помощью printf()
.
Выполнение этого в HW, где некоторые из вышеперечисленных не выполняются, приведет к появлению мусорного текста (или, возможно, даже доступа за пределы).
basic.start.main
3.6.1 / 3 с той же формулировкой.
main()
или заменить его вызовом API для форматирования жесткого диска или чего-либо еще.
Возможно, самый простой способ понять код - это работать в обратном порядке. Мы начнем со строки для печати - для баланса мы будем использовать «C ++ Rocks». Важный момент: точно так же, как и оригинал, он ровно восемь символов. Поскольку мы собираемся сделать (примерно) как оригинал и распечатать его в обратном порядке, мы начнем с того, что поместим его в обратном порядке. Для нашего первого шага мы просто рассмотрим этот битовый шаблон как double
и распечатаем результат:
#include <stdio.h>
char string[] = "skcoR++C";
int main(){
printf("%f\n", *(double*)string);
}
Это производит 3823728713643449.5
. Итак, мы хотим манипулировать этим каким-то образом, что не очевидно, но легко изменить. Я буду полу-произвольно выбирать умножение на 256, что дает нам 978874550692723072
. Теперь нам просто нужно написать некоторый запутанный код, чтобы разделить его на 256, а затем распечатать отдельные байты в обратном порядке:
#include <stdio.h>
double x [] = { 978874550692723072, 8 };
char *y = (char *)x;
int main(int argc, char **argv){
if (x[1]) {
x[0] /= 2;
main(--x[1], (char **)++y);
}
putchar(*--y);
}
Теперь у нас есть много приведений, передачи (рекурсивных) аргументов main
, которые полностью игнорируются (но оценка для получения приращения и уменьшения крайне важна), и, конечно, это совершенно произвольно выглядящее число, чтобы скрыть тот факт, что мы делаем это действительно довольно просто.
Конечно, поскольку все дело в запутывании, если мы хотим этого, мы можем сделать больше шагов. Например, мы можем воспользоваться оценкой короткого замыкания, чтобы превратить наш if
оператор в одно выражение, поэтому тело main выглядит следующим образом:
x[1] && (x[0] /= 2, main(--x[1], (char **)++y));
putchar(*--y);
Для тех, кто не привык к запутанному коду (и / или коду для гольфа), это начинает выглядеть довольно странно - вычисление и отбрасывание логики and
некоторого бессмысленного числа с плавающей запятой и возвращаемого значения main
, которое даже не возвращает ценность. Хуже того, не осознавая (и не задумываясь) о том, как работает оценка короткого замыкания, может быть даже не сразу очевидно, как избежать бесконечной рекурсии.
Наш следующий шаг, вероятно, будет отделять печать каждого символа от поиска этого символа. Мы можем сделать это довольно легко, сгенерировав правильный символ в качестве возвращаемого значения main
и распечатав, что main
возвращает:
x[1] && (x[0] /= 2, putchar(main(--x[1], (char **)++y)));
return *--y;
По крайней мере, мне это кажется достаточно запутанным, поэтому я оставлю это на этом.
Это просто создание двойного массива (16 байтов), который - если интерпретировать его как массив символов - создает коды ASCII для строки "C ++ Sucks"
Тем не менее, код работает не на каждой системе, он опирается на некоторые из следующих неопределенных фактов:
Следующий код печатает C++Suc;C
, поэтому все умножение только для последних двух букв
double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
Другие довольно подробно объяснили вопрос, я хотел бы добавить, что это неопределенное поведение в соответствии со стандартом.
C ++ 11 3.6.1 / 3 Основная функция
Функция main не должна использоваться внутри программы. Связь (3.5) с main определяется реализацией. Программа, которая определяет main как удаленный или объявляет main как встроенный, статический или constexpr, неверна. Имя main не является зарезервированным. [Пример: функции-члены, классы и перечисления могут называться main, как и сущности в других пространствах имен. - конец примера]
Код можно переписать так:
void f()
{
if (m[1]-- != 0)
{
m[0] *= 2;
f();
} else {
printf((char*)m);
}
}
Он производит набор байтов в double
массиве, m
которые соответствуют символам «C ++ Sucks», за которыми следует нулевой терминатор. Они запутали код, выбрав удвоенное значение, которое при удвоении 771 раз в стандартном представлении выдает тот набор байтов с нулевым терминатором, который предоставляется вторым членом массива.
Обратите внимание, что этот код не будет работать в другом представлении с прямым порядком байтов. Кроме того, звонить main()
строго запрещено.
f
возвращаете int
?
int
ответ в вопросе. Позвольте мне это исправить.
Сначала мы должны вспомнить, что числа двойной точности хранятся в памяти в двоичном формате следующим образом:
(i) 1 бит для знака
(ii) 11 битов для показателя степени
(iii) 52 бита для величины
Порядок битов уменьшается с (i) до (iii).
Сначала десятичное дробное число преобразуется в эквивалентное дробное двоичное число, а затем оно выражается в виде порядка величины в двоичном виде.
Таким образом, номер 7709179928849219.0 становится
(11011011000110111010101010011001010110010101101000011)base 2
=1.1011011000110111010101010011001010110010101101000011 * 2^52
Теперь при рассмотрении величины битов 1 пренебрегают, так как все методы порядка величин должны начинаться с 1.
Таким образом, часть величины становится:
1011011000110111010101010011001010110010101101000011
Теперь степень 2 равна 52 , нам нужно добавить к ней число смещения как 2 ^ (биты для показателя -1) -1, т.е. 2 ^ (11 -1) -1 = 1023 , поэтому наш показатель степени становится 52 + 1023 = 1075
Теперь наш код mutiplies номер с 2 , 771 раз , что делает показатель увеличится на 771
Таким образом, наш показатель равен (1075 + 771) = 1846 , бинарный эквивалент которого (11100110110)
Теперь наше число положительное, поэтому наш бит знака равен 0 .
Таким образом, наш модифицированный номер становится:
знаковый бит + экспонента + величина (простая конкатенация битов)
0111001101101011011000110111010101010011001010110010101101000011
поскольку m преобразуется в указатель на символ, мы разделим битовую комбинацию на 8 частей от LSD
01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
(чей эквивалент Hex :)
0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43
Который из карты персонажей, как показано:
s k c u S + + C
Теперь, когда это сделано, m [1] равно 0, что означает символ NULL
Теперь предположим, что вы запускаете эту программу на машине с прямым порядком байтов (бит младшего разряда хранится в младшем адресе), так что указатель m указывает на бит младшего адреса, а затем продолжает работу, занимая биты в порциях 8 (как тип, приведенный к char * ) и printf () останавливается, если в последнем чанке установлено значение 00000000 ...
Этот код, однако, не является переносимым.