Что такое IOCCC?

International Obfuscated C Code Contest (сокращенно IOCCC) — это конкурс компьютерного программирования на самый творчески обфусцированный C код. Проводимый ежегодно, он описывается как прославление синтаксической непрозрачности [C]. Код выигрыша 27-го конкурса, проведенного в 2020 году, был опубликован в июле 2020 года.

Как это работает ?

Шаг 1: замена макросов

Чтобы понять логику кода, который у нас есть, я решил заменить тот макрос, который я видел, его значением, избавиться от ненужного пробела и попытаться сделать отступ кода, когда я почувствовал, что это нужно. Благодаря макросу DAHDIT со значением for мы уже можем видеть три вложенных цикла for. В C цикл for – это фрагмент кода, который выполняется несколько раз, пока не будет достигнуто конечное условие, а вложенный цикл – это просто цикл внутри цикла. Мы также можем видеть использование {}, которое указывает на то, что у нас есть несколько функций: main(), которая является нашей точкой входа, код, который программа запустит в первую очередь, __DIT() и _DAH.

Шаг 2: замена имен функций и переменных

Внутри и вне функций мы можем объявить переменную вроде _DAH_[], которая представляет собой массив символов или _DIT. Для большей ясности я решил дать имена этим переменным и изменить имена функций, которые у нас уже есть.

  • _DIT станет строкой
  • DAH_ станет c
  • DIT_ станет следующим
  • _DAH_[] станет морзе
  • _DIT_ станет morsecpy

Обратите внимание: благодаря макросу _DAHDIT мы знаем, что все эти переменные имеют тип char *, что означает указатель на char.

Немного проанализировав поведение функций, я увидел, что __DIT() делает то же самое, что и библиотечная функция putchar(), которая выводит символ в стандартный вывод. strong>, наше окно терминала. Поэтому я решил переименовать его в _putchar(). Затем я изменил имя функции _DAH() на translate(), я объясню в следующих шагах.

Шаг 3: роль функции translate()

char translate(int c)
{
 if (c > 3)
 _putchar(translate(c >> 1));
 else
 _putchar(‘\0’);if (c & 1)
 return (‘-’);
 else
 return (‘.’);
}

Насколько я понял, функции нужно передать параметр типа int, потому что он сравнивается с int. Поэтому я решил передать ему параметр c. Теперь функция является рекурсивной, что означает, что она будет вызывать сама себя и каждый раз создавать стек вызовов с разными значениями, пока не достигнет базового случая. Здесь наш базовый случай: если c меньше или равно 3, то будет напечатан завершающий нулевой символ ('\0'), который отмечает конец строки в C. Мы уже можем видеть два бинарных оператора в функции (& и ››), поэтому должно быть что-то о двоичном значении параметра c. После некоторых исследований я обнаружил, что 3 — это последнее положительное число, двоичный эквивалент которого равен 2 битам (11). Но подождите, разве азбука Морзе не двоичная? Да, это! Теперь становится понятно, почему мы имеем дело с двоичными значениями! Вернемся к нашей функции translate(). По сути, если c больше 3, то есть если c имеет более двух битов в двоичном формате, мы будем печатать то, что возвращает вызов функции translate(), когда мы передаем его c, сдвинутый на 1 бит вправо (это то, что ›› означает оператор), пока не достигнет 3 и не напечатает символ '\0'. Но как насчет второй части? Вот где начинается настоящий перевод: он возвращает символ «-», который является азбукой Морзе, эквивалентной 1, когда c & 1 отличается от 0, или же он возвращает символ «.», который является азбукой Морзе, эквивалентной 0. Два здесь есть интересные вещи: тот факт, что эти операторы return возвращают символы, позволил мне определить тип возвращаемого значения функции translate(), то есть char, и операцию c & 1. означает, что мы будем сравнивать биты в c с битами в 1 (которые всего лишь… 1), и он будет равен 1, только если оба бита равны 1 (отсюда '-'), в противном случае он равен 0 (отсюда '.').

Эту часть сложно понять, но это ключ к нашему переводу Морзе. Вот почему я выбрал для этой функции название «перевод».

Шаг 4: настройка нашей функции _putchar()

int _putchar(char c)
{
     return (write(1 , &c , 1));
}

Просто для ясности я подумал, что будет разумнее сделать нашу функцию _putchar() похожей на настоящую из стандартной библиотеки. Поэтому я добавил тип возврата, int и оператор возврата, в который мы поместим системный вызов «запись», заданный исходной программой. . Я также добавил параметр c типа char, который является символом, который мы хотим напечатать.

Шаг 5: объяснение функции main()

Это, безусловно, самая сложная часть кода для понимания, поэтому я сделаю все возможное, чтобы объяснить ее. Я разберу каждый вложенный цикл и объясню, для чего нужна каждая переменная.

  • morse[]: строка, содержащая все символы, с которыми мы будем сравнивать нашу входную строку.
  • *string: указатель на строку, которую мы все равно получим из стандартного ввода, также называемую клавиатурой, которую мы будем перебирать, символ за символом.
  • *c: указатель на строку, содержащую текущий символ во входной строке.
  • *next: указатель на символ-заполнитель
  • *morsecpy: указатель на строку, содержащую символы Морзе, которая будет изменена
  • *gets: это функция стандартной библиотеки, которая получает строку из стандартного ввода.

