Собеседование по C++: 7 ловушек, которые валят 90% кандидатов

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

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

Начать курс

Почему C++ до сих пор пугает даже сеньоров?

C++ — это язык, который даёт вам полный контроль над памятью, но ровно до тех пор, пока вы не забудете поставить точку с запятой или случайно не вызовете виртуальную функцию из конструктора. Собеседования по C++ славятся своей жестокостью: вас могут спросить и про битовые поля, и про устройство виртуальной таблицы, и про тонкости std::move. Если вы готовитесь к техническому интервью, эта статья — ваш чек-лист. Мы разберём самые коварные темы, которые почти гарантированно всплывут на собеседовании, и покажем, как на них отвечать, чтобы не провалиться.

На образовательном портале PythonLib мы часто обсуждаем, как переходить с Python на C++ и наоборот. Но сегодня — чистый C++. Никаких компромиссов.

1. Виртуальные функции и таблицы VTable: как не попасть в ловушку

Вопрос: "Что будет, если вызвать виртуальную функцию из конструктора базового класса?" — классика жанра. 70% кандидатов отвечают неправильно. Давайте разберёмся.

Почему это опасно?

Когда вы вызываете виртуальную функцию в конструкторе базового класса, объект производного класса ещё не создан. Виртуальная таблица (VTable) в этот момент указывает на методы базового класса, а не производного. В результате полиморфизм не работает.

#include <iostream>

class Base {
public:
    Base() { callFoo(); }  // Вызов в конструкторе
    virtual void foo() { std::cout << "Base::foo\n"; }
    void callFoo() { foo(); }
};

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

int main() {
    Derived d;  // Выведет: Base::foo, а не Derived::foo
    return 0;
}

Вывод: никогда не вызывайте виртуальные функции в конструкторах и деструкторах. Если интервьюер спросит — смело объясняйте механизм VTable.

2. Умные указатели: RAII и управление памятью

Без этой темы не обходится ни одно серьёзное собеседование по C++. Вопросы про std::unique_ptr, std::shared_ptr и std::weak_ptr — база. Но часто спрашивают про циклические ссылки и std::enable_shared_from_this.

Как не создать утечку с shared_ptr?

Представьте, что у вас есть два объекта, которые ссылаются друг на друга через shared_ptr. Счётчик ссылок никогда не обнулится — память утечёт. Решение: один из указателей должен быть weak_ptr.

#include <memory>
#include <iostream>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Слабая ссылка, чтобы избежать цикла
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->prev = a;  // weak_ptr не увеличивает счётчик
    // При выходе из области видимости оба узла корректно удалятся
    return 0;
}

На собеседовании обязательно упомяните концепцию RAII (Resource Acquisition Is Initialization) — это философия C++, которая связывает время жизни ресурса с временем жизни объекта.

3. Move-семантика и std::move: магия или подвох?

Типичный вопрос: "Что делает std::move?" Ответ "перемещает объект" — неверный. std::move всего лишь приводит тип к rvalue-ссылке. Реальное перемещение происходит в move-конструкторе или move-операторе присваивания.

Пример для собеседования

#include <string>
#include <vector>
#include <iostream>

class MyString {
    std::string data;
public:
    MyString(const std::string& s) : data(s) {}
    // Move-конструктор
    MyString(MyString&& other) noexcept : data(std::move(other.data)) {
        std::cout << "Move constructor called\n";
    }
};

int main() {
    std::vector<MyString> vec;
    MyString s("Hello, C++!");
    vec.push_back(std::move(s));  // Перемещаем, а не копируем
    return 0;
}

Важный нюанс: после перемещения объект находится в "valid but unspecified state" — он существует, но его содержимое не определено. Использовать его можно, но только присвоив новое значение.

4. STL: контейнеры и алгоритмы, которые обязан знать каждый

Спросят про std::vector vs std::list — это как "в чём разница между массивом и связным списком". Но копните глубже: инвалидация итераторов.

Когда итераторы становятся бесполезны?

  • std::vector: при вставке или удалении элемента все итераторы, ссылки и указатели на элементы после точки вставки инвалидируются. При перераспределении памяти — все.
  • std::deque: вставка в начало/конец не инвалидирует ссылки на другие элементы, но итераторы могут стать невалидными.
  • std::list и std::forward_list: итераторы остаются валидными, кроме удалённого элемента.
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    auto it = v.begin() + 2;  // указывает на 3
    v.insert(v.begin(), 0);   // перераспределение! it теперь невалиден
    // std::cout << *it;     // Ошибка: неопределённое поведение
    return 0;
}

5. Шаблоны и метапрограммирование: от простого к сложному

Вопрос "Что такое специализация шаблонов?" — обязателен. Но если хотите блеснуть, расскажите про SFINAE (Substitution Failure Is Not An Error). Это механизм, который позволяет компилятору выбирать перегрузку шаблона, если подстановка аргументов не удалась.

Пример SFINAE в действии

#include <type_traits>
#include <iostream>

template<typename T>
auto check(T t) -> decltype(t.foo(), void()) {
    std::cout << "T has foo()\n";
}

void check(...) {
    std::cout << "T does not have foo()\n";
}

struct A { void foo() {} };
struct B {};

int main() {
    check(A{});  // T has foo()
    check(B{});  // T does not have foo()
    return 0;
}

На собе

Блоги

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