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

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

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

Начать курс

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

Собеседование на позицию C++ разработчика — это всегда вызов. Если базовые вопросы (разница между new и malloc, что такое виртуальный деструктор) вы, скорее всего, знаете, то продвинутый уровень требует глубокого понимания внутреннего устройства языка. В этой статье мы разберем ключевые темы, которые обязательно всплывут на техническом интервью для Middle и Senior позиций. Мы не просто перечислим вопросы, но и дадим развернутые ответы с примерами кода.

1. Умные указатели и их внутреннее устройство

Вопрос: "Расскажите, чем отличается std::unique_ptr от std::shared_ptr? Как реализована потокобезопасность счетчика ссылок?"

Это классика. Интервьюер хочет увидеть, что вы понимаете не только синтаксис, но и семантику владения ресурсами.

  • std::unique_ptr — эксклюзивное владение. Не копируется, только перемещается. Идеален для ресурсов, у которых один владелец.
  • std::shared_ptr — разделяемое владение. Использует счетчик ссылок (control block). Когда счетчик падает до нуля, ресурс уничтожается.
  • std::weak_ptr — "наблюдатель". Не увеличивает счетчик ссылок. Используется для разрыва циклических зависимостей в shared_ptr.

Продвинутый нюанс: Счетчик ссылок в shared_ptr потокобезопасен (используются атомарные операции), но это не делает потокобезопасным сам управляемый объект. Также существует два счетчика: сильных ссылок (strong) и слабых ссылок (weak). Пока есть хотя бы одна слабая ссылка, control block не удаляется, даже если объект уже уничтожен.

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    void doWork() { std::cout << "Working...\n"; }
};

int main() {
    std::unique_ptr<Resource> uptr = std::make_unique<Resource>();
    uptr->doWork();
    // std::unique_ptr<Resource> uptr2 = uptr; // Ошибка компиляции!
    std::unique_ptr<Resource> uptr2 = std::move(uptr); // Ок

    std::shared_ptr<Resource> sptr1 = std::make_shared<Resource>();
    std::shared_ptr<Resource> sptr2 = sptr1; // Счетчик ссылок = 2
    sptr1.reset(); // Счетчик = 1
    sptr2.reset(); // Счетчик = 0, объект уничтожен
    return 0;
}

2. Виртуальные таблицы (vtable) и механизм позднего связывания

Вопрос: "Как работает виртуальная функция на уровне памяти? Где хранится vtable?"

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

  • Для каждого класса, имеющего хотя бы одну виртуальную функцию, компилятор создает статическую таблицу (vtable). Она содержит указатели на виртуальные функции.
  • Каждый объект такого класса содержит скрытый указатель __vptr, который указывает на vtable своего класса.
  • При вызове виртуальной функции через указатель или ссылку, компилятор генерирует код, который идет по __vptr в vtable, находит нужную функцию по смещению и вызывает её.
  • vtable хранится в статической памяти (как глобальная переменная), а не в стеке или куче.

Продвинутый нюанс: Сложности с множественным наследованием. В этом случае у объекта может быть несколько __vptr, и возникает необходимость в thunk-функциях (корректировка указателя this). Это одна из причин, почему множественное наследование в C++ считается опасным и медленным.

#include <iostream>

class Base {
public:
    virtual void foo() { std::cout << "Base::foo\n"; }
    virtual ~Base() = default;
};

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

int main() {
    Base* ptr = new Derived();
    ptr->foo(); // Вызов через vtable: выведет "Derived::foo"
    delete ptr;
    return 0;
}

3. Move-семантика и Perfect Forwarding

Вопрос: "В чем разница между std::move и std::forward? Когда использовать rvalue-ссылки?"

Move-семантика — это одна из самых мощных (и сложных) фич современного C++. Она позволяет избежать дорогих копирований.

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

Продвинутый нюанс: Возврат из функции по значению (RVO/NRVO) часто делает явный std::move в return вредным. Компилятор и так оптимизирует код, а явный move может заблокировать NRVO.

#include <string>
#include <utility>
#include <iostream>

void processValue(std::string& s) {
    std::cout << "Lvalue: " << s << std::endl;
}
void processValue(std::string&& s) {
    std::cout << "Rvalue: " << s << std::endl;
}

template<typename T>
void forwarder(T&& arg) {
    // Perfect forwarding                

Блоги

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