Почему 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;
}
На собе