Найти в Дзене

Подготовка программиста к техническому собеседованию. Лучший способ.

В своей карьере я провёл сотни собеседований программистов. И, к сожалению, на большей части из них, кандидаты меня разочаровали. Очень редко кто мог подтвердить заявленные навыки. Было как в стихе: «В резюме он Лев Толстой, а не деле Хлестаков». В результате, мог делать офферы менее, чем 5% кандидатов и многие вакансии оставались не заполненными на протяжении месяцев. Несмотря на то, что требования снижал ниже плинтуса. Как же получалось, что приходили кандидаты, которые где-то работали годами, но не могли пройти простое собеседование по С++? (простите, я буду писать про С++, т.к. именно таких специалистов я в большинстве случаев и искал, но, предполагаю, что мои соображения актуальны и для других IT-специальностей). Некоторые даже обижались, когда я задавал более-менее глубокие вопросы! Считали, что упоминания об их многолетнем опыте должно ввести меня в благоговейный трепет, чтобы их нанять без дополнительных вопросов. При этом, не могли на эти вопросы ответить. Несколько человек пр

В своей карьере я провёл сотни собеседований программистов. И, к сожалению, на большей части из них, кандидаты меня разочаровали. Очень редко кто мог подтвердить заявленные навыки. Было как в стихе: «В резюме он Лев Толстой, а не деле Хлестаков».

В результате, мог делать офферы менее, чем 5% кандидатов и многие вакансии оставались не заполненными на протяжении месяцев. Несмотря на то, что требования снижал ниже плинтуса.

Как же получалось, что приходили кандидаты, которые где-то работали годами, но не могли пройти простое собеседование по С++? (простите, я буду писать про С++, т.к. именно таких специалистов я в большинстве случаев и искал, но, предполагаю, что мои соображения актуальны и для других IT-специальностей).

Некоторые даже обижались, когда я задавал более-менее глубокие вопросы! Считали, что упоминания об их многолетнем опыте должно ввести меня в благоговейный трепет, чтобы их нанять без дополнительных вопросов. При этом, не могли на эти вопросы ответить. Несколько человек просто вставали и уходили с оскорблённым видом.

У меня ощущение, что значительная часть IT-шников не может адекватно оценить своих способностей. Как правило, преувеличивают их и многократно. Поэтому считают, что готовиться к собеседованию, освежать знания – ниже их достоинства.

А многолетний опыт и название должности не всегда что-то означает. Есть много компаний, где от сотрудников практически ничего не требуют; уволить оттуда, даже самого бестолкового сотрудника, могут лишь за какую-то дисциплинарную провинность. Да и вообще в IT увольнение, кроме случаев планового сокращения персонала – достаточно редкое явление. Сказывается дефицит кадров. А если программист не тянет свои текущие задачи, ему начинают давать что-то попроще: обычно имеются задачи самого разного уровня, включая и те, с которыми справится хоть обезьяна.

Кроме того, язык С++ весьма гибок и мощен. Любую программу можно написать, используя лишь малый арсенал его возможностей. И в некоторых организациях годами вяленько пилят какое-нибудь ПО, используя морально устаревшие стандарты и средства разработки (например, Borland C++ или MFC). А если такой коллектив ещё и не пополняется «свежей кровью», то наступает технологический застой и деградация. Пишут на С++98, да и то ограничиваясь лишь базовыми возможностями (боятся виртуальных функций, шаблонов, STL и подобного). Что толку, если на собеседование приходит заслуженный сотрудник такого «отстойника» с надутыми щеками?

Но вернёмся к теме заметки. Как программисту объективно оценить свои способности, как понять в каких областях присутствуют пробелы и каким способом из заполнить?

Нет, я не буду советовать вам читать какие-либо книги, записываться на курсы, читать форумы. Сейчас идёт 25й год и всё перечисленное устарело.

Используйте нейросети! Неважно какую - ChatGPT, DeepSeek, Grok, Gemini, Llama – все они справятся с задачей. Просто отправьте промт, например, такой: «Я готовлюсь к собеседованию на вакансию С++ программиста. Проверь мои знания, задавая вопросы. Не забывай о STL и последних стандартах».

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

