Как пройти собеседование по C++: топ вопросов с примерами кода
Собеседование по C++ — одно из самых сложных испытаний для разработчика. Этот язык сочетает мощь низкоуровневого программирования с современными парадигмами ООП и шаблонов. Чтобы успешно пройти интервью, нужно не просто знать синтаксис, но и понимать, как работает память, компилятор и стандартная библиотека. В этой статье мы разберём топ-10 вопросов, которые чаще всего задают на собеседованиях по C++, с подробными ответами и примерами кода.
1. Что такое RAII и зачем он нужен?
RAII (Resource Acquisition Is Initialization) — одна из ключевых идиом C++. Суть проста: ресурс (память, файл, мьютекс) захватывается в конструкторе объекта и освобождается в деструкторе. Это гарантирует корректную очистку даже при исключениях.
Пример неправильного кода (без RAII):
void process() {
int* ptr = new int(42);
// ... код, который может выбросить исключение
delete ptr; // утечка, если исключение случилось до delete
}
Пример с RAII (используем std::unique_ptr):
#include <memory>
void process() {
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// ... код с исключениями
// ptr автоматически удалится при выходе из области видимости
}
Почему это спрашивают: RAII — основа безопасного управления ресурсами в C++. Без него невозможно писать надёжный код.
2. Чем отличается виртуальная функция от чисто виртуальной?
Виртуальная функция (virtual) может быть переопределена в производном классе, но имеет реализацию в базовом. Чисто виртуальная функция (= 0) не имеет реализации в базовом классе и делает класс абстрактным — нельзя создать объект такого класса.
Пример:
class Base {
public:
virtual void foo() { std::cout << "Base foo\n"; } // виртуальная
virtual void bar() = 0; // чисто виртуальная
};
class Derived : public Base {
public:
void foo() override { std::cout << "Derived foo\n"; }
void bar() override { std::cout << "Derived bar\n"; }
};
Дополнительный вопрос: Как работает vtable? Компилятор создаёт таблицу виртуальных функций для каждого класса с виртуальными методами. Каждый объект содержит указатель на эту таблицу (vptr). При вызове виртуальной функции происходит динамическая диспетчеризация через vtable.
3. Какие умные указатели есть в C++ и когда их использовать?
В C++11 появились три типа умных указателей:
- std::unique_ptr — единоличное владение. Нельзя копировать, только перемещать. Идеален для ресурсов с одним владельцем.
- std::shared_ptr — разделяемое владение. Использует счётчик ссылок. Подходит, когда несколько объектов должны владеть одним ресурсом.
- std::weak_ptr — не владеет ресурсом, но может наблюдать за shared_ptr. Нужен для разрыва циклических ссылок.
Пример использования unique_ptr:
#include <memory>
class MyClass { /* ... */ };
void example() {
auto ptr = std::make_unique<MyClass>();
// auto ptr2 = ptr; // Ошибка! Нельзя копировать
auto ptr3 = std::move(ptr); // ОК, перемещение
}
Пример shared_ptr и weak_ptr для предотвращения циклических ссылок:
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak_ptr, чтобы избежать цикла
};
4. Что такое move-семантика и std::move?
Move-семантика позволяет «перемещать» ресурсы из одного объекта в другой вместо копирования. Это особенно полезно для временных объектов или когда копирование дорого (например, std::string или std::vector).
Пример до C++11 (копирование):
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = v1; // глубокое копирование всех элементов
Пример с move-семантикой:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // перемещение: v1 теперь пуст
Важно: std::move не перемещает сам объект. Он просто преобразует lvalue в rvalue-ссылку, что позволяет вызвать конструктор перемещения или оператор присваивания перемещением.
Реализация своего класса с поддержкой move:
class MyBuffer {
public:
MyBuffer(size_t size) : data_(new int[size]), size_(size) {}
// Конструктор перемещения
MyBuffer(MyBuffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
~MyBuffer() { delete[] data_; }
private:
int* data_;
size_t size_;
};
5. Как работает полиморфизм в C++?
Полиморфизм в C++ бывает двух видов:
- Компиляционный (статический) — через перегрузку функций и шаблоны. Выбор версии происходит на этапе компиляции.
- Динамический (run-time) — через виртуальные функции и наследование. Выбор происходит во время выполнения через vtable.
Пример динамического полиморфизма:
class Animal {
public:
virtual void speak() const { std::cout << "Animal sound\n"; }
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof!\n"; }
};
class Cat : public Animal {
public:
void speak() const override { std::cout << "Meow!\n"; }
};
void makeSound(const Animal& animal) {
animal.speak(); // динамическая диспетчеризация
}
int main() {
Dog dog;
Cat cat;
makeSound(dog); // Woof!
makeSound(cat); // Meow!
}