Как пройти собеседование по C++: топ вопросов продвинутого уровня

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как пройти собеседование по C++: топ вопросов продвинутого уровня

Собеседование на позицию C++ разработчика среднего и старшего уровня — это не просто проверка синтаксиса. Интервьюеры хотят увидеть глубокое понимание внутреннего устройства языка, умение работать с памятью, шаблонами и многопоточностью. В этой статье мы разобрали самые частые и сложные вопросы, которые задают на собеседованиях по C++ продвинутого уровня. Каждый вопрос сопровождается объяснением и примером кода.

1. Умные указатели и RAII

Вопрос об умных указателях — классика. Ожидается, что вы не просто назовете std::unique_ptr, std::shared_ptr и std::weak_ptr, но и объясните их внутреннее устройство и когда какой использовать.

Типичный вопрос: В чем разница между unique_ptr и shared_ptr? Как работает счетчик ссылок в shared_ptr? Зачем нужен weak_ptr?

Ответ: std::unique_ptr — это эксклюзивное владение ресурсом. Его нельзя копировать, только перемещать (move semantics). std::shared_ptr использует подсчет ссылок (reference counting) для разделяемого владения. Когда счетчик падает до нуля, объект удаляется. std::weak_ptr — это "наблюдатель", который не увеличивает счетчик ссылок. Он используется для предотвращения циклических зависимостей (например, в графах или деревьях).

#include <memory>
#include <iostream>

struct Resource {
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource released\n"; }
};

int main() {
    // unique_ptr
    std::unique_ptr<Resource> ptr1 = std::make_unique<Resource>();
    // std::unique_ptr<Resource> ptr2 = ptr1; // Ошибка компиляции - копирование запрещено
    std::unique_ptr<Resource> ptr2 = std::move(ptr1); // Перемещение разрешено

    // shared_ptr
    std::shared_ptr<Resource> sptr1 = std::make_shared<Resource>();
    std::shared_ptr<Resource> sptr2 = sptr1; // Копирование разрешено, счетчик = 2

    // weak_ptr
    std::weak_ptr<Resource> wptr = sptr1; // Счетчик не увеличивается
    if (auto locked = wptr.lock()) {
        std::cout << "Resource is still alive\n";
    }

    return 0;
}

Дополнительный вопрос: Как реализовать свой SharedPtr? Здесь важно упомянуть RAII (Resource Acquisition Is Initialization) — принцип, при котором захват ресурса происходит в конструкторе, а освобождение — в деструкторе.

2. Виртуальные деструкторы и таблица виртуальных функций (vtable)

Этот вопрос проверяет понимание полиморфизма и устройства виртуальных функций в C++.

Типичный вопрос: Почему в базовом классе с виртуальными функциями обязательно должен быть виртуальный деструктор? Как работает vtable?

Ответ: Если деструктор базового класса не виртуальный, то при удалении объекта производного класса через указатель на базовый класс будет вызван только деструктор базового класса. Это приведет к утечке ресурсов, которые выделил производный класс.

Каждый класс, имеющий хотя бы одну виртуальную функцию, содержит статическую таблицу (vtable). В ней хранятся указатели на реализации виртуальных функций. Каждый объект такого класса имеет скрытый указатель (vptr), который указывает на vtable своего класса. Вызов виртуальной функции происходит через двойную косвенность: объект → vptr → vtable → функция. Это добавляет небольшой оверхед по сравнению с обычным вызовом.

#include <iostream>

class Base {
public:
    virtual void show() { std::cout << "Base\n"; }
    virtual ~Base() = default; // Виртуальный деструктор
};

class Derived : public Base {
public:
    void show() override { std::cout << "Derived\n"; }
    ~Derived() override { std::cout << "Derived destructor\n"; }
};

int main() {
    Base* ptr = new Derived();
    ptr->show(); // Выведет "Derived"
    delete ptr;   // Вызовет сначала ~Derived(), потом ~Base()
    return 0;
}

Дополнительный вопрос: Можно ли вызвать виртуальную функцию из конструктора? Ответ: Да, но это опасно. Во время работы конструктора базового класса vptr указывает на vtable базового класса, а не производного. Поэтому будет вызвана версия из базового класса, даже если вы переопределили функцию в производном.

3. Move-семантика и идеальная передача (perfect forwarding)

Move-семантика — одна из ключевых фич C++11. Ожидается, что вы понимаете, что такое rvalue-ссылки, std::move и std::forward.

Типичный вопрос: В чем разница между std::move и std::forward? Что такое collapsing rules?

Ответ: std::move — это просто static_cast<T&&>. Он безусловно превращает lvalue в rvalue, чтобы можно было вызвать move-конструктор или move-оператор присваивания. std::forward используется в шаблонах для идеальной передачи. Он сохраняет категорию значения (lvalue/rvalue) аргумента, переданного в шаблонную функцию.

Collapsing rules (сворачивание ссылок) работают так: если в шаблоне тип выводится как T&, то T& & сворачивается в T&, а T&& — в T&. Только T&& && дает T&&. Это позволяет отличать lvalue от rvalue.

#include <iostream>
#include <utility>
#include <vector>

void process(const std::vector<int>& v) {
    std::cout << "Lvalue overload\n";
}

void process(std::vector<int>&& v) {
    std::cout << "Rvalue overload\n";
}

template <typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg)); // Идеальная передача
}

int main() {
    std::vector<int> v = {1                

Блоги

Рекомендаций по книгам