Нет, нейросеть свои вопросы будет каждый раз формулировать по-разному, с акцентом на разные нюансы. И ответ нужно будет дать в письменном виде, который нейросеть проверит, оценит, при необходимости дополнит. А если вы ошиблись – подробно объяснит в чём вы неправы и озвучит правильный ответ. Последующие же вопросы нейросеть будет задавать с учётом ваших предыдущих ответов – либо повышая сложность, либо возвращаясь к более простым понятиям. Это обеспечит не тупую зубрёжку, а понимание и готовность к самым заковыристым техническим вопросам.

И что не менее важно – нейросеть готова заниматься с вами хоть целыми днями напролёт, практически бесплатно. Даже самый тупой ученик никогда не выведет её из себя – она может разжёвывать одно и тоже часами, при этом сохраняя вежливое и доброжелательное отношение, психологически поддерживая в любых неудачах. Ни один «кожаный» на такое не способен!

Удивительно, я активно пользуюсь LLM уже несколько лет и продолжаю находить новые сферы применения. И то, что мне больше не нужны учебники – не преувеличение. Так недавно изучил язык программирования Rust – исключительно в форме дружелюбной беседы с ChatGPT. Сообщил ему – что я знаю С++ и мне нужно объяснить разницу в этих языках. Что он и сделал. Те моменты, которые я не понял – переспрашивал. Давал ему кусочки кода на С++ и просил написать то же самое на расте и т.д. В результате – буквально через пару дней такого обучения смог написать достаточно сложную программу на новом для меня языке.

Приложение.
Вот фрагмент моей беседы с ChatGPT, где он проверяет мои знания. Как видите, меня он тоже «вывел на чистую воду» - мои знания не безупречны.

Я: Приветствую. Я готовлюсь к собеседованию на вакансию С++ программиста. Проверь мои знания, задавая вопросы по одному и проверяя ответы.

GPT: Привет! Давай начнем с одного вопроса и будем двигаться дальше.

Вопрос 1: Что такое RAII, и как оно используется в C++?

Я: это способ управления ресурсами, например памятью или открытыми файлами. Создаём дополнительный объект, где в конструкторе выделяем ресурс, а в деструкторе освобождаем. Когда объект выходит из области видимости, деструктор вызывается автоматически.

GPT: Отличный ответ! Ты правильно описал принцип RAII (Resource Acquisition Is Initialization). Это действительно важная идиома в C++, используемая для управления ресурсами.

Кратко выделю основные моменты:

- Инициализация ресурса в конструкторе: Когда объект создается, нужный ресурс (например, память, файл или синхронизирующий примитив) инициализируется в конструкторе.

- Освобождение ресурса в деструкторе: Когда объект выходит из области видимости, его деструктор вызывается автоматически, что гарантирует освобождение занятых ресурсов (например, освобождение памяти или закрытие файла).

- Гарантия освобождения ресурсов: Это предотвращает утечки памяти и утечки других ресурсов, так как освобождение происходит автоматически при выходе из блока, даже если происходит исключение.

Вопрос 2: Объясните, в чем разница между std::vector и std::list. В каких случаях каждый из них предпочтительнее использовать?

Я: это близкие по смыслу контейнеры. List больше подходит для случаев, когда нужно вставлять элементы в произвольное место. А Vector – когда новые элементы добавляются в конец. Vector шустрее.

GPT: Да, вы правы в отношении контейнеров std::list и std::vector в C++.

- std::list: Это двусвязный список, который обеспечивает быструю вставку и удаление элементов в любом месте контейнера. Однако, доступ к элементам по индексу медленный, поскольку приходится перемещаться по списку, чтобы попасть в нужное место. Он подходит, когда вам нужно часто вставлять или удалять элементы в середине списка.

- std::vector: Это динамический массив, который обеспечивает быстрый доступ к элементам по индексу и эффективен для добавления элементов в конец (хотя это может потребовать перераспределения памяти, если начальная ёмкость сильно отличается). Он хорошо подходит для случаев, когда элементы в основном добавляются в конец и нужны быстрые операции доступа по индексу.

Таким образом, выбор контейнера зависит от специфики задачи: требуется ли больше операций вставки/удаления в середину (тогда std::list), или чаще необходим произвольный доступ и добавление в конец (тогда std::vector).