Для каждого цикла for я разобью его на три этапа: инициализация, которая устанавливает значение переменной в какое-то значение и является нашей отправной точкой, условие для достижения , мы зацикливаемся, пока не достигнем его, и итерация, которая представляет собой действие, которое происходит каждый раз, когда цикл зацикливается. Каждый шаг отделяется знаком «;», и все они заключены в круглые скобки, но каждый шаг может содержать несколько действий, разделенных знаком «,».

Но зачем столько петель? Мне тоже это было интересно, и я думаю, это потому, что каждая буква, преобразованная в азбуку Морзе, может состоять из нескольких символов (например, «H» — это «….»).

Первая петля

for (string = malloc(81), next = string++; gets(string); _putchar(‘\n’))
  1. Инициализация: мы динамически выделяем 81 байт в памяти для нашей входной строки и устанавливаем рядом со следующим символом внутри строки
  2. Условие: мы выполняем цикл до тех пор, пока не достигнем конца входной строки, также известного как символ «\0».
  3. Итерация: мы печатаем новую строку каждый раз

Что делает этот цикл, так это то, что он берет строку и выполняет следующий код, а затем печатает новую строку, поэтому он готов взять новую строку из стандартного ввода.

Второй цикл

for (c = string; *c; _putchar(*morsecpy ? translate(*next) : ‘?’), _putchar(‘ ‘), c++)
  1. Инициализация: мы устанавливаем текущий символ c в текущий символ во входной строке
  2. Условие: пока строка c не достигнет символа ‘\0’
  3. Итерация: здесь у нас есть тернарный оператор внутри вызова _putchar, который работает как условный оператор if/else. Отличается ли текущий символ в morsecpy от ‘\0’? Если да, напечатайте возвращаемое значение, данное вызовом translate(), с параметром next (напоминание: «-» или «.»); если нет, напечатайте ‘?’. Затем у нас есть еще одно действие, которое просто говорит напечатать пробел, который будет пробелом между каждой буквой Морзе, а затем мы переходим к следующему символу в c (следующий символ во входной строке)

Этот цикл будет тем, который печатает символы Морзе для каждой буквы. Этот цикл будет выполняться полностью для каждой итерации предыдущего цикла.

Последний вложенный цикл

for (*next = 2, morsecpy = morse; *morsecpy && (*morsecpy != (*c >= ‘a’ ? *c & 223 : *c)); (*next)++, morsecpy++)
  1. Инициализация: мы устанавливаем текущий символ рядом с 2; и мы устанавливаем наш символ morsecpy на символ в морзе
  2. Условие: пока мы не достигнем конца morsecpy AND (&&), пока morsecpy отличается от результата другого тернарного оператора. Является ли *c ≥ ‘a’, то есть строчными буквами (все символы на самом деле являются положительными числовыми значениями, см. таблицу ASCII)? Если да, то результат *c & 223, если нет, то просто *c. Это выражение «*c & 223» означает, что мы применяем бинарный оператор AND к *c и 223, и с помощью онлайн-компилятора я обнаружил, что он преобразует символ в его значение в верхнем регистре! Это означает, что вторая часть конечного условия *morsecpy отличается от *c или его значения в верхнем регистре, в зависимости от того, является ли *c строчным. По сути, цикл продолжается до тех пор, пока переменная *c не соответствует символам в morse[]
  3. Итерация: увеличить следующий на один и перейти к следующему символу в morsecpy

Этот цикл будет выполняться полностью для каждой итерации предыдущего цикла.

Код внутри циклов

if (*morsecpy >= ‘a’)
    *next += *morsecpy — ‘a’;
else
    *next += 0;

Здесь я изменил тернарный оператор на if/else для ясности. Этот код выполняется для каждой итерации последнего цикла.

Итак, здесь происходит следующее: если символ в morsecpy находится в нижнем регистре, я добавляю ASCII-значение *morsecpy — «a» к следующему, в противном случае он ничего не добавляет к следующему.

Пример:

Думаю будет проще понять на примере. Возьмем букву «Т». Итак, эта буква является первой буквой внутри нашей входной строки. Итак, во втором цикле c инициализируется как «T», затем мы _putchar (мы печатаем) результат троичной операции. Мы не достигли конца morsecpy, поэтому мы печатаем возвращаемое значение вызова translate(*next). 'T' — вторая буква внутри азбуки Морзе, поэтому *next будет равно 3. translate(3) напечатает символ '\0' в конце предыдущей строки, а поскольку 3 & 1 = 1, он вернет '- '. Таким образом, цикл печатает «-», что является правильным переводом «T» по Морзе.

Теперь, если мы скомпилируем программу и назовем ее «f», мы сможем запустить ее, введя «./f» в командной строке и нажав клавишу ввода. Программа ждет, пока мы введем строку (вспомните нашу функцию gets()). Итак, я могу ввести что-то вроде Hello, Holberton, и результатом будет:

.... . .-.. .-.. --- --..-- ? .... --- .-.. -... . .-. - --- -.

Мы можем проверить с помощью онлайн-переводчика азбуки Морзе, и это работает!

Не забывайте о стандартных библиотеках

Раньше у нас не было стандартных библиотек, включенных в наш файл, и мы использовали функции, определенные в этих библиотеках, такие как adgets() или malloc(). Компилятору это очень не понравилось, поэтому, чтобы он скомпилировался, я включил их в начало файла:

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

Небольшие изменения для ясности

Я также позволял себе добавлять {}, когда чувствовал, что это необходимо, и добавлял в основную функцию такие мелочи, как тип возвращаемого значения (int) и возвращаемое значение (0 в случае успеха). Это просто для того, чтобы мне было легче читать и компилировать.

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