Yesterday

C++. Средний уровень

Дальше — не о синтаксисе, а об аккуратном пользовании стандартной библиотекой. Здесь спрашивают про итераторы и их валидность, компараторы в sort, умные указатели, исключения и RAII, шаблоны и перегрузки, базовые отношения между классами.


Вопрос 1. Выберите правильный формат для объявления динамического массива A из float, размерностью n.

Варианты ответов:

1.     float **A = new float[n]

2.     float A* = new(n)

3.     float A = new float[n]

4.     float A[n]

5.     float *A = new float[n]

Нужен динамический массив — значит, указатель на первый элемент и new [].
— (1) указатель на указатель — лишний уровень.
— (2) синтаксис неверный, похоже на placement new, но записано неправильно.
— (3) слева float, а справа — адрес из new → несовместимо.
— (4) это автоматический массив (и VLA в стандарте C++ нет) — не динамическая аллокация.
— (5) корректный указатель на float с выделением n элементов. Не забыть delete[] A; после использования.

Выбранный ответ: №5 — float *A = new float[n]


Вопрос 2. Как правильно прочитать строку с пробелами?

std::string line;

____;

Варианты ответов:

1.     std::cin.read(line)

2.     std::cin >> line

3.     std::cout >> line

4.     std::cout << line

5.     std::getline(std::cin, line)

Нужно считать всю строку, включая пробелы.
— Оператор >> (вариант 2) читает только до первого пробела — потеряем остаток.
— std::cin.read (1) работает с сырым буфером (char* и количеством байт), не со std::string.
— Варианты с std::cout (3, 4) — это вывод, а не ввод.
— std::getline (5) читает всю строку до '\n', как раз то, что нужно. На практике помню про нюанс: если до этого читали числа через >>, нужно сначала съесть оставшийся '\n'.

Выбранныйответ: std::getline(std::cin, line)


Вопрос 3. Для хранения номеров заказов клиентов нужна структура данных, которая позволяет хранить уникальные элементы и быстро проверять их наличие. Какой контейнер выбрать?

Варианты ответов:

1.     std::stack

2.     std::unordered_set

3.     std::vector

4.     std::list

5.     std::deque

Нужны уникальные значения и быстрый поиск/вставка.
На что обращаю внимание:
— std::unordered_set — хеш-таблица: автоматом обеспечивает уникальность, среднее O(1) на insert/find.
— vector, list, deque требуют линейного поиска O(n) для проверки наличия.
— stack — это обёртка LIFO без произвольного поиска.

Вывод: оптимально хеш-множество.

Выбранный ответ: №2 — std::unordered_set


Вопрос 4. Какой тип связи используется между Teacher и Student?

class Teacher;

class Student {

public:

void attendClass(Teacher* teacher) {

std::cout << "Attending class by teacher " << teacher;

}

};

class Teacher {

public:

void teach(Student* student) {

std::cout << "Teaching student " << student;

}

};

Варианты ответов:

1.     Композиция

2.     Ассоциация

3.     Наследование

4.     Инкапсуляция

5.     Агрегация

Классы не хранят друг друга как поля, а лишь принимают указатель в параметрах методов.
На что обращаю внимание? Передача указателя на время вызова — это «знает/взаимодействует», без владения и без жизненного цикла. Для композиции/агрегации нужен член-объект; наследования здесь нет; «инкапсуляция» — не тип связи между классами.
Какой вывод? Это обычная ассоциация: объекты могут ссылаться друг на друга при взаимодействии, но не владеют.

Выбранный ответ: №2 — Ассоциация


Вопрос 5. Программист, разрабатывая класс UserManager, добавил функции, которые работают на разном уровне абстракции. Какаяфункциялишняяв UserManager?

class User {

public:

string name;

string email;

};

class UserManager {

public:

void addUser(User user) { /* добавление */ }

void deleteUser(int id) { /* удаление */ }

void sendEmail(User user) { /* рассылка */ }

};

Варианты ответов:

1.     Хранение данных пользователя

