[Из песочницы] Тест на знание языка Си, найденный в первоапрельской шутке

Прошло 1 апреля. Часто первоапрельские шутки, выложенные в Интернете, продолжают свое шествие, и всплывают совершенно в неожиданное время. О такой шутке про язык Си и будет эта статья. В каждой шутке есть только доля шутки, и я ее взял на вооружение для беглого тестирования на знание языка Си.

Надо написать программу (с пояснениями), в которой будет работать следующая строка:

for(;P("\n"),R--;P("|"))for(e=C;e--;P("_"+(*u++/8)%2))P("| "+(*u/4)%2);

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

На заре своей карьеры программиста, мне друг показал статью про то, что язык Си и UNIX первоапрельская шутка. В качестве доказательства абсурдности языка приводилась вышеприведенная строка кода. На мой взгляд, вполне рабочая. Через некоторое время при проведении собеседования вспомнилась эта шутка. Как и при решении многих других тестов, здесь важен не результат (он задает цель работы), а сам процесс разбора и понимания.

Где мы взяли статью я уже не помню. Каждый раз его нахожу в поисковике по фразе «си и unix первоапрельская шутка». В этих репостах когда-то потерялся один минус в инкременте после «R» и «e», и появился второй обратный слеш в строке »\n».

Попробуйте разобраться с заданием сами. Не задумывайтесь пока над смыслом программы.

Форматирование творит чудеса
Настоятельно советую привести это однострочное безобразие в читаемый вид, расставив переносы строк, отступы и пробелы.
for ( ; P("\n"), R--; P("|"))
    for (e = C; e--; P("_" + (*u++ / 8) % 2))
        P("| " + (*u / 4) % 2);

Это совсем легко, если видели текст нормальных программ. На пробелы можно закрыть глаза, но циклы должны быть на разных строках с разными отступами.

Надо написать элементарную программу, типа «Hello world!»
Вместо вывода приветствия всему миру, надо вставить текст самого задания и объявить некие переменные (это будет далее).
#include 
int main()
{
...
    for ( ; P("\n"), R--; P("|"))
        for (e = C; e--; P("_" + (*u++ / 8) % 2))
            P("| " + (*u / 4) % 2);
    return 0;
} 

Это уже можно обсуждать. Зачем нужен include? И нужен ли он здесь? Можно ли без return? И совсем жестокий вопрос. Какие параметры у функции main?

Не поленитесь, и попробуйте ответить на эти вопросы сами.


Разбор внешнего цикла
Если человек успешно дошел до этого этапа, то он уже понимает, что есть два вложенных цикла. Разберем внешний.
for ( ; P("\n"), R--; P("|"))

Здесь встречаем совсем простую проблемку. Нет инициализатора (после открытой скобки идет сразу точка с запятой). Некоторых это смущает. Это часто бывает, если человек пишет программы на другом языке, например, на Паскале.

Настоящим камнем преткновения, даже у достаточно опытных программистов, встречает выражение «P (»\n»), R--». Многие просто не знают, что есть такая операция «запятая», и что результатом его работы будет результат выражения, стоящего после запятой. Выражение до запятой тоже вычисляется, но его результат не используется. Причем эта операция имеет самый низкий приоритет. Следовательно, сначала выполняется P (»\n»), а потом R--.

Результат выражения R-- здесь является условием выполнения. Это тоже некоторых смущает, хотя этот прием часто используется. Многие программисты считают излишним писать в условных операторах if, выражения типа if (a!= 0) … Тут аналогичный случай (R-- != 0). Настала пора добавить объявление первой переменной. Инкремент говорит о том, что это точно не вещественное число. Подойдет любой целочисленный тип, даже беззнаковый. Эту переменную надо не только объявить, но и проинициализировать каким-либо положительным значением (лучше небольшим).

