Чому цей код дає вихід 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:
Основна функція не повинна використовуватися в межах програми.
Це, окрім цього, спирається на той факт, що на типовому споживчому комп’ютері a double
довжиною становить 8 байт і використовує певне відоме внутрішнє представлення. Початкові значення масиву обчислюються так, що при виконанні "алгоритму" остаточне значення першого double
буде таким, що внутрішнє представлення (8 байт) буде кодами ASCII з 8 символів C++Sucks
. Потім другий елемент масиву 0.0
, перший байт якого 0
знаходиться у внутрішньому поданні, що робить це дійсним рядком у стилі C. Потім він надсилається на вихід, використовуючи printf()
.
Якщо запустити це на HW там, де деякі з перерахованих вище не містять, це призведе до сміттєвого тексту (або, можливо, навіть доступу поза межами).
basic.start.main
3.6.1 / 3 з тим же формулюванням.
main()
або замінити його викликом API для форматування жорсткого диска чи будь-чого іншого.
Мабуть, найпростіший спосіб зрозуміти код - це працювати над речами в зворотному напрямку. Почнемо з друку рядка - для балансу будемо використовувати "С ++ Скелі". Важливий момент: так само, як і оригінал, довжина рівно восьми символів. Оскільки ми будемо робити (приблизно) як оригінал, і роздруковувати його у зворотному порядку, ми почнемо, розміщуючи його у зворотному порядку. Для нашого першого кроку ми просто розглянемо цей бітовий візерунок як а 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
твердження в єдиний вираз, тому тіло головного виглядає так:
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 байт), який - якщо трактувати як масив char - створює коди ASCII для рядка "C ++ Sucks"
Однак код працює не в кожній системі, він спирається на деякі з наступних невизначених фактів:
Наступний код друкується C++Suc;C
, тому все множення є лише для двох останніх літер
double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);
Інші пояснили питання досить грунтовно, я хотів би додати зауваження, що це не визначена поведінка відповідно до стандарту.
C ++ 11 3.6.1 / 3 Основна функція
Основна функція не повинна використовуватися в межах програми. Зв'язок (3.5) головного визначається реалізацією. Програма, яка визначає основну як видалену або яка визначає головну як вбудовану, статичну чи конвекспр, неправильно формується. Ім'я 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
Тепер наш код знімає число в 2 , 771 раз, що змушує показник збільшуватися на 771
Отже наш показник дорівнює (1075 + 771) = 1846 , двійковий еквівалент якого (11100110110)
Зараз наше число позитивне, тому наш біт знаків 0 .
Отже, наша змінена кількість стає:
знак біта + показник + величина (просте з'єднання бітів)
0111001101101011011000110111010101010011001010110010101101000011
оскільки m перетворений на покажчик char, ми розділимо бітовий малюнок на шматки 8 від LSD
01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
(чий шестнадцятковий еквівалент :)
0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43
Що з карти символів, як показано:
s k c u S + + C
Тепер, коли це було зроблено m [1] є 0, що означає символ NULL
Тепер, якщо припустити, що ви запускаєте цю програму на машині малої ендіанки (біт нижчого порядку зберігається за нижньою адресою), тож вказівник m вказує на біт нижчої адреси, а потім продовжує, беручи біти в патрони 8 (як тип передається в char * ), і printf () зупиняється, коли зустрічається 00000000 в останній шматок ...
Однак цей код не є портативним.