2.     Обработка данных пользователя

3.     Рассылка сообщений

4.     Удаление пользователей

5.     Добавление пользователей

UserManager выполняет операции управления — добавить/удалить. Отправка писем — это другая ответственность (сервис уведомлений). По хорошему дизайну и принципу единственной ответственности, рассылка не должна жить в менеджере пользователей. Её стоит вынести в EmailService/Notifier, а UserManager максимум вызывает тот сервис, но сам письма не шлёт.

«рассылка сообщений» — лишняя функция для UserManager.

Выбранный ответ: №3 — Рассылка сообщений


Вопрос 6. Вам нужно передать объект по указателю в функцию, при этом важно обеспечить отсутствие других указателей на этот же объект. Какой тип указателей подходит для этих целей?

Варианты ответов:

1.     std::single_ptr

2.     std::shared_ptr

3.     std::unit_ptr

4.     std::unique_ptr

5.     std::weak_ptr

Требуется гарантия единственного владельца.
На что обращаю внимание:
— std::unique_ptr именно это и даёт — эксклюзивное владение, копировать нельзя, только move. Значит, других «живых» владельцев быть не может.
— std::shared_ptr допускает много владельцев — не подходит.
— std::weak_ptr не владеет объектом вообще.
— std::single_ptr и std::unit_ptr в стандартной библиотеке отсутствуют.

Нужен умный указатель с эксклюзивным владением.

Выбранный ответ: №4 — std::unique_ptr


Вопрос 7. Проанализируйте код и выберите утверждение, верное относительно возможной утечки памяти.

struct Player {

std::shared_ptr<Player> companion;

~Player() { std::cout << "~Player\n"; }

};

int main() {

std::shared_ptr<Player> A = std::make_shared<Player>();

std::shared_ptr<Player> B = std::make_shared<Player>();

A->companion = B;

B->companion = A;

}

Варианты ответов:

1.     В коде есть проблема утечки памяти, и ее можно решить с помощью std::weak_ptr

2.     В коде утечек памяти нет, но при использовании std::weak_ptr они появятся

3.     В коде есть проблема утечки памяти, но решить ее заменой на другой умный указатель невозможно

4.     В коде есть проблема утечки памяти, и ее можно решить с помощью std::unique_ptr

5.     В коде утечек памяти нет и не будет при использовании любого умного указателя

Два объекта Player владеют друг другом через std::shared_ptr. Получается цикл владения: A держит B, B держит A. Счётчики ссылок никогда не станут нулём — деструкторы не вызовутся (строка ~Player\n не напечатается). Это типичный пример утечки из-за цикла shared_ptr.
На что обращаю внимание: разорвать цикл можно, если одна из ссылок не будет владеть объектом — для этого и существует std::weak_ptr. Он не увеличивает счётчик владения.
Почему не unique_ptr: взаимная ссылка unique_ptr невозможна (эксклюзивное владение); переработка модели возможна, но прямой заменой здесь проблему не решишь.

Выбранный ответ: №1 — В коде есть проблема утечки памяти, и ее можно решить с помощью std::weak_ptr


Вопрос 8. Какое выражение нужно вставить на место пропусков в коде ниже, чтобы обеспечить безопасность при исключении?

#include <memory>

void process() {

auto ptr = std::make_unique<int>(42);

_____

throw std::runtime_error("Error");

_____

}

int main() {

try { process(); } catch(...) {}

}

Варианты ответов:

1.     ptr.erase(nullptr)

2.     std::lock_guard<std::mutex> и lock(mutex)

3.     ptr.reset(nullptr)

4.     unique_ptr уже обеспечивает безопасность

5.     try { } catch(...) { delete ptr; }

Ресурс обёрнут в std::unique_ptr. По правилу RAII его деструктор вызовется при выходе из функции, даже если происходит throw. Значит, память освободится автоматически.
На что обращаю внимание:
— erase у unique_ptr не существует.
— reset(nullptr) лишний: освобождение и так произойдёт при выходе из области видимости.
— lock_guard не к месту.
— Вариант с delete в catch неверен и не компилируется: ptr вне области видимости catch.

