Як працює код C, який друкує від 1 до 1000 без циклів чи умовних операторів?


148

Я знайшов Cкод, який друкує від 1 до 1000 без циклів або умовних умов : Але я не розумію, як це працює. Чи може хтось пройти код і пояснити кожен рядок?

#include <stdio.h>
#include <stdlib.h>

void main(int j) {
  printf("%d\n", j);
  (&main + (&exit - &main)*(j/1000))(j+1);
}

1
Ви компілюєте як C або як C ++? Які помилки ви бачите? Не можна телефонувати mainна C ++.
ніндзя

@ninjalj Я створив проект C ++ і скопіював / минув код, помилка: незаконна, лівий операнд має тип 'void (__cdecl *) (int)', а вираз повинен бути вказівником на повний тип об'єкта
ob_dev

1
@ninjalj Цей код працює на ideone.org, але не у візуальній студії ideone.com/MtJ1M
ob_dev

@oussama Схоже, але трохи більш важко зрозуміти: ideone.com/2ItXm Ви довгоочікуваний. :)
Марк

2
я видалив усі символи '&' з цього рядка (& main + (& exit - & main) * (j / 1000)) (j + 1); і цей код все ще працює.
ob_dev

Відповіді:


264

Ніколи не пишіть такий код.


Для j<1000, j/1000дорівнює нулю (ціле ділення). Так:

(&main + (&exit - &main)*(j/1000))(j+1);

еквівалентно:

(&main + (&exit - &main)*0)(j+1);

Який є:

(&main)(j+1);

Які дзвінки mainз j+1.

Якщо j == 1000, тоді виходять ті самі рядки, що й:

(&main + (&exit - &main)*1)(j+1);

Який зводиться до

(&exit)(j+1);

Що є exit(j+1)і залишає програму.


(&exit)(j+1)і exit(j+1)по суті те саме - цитуючи C99 §6.3.2.1 / 4:

Позначення функції - це вираз, який має тип функції. За винятком випадків, коли це операнд оператора sizeof або unary & operator , позначення функції з типом " функція, що повертає тип " перетворюється у вираз, що має тип " покажчик на тип повернення функції ".

exitє позначенням функції. Навіть без одинарної &адреси оператора, це трактується як вказівник на функціонування. ( &Просто робить це явним.)

А виклики функцій описані в §6.5.2.2 / 1 і наступні:

Вираз, що позначає викликану функцію, повинен мати вказівник типу на функцію повернення void або повернення типу об'єкта, відмінного від типу масиву.

Так exit(j+1)працює через автоматичне перетворення типу функції в тип вказівника на функцію, а також (&exit)(j+1)працює з явним перетворенням у тип вказівника на функцію.

Зважаючи на це, наведений вище код не відповідає ( mainприймає або два аргументи, або взагалі жодного), і &exit - &main, я вважаю, не визначений відповідно до §6.5.6 / 9:

Коли віднімаються два покажчики, обидва повинні вказувати на елементи одного і того ж об’єкта масиву або один минулий останній елемент об’єкта масиву; ...

Додавання (&main + ...)було б дійсним саме по собі, і його можна було б використати, якщо додана кількість дорівнювала нулю, оскільки §6.5.6 / 7 говорить:

Для цілей цих операторів вказівник на об’єкт, який не є елементом масиву, поводиться так само, як вказівник на перший елемент масиву довжини один із типом об'єкта як його тип елемента.

Тому додавання нуля &mainбуло б нормальним (але не дуже корисним).


4
foo(arg)і (&foo)(arg)еквівалентні, вони називають foo аргументом arg. newty.de/fpt/fpt.html - цікава сторінка про покажчики функцій.
мат

1
@Krishnabhadra: у першому випадку fooце вказівник, &fooце адреса цього вказівника. У другому випадку fooце масив і &fooеквівалентно foo.
мат

8
Не зайвий складний, принаймні для C99:((void(*[])()){main, exit})[j / 1000](j + 1);
Пер Йоханссон

1
&fooне те саме, що, fooколи мова йде про масив. &fooє вказівником на масив, fooє вказівником на перший елемент. Вони мають однакову цінність. Для функцій funі &funє обома вказівниками на функцію.
Пер Йоханссон

1
FYI, якщо ви подивитеся на відповідну відповідь на інше питання , про яке було сказано вище , ви побачите, що є варіація, яка насправді відповідає C99. Страшно, але правда.
Даніель Приден

41

Він використовує рекурсію, арифметику вказівника та використовує поведінку округлення цілого поділу.

У j/1000перспективі округляється до 0 для всіх j < 1000; як тільки jдосягає 1000, він оцінює до 1.

Тепер, якщо у вас є a + (b - a) * n, де nабо 0, або 1, ви закінчите, aякщо n == 0і bякщо n == 1. Використовуючи &main(адресу main()) і &exitдля aі b, термін (&main + (&exit - &main) * (j/1000))повертається, &mainколи jнижче 1000, &exitінакше. Потім вказівник функції подається в аргумент j+1.

Ця вся конструкція призводить до рекурсивної поведінки: хоча jвона нижче 1000, mainназиває себе рекурсивно; коли jдосягає 1000, він викликає exitнатомість, роблячи вихід програми з кодом виходу 1001 (який начебто брудний, але працює).


1
Хороша відповідь, але сумніваюсь ... Який головний вихід з кодом виходу 1001? Основне нічого не повертає. Будь-яке значення повернення за замовчуванням?
Крішнабхадра

2
Коли j досягає 1000, головний більше не повторюється в собі; натомість він викликає функцію libc exit, яка приймає код виходу як аргумент і, ну, виходить із поточного процесу. У цій точці j дорівнює 1000, тому j + 1 дорівнює 1001, що стає вихідним кодом.
tdammers
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.