Билеты 17-33
№17 Какие методы класса могут быть автоматически сгенерированы компилятором?
В C++ компилятор может автоматически сгенерировать специальные методы класса, если они не объявлены явно.
1. Конструктор по умолчанию (Default Constructor)
- Генерируется, если нет других пользовательских конструкторов.
- Не генерируется, если:
- Есть любой пользовательский конструктор.
- Есть поле без инициализатора, которое нельзя инициализировать по умолчанию (например, ссылка или const-поле без инициализатора).
2. Деструктор (Destructor)
- Генерируется автоматически, если не объявлен пользовательский деструктор.
- Обычно объявляется как noexcept (если не выбрасывает исключений).
- Важно: Если класс управляет ресурсами (память, файлы), деструктор нужно определять вручную!
3. Копирующий конструктор (Copy Constructor)
- Генерируется, если нет пользовательских (хотя бы одного из списка):
- Конструктора копирования.
- Перемещающего конструктора.
- Перемещающего оператора присваивания.
- Деструктора.
- Поведение: Поэлементное копирование всех полей.
4. Копирующий оператор присваивания (Copy Assignment Operator)
- Генерируется при тех же условиях, что и копирующий конструктор.
- Поведение: Поэлементное присваивание полей.
5. Перемещающий конструктор (Move Constructor)
- Генерируется, если:
- Нет пользовательских (хотя бы одного из списка):
- Деструктора.
- Конструктора копирования.
- Оператора копирования.
- Оператора перемещения.
- Все поля и базовые классы поддерживают перемещение.
- Нет пользовательских (хотя бы одного из списка):
- Поведение: Поэлементное перемещение полей (через std::move).
6. Перемещающий оператор присваивания (Move Assignment Operator)
- Генерируется при тех же условиях, что и перемещающий конструктор.
- Поведение: Поэлементное перемещение полей.
№18 Виды передачи параметров в функцию
1. Передача по значению (by value)
void func(int x) { x = 10; }
- Создается копия передаваемого значения
- Изменения внутри функции не влияют на оригинал
- Подходит для простых типов (int, float, char)
- Неэффективно для больших объектов (происходит полное копирование)
2. Передача по указателю (by pointer)
void func(int* ptr) { *ptr = 10; }
- Передается адрес переменной
- Можно изменять оригинальное значение
- Может быть nullptr
- Требует явного разыменования (*) для доступа к значению
- Используется для:
- Опциональных параметров (может быть nullptr)
- Массивов в C-стиле
- Низкоуровневых операций
3. Передача по ссылке (by reference)
void func(int& ref) { ref = 10; }
- Работает с оригинальным объектом (псевдоним)
- Не может быть nullptr
- Синтаксически проще указателей (не требует разыменования)
- По умолчанию используется в C++ для модификации объектов
4. Передача по константной ссылке (by const reference)
void func(const std::string& str) { /* ... */ }
- Избегает копирования больших объектов
- Гарантирует, что объект не будет изменен
- Оптимальный выбор для передачи:
- Строк
- Векторов
- Крупных объектов
- Объектов, которые дорого копировать
5. Передача по rvalue-ссылке (by rvalue reference)
void process(std::string&& str) { // Принимает только rvalue
// Можно безопасно перемещать ресурсы
std::string local = std::move(str);
}
process(getString()); // OK - временный объект
process("temporary"); // OK - строковый литерал
std::string s = "test";
process(std::move(s)); // OK - явное преобразование
// process(s); // Ошибка - s это lvalue
- Для временных объектов (rvalues)
- Позволяет эффективно "забирать" ресурсы
- Используется в:
- Конструкторах перемещения
- Операторах перемещающего присваивания
- Функциях, оптимизированных для временных объектов
6. Передача по универсальной ссылке (universal reference)
template<typename T>
void forwardExample(T&& arg) {
// Передаёт аргумент дальше с сохранением категории значения
otherFunction(std::forward<T>(arg));
- Работает как с lvalue, так и с rvalue
- Позволяет реализовать perfect forwarding
- Когда используется:
- Когда функция-обёртка должна передать аргумент в другую функцию без изменения его категории (lvalue/rvalue)
- В реализациях фабричных функций, конструкторов, делегирующих вызовов
Тип передачи | Можно изменить оригинал | Защита от изменений | Нулевые значения | Эффективность для больших объектов |
---|---|---|---|---|
По значению | ❌ Нет | ✅ Да | ❌ Нет | ⚠️ Низкая (копирование) |
По указателю | ✅ Да | ❌ Нет | ✅ Да | ✅ Высокая |
По ссылке | ✅ Да | ❌ Нет | ❌ Нет | ✅ Высокая |
По const ссылке | ❌ Нет | ✅ Да | ❌ Нет | ✅ Высокая |
По rvalue-ссылке | ✅ Да (перемещение) | ❌ Нет | ❌ Нет | ✅ Максимальная |
№19 Как передать в функцию строковый параметр - различные варианты
применимы способы из билета №18, также есть еще один
По string_view с версии C++17
Невладеющий (non-owning) указатель на неизменяемую строку (или её часть).
Зачем нужен?
- Эффективный доступ к строке без копирования (например, для чтения).
- Работа с подстроками без аллокаций.
- Универсальность: работает с std::string, char*, литералами.
void print(std::string_view sv) {
std::cout << sv.substr(0, 5); // Без копирования!
}
print("Hello, world!"); // "Hello"
Осторожно!
Не владеет памятью → может стать "висячим" при уничтожении исходной строки.
Сравнение
Критерий | std::string_view | std::string |
---|---|---|
Владение памятью | Нет | Да |
Изменяемость | Только чтение | Чтение и запись |
Производительность | Быстрый (без копирования) | Медленнее (аллокации) |
Безопасность | Риск висячих ссылок | Безопасен |
№20 Как вернуть результат(ы) из функции - различные варианты
1. Возврат по значению (копирование)
std::string getString() {
return "Hello";
}
- Срабатывает copy elision (пропуск копирования)
- Компилятор может пропускать копирование при возврате значений
- Обязательно с C++17 для prvalue
- Применимо к любым типам
- Для сложных объектов использует RVO (Return Value Optimization)
std::string createString() { return std::string("Hello"); // Анонимный временный объект }
- Оптимизирует возврат анонимных временных объектов (prvalue).
- Работает даже без флагов оптимизации (гарантировано стандартом C++17).
- NRVO (Named Return Value Optimization)
std::string createNamedString() { std::string local = "Hello"; // Именованная локальная переменная return local; // NRVO применяется здесь }
- Оптимизирует возврат именованных локальных переменных (lvalue).
- Не гарантировано стандартом (зависит от компилятора).
- Требует включённой оптимизации (-O2, /O2).
2. Возврат по ссылке/указателю
const std::string& getConstRef() {
static std::string s = "Hello";
return s; // Возврат ссылки на существующий объект
}
- ❗ Опасность: висячие ссылки при возврате локальных объектов
- Используется для:
- Доступа к элементам контейнеров (std::vector::operator[])
- Возврата статических/глобальных переменных
Ограничение!
std::string& badExample() {
std::string local; // Локальная переменная
return local; // UB при использовании!
}
- Время жизни local:
- Создаётся при входе в функцию.
- Уничтожается при выходе из функции (вызов деструктора ~string()).
- Возвращаемая ссылка:
- После возврата из функции ссылка указывает на уничтоженный объект.
- Любое использование этой ссылки — UB (чтение, запись, вызов методов).
3. Возврат rvalue-ссылки (перемещение)
std::string&& getMovableString() {
static std::string s = "Temporary";
return std::move(s);
}
- Применение:
- Явное перемещение ресурсов
- Move-семантика в STL:
std::unique_ptr<int>&& getUniquePtr();
4. Возврат через параметры (out-параметры)
void getNumbers(int& outX, int& outY) {
outX = 10;
outY = 20;
}
- Устаревший, но иногда полезный подход
5. Возврат структуры
std::tuple<int, std::string> getData() {
return {42, "Answer"};
}
Современное использование (C++17):
auto [num, str] = getData(); // Structured bindings
6. Возврат умных указателей
std::unique_ptr<Resource> createResource() {
return std::make_unique<Resource>();
}
- Преимущества:
- Безопасная передача владения
- Используется в фабричных функциях
Способ | Владение ресурсами | Безопасность | Эффективность |
---|---|---|---|
По значению | Полное | ✅ | Зависит от RVO |
По ссылке | Отсутствует | ⚠️ | ✅ |
По rvalue-ссылке | Перемещаемое | ✅ | ✅ |
Через out-параметры | Запутанное | ❌ | ✅ |
Структуры | Полное | ✅ | Зависит от RVO |
Умные указатели | Явное | ✅ | ✅ |
№21 Каков порядок вычисления выражений, передаваемых в качестве аргументов при вызове функции?
Основное правило
- Порядок вычисления аргументов функции не определён стандартом (unspecified behavior).
Компилятор может вычислять их в любом порядке.
void foo(int a, int b) {} int i = 0; foo(i++, i++); // Неопределённое поведение!
- Результат зависит от компилятора: может быть foo(0, 1) или foo(1, 0).
Исключения
- Полные выражения (full expressions) разделены точками следования (sequence points):
f(a(), b()); // a() и b() — отдельные полные выражения
- Точка следования — это моменты, когда все побочные эффекты предыдущих вычислений завершены (например, ;, &&, ||, ? :).
Специальные случаи
- Встроенные операторы имеют строгий порядок:
f(a && b); // Сначала вычисляется a, затем (если нужно) b f(a || b); // Аналогично
- Оператор запятая (,) гарантирует порядок слева направо:
f((a=1, b=2), c); // a=1 → b=2 → c
№22 Пост- и пре- инкремент, в чем разница
- Пре-инкремент (++x):
- Увеличивает значение перед использованием.
- Возвращает ссылку на измененный объект.
- Эффективнее (не создает временный объект).
- Пост-инкремент (x++):
- Увеличивает значение после использования.
- Возвращает копию исходного значения.
- Менее эффективен (из-за копирования).
int main() {
int a = 5;
int b = ++a; // a=6, b=6 (пре-инкремент)
int c = a++; // a=7, c=6 (пост-инкремент)
}
№23 * Что такое абстрактный класс и в чём его отличия от обычных?
Абстрактный класс — класс, содержащий хотя бы одну чистую виртуальную функцию (объявленную с = 0). Нельзя создать объект такого класса напрямую.
Пример:
class Shape {
public:
virtual double area() const = 0; // Абстрактный метод
virtual ~Shape() {} // Виртуальный деструктор (обязателен!)
};
class Circle : public Shape {
double radius;
public:
double area() const override { return 3.14 * radius * radius; }
};
Отличия от обычного класса:
- Нельзя инстанцировать:
abstract class Animal { virtual void makeSound() = 0; // Чистая виртуальная функция }; // Animal a; // Ошибка!
- Наследование: Дочерние классы обязаны переопределить все чистые виртуальные методы, иначе тоже станут абстрактными
- Цель:
- Интерфейс: Задает контракт для наследников.
- Полиморфизм: Позволяет работать с разными наследниками через указатель/ссылку на базовый класс.
№24 Тернарный оператор - что это, в чём отличие от ветвления
Тернарный оператор — это условное выражение, которое возвращает одно из двух значений в зависимости от условия.
условие ? выражение_если_истина : выражение_если_ложь;
Пример
int x = (argc > 5) ? 100 : -100; // Тернарный оператор
if (argc > 5) x = 100; else x = -100; // if-else
Критерий | Тернарный оператор | if-else (ветвление) |
---|---|---|
Тип | Выражение (возвращает значение) | Инструкция (не возвращает значение) |
Использование | В присваиваниях, возвратах, инициализациях | Для ветвления кода |
Читаемость | Лаконичен, но сложен для вложенных условий | Удобен для сложных условий |
Производительность | Одинакова (оптимизируются одинаково) | Одинакова |
№25.1 В чем отличие & от && ? (Если & - Lvalue-ссылка, а && - Rvalue-ссылка)
Rvalue - временные объекты, например, результаты выражений
Пример
int x = 10;
int& ref = x; // OK
// int& ref2 = 42; // Ошибка: 42 — Rvalue
- Где используется:
- Реализация перемещающей семантики (например, std::move).
- Оптимизация производительности (избегание копирований).
Lvalue - объекты, имеющие имя и адрес в памяти
Пример
int&& rref = 42; // OK
int x = 10;
// int&& rref2 = x; // Ошибка: x — Lvalue
- Где используется:
- Передача параметров в функции без копирования.
- Возврат ссылки на существующий объект.
void process(int& x) { std::cout << "Lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "Rvalue: " << x << "\n"; }
int main() {
int a = 5;
process(a); // Вызовет Lvalue-версию
process(a + 3); // Вызовет Rvalue-версию
}
Когда что использовать?
- &:
- Нужно изменить исходный объект.
- Передача больших объектов без копирования.
- &&:
- Оптимизация (перемещение вместо копирования).
- Работа с временными объектами.
№25.2 В чем отличие & от && ? (Если & - битовое и, а && - логическое и)
Ленивое вычисление — это стратегия, при которой второй операнд логического выражения (&& или ||) вычисляется только при необходимости. Данным
Как работает? Примеры:
-
Для && (И): Если первый операнд false, второй не вычисляется (результат уже известен).
if (false && expensive_function()) { // expensive_function() не вызовется! }
-
Для || (ИЛИ): Если первый опенд true, второй не вычисляется.
if (true || expensive_function()) { // expensive_function() не вызовется! }
№26 В чем отличие = и == ?
=
— это оператор присваивания, который используется для присвоения значения переменной.
==
— это оператор сравнения, который проверяет, равны ли два значения.
№27 Переопределение стандартных операторов для пользовательских классов: 2 подхода
1. Как член класса (operator@ внутри класса)
class Vector {
public:
Vector operator+(const Vector& other) const {
return Vector(x + other.x, y + other.y);
}
};
- Особенности:
- Левый операнд — всегда объект этого класса (this).
- Нужен доступ к приватным полям.
- Пример:
obj + 5; // OK (obj.operator+(5)) 5 + obj; // Ошибка (5.operator+(obj) не существует)
2. Как свободная функция (operator@ вне класса)
class Vector {
public:
int x, y;
};
Vector operator+(const Vector& a, const Vector& b) {
return Vector(a.x + b.x, a.y + b.y);
}
- Особенности:
- Работает, даже если левый операнд — не класс (например, int + Vector).
- Для доступа к приватным полям требует friend.
- Примеры:
obj + 5; // OK (operator+(obj, 5)) 5 + obj; // OK (operator+(5, obj))
// Перегрузка << как свободной функции friend std::ostream& operator<<(std::ostream& os, const Vector& v) { return os << "(" << v.x << ", " << v.y << ")"; } // Использование: Vector v(3, 5); std::cout << v; // Вывод: (3, 5)
№28 В чём отличие определения от объявления?
1. Объявление (Declaration)
- Что делает:
- Сообщает компилятору о существовании сущности (функции, переменной, класса).
- Не выделяет память и не содержит реализацию.
2. Определение (Definition)
- Что делает:
- Создает конкретную реализацию сущности.
- Выделяет память (для переменных) или предоставляет тело (для функций/классов).
// Объявления:
int max(int a, int b); // Компилятор знает, что max существует
class Point; // Неполный тип
// Определения:
int max(int a, int b) { return (a > b) ? a : b; } // Реализация
class Point { int x, y; }; // Полный тип
№29 Что такое перемещение (move), для чего и как его использовать?
Перемещение — это способ передачи ресурсов (памяти, файловых дескрипторов и т.д.) из одного объекта в другой без копирования.
Для чего нужно?
- Оптимизация производительности: избегаем дорогих копирований (например, для std::vector, std::string).
- Работа с уникальными ресурсами (например, файлы, сокеты), которые нельзя копировать.
1. Перемещающий конструктор
class String {
char* data;
public:
// Перемещающий конструктор
String(String&& other) noexcept
: data(other.data) // Забираем ресурсы
{
other.data = nullptr; // Обнуляем источник
}
~String() { delete[] data; }
};
2. std:move
std::vector<std::string> create_strings() {
std::vector<std::string> v;
v.push_back("abc");
v.push_back("def");
return v; // Автоматическое перемещение (не копирование!)
}
int main() {
std::vector<std::string> strings = create_strings(); // Ресурсы перемещены
}
3. std::unique_ptr
std::unique_ptr<int> create_ptr() {
return std::unique_ptr<int>(new int(42)); // Перемещение
}
int main() {
std::unique_ptr<int> ptr = create_ptr(); // Ресурс перемещен
// ptr владеет int(42), исходный временный объект уничтожен
}
Ключевые моменты
- После std::move объект остается валидным, но его состояние не определено (например, vector становится пустым).
- noexcept: Перемещающие методы должны быть помечены noexcept, чтобы работать с STL-контейнерами.
- Не все объекты можно перемещать — примитивы (например, int) всегда копируются.
№30 Какие есть типы циклов в C++ и в чем разница между ними?
for
for (int i = 0, j = 10; i < j; ++i, --j) { ... }
while
while (std::cin >> x) { ... } // Чтение до EOF
do-while
- Когда использовать: Когда тело нужно выполнить хотя бы один раз.
- Условие проверяется после тела.
int x;
do {
std::cin >> x;
} while (x < 0); // Повторять, пока не введут положительное число
Range-based for (C++11)
- Когда использовать: Для обхода контейнеров (vector, map и др.).
- Особенности:
- Работает через итераторы.
- Не требует явного указания размера.
for (const auto& [key, val] : map) { ... }
№31 Зачем нужны .h файлы и как это реализуется?
Заголовочные файлы (.h) в C++ — это файлы, содержащие объявления (декларации) программных сущностей (функций, классов, переменных, шаблонов)
- Служат интерфейсом между разными частями программы, позволяя компилятору узнать о существовании сущностей до их использования.
- Обеспечивают раздельную компиляцию, так как:
- Содержат:
- Прототипы функций
- Определения классов (без реализации методов, если они не inline)
- Объявления extern-переменных
- Шаблоны (так как они должны быть полностью определены в заголовках)
- Макросы препроцессора
Структура проэкта
project/
│
├── include/
│ └── utils.h // Объявления
├── src/
│ └── utils.cpp // Реализация
└── main.cpp
utils.h
#pragma once
void print_hello(); // Объявление
utils.cpp
#include "include/utils.h"
#include <iostream>
void print_hello() { std::cout << "Hello!\n"; } // Определение
main.cpp
#include "include/utils.h"
int main() {
print_hello(); // Вызов
return 0;
}
№32 Что является единицей трансляции в С++?
Единица трансляции — это отдельный исходный файл (.cpp) после обработки препроцессором (т.е. со всеми включенными через #include заголовочными файлами), который компилятор преобразует в объектный файл (.o/.obj).
Из чего состоит?
- Исходный код (.cpp-файл).
- Включённые заголовки (#include "file.h").
- Результат макроподстановок (#define, #ifdef и др.).
main.cpp → [Препроцессор] → main.i (единица трансляции) → [Комплятор] → main.o
Ключевые свойства
- Компилируется независимо от других единиц трансляции.
- Содержит:
- Все определения (функций, переменных, классов), которые компилятор видит после препроцессинга.
- Неявные инстанциации шаблонов (если они используются в коде).
Правило одного определения (ODR) - Каждая сущность (функция, класс, глобальная переменная) должна быть определена ровно один раз в единице трансляции (если не inline/constexpr).
#include "math.h" // Включённый заголовок
int add(int a, int b) { // Определение функции
return a + b;
}
После препроцессинга math.cpp + math.h становятся одной единицей трансляции.
№33 Что такое объектный файл?
Объектный файл (.o/.obj) — это бинарный файл, создаваемый компилятором из единицы трансляции (обработанного .cpp-файла). Содержит машинный код, метаданные и символы для последующей линковки.
Что внутри?
- Машинный код функций/методов из .cpp.
- Символы (имена функций, переменных) с информацией о:
- Типе (data/code)
- Видимости (global/local).
- Расположении (адреса или смещения).
- Релокации (записи о неразрешённых внешних ссылках).
- Отладочная информация (если компиляция с -g).
Как создаётся?
- Препроцессор обрабатывает .cpp + включает .h → единица трансляции.
- Компилятор переводит её в ассемблер → объектный файл.
- Не содержит исполняемого кода до линковки!
Команда (g++):
g++ -c math.cpp # Создаёт math.o