Как эта программа дублирует себя?

Этот код взят от Hacker's Delight. Там написано, что это самая короткая такая программа на C и имеет длину 64 символа, но я этого не понимаю:

    main(a){printf(a,34,a="main(a){printf(a,34,a=%c%s%c,34);}",34);}

Я попытался его скомпилировать. Он компилируется с 3 предупреждениями и без ошибок.


person PDP    schedule 24.04.2015    source источник
comment
3 предупреждения и отсутствие ошибок означает успешную компиляцию, почему бы вам не запустить ее?   -  person Cestarian    schedule 24.04.2015
comment
@Cestarian Вопрос не в том, что он делает, а в том, как он это делает? Отсюда и титул.   -  person Barry    schedule 24.04.2015
comment
На самом деле это не самая короткая программа. Фактическая самая короткая длина составляет 0 байт. Вы можете заставить компилятор успешно скомпилировать 0-байтовый файл c в исполняемый файл. Запуск этого исполняемого файла приводит к печати 0 байтов, что представляет собой весь исходный код исходной программы.   -  person Grant Peters    schedule 24.04.2015
comment
@GrantPeters: Можно? Как? Пустой исходный файл является допустимой единицей перевода, но не действительным исходным файлом.   -  person Keith Thompson    schedule 24.04.2015
comment
@KeithThompson см. stackoverflow.com/questions/17515790/ для записи ioccc, которая использовала этот   -  person Grant Peters    schedule 24.04.2015
comment
@GrantPeters: Очевидно, пустой исходный файл даже не используется.   -  person Keith Thompson    schedule 24.04.2015
comment
@KeithThompson Да, посмотрите на make-файл для него. Он передается компилятору. Справедливости ради следует отметить, что заявка выиграла за нарушение правил конкурса.   -  person Grant Peters    schedule 24.04.2015


Ответы (4)


Эта программа основана на предположениях, что

  • возвращаемый тип main - int
  • тип параметра функции int по умолчанию и
  • аргумент a="main(a){printf(a,34,a=%c%s%c,34);}" будет оцениваться первым.

Это вызовет неопределенное поведение. Порядок вычисления аргументов функции не гарантируется в C.
Тем не менее, эта программа работает следующим образом:

выражение присваивания a="main(a){printf(a,34,a=%c%s%c,34);}" присвоит строке "main(a){printf(a,34,a=%c%s%c,34);}" значение a, а значение выражения присваивания также будет "main(a){printf(a,34,a=%c%s%c,34);}" в соответствии со стандартом C --C11: 6.5.16

Оператор присваивания сохраняет значение в объекте, обозначенном левым операндом. выражение присваивания имеет значение левого операнда после присваивания [...]

Принимая во внимание приведенную выше семантику оператора присваивания, программа будет расширена как

 main(a){
      printf("main(a){printf(a,34,a=%c%s%c,34);}",34,a="main(a){printf(a,34,a=%c%s%c,34);}",34);
}  

ASCII-код 34 равен ". Спецификаторы и соответствующие им аргументы:

%c ---> 34 
%s ---> "main(a){printf(a,34,a=%c%s%c,34);}" 
%c ---> 34  

Лучше версия будет

main(a){a="main(a){a=%c%s%c;printf(a,34,a,34);}";printf(a,34,a,34);}  

Он на 4 символов длиннее, но, по крайней мере, следует за K&R C.

person haccks    schedule 24.04.2015

Он основан на нескольких причудах языка C и (как мне кажется) на неопределенном поведении.

