Билеты 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_viewstd::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