C/C++ | Вопросы собесов
Разбираем вопросы с собеседований на С/С++ разработчика. Сайт: https://easyoffer.ru Написать: @easyoffer_adv
إظهار المزيد2 940
المشتركون
+15624 ساعات
+2227 أيام
+1 22730 أيام
- المشتركون
- التغطية البريدية
- ER - نسبة المشاركة
جاري تحميل البيانات...
معدل نمو المشترك
جاري تحميل البيانات...
Зачем нужен чисто виртуальный метод и какой синтаксис ?
Спросят с вероятностью 17%
Чисто виртуальный метод используется для создания абстрактного базового класса, который определяет интерфейс для всех производных классов, не предоставляя конкретной реализации этого метода. Это позволяет обеспечить единообразие и строгую структуру для семейства классов, а также гарантировать, что каждый производный класс предоставляет свою реализацию абстрактных методов.
Зачем они нужны
1️⃣Определение интерфейса: Чисто виртуальные методы определяют интерфейс, который должны реализовать все производные классы. Это гарантирует, что все производные классы будут иметь согласованное поведение.
2️⃣Предотвращение создания экземпляров базового класса: Классы с чисто виртуальными методами называются абстрактными классами. Экземпляры абстрактных классов не могут быть созданы, что помогает предотвратить ошибки использования неполных или некорректных реализаций.
3️⃣Поддержка полиморфизма: Использование абстрактных классов вместе с чисто виртуальными методами позволяет применять полиморфное поведение, где вызов метода через базовый класс приведет к выполнению соответствующей реализации в производном классе.
Чисто виртуальный метод объявляется с помощью синтаксиса
= 0
в конце объявления метода в классе. Вот пример абстрактного базового класса с чисто виртуальным методом:
class AbstractClass {
public:
virtual void abstractMethod() = 0; // Чисто виртуальный метод
virtual ~AbstractClass() {} // Виртуальный деструктор для корректного удаления
};
class ConcreteClass : public AbstractClass {
public:
void abstractMethod() override {
std::cout << "Реализация метода в производном классе." << std::endl;
}
};
int main() {
// AbstractClass a; // Ошибка: нельзя создать объект абстрактного класса
AbstractClass* ptr = new ConcreteClass();
ptr->abstractMethod(); // Вызывает метод, реализованный в ConcreteClass
delete ptr; // Освобождение памяти
return 0;
}
В этом примере, AbstractClass
содержит чисто виртуальный метод abstractMethod()
, что делает его абстрактным. Вы не можете создать экземпляр AbstractClass
, но можете использовать указатели и ссылки на AbstractClass
для доступа к объектам ConcreteClass
. ConcreteClass
реализует этот метод, предоставляя конкретные действия.
Чисто виртуальные методы являются ключевыми для проектирования расширяемых и модульных систем, использующих полиморфизм и абстракцию, что особенно важно в больших программных проектах и системах с развитой иерархией классов.
👉 Можно посмотреть Примеры как отвечают люди на этот вопрос, или перейти К списку 434 вопроса на C/C++ разработчика. Ставь 👍 если нравится контент
🔐 База собесов | 🔐 База тестовых👍 8
Какое присваивание разрешает unique_ptr ?
Спросят с вероятностью 17%
unique_ptr — это умный указатель, предоставляющий строгую собственность и управление одним объектом через указатель. Один из его ключевых аспектов заключается в том, что он не позволяет копирование объекта, чтобы гарантировать, что только один
unique_ptr
может владеть объектом. Однако unique_ptr
поддерживает перемещение, что позволяет передавать право собственности на объект от одного unique_ptr
к другому.
Разрешенные операции присваивания
1️⃣Присваивание перемещением (Move assignment):
unique_ptr
может быть перемещен с помощью оператора присваивания перемещения. Это изменяет владельца ресурса, освобождая предыдущего владельца от ответственности и перенося права на новый unique_ptr
.
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2;
ptr2 = std::move(ptr1); // ptr1 теперь пуст, ptr2 владеет ресурсом
После этого присваивания ptr1
будет пустым (null), а ptr2
будет владеть ресурсом, который ранее принадлежал ptr1
.
2️⃣Присваивание `nullptr`:
Вы можете присвоить unique_ptr
значение nullptr
для освобождения ресурса, которым он владеет, и сделать указатель пустым.
std::unique_ptr<int> ptr = std::make_unique<int>(10);
ptr = nullptr; // освобождает память и обнуляет ptr
Это автоматически освободит память (или другой ресурс), которым управлял unique_ptr
, и установит указатель в состояние null
.
Запрещенные операции присваивания
✅Копирование и присваивание копированием:
unique_ptr
не поддерживает копирование или присваивание копированием. Это означает, что вы не можете скопировать unique_ptr
напрямую, так как его конструктор копирования и оператор присваивания копированием удалены.
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2;
// ptr2 = ptr1; // Ошибка компиляции: оператор присваивания удален
// std::unique_ptr<int> ptr3(ptr1); // Ошибка компиляции: конструктор копирования удален
unique_ptr
разрешает только присваивание перемещением и присваивание nullptr
, поддерживая тем самым уникальное владение ресурсом. Это обеспечивает безопасность при управлении ресурсами и предотвращает случайное создание нескольких владельцев одного и того же ресурса. Это делает unique_ptr
отличным выбором для управления ресурсами, где требуется четкая собственность и автоматическое управление ресурсами.
👉 Можно посмотреть Примеры как отвечают люди на этот вопрос, или перейти К списку 434 вопроса на C/C++ разработчика. Ставь 👍 если нравится контент
🔐 База собесов | 🔐 База тестовых👍 2
Что известно о таком контейнере как куча ?
Спросят с вероятностью 17%
Куча (heap) — это специализированный тип дерева, который удовлетворяет свойству кучи. Это свойство гарантирует, что элемент в каждом узле является больше (в максимальной куче) или меньше (в минимальной куче) всех элементов в его дочерних узлах. Это дает ряд полезных свойств, особенно для организации и управления приоритетными очередями.
Типы:
1️⃣Минимальная куча (Min-Heap): Ключ в каждом родительском узле меньше или равен ключам в дочерних узлах. Корень, следовательно, содержит минимальный ключ во всем дереве.
2️⃣Максимальная куча (Max-Heap): Ключ в каждом родительском узле больше или равен ключам в дочерних узлах. Корень содержит максимальный ключ.
Основные операции:
✅Вставка (Insert): Добавление нового элемента в кучу. Новый элемент добавляется в конец, чтобы сохранить полноту бинарного дерева, и затем "всплывает" до правильной позиции, чтобы восстановить свойство кучи. Эта операция требует \(O(\log n)\) времени.
✅Удаление максимума/минимума (Extract Max/Min): Удаление максимального (в максимальной куче) или минимального (в минимальной куче) элемента из кучи. Обычно удаляется элемент на вершине кучи, затем последний элемент в куче перемещается на вершину, и "проседает" вниз, чтобы восстановить свойство кучи. Это также требует \(O(\log n)\) времени.
✅Просеивание вверх (Percolate Up) или просеивание вниз (Percolate Down): Эти операции используются для восстановления свойств кучи после вставки или удаления элементов.
✅Построение кучи (Build Heap): Создание кучи из неупорядоченного массива. Может быть выполнено за \(O(n)\) времени.
✅Поиск максимума/минимума (Find Max/Min): Получение максимального или минимального элемента кучи без его удаления. Это операция \(O(1)\), так как максимальный или минимальный элемент всегда находится на вершине.
Кучи часто используются в приложениях, требующих частого доступа к наибольшим или наименьшим элементам. Наиболее распространенные случаи использования включают:
✅Приоритетные очереди: Структуры данных, где элементы извлекаются на основе приоритета, а не порядка добавления. Приоритетные очереди широко используются в алгоритмах планирования, сетевом маршрутизировании и в симуляции дискретных событий.
✅Алгоритмы на графах: Например, алгоритм Дейкстры для поиска кратчайшего пути использует минимальную кучу (или приоритетную очередь) для выбора следующей вершины для обработки.
✅Сортировка кучей (Heap Sort): Эффективный алгоритм сортировки, который использует кучу для сортировки элементов.
Имплементация
Может быть реализована с помощью контейнера
std::priority_queue
, который по умолчанию является максимальной кучей. Для создания минимальной кучи можно использовать стандартный контейнер с реверсированным компаратором.
👉 Можно посмотреть Примеры как отвечают люди на этот вопрос, или перейти К списку 434 вопроса на C/C++ разработчика. Ставь 👍 если нравится контент
🔐 База собесов | 🔐 База тестовых👍 3
Что будет если несколько раз вызвать lock ?
Спросят с вероятностью 17%
Если несколько раз вызвать метод lock на одном и том же мьютексе из одного и того же потока, произойдет взаимоблокировка самого себя, что приведет к зависанию программы. Это связано с тем, что мьютекс не позволяет одному и тому же потоку захватывать его несколько раз подряд, так как он не является рекурсивным мьютексом.
Пример:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void threadFunction() {
mtx.lock(); // Первый захват мьютекса
std::cout << "Mutex locked once" << std::endl;
// Попытка повторного захвата того же мьютекса
mtx.lock(); // Здесь произойдет взаимоблокировка
std::cout << "This will never be printed" << std::endl;
mtx.unlock();
mtx.unlock();
}
int main() {
std::thread t(threadFunction);
t.join();
return 0;
}
Что происходит:
1️⃣Первый вызов mtx.lock()
успешно захватывает мьютекс.
2️⃣Второй вызов mtx.lock()
пытается захватить мьютекс, который уже захвачен тем же потоком. Так как мьютекс не рекурсивный, поток заблокируется и будет ждать освобождения мьютекса самим собой, что невозможно, поэтому поток зависнет навсегда.
Решение проблемы:
Для ситуаций, когда требуется захват мьютекса несколько раз из одного и того же потока, следует использовать рекурсивный мьютекс (std::recursive_mutex
). Он позволяет одному и тому же потоку захватывать мьютекс несколько раз, и каждый вызов lock
должен сопровождаться соответствующим вызовом unlock
.
Пример:
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex mtx;
void threadFunction() {
mtx.lock(); // Первый захват мьютекса
std::cout << "Mutex locked once" << std::endl;
// Повторный захват того же мьютекса
mtx.lock(); // Успешный повторный захват
std::cout << "Mutex locked twice" << std::endl;
mtx.unlock(); // Первый вызов unlock
mtx.unlock(); // Второй вызов unlock
}
int main() {
std::thread t(threadFunction);
t.join();
return 0;
}
Если несколько раз вызвать lock
на обычном мьютексе (std::mutex
), произойдет взаимоблокировка самого себя, и поток зависнет. Для многократного захвата мьютекса одним и тем же потоком следует использовать рекурсивный мьютекс (std::recursive_mutex
), который позволяет это делать.
👉 Можно посмотреть Примеры как отвечают люди на этот вопрос, или перейти К списку 434 вопроса на C/C++ разработчика. Ставь 👍 если нравится контент
🔐 База собесов | 🔐 База тестовых👍 5
Photo unavailableShow in Telegram
Пс..
Тут два С++ сеньора создали канал, где простым языком поясняют за плюсы, метапрограммирование, фишки новых стандартов, алгоритмы, вопросы с собеседований и другие непонятные штуки из современного программирования на С++.
Прямо сейчас Вы можете бесплатно забрать гайды по собеседованиям, по категориям выражений и мув-семантике и по ключевому слову inline. Все найдете в закрепе канала.
Подписывайтесь, чтобы пояснять друзьям за оптимизации компилятора, о которых вам никто не расскажет: Грокаем С++
👍 2🔥 1
В чем асимптотическая сложность вставки и удаления в list и vector ?
Спросят с вероятностью 17%
Структуры данных std::list и std::vector, обе являются частью стандартной библиотеки шаблонов (STL), предоставляют различные характеристики производительности для операций вставки и удаления элементов. Понимание их асимптотической сложности помогает выбирать между этими контейнерами в зависимости от требований конкретной задачи.
std::vector
Представляет собой динамический массив, и его производительность оптимизирована для эффективного доступа к элементам и добавления элементов в конец.
1️⃣Вставка элементов:
✅Вставка в конец (
push_back
): Амортизированно \(O(1)\). Это означает, что большинство операций вставки занимают константное время, но иногда, когда требуется увеличение размера внутреннего массива, операция может занять \(O(n)\), где \(n\) — текущее количество элементов.
✅Вставка в середину или начало: \(O(n)\), так как может потребоваться сдвигать все последующие элементы для освобождения места.
2️⃣Удаление элементов:
✅Удаление из конца (pop_back
): \(O(1)\), так как элемент просто удаляется, а размер уменьшается на один.
✅Удаление из середины или начала: \(O(n)\), поскольку, как и при вставке, требуется сдвигать элементы для заполнения освободившегося места.
std::list
Это двусвязный список, который позволяет быстро вставлять и удалять элементы на любой позиции, но с затратами на производительность при доступе к элементам.
1️⃣Вставка элементов:
✅Вставка на любую позицию: \(O(1)\), предполагая, что у вас уже есть итератор на эту позицию. Однако, если необходимо сначала найти позицию, это может занять \(O(n)\).
2️⃣Удаление элементов:
✅Удаление на любой позиции: \(O(1)\), также предполагая наличие итератора на удаляемый элемент. Поиск удаляемого элемента займет \(O(n)\).
Общие рассуждения
✅std::vector
идеален, когда нужен быстрый произвольный доступ к элементам и основными операциями являются добавление и удаление элементов в конец списка.
✅std::list
лучше использовать, когда важны быстрые вставки и удаления в любом месте списка, но произвольный доступ не требуется или не является критичным.
Выбор между std::list
и std::vector
должен базироваться на конкретных требованиях к производительности и типе операций, которые наиболее часто выполняются с контейнером.
👉 Можно посмотреть Примеры как отвечают люди на этот вопрос, или перейти К списку 434 вопроса на C/C++ разработчика. Ставь 👍 если нравится контент
🔐 База собесов | 🔐 База тестовых👍 6❤ 2
Можно ли сменить владельца у unique_ptr ?
Спросят с вероятностью 17%
Владельца объекта, управляемого std::unique_ptr, можно сменить. Это одна из основных особенностей
std::unique_ptr
. Смена владельца достигается с помощью операций перемещения (move). std::unique_ptr
не поддерживает копирование, но позволяет передавать владение через перемещение, что можно сделать с помощью функции std::move
или при инициализации/присваивании другому std::unique_ptr
.
Пример смены владельца с помощью std::move
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed\n"; }
~MyClass() { std::cout << "MyClass destroyed\n"; }
};
void transferOwnership(std::unique_ptr<MyClass> ptr) {
std::cout << "Ownership transferred\n";
// ptr владеет объектом до конца функции
}
int main() {
std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
std::cout << "Transferring ownership...\n";
transferOwnership(std::move(ptr1)); // Передача владения
if (!ptr1) {
std::cout << "ptr1 no longer owns the object\n";
}
return 0;
}
Объяснение кода:
1️⃣Создание unique_ptr
: ptr1
создается и владеет объектом типа MyClass
.
2️⃣Передача владения: std::move(ptr1)
передает владение объектом в функцию transferOwnership
.
3️⃣Функция transferOwnership
: Принимает владение объектом и будет владеть им до завершения функции.
4️⃣Проверка владения: После передачи владения ptr1 больше не владеет объектом (ptr1 становится nullptr).
Владение можно также передавать путем присваивания одному unique_ptr другому с использованием std::move.
int main() {
std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
std::unique_ptr<MyClass> ptr2;
ptr2 = std::move(ptr1); // ptr2 теперь владеет объектом
if (!ptr1) {
std::cout << "ptr1 no longer owns the object\n";
}
if (ptr2) {
std::cout << "ptr2 owns the object\n";
}
return 0;
}
Объяснение кода:
1️⃣Создание unique_ptr: ptr1 создается и владеет объектом типа MyClass.
2️⃣Присваивание с перемещением: ptr2 = std::move(ptr1) передает владение объектом от ptr1 к ptr2.
3️⃣Проверка владения: После присваивания ptr1 больше не владеет объектом (ptr1 становится nullptr), а ptr2 теперь владеет объектом.
Сменить владельца у std::unique_ptr можно с помощью перемещения (std::move). Это позволяет передавать владение объектом от одного unique_ptr к другому, обеспечивая уникальное владение в каждый момент времени.
👉 Можно посмотреть Примеры как отвечают люди на этот вопрос, или перейти К списку 434 вопроса на C/C++ разработчика. Ставь 👍 если нравится контент
🔐 База собесов | 🔐 База тестовых👍 1
Photo unavailableShow in Telegram
Привет! Ты сейчас ищешь работу?
Если да, то у меня для тебя классные новости.
Мы с Максом решили провести вебинар на тему поиска работы и того, как быстрее получить оффер.
Зачем?
Да потому что найти работу просто откликаясь на вакансии теперь практически нереально.
На Junior вакансии откликаются по 1500 кандидатов. 1500 человек, Карл...
Вопрос: Как искать работу в таких условиях?
Ответ: Нужно менять подходы, и использовать новые способы поиска работы.
А вот какие способы, и как искать работу в 2024 году, расскажет мой товарищ - Макс, который помогает разработчикам с трудоустройством.
Он расскажет тебе как ПРАВИЛЬНО откликаться на вакансии, на что смотрят рекрутеры, и как ты должен быть упакован, чтобы получить работу в это непростое время.
🗓 Когда? Во вторник – 18 июня, в 19:00 по мск.
🎁 После регистрации он также обещал прислать: 1) Анализ рынка труда. 2) Разбор кейсов тех, кто сейчас находит работу. 3) Пошаговый план, что нужно делать, чтобы прийти к оферу.
👉 Записаться на вебинар по поиску работы.
👍 1
Какая сложность работы с кучей ?
Спросят с вероятностью 17%
Куча (heap) — это специализированная структура данных, которая эффективно реализует абстрактный тип данных приоритетной очереди. Наиболее часто используемый тип кучи — это бинарная куча, включая её вариации, такие как минимальная куча (min-heap), где наименьший элемент находится в корне, и максимальная куча (max-heap), где наибольший элемент находится в корне. Сложность работы с кучей зависит от операций, которые с ней выполняются. Вот основные операции и их сложности:
1️⃣Вставка элемента (Insert) - \(O(\log n)\):
✅Для вставки нового элемента он сначала помещается в конец кучи. Затем, чтобы сохранить свойства кучи, производится "просеивание вверх" (или "подъем"): элемент сравнивается с его родителем, и, если нарушается порядок кучи, они меняются местами. Это повторяется, пока не будет найдено правильное место для элемента или он не достигнет корня.
2️⃣Удаление минимального/максимального элемента (Delete Min/Max) - \(O(\log n)\):
✅Элемент, находящийся в конце кучи, перемещается на место корневого элемента. Затем производится "просеивание вниз", чтобы восстановить свойства кучи: новый корень сравнивается с детьми, и если порядок нарушается, он меняется местами с наименьшим (в min-heap) или наибольшим (в max-heap) ребенком. Процесс повторяется, пока не будет восстановлен порядок.
3️⃣Поиск минимального/максимального элемента (Find Min/Max) - \(O(1)\):
✅В куче минимальный или максимальный элемент всегда находится в корне, поэтому доступ к нему можно получить за константное время.
4️⃣Слияние двух куч (Merging two heaps) - \(O(n)\):
✅Можно выполнить за линейное время относительно общего числа элементов. Обычно это делается путем простого объединения элементов двух куч и последующего построения новой кучи из получившегося списка элементов.
5️⃣Уменьшение ключа (Decrease key) - \(O(\log n)\) для бинарной кучи:
✅В куче обычно влечет за собой его "подъем" вверх по куче (если это min-heap), так как элемент становится "легче". Элемент сравнивается с родителем, и они меняются местами, если порядок кучи нарушен.
6️⃣Увеличение ключа (Increase key) - \(O(\log n)\) для бинарной кучи:
✅Аналогично уменьшению ключа, но в данном случае элемент "опускается" вниз в куче (если это min-heap), так как он становится "тяжелее".
Эти операции делают кучу идеальным выбором для приоритетных очередей, где требуется быстрый доступ к элементу с наивысшим или наименьшим приоритетом, а также эффективная поддержка вставки и удаления элементов.
👉 Можно посмотреть Примеры как отвечают люди на этот вопрос, или перейти К списку 434 вопроса на C/C++ разработчика. Ставь 👍 если нравится контент
🔐 База собесов | 🔐 База тестовых