Во-первых, он определяет функцию main. Законно объявить функцию без типа возвращаемого значения или типов параметров, и они будут считаться int. Вот почему часть main(a){ работает.

Затем он вызывает printf с 4 параметрами. Поскольку у него нет прототипа, предполагается, что он возвращает int и принимает int параметров (если только ваш компилятор неявно не объявляет иначе, как это делает Clang).

Первый параметр предполагается int и равен argc в начале программы. Второй параметр — 34 (это ASCII для символа двойной кавычки). Третий параметр — это выражение присваивания, которое присваивает строку формата a и возвращает ее. Он основан на преобразовании указателя в целое число, допустимом в C. Последний параметр — это еще один символ кавычек в числовой форме.

Во время выполнения спецификаторы формата %c заменяются кавычками, %s заменяется строкой формата, и вы снова получаете исходный код.

Насколько я знаю, порядок оценки аргументов не определен. Этот квайн работает, потому что присваивание a="main(a){printf(a,34,a=%c%s%c,34);}" оценивается до того, как a будет передано в качестве первого параметра в printf, но, насколько я знаю, нет правила для его применения. Кроме того, это не может работать на 64-разрядных платформах, поскольку преобразование указателя в целое усекает указатель до 32-разрядного значения. На самом деле, хотя я и вижу, как это работает на некоторых платформах, это не работает на моем компьютере с моим компилятором.

person zneak    schedule 24.04.2015
comment
Да, есть UB из-за порядка оценки. Там также больше UB, потому что тип a (int) отличается от типа, ожидаемого преобразованием %s (char *). - person Jerry Coffin; 24.04.2015
comment
@zneak спасибо, теперь я понимаю эту программу, и ты действительно хорош в c - person PDP; 24.04.2015
comment
@ДжерриКоффин; Что такое более или менее UB? - person haccks; 24.04.2015
comment
@hackks: еще один пример неопределенного поведения, поэтому даже если (например) UB, указанный в ответе, был каким-то образом исправлен, программа в целом все равно имела бы UB. IOW, это не поведение (более неопределенное), а (большее поведение) неопределенное. - person Jerry Coffin; 24.04.2015

Это работает на основе множества причуд, которые позволяет вам делать C, и некоторого неопределенного поведения, которое работает в вашу пользу. Чтобы:

main(a) { ...

Предполагается, что типы равны int, если они не указаны, поэтому это эквивалентно:

int main(int a) { ...

Несмотря на то, что main должен принимать либо 0, либо 2 аргумента, и это поведение undefined, это может быть разрешено как простое игнорирование отсутствующего второго аргумента.

Далее тело, которое я буду развешивать. Обратите внимание, что a — это int согласно main:

printf(a,
       34,
       a = "main(a){printf(a,34,a=%c%s%c,34);}",
       34);

Порядок оценки аргументов не определен, но мы полагаемся на 3-й аргумент — присваивание — который оценивается первым. Мы также полагаемся на неопределенное поведение, позволяющее присвоить char * int. Также обратите внимание, что 34 — это значение ASCII для ". Таким образом, ожидаемое воздействие программы:

int main(int a, char** ) {
    printf("main(a){printf(a,34,a=%c%s%c,34);}",
           '"',
           "main(a){printf(a,34,a=%c%s%c,34);}",
           '"');
    return 0; // also left off
}

Что при оценке дает:

main(a){printf(a,34,a="main(a){printf(a,34,a=%c%s%c,34);}",34);}

что было исходной программой. Тада!

person Barry    schedule 24.04.2015

Программа должна печатать собственный код. Обратите внимание на сходство строкового литерала с кодом программы в целом. Идея состоит в том, что литерал будет использоваться как строка формата printf(), потому что его значение присваивается переменной a (хотя и в списке аргументов) и что он также будет передан как строка для печати (поскольку выражение присваивания оценивается как значение что было назначено). 34 — это код ASCII для символа двойной кавычки ("); его использование позволяет избежать строки формата, содержащей экранированные литеральные символы кавычек.

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

Кроме того, тип a по умолчанию равен int, и нет никакой гарантии, что int будет достаточно широким, чтобы содержать указатель объекта без его усечения.

Кроме того, стандарт C определяет только две разрешенные подписи для main(), и используемая подпись не входит в их число.

Более того, тип printf(), выведенный компилятором в отсутствие прототипа, неверен. Ни в коем случае не гарантируется, что компилятор сгенерирует последовательность вызовов, которая ему подходит.

person John Bollinger    schedule 24.04.2015