Как работает код 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);
}
ob_dev
источник
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

Никогда не пишите такой код.


For 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 или унарный оператор & , указатель функции с типом « тип, возвращающий функцию » преобразуется в выражение, имеющее тип « указатель на тип, возвращающий функцию ».

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);
Per Johansson
1
&fooне то же самое, что fooкогда дело доходит до массива. &fooуказатель на массив, fooуказатель на первый элемент Они имеют одинаковую ценность, хотя. Для функций, funи &funоба указатели на функцию.
Пер Йоханссон
1
К вашему сведению, если вы посмотрите на соответствующий ответ на другой вопрос, упомянутый выше , вы увидите, что есть вариант, который на самом деле соответствует C99. Страшно, но правда.
Даниэль Приден
41

Он использует рекурсию, арифметику указателей и использует поведение округления целочисленного деления.

В j/1000перспективе округляется до 0 для всех j < 1000; однажды jдостигает 1000, это оценивает к 1.

Теперь, если у вас есть a + (b - a) * n, где n0 или 1, вы в конечном итоге с aif n == 0и bif 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 (что немного грязно, но работает).

tdammers
источник
1
Хороший ответ, но одно сомнение. Как главный выход с кодом выхода 1001? Main ничего не возвращает .. Любое возвращаемое значение по умолчанию?
Кришнабхадра
2
Когда j достигает 1000, main больше не возвращается в себя; вместо этого он вызывает функцию libc exit, которая принимает код выхода в качестве аргумента и, в общем, выходит из текущего процесса. В этот момент j равно 1000, поэтому j + 1 равно 1001, что становится кодом выхода.
tdammers