Вопрос 3: Можете ли вы объяснить, что такое "rule of three" (правило трех) в C++ и почему его следование важно при проектировании классов?

Я: это порядок объявления полей: вначале public, далее private, после protected

GPT: На самом деле, "rule of three" (правило трёх) в C++ не связано с порядком объявления полей, а относится к правильному управлению ресурсами в классах. Оно гласит, что если вы реализуете один из следующих трёх функций-членов, то, скорее всего, вам нужно будет реализовать все три:

1. Деструктор (~ClassName()): Для корректного освобождения всех ресурсов, которые класс выделяет.

2. Конструктор копирования (ClassName(const ClassName&)): Для корректного копирования ресурсов, если экземпляр класса копируется.

3. Оператор присваивания копированием (ClassName& operator=(const ClassName&)): Для корректного копирования ресурсов при присваивании одного объекта другому после их создания.

Это правило важно для управления ресурсами, такими как память, файлы или сетевые соединения, и предотвращения проблем, связанных с их неправильным копированием или освобождением, таких как утечки памяти или некорректное освобождение ресурсов. В C++11 и позднее появилось "правило пяти" и "правило нуля", учитывающее также move-семантику и помогающее более безопасно управлять ресурсами.

Я: точно, я знал, но забыл

GPT: Это нормально! C++ — это язык с множеством тонкостей, и такие вещи легко забыть.

Вопрос 4: Что такое перегрузка функций в C++ и чем она отличается от переопределения функций? Можете привести пример каждого из этих понятий?

Я: перегрузка ф-ций, это когда есть несколько ф-ций с одинаковым именем, но с разными параметрами. А переопределение - когда в классе-наследнике объявляем такую-же (имя+параметры).

GPT: Да, вы абсолютно правы в своем определении.

Перегрузка функций (function overloading) — это механизм, который позволяет в одном классе или пространстве имен иметь несколько функций с одинаковым именем, но с различными параметрами (разное количество параметров и/или разные типы параметров). Это позволяет использовать одну и ту же функциональность с разными типами входных данных.

Переопределение функций (function overriding) — это механизм, при котором функция в базовом классе становится виртуальной (или явным образом помеченной как таковая), а в классе-наследнике предоставляется своя реализация этой функции с теми же самыми именем и параметрами. Переопределение используется для реализации полиморфизма в объектно-ориентированном программировании.

Вопрос 5: Что такое умные указатели в C++ и чем они отличаются от обычных указателей? Какие виды умных указателей предоставляет стандартная библиотека C++ и в каких ситуациях их следует использовать?

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

GPT: Да, вы описали один из видов умных указателей — std::shared_ptr. Он действительно управляет счетчиком ссылок, чтобы гарантировать, что ресурс будет освобожден только тогда, когда больше нет ни одного shared_ptr, ссылающегося на этот ресурс.

Умные указатели — это объекты, которые сами управляют временем жизни динамически выделенных ресурсов, таких как память. Они помогают избежать утечек памяти и других проблем, связанных с ручным управлением временем жизни объектов.

В стандартной библиотеке C++ есть несколько типов умных указателей:

1.std::unique_ptr: Обеспечивает уникальное владение ресурсом. Только один unique_ptr может владеть ресурсом в любой момент времени. Когда unique_ptr умирает, он уничтожает управляемый ресурс. Это наиболее простой вид умного указателя и рекомендуется использовать его, когда вам не требуется делиться владением ресурсом.

2.std::shared_ptr: Поддерживает совместное владение ресурсом, используя счетчик ссылок. Более одного shared_ptr может владеть ресурсом, и ресурс будет освобожден только тогда, когда последний shared_ptr, ссылающийся на ресурс, будет уничтожен.

3.std::weak_ptr: Это некопируемый указатель, который используется для устранения циклических зависимостей, возникающих при использовании shared_ptr. Он не увеличивает счетчик ссылок, и его необходимо преобразовать в shared_ptr для получения доступа к управляемому объекту (при этом сначала проверяется, не был ли объект уничтожен).

Таким образом, основные преимущества умных указателей — это автоматическое управление временем жизни объектов и предотвращение утечек памяти, что значительно упрощает управление ресурсами в C++ приложениях.