Обычно, дойдя до сюда, всем уже ясно, что есть функция P, которая принимает на вход строку. Тут проблем уже нет. Надо объявить эту функцию. Поскольку смысл нам не важен, то она может быть даже пустой. Мне больше нравится функция, выводящая текст на экран (тут и пригодился заранее написанный #include ). Считаю, что эту функцию должен уметь писать программист любой уровня.


Разбор внутреннего цикла
 for (e = C; e--; P("_" + (*u++ / 8) % 2) )

Здесь в цикле уже все знакомо. Инкремент в проверке на выполнении цикла, как было выше. Добавляем переменную e, по аналогии с R. Можно сразу объявить и переменную C того же типа, хотя это может быть и константа, или даже define. Тут воля автора.

Интерес тут вызывает вызов функции P.

 P("_" + (*u++ / 8) % 2) 

Если посмотреть дальше, то мы увидим в теле функции подобную конструкцию.
 P("| " + (*u / 4) % 2);

Тут стоит набраться терпения. Цель близка. Это венец этого «шедевра». Не спешите открывать следующее разъяснение, подумайте.

Изюминка
Разбираем два выражения:
"_" + (*u++ / 8) % 2

"| " + (*u / 4) % 2

Далее будем рассматривать первое выражение. Оно более сложное. Понятно, что здесь сперва вычисляется выражение в скобках, потом берется от него остаток от деления на 2, в конце это число добавляется к строке.

Самое простое, это вычисление остатка от деления. Изредка встречаются программисты не использующие такую операцию. Они могут смутиться. Главное, то что эта операция производится над целочисленными типами и результат тоже целочисленный. Коварный вопрос для самостоятельной проработки, может ли быть результат выражения (*u++ / 8) % 2 отрицательным?

Поскольку результат выражения в скобках должен быть целочисленным, то и операция деление целочисленное, и делимое целочисленное. У начинающих программистов выражение *u++ может вызвать неуверенность: в наличии постинкремента в выражении и в приоритете выполнения операций постинкремента и разыменование указателя. Данный прием иногда используется в программах на Си при движении по массиву. Выражение возвращает значение по текущему указателю (до инкрементации) и смещает указатель на следующий элемент. Следовательно, переменная u не просто указатель, но и является массивом. Дополнительный вопрос, какого размера (в элементах) должен быть этот массив?

Самый «красивый» прием — это прибавление числа к строке. Надо помнить, что это язык Си. Не стоит ждать преобразования числа в строку, а тем более строки в число. Все гораздо более странно, чем может показаться с первого взгляда, но очень логично для Си. Строка — это массив символов, а значит указатель на память, где находится первый символ. Если это указатель, то прибавление к нему целого числа означает вычисление адреса, сдвинутого относительно исходного указателя на заданное число элементов. В данном примере после получения остатка от деления на 2 выходит либо 0, либо 1. Соответственно, либо строку передаем в функцию P без смещения (как есть), либо смещаем на один символ в конец строки. Простой вопрос, могут ли возникнуть проблемы при смещении на один символ в строке, состоящей из одного символа (как в нашем случае)?

Выражение (X / 8) % 2 — это просто получение четвертого бита. Для беззнакового целого числа это эквивалентно (X >> 3) & 1. И в заключении, дополнительное задание — проверить это утверждение для отрицательных чисел.


Специально не буду давать текст программы. Считаю, что после всех подсказок эту программу можно легко написать.

Если Вы думаете, что это выдуманный пример, то я могу поспорить. Реально попадается гораздо более тяжелый для разбора код. И не подумайте, что я призываю писать такой ужас.

Для тех, кто попадет на такое тестирование: тут главное не пугаться.

Для тех, кто захочет использовать на собеседование: если Вы решите дать эту задачку на собеседование, а в глазах у соискателя блеснет улыбка, то это значит, что вы оба читали этот пост. Но Вы не расстраивайтесь, пусть повторит с объяснением…

Комментарии (23)

  • 6 апреля 2017 в 19:57

    +12

    Через некоторое время при проведении собеседования вспомнилась эта шутка.

    Очень надеюсь, что это было собеседование с самим собой в зеркале 1-го января и вам не пришло в голову принимать решение о найме программиста на основе этой бредятины.
    • 6 апреля 2017 в 20:30

      0

      Почему же, очень простая задача, для сишника со стажем более полутора лет. Только вот, к сожалению, с каждым годом новых специалистов по C/C++ все меньше и меньше:(
      • 6 апреля 2017 в 20:42

        +1

        Работу найти сложней, кнопочки на JS ляпать проше и работу найти легче.
      • 6 апреля 2017 в 20:52

        +5

        Сама по себе задача простая без вопросов, вполне можно развлечь себя ей. Однако — смотрите, как бы вы прокомментировали собеседование, на котором кандидату (видимо на должность C программиста) задавали бы вопросы про оператор «запятая», сложение числа со строкой и приоритет операций? Готов человек блестяще решивший эту задачку писать операционные системы, базы данных или какие-нибудь cache oblivious алгоритмы? Ведь сейчас перед программистами С стоят уже в основном действительно сложные и нетривиальные задачи (все простое пишем на питоне).

        Была одно время мода на такого рода собеседования (там еще триграфы любили спрашивать), но казалось бы уже прошла.

        • 7 апреля 2017 в 05:06

          –1

          Я при поиске C программиста показывал кусок кода из продакшена и просил его прокомментировать. Т.к. это был код in-memory b-tree базы данных, то там были и макроопределения с переменным количеством параметров, и блокировки и rwlock’и, и ссылочные типы данных. В общем полный набор. Большинство заваливается уже на разнице в #Include и #include «common.h». Более-менее адекватного программиста нашли только через полгода, в основном люди пишут в порядке возрастания популярности на Delphi/Python/PHP/Java. О существовании C/C++ знают, но архитектуры отличные от ARM и x86 приводят в ступор, а ведь там не все тривиально, интел нам многое прощает, то же выравнивание блоков и межсегментную адресацию памяти. На ARM или MIPS мы получим исключение если участок памяти находится между сегментами и мы делаем что то вроде object→timespec.tv_nsec;
          • 7 апреля 2017 в 07:46

            0

            На ARM мы получим исключение если участок памяти находится между сегментами

            А можете пояснить, что вы подразумеваете под понятием «сегментам» на ARM,
            и ARM с MMU или с MPU имеется ввиду?

  • 6 апреля 2017 в 20:06

    +2

    Вот за эти заковырки многие и любят/ненавидят (кому что больше нравится) С/С++
  • 6 апреля 2017 в 20:19 (комментарий был изменён)

    –1

    Странно. Я пишу на более-менее современном С++, и считаю, что язык С знаю не очень хорошо (как и некоторые заморочки С++). Тем не менее, ровно за 3 минуты написал обвязку, с которой приведенный фрагмент компилируется и даже не падает в рантайме. Это я не для похвастаться, это я к тому, что уж слишком просто :)

    Но есть нюанс: я считерил. Вообще не пришло в голову, что Р можно сделать функцией. Почему-то сразу подумал про макрос, и написал

    #define P(x) 0
    
    (хотя макросы не люблю и без крайней необходимости обычно не использую).
  • 6 апреля 2017 в 20:45

    0

    Из той же оперы в Python:
    [r'% r',][~0] % {(): '''^''' uR'|' in 2**2*__name__[::-8//(lambda _:~_)(3 or 2)]*2}<2>3j,(`3`) is ([])
    

    Кто-нибудь скажет, не запуская, чему это будет равно? (отсюда)
    • 7 апреля 2017 в 04:19

      0

      Вообще-то, если попытаться запустить, то падает с ошибкой
    • 7 апреля 2017 в 04:21

      0

      False?
  • 6 апреля 2017 в 20:49

    0

    2 минуты http://cpp.sh/3pmon
    • 6 апреля 2017 в 21:41

      +3

      Эк вас занесло. 3 секунды: http://cpp.sh/6wx7

      • 6 апреля 2017 в 21:43

        0

        вы читаете мои мысли
        • 6 апреля 2017 в 21:44

          0

          Подумал то же самое. Но вы и их прочитали : D

      • 6 апреля 2017 в 21:44

        0

        Похоже на эту оптимизацию в GCC.
        • 6 апреля 2017 в 21:50 (комментарий был изменён)

          0

          Оптимизация хвостовой рекурсии с аккумулятором? Круто, давно пора, я наверное еще 2 года назад удивлялся, что ее нет. Самое сложное — представить как можно больше паттернов рекурсий таким образом. Возможно, даже все представимы


          Только что-то из патча не совсем понятно, как это сделано. Просто присвоили одному хитрому макросу результат другого хитрого макроса ???

          • 6 апреля 2017 в 21:58

            0

            А, так это первоапрельская шутка… Жаль, а ведь можно было реально сделать подобную оптимизацию (с рекурсией, а не отправкой тел в /dev/null)

            • 6 апреля 2017 в 22:11

              0

              Кстати, весьма полезная «оптимизация»: иногда возникает задача создать библиотеку-заглушку для линковки
  • 6 апреля 2017 в 21:42

    +5

    #define for(...)
    #define P(...)
    int main() {
        for(;P("\n"),R--;P("|"))for(e=C;e--;P("_"+(*u++/8)%2))P("| "+(*u/4)%2);
    }
    
    • 6 апреля 2017 в 23:23

      +1

      Интересно, что понимается под словом «работает».

  • 7 апреля 2017 в 00:20

    +4

    Блин, я специально не раскрывал подсказки. Думал, тут есть содержательный смысл и все эти вызовы
    void P(const char *s){
        printf("%s", s);
    }
    
    в итоге напечатают красивую картинку в консоли. Понятно, что если сделать
    char *u,
    

    то можно разными строками получать разные картинки. Я пытался сделать
    int k = 8; /* 0, 1, 2 .. 16 */
    int *u = &k;
    

    Ничего красивого не вышло. Потом прочитал спойлеры и разочаровался. И неинтересно, и не смешно. Чувствую себя обманутым! После такого собеседования я бы и сам к вам на работу не пошел!
  • 7 апреля 2017 в 01:16

    0

    Практически на рефлексе: слова, написанные целиком капсом — макросы. Сразу после этого заставить пример компилироваться и ничего не делать (или делать всё, что нам заблагорассудится) — тривиально (аргументы макросов можно не использовать вообще — пример резко упростится).


    Но разбор прочёл не без интереса.

© Habrahabr.ru