Вывод: ничего добавлять не нужно — уже безопасно.

Выбранный ответ: №4 — unique_ptr уже обеспечивает безопасность


Вопрос 9. Что выведет программа ниже?

#include <iostream>

using namespace std;

int test(int x) {

static int y = 2;

y += x;

return y;

}

int main() {

cout << test(3) << test(4);

return 0;

}

Вариантыответов:

1.     59

2.     34

3.     54

4.     39

5.     95

В test есть статическая переменная y, она инициализируется один раз (2) и сохраняет значение между вызовами.
На что обращаю внимание: выражение cout << test(3) << test(4) вычисляется слева направо — сначала печатаем результат test(3), затем test(4), без пробелов между ними.
Считаю:
— первый вызов: y = 2 + 3 = 5, печатаем 5;
— второй вызов: y = 5 + 4 = 9, печатаем 9.
Выбранный ответ: 59


Вопрос 10. Проанализируйте код ниже. Каков будет результат выполнения кода?

template<typename T>

T f(T a, T b)

{

return a > b ? a : b;

}

int main() {

cout << f(5, 3.5);

}

Варианты ответов:

1.     Ошибка компиляции: неоднозначность вывода типа T

2.     Выведет 5.0

3.     Ошибка компиляции: нельзя сравнивать разные типы

4.     Выведет 5

5.     Выведет 5.4

Шаблон требует, чтобы оба параметра имели один и тот же тип T.
На что обращаю внимание: вызываем f(5, 3.5) — первый аргумент int, второй double. При выводе шаблонного параметра компилятор пытается вывести T из каждого аргумента: получается T=int и T=double одновременно — это конфликт. Шаблон так вызвать нельзя без явного указания типа (например, f<double>(5, 3.5) тогда бы вывело 5.0).
Вывод: тип T по аргументам вывести нельзя, получаем ошибку компиляции.

Выбранный ответ: №1 — Ошибка компиляции: неоднозначность вывода типа T


Вопрос 11. Какой из вариантов вызовет перемещение (move semantics), а не копирование?

Варианты ответов:

1.     std::string s1 = s2

2.     std::string s1 = std::move(s2)

3.     std::string s1 = new String(s2)

4.     std::string s1 = const_cast<const std::string&>(s2)

5.     std::string s1 = &s2

Чтобы сработал move-конструктор, источником должен быть rvalue (временный/«перемещаемый» объект).
— Вариант 1: s2 — lvalue → копирование.
— Вариант 2: std::move(s2) превращает s2 в rvalue → сработает перемещение.
— Вариант 3: new String(s2) возвращает указатель и ещё и тип String — не подходит.
— Вариант 4: получаем const std::string& — константная lvalue-ссылка, значит копирование.
— Вариант 5: &s2 — это указатель, типы несовместимы.

Выбранныйответ: №2 — std::string s1 = std::move(s2)


Вопрос 12. После добавления элемента в std::vector какие итераторы останутся валидными?

Варианты ответов:

1.     Только end()

2.     Только итераторы на добавленные элементы

3.     Только cbegin()

4.     Все итераторы станут невалидными

5.     Все итераторы

При добавлении элемента vector может перераспределить память (reallocation). Если это происходит, инвалидируются все итераторы и ссылки, в том числе end(). Бывают случаи без перераспределения (часть итераторов остаётся валидной), но задача спрашивает про гарантии — в общем случае нужно считать, что всё сломается.

Выбранный ответ: Все итераторы станут невалидными

Заключение

Думайте о владении ресурсом и жизненном цикле объектов. Избегайте слайсинга и циклов владения, всегда пишите предикат в cv.wait, помните: модификация vector может инвалидировать все итераторы. Шаблоны требуют видимости деклараций — объявляйте до использования.

Средний уровень — это дисциплина владения ресурсами и знание «острых углов» STL и многопоточности.