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

в 16:44, , рубрики: C, c++, Занимательные задачки, ненормальное программирование, собеседование вопросы, тестовое задание, язык си

Прошло 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 <stdio.h>
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 <stdio.h>). Считаю, что эту функцию должен уметь писать программист любой уровня.

Разбор внутреннего цикла

 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. И в заключении, дополнительное задание – проверить это утверждение для отрицательных чисел.

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

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

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

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

Автор: Algoritmist

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js