M3137 -e bash-it
Техали:
- Павел Родыгин
- Марсель Меркулов
- Георгий Соболев
- Артём Дунаев
- Николай Гавришок
- Никита Селезнёв
Модератор:
- Даниил Анфилофьев
Список билетов для коллоквиума по C++
1. Разница между указателями и ссылками
2. Конструктор по-умолчанию, типы конструкторов
3. Типы размещения, смысл и разница между ними
4. В каком порядке происходит инициализация объектов со статическим размещением в программе?
5. Что такое объект в C++?
6. Этапы жизни объекта
7. const
и его использование
8. static
и его использование
9. * virtual
и его использование
10. * override
и его использование
11. * template
и его использование
12. void
и его использование
13. nullptr
и его использование
14. friend
и его использование
15. explicit
и его использование
16. inline
и его использование
17. Какие методы класса могут быть автоматически сгенерированы компилятором?
18. Виды передачи параметров в функцию
19. Как передать в функцию строковый параметр - различные варианты
20. Как вернуть результат(ы) из функции - различные варианты
21. Каков порядок вычисления выражений, передаваемых в качестве аргументов при вызове функции?
22. Пост- и пре- инкремент, в чем разница
23. * Что такое абстрактный класс и в чём его отличия от обычных?
24. Тернарный оператор - что это, в чём отличие от ветвления
25. В чём разница & от && ?
26. В чём разница = и == ?
27. Переопределение стандартных операторов для пользовательских классов: 2 подхода
28. В чём разница определения от объявления?
29. Что такое перемещение (move), для чего и как его использовать?
30. Какие есть типы циклов в C++ и в чем разница между ними?
31. Зачем нужны .h файлы и как это реализуется?
32. Что является единицей трансляции в C++?
33. Что такое объектный файл?
34. Чем инициализируются переменные типа bool по умолчанию?
35. Какие варианты приведения типов по иерархии наследования существуют и чем отличаются?
36. В чем разница между enum и enum class, зачем нужны последние?
37. Есть ли разница между структурой struct и классом class?
38. Какие есть спецификаторы прав доступа в C++ и в чем отличие между ними?
39. Что такое конструктор и когда он вызывается?
40. Что такое деструктор и когда он вызывается?
41. Можно ли явно вызвать конструктор?
42. Можно ли явно вызвать деструктор?
43. В чем разница между оператором присваивания и конструктором копирования?
44. Чем отличается new int(10)
от new int[10]
?
45. Можно ли делать delete ptr;
после auto ptr = new T[5];
и почему?
46. Что такое placement new (размещающее new)?
47. Как обеспечить корректное удаление объекта, созданного при помощи размещающего new?
48. Undefined, unspecified and implementation-defined behavior
49. Что такое наблюдаемое поведение программы (observable behaviour), зачем нужно это понятие?
50. Что такое this
? Можно ли делать присваивание в this
и почему?
51. Можно ли делать delete this
, почему?
52. Что такое явное/неявное преобразование типов?
53. Какие есть варианты явного приведения типов (cast) в C++ и чем отличаются?
54. Что такое макросы? В чем отличия от функций (для макросов в духе #define MAX(a, b)
) ?
55. Что делает препроцессор?
56. Что такое ODR?
57. В каких случаях допустимо размещать определение функции в заголовочном файле, а в каких - нет?
58. Что такое предварительное объявление (forward declaration) и для каких элементов языка это актуально?
59. Когда начнётся и когда закончится время жизни объекта с именем a: void f(X a) {}?
60. Когда начнётся и когда закончится время жизни объекта доступного по указателю pa: void f() { auto pa = new X; }?
61. В каком порядке будут созданы, а в каком разрушены объекты с именами a и b: void f() { X a; { Y b; } }?
62. Какие подобъекты будут присутствовать в объекте класса X: struct X : Y, Z { W w; V v; };, каков порядок их создания и разрушения?
63. Что такое временный объект и чем ограничено его время жизни?
64. Чем отличается инициализация по умолчанию для базовых и пользовательских типов (классов)?
65. Чем отличаются методы std::vector
push_back
и emplace_back
66. Ссылки на какие объекты уместно возвращать из функции?
67. Что такое std::move
(вариант, принимающий один аргумент) и зачем она нужна?
68. Что такое std::forward
и зачем она нужна?
69. Какие конструкторы (и сколько раз) класса T
будут вызваны: T f(int n) { return n; } auto x = f(101);
?
70. Какие конструкторы (и сколько раз) класса T
будут вызваны: T f(T x) { return x; } auto y = f(101);
?
71. Что произойдёт в T x; x = T();
?
72. Корректно ли выражение f(x.get_value(), std::move(x))
?
73. Какие операции над объектами типа T
будут вызваны в коде void f(std::vector<T> & v) { v.resize(100); }
и сколько раз?
74. Что будет в class X : public Y { public: void f(); }
, если класс Y
уже содержит метод void f()
?
Билеты 1-16
1. Разница между указателями и ссылками ?
указатель – переменная, которая хранит адрес другой переменной в памяти (int* ptr = &x) ссылка – альтернативное имя для уже существующей переменной (int& ref = x) Разница ссыллок и указателей:
- Указатель может принимать значение nullptr
- Указатель является объектом (существует арифметика указателей)
- Ссылки не изменяемы и не обладает квалификатором
const
- Ссылки Синтаксически работают как обычные переменные
- Cсылка, в отличие от указателя, не может быть неинициализированной
- Нельзя объявить массив ссылок
- У ссылки нет адреса
2. Конструктор по-умолчанию, типы конструкторов
Конструктор — это специальная не статическая функция-элемент класса, которая используется для инициализации объектов своего классового типа.
- Имя функции совпадает с именем класса.
- При создании объекта класса всегда вызывается один конструктор.
- Конструктор нельзя вызвать явно.
- У класса может быть произвольное число конструкторов (в том числе ноль).
Примеры иницилизации
- По умолчанию (те, которые не имеют параметров)
struct S{
S();
~S();
}
- С параметрами
struct S{
S(int);
~S();
}
- Неявный конструктор (Если класс не имеет конструкторов)
class S{
S(){};
~S();
}
- Список инициализации
struct S {
S(int x) : a(x), b(a * 2) {} // Список инициализации
int a;
int b = 10; // a:= x; b:= 2 * a или если b не указано b:= 10;
};
В C++ для наследования конструкторов базового класса нужно использовать директиву using A::A
.
struct A {
A(int, double, char);
};
struct B : A {};
struct C : A {
using A::A;
};
int main() {
B b(10, 0.5, 'a'); // error
C c(10, 0.5, '\n'); // OK
}
3. Типы размещения, смысл и разница между ними
В C++ объекты могут размещаться в разных областях памяти, что влияет на их время жизни, производительность и управление ресурсами.
Автоматический
Размещается в стеке вызовов и работает очень быстро Область видимости: до выхода из области Пример
void foo() {
int x = 10; // Автоматическая переменная
std::string s = "Hi"; // Объект на стеке
} // x и s уничтожаются здесь
Динамический
Размещается в куче и работает медленее автоматической. Работает с оператором new
, позволяет управлять временем их существования
Время жизни: пока не вызван delete
int* p = new int(42); // Выделение памяти в куче
std::string* s = new std::string("Hello");
delete p; // Освобождение памяти
delete s;
Статический
Объекты, объявленные на глобальном уровне со спецификатором static, создаются до входа в функцию main() и уничтожаются после её завершения. Время жизни: вся программа
int global = 100; // Глобальная переменная
void foo() {
static int x = 0; // Статическая локальная переменная
x++;
}
Thread-local
Память уникальная для каждого потока и живет до конца жизни потока
thread_local int x = 0; // У каждого потока своя копия x
4. В каком порядке происходит инициализация объектов со статическим размещением в программе?
Категории статических объектов
- 1 Глобальные переменные (вне функций и классов).
- 2 Статические поля классов (static-члены).
- 3 Локальные static-переменные в функциях.
- 4 thread_local-переменные (для каждого потока свои копии).
Основные правила инициализации
- 1 Статическая: выполняется во время компиляции, применяется к переменным, которые можно инициализировать константными выражениями (constexpr). 1.1 Нулевая инициализация: Все объекты сначала получают нулевое значение (для встроенных типов – ноль, для указателей – nullptr) 1.2 Константная инициализация: Если значение можно вычислить на этапе компиляции, то выполняется константная инициализация
- 2 Динамическая инициализация: Выполняется после статической инициализации, но до входа в main(). Применяется, если инициализатор не является константным выражением.
- 3 thread_local при первом обращении в потоке
Порядок инициализации
- Глобальные переменные
- Статические переменные в функциях
- Статические поля классов
Деинициализация
- Происходит в обратном порядке относительно инициализации.
- Локальные static-переменные уничтожаются при завершении программы (после main()).
5. Что такое объект в C++?
Объект в C++ – это сущность, представляющая собой выделенный участок памяти, к которому привязан определённый тип. Это переменная или экземпляр класса, который хранит данные и, возможно, предоставляет функции для работы с этими данными Обладает типом (а значит и имеет выделенную память), а также хранит состояние – то есть набор значений, соответствующих его членам (либо же самому знанчению для примитивных типов)
6. Этапы жизни объекта
0. Объявление
- На этом этапе компилятору сообщается о существовании объекта, его типе и имени. Объявление может сопровождаться или не сопровождаться выделением памяти в зависимости от типа хранения
1 Выделение памяти
- Для примитивных типов (int, char) память выделяется сразу.
- Для классов вызывается operator new (если используется new).
int x; // Выделение в стеке
Circle* c = new Circle(); // Выделение в куче
Инициализация
- Вызывается конструктор объекта.
class Circle {
public:
Circle() { std::cout << "Конструктор\n"; }
};
Circle c; // Вызывается конструктор
Использование
Объект активен, его методы и поля доступны.
- Для стековых объектов — до выхода из области видимости
- Для динамических (new) — до вызова delete
Уничтожение
- Вызывается деструктор
Освобождение памяти
Память возвращается системе.
7. const
и его использование
const
— это ключевое слово, которое указывает, что объект или метод не может изменяться после инициализации.
Основные варианты использования
- Константные переменные
const int x = 10; // Значение нельзя изменить
x = 20; // Ошибка компиляции!
- Указатели и const
- Указатель на константу
const int* ptr = &x; // Данные нельзя изменить через ptr
*ptr = 30; // Ошибка!
- Константный указатель
int y = 5;
int* const ptr = &y; // ptr нельзя перенаправить
ptr = &x; // Ошибка!
*ptr = 10; // OK, данные можно менять
- Константный указатель на константу
const int* const ptr = &x; // Ни ptr, ни данные нельзя изменить
- Константные методы класса Метод не изменяет состояние объекта. Можно вызывать у константных объектов.
class Circle {
double radius;
public:
double getRadius() const { // Константный метод
return radius;
}
};
const Circle c;
double r = c.getRadius(); // OK
- Константные объекты Не могут вызывать не-константные методы. Поля нельзя изменить после создания.
const Circle c(5.0);
c.setRadius(10.0); // Ошибка: setRadius() не константный
- mutable исключение Поле, помеченное mutable, можно изменять даже в константных методах
8. static
и его использование
static для локальных переменных в функциях
- Переменная сохраняет своё значение между вызовами функции.
- Инициализируется только один раз (при первом вызове функции). Пример:
void counter() {
static int count = 0; // Инициализируется один раз
count++;
std::cout << "Count: " << count << "\n";
}
int main() {
counter(); // Выведет "Count: 1"
counter(); // Выведет "Count: 2"
counter(); // Выведет "Count: 3"
}
static для глобальных переменных и функций
-
Ограничивает область видимости переменной/функции текущим файлом (единицей трансляции).
-
Предотвращает конфликты имён между разными .cpp-файлами.
static для членов класса
Свойства
-
Общие для всех объектов класса.
-
Существуют в единственном экземпляре.
-
Должны быть определены вне класса (в .cpp-файле).
class Player {
public:
static int count; // Общий счётчик игроков
Player() { count++; }
~Player() { count--; }
};
int Player::count = 0; // Определение статической переменной
int main() {
Player p1;
Player p2;
std::cout << Player::count; // Выведет 2
}
Статические методы класса
-
Принадлежат классу, а не объекту.
-
Не имеют доступа к нестатическим полям и методам.
-
Вызываются через имя класса, а не через объект.
class Math {
public:
static double square(double x) {
return x * x;
}
};
int main() {
double result = Math::square(5.0); // Вызов без создания объекта
}
9. * virtual
и его использование
Ключевое слово virtual используется для реализации полиморфизма в C++. Оно позволяет переопределять методы в производных классах и работать с объектами через указатели/ссылки базового класса.
Виртуальные функции
- Пример
class Animal {
public:
virtual void makeSound() { // Виртуальная функция
std::cout << "Some sound\n";
}
};
class Dog : public Animal {
public:
void makeSound() override { // Переопределение
std::cout << "Woof!\n";
}
};
int main() {
Animal* animal = new Dog();
animal->makeSound(); // Выведет "Woof!" (а не "Some sound")
delete animal;
}
-
Метод makeSound() помечен как virtual.
-
При вызове через указатель на базовый класс выполняется позднее связывание (динамический полиморфизм).
-
Вызывается версия метода из реального типа объекта (Dog), а не из типа указателя (Animal). Если функция не имеет реализации в базовом классе:
class Animal {
public:
virtual void makeSound() = 0; // Чисто виртуальная функция
};
// Теперь Animal — абстрактный класс (нельзя создать его экземпляр)
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Woof!\n";
}
};
Виртуальный деструктор
Если деструктор базового класса не виртуальный, при удалении объекта через указатель на базовый класс может вызваться неправильный деструктор.
class Base {
public:
virtual ~Base() {} // Виртуальный деструктор
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destroyed\n"; }
};
int main() {
Base* obj = new Derived();
delete obj; // Корректно вызовет ~Derived(), затем ~Base()
}
:NOTE:
- Компилятор создает виртуальную таблицу (vtable) для классов с виртуальными функциями. Каждый объект такого класса содержит указатель на эту таблицу, что позволяет во время выполнения определить, какую именно версию функции следует вызвать
10. * override
и его использование
Ключевое слово override появилось в C++11 для явного указания, что метод переопределяет виртуальную функцию базового класса. Оно не изменяет поведение кода, но делает его более безопасным и читаемым. Примеры использования в прошлом билете
- при определении нужно сохранить сигнатуру к примеру
class Base {
public:
virtual void log() const;
};
class Derived : public Base {
public:
void log() override; // Ошибка: пропущен const
};
11. * template
и его использование
Шаблоны — это мощный механизм C++, позволяющий писать обобщённый код, который работает с разными типами данных без их явного указания. Они применяются для функций, классов и даже переменных (начиная с C++14).
Шаблоны функций
Позволяют создавать функции, работающие с любыми типами.
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << max(3, 5); // Выведет 5 (int)
std::cout << max(3.14, 2.99); // Выведет 3.14 (double)
}
template <typename T1, typename T2>
void printPair(T1 a, T2 b) {
std::cout << a << ", " << b << "\n";
}
printPair<double, int>(3.14, 5); // Явно задаём типы
Шаблоны классов
Позволяют создавать обобщённые классы (например, std::vector
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& value) {
elements.push_back(value);
}
T pop() {
T top = elements.back();
elements.pop_back();
return top;
}
};
int main() {
Stack<int> intStack; // Стек для int
intStack.push(42);
Stack<std::string> strStack; // Стек для строк
strStack.push("Hello");
}
А также можно сделать отдельную реализацию для типа указав аргумент
Шаблонные параметры
- Шаблоны могут принимать не только типы (typename), но и значения и шаблоны
template <typename T, int N>
class FixedArray {
private:
T data[N];
public:
int size() const { return N; }
};
FixedArray<double, 10> arr; // Массив из 10 double
:NOTE:
template может не иметь параметров и выглядеть как template<>. Это называется полной либо частичной реализацией. В таком случае предоставляется специальная реализация шаблона для конкретного типа
template <>
class BoolContainer<bool> {
public:
// smth
};
12. void
и его использование
Тип void в C++ — это специальный тип, который означает "отсутствие типа". Он используется в нескольких ключевых контекстах, каждый из которых имеет свои особенности.
Функции, которые не возвращают значение
void printHello() {
std::cout << "Hello, World!\n";
// return; // Можно написать, но не обязательно
}
int main() {
printHello(); // Вызов функции
}
Если попытаться вернуть значение в void-функции — будет ошибка компиляции.
Функции, которые не принимают аргументы
- C++ (в отличие от C) пустые скобки () эквивалентны (void)
- Не поддерживает арифметику указателей (void* + 1 — ошибка).
- Нельзя разыменовывать (*ptr — ошибка).
void doSomething() { /* ... */ } // Предпочтительный стиль в C++
void doSomething(void) { /* ... */ } // Устаревший стиль (из C)
Указатель на void (void*)
- void* — это указатель на данные произвольного типа. Он используется, когда тип данных неизвестен на этапе компиляции.
int x = 42;
void* ptr = &x; // Указатель на int (без информации о типе)
// Чтобы использовать, нужно явное приведение:
int* intPtr = static_cast<int*>(ptr);
std::cout << *intPtr; // 42
13. nullptr
и его использование
nullptr
- специальное слово, литерал для указателей, имеет тип nullptr_t
, который неявно может быть преобразован в указатель любого типа. В C и до C++11 использовали NULL
, который ну как бы это, #define NULL 0
(ну или (void*)0)...
. Так как сейчас у нас есть отдельный тип для такого, то все стало безопаснее, отпадают проблемы с неоднозначной перегрузкой.
Можно явно обрабатывать в if
, например, что указатель нулевой или нет, что активно используется и используется для перегрузки. Никто нам не мешает принимать nullptr_t
(хоть у этого типа и одно значение).
14. friend
и его использование
Краткий ответ
Нужно, чтобы другой класс либо внешняя для класса функция имела доступ к приватным полям нашего класса когда вам это необходимо.
Данное ключевое слово может использоваться в двух случаях:
-
friend функции
class MyClass { private: int value; public: MyClass(int v) : value(v) {} // Объявляем функцию как дружественную friend void printValue(const MyClass& obj); }; // Определение дружественной функции void printValue(const MyClass& obj) { std::cout << "Value: " << obj.value << std::endl; // Доступ к private member } int main() { MyClass myObj(42); printValue(myObj); // Вывод: Value: 42 return 0; }
Такие функции могут иметь доступ к внутренним полям класса, хотя на самом деле внешние. Плюс для таких функций запрещено неявное приведение типов, что может быть активно использовано. Важно: функции внешиние - не являются методами класса.
-
friend классы
class ClassB; // Предварительное объявление class ClassA { private: int valueA; public: ClassA(int v) : valueA(v) {} // Объявляем ClassB как дружественный friend class ClassB; }; class ClassB { public: void showValueA(const ClassA& obj) { std::cout << "Value from ClassA: " << obj.valueA << std::endl; // Доступ к private member } }; int main() { ClassA a(100); ClassB b; b.showValueA(a); // Вывод: Value from ClassA: 100 return 0; }
Тут идея схожая: класс-друг может сувать свой нос в наши приватные поля и вообще что мы все свои. Такое используется, например, когда у тебя есть дерево поиска, нужно написать итератор, и там эти два класса друг - другу друзья. Важно понимать, что friend классы не наследуются.
Важно отметить, что
friend
нарушает ООП, а именно инкапсуляцию. Так что использование его по мере необходимости и связности концепций.
15. explicit
и его использование
Краткий ответ
Данное ключевое слово служит для запрета неявных преобразований типов
class MyClass {
public:
explicit MyClass(int value) {
std::cout << "Constructor called with value: " << value << std::endl;
}
explicit operator int() { return 0; }
};
void func(MyClass obj) {
// Делаем что-то с obj
}
int main() {
MyClass arg(10);
func(arg); // Ok
func(10); // Error: Неявное преобразование int в MyClass
int m = static_cast<int>(arg) // Ok
int n = arg // Error: неявное преобразование MyClass в int
return 0;
}
Итого, данное ключевое слово служит для большей безопасности в работе с типами, если вы не хотите всяческих неявных преобразований или не хотите возможной путаницы при конструкторах, например от одного аргумента. Также оно может быть использовано в операторах преобразований для запрета неявных преобразований нашего класса в другой класс.
16. inline
и его использование
Краткий ответ
Данное ключевое слово формально нужно, чтобы показать линковщику, что данная функция или переменная может быть определена множество раз в разных еденицах трансляции и определена одинаково (см. 56 билет: ODR).
Порой накладные расходы на вызов функции сильно выше, чем время исполнения самой функции. И было бы эффективнее вставить в момент ее вызова непосредственно ее определение (например функции типа size() { return size; }
).
В целом, если вы создаете класс и пишете данную функцию, и определили ее в хедере, то за вас она автоматически станет inline, и данное ключевое слово писать не требуется.
Также delete, constexpr и consteval функции неявно являются inline функциями.
Что касается переменных, определять их в хедерах можно только если они inline (еще можно использовать constexpr
), тут придется явно указать это слово, так как в противном случае нарушим ODR и ошибка линковки.
Что по поводу производительности: важно отметить, что компилятор - не дурак; то что вы сказали ему, что данную функцию можно встроить вовсе ему не указ. Он сам решает повысит эта вставка производительность или нет. Однако есть некоторые пределы, например количество строчек функции, при которых компилятор даже не будет думать о вставке. При использовании inline это количество увеличивается, но не более. Компилятору не указ даже __forceinline
, так что мысли о применении данной эвристики лучше оставить компилятору.
Билеты 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
Билеты 34-43
34. Чем инициализируются переменная типа bool по умолчанию?
Локальные переменные (автоматическая область видимости) не инициализируются по умолчанию, то есть их значение неопределено, если их не инициализировать явно.
Глобальные переменные, статические переменные и члены классов, имеющие статическую длительность хранения, инициализируются нулём, что для bool
означает значение false
.
35. Какие варианты приведения типов по иерархии наследования существуют и чем отличаются?
Upcasting (приведение к базовому типу)
Преобразование указателя или ссылки на производный класс к указателю/ссылке на базовый класс. Это безопасное преобразование, часто выполняется неявно, так как производный всегда является базовым.
Downcasting (приведение к производному типу)
Преобразование указателя или ссылки базового класса к указателю/ссылке на производный класс. Это преобразование может быть небезопасным, если объект на самом деле не является экземпляром производного класса.
static_cast
Может использоваться для downcasting, но не выполняет проверку во время выполнения. Его следует применять, если вы уверены в типе объекта.
dynamic_cast
Выполняет проверку типа во время выполнения и возвращает nullptr (для указателей) или генерирует исключение (для ссылок) при неудачном приведении. Его используют для безопасного downcasting, когда базовый класс имеет хотя бы одну виртуальную функцию.
Другие виды приведения
const_cast
Удаляет или добавляет квалификатор const (или volatile).
reinterpret_cast
Выполняет низкоуровневое преобразование между указателями, не связанными с наследованием (небезопасное, платформозависимое).
36. В чем разница между enum и enum class, зачем нужны последние?
В C++ существуют два типа перечислений: классические enum (обычные или "неограниченные" перечисления) и enum class (ограниченные перечисления, введенные в C++11). Вот их ключевые различия и преимущества enum class:
Основные различия
Область видимости (scoping):
enum
: Имена перечислителей находятся в той же области видимости, что и само перечисление (может приводить к конфликтам имен)
enum Color { RED, GREEN, BLUE };
enum TrafficLight { RED, YELLOW, GREEN }; // Ошибка: повторное определение
enum class
: Имена перечислителей находятся внутри области видимости перечисления
enum class Color { RED, GREEN, BLUE };
enum class TrafficLight { RED, YELLOW, GREEN }; // OK
Неявное преобразование типов:
enum
: Неявно преобразуется в целочисленные типы
enum class
: Нет неявного преобразования в целочисленные типы
Тип перечисления:
enum
: Базовый тип не фиксирован (компилятор выбирает)
enum class SmallEnum : uint8_t { VALUE1, VALUE2 }; // 8 бит
enum class
: Можно явно указать базовый тип
Преимущества enum class: Предотвращение конфликтов имен
Типобезопасность
Явное указание размера
enum class NetworkPacketType : uint16_t {
DATA = 0x0102,
ACK = 0x0203
};
enum class
: Всегда, когда возможен C++11 и выше (рекомендуемый выбор)
Вывод:
enum class
обеспечивают:
- Лучшую инкапсуляцию (избегают "загрязнения" пространства имен)
- Повышенную типобезопасность
- Возможность явного контроля размера
- Более чистый и поддерживаемый код
37. Есть ли разница между структурой struct и классом class?
Различие между структурами и классами
- struct – по умолчанию, public права доступа к членам, public наследование
- class – по умолчанию, private права доступа к членам, private наследование
Стилистические различия
Хотя функционально они почти идентичны, сложились соглашения об использовании:
class
используют для:
- Сложных объектов с инкапсуляцией
- Когда нужны private-члены и методы
- Для реализации ООП (наследование, полиморфизм)
struct
используют для:
- Простых контейнеров данных (POD — Plain Old Data)
- Когда все члены должны быть публичными
- Для совместимости с C
38. Какие есть спецификаторы прав доступа в C++ и в чем отличие между ними?
В C++ существуют три спецификатора доступа, которые определяют видимость членов класса (полей и методов) и базовых классов:
public
private
protected
public
-Доступ: Из любого места программы -Для членов класса:
class MyClass {
public:
int publicVar; // Доступно отовсюду
void publicMethod() {}
};
-Для наследования:
class Derived : public Base {}; // Публичное наследование
protected
(ограниченный доступ)
Доступ:
- Из методов самого класса
- Из методов производных классов
- Недоступно извне иерархии наследования
private
(максимально закрытый)
Доступ: Только из методов самого класса
Пример
class BankAccount {
private: // Скрытая реализация
double balance;
protected: // Для наследников
void logTransaction() {}
public: // Интерфейс
void deposit(double amount) {
balance += amount;
logTransaction();
}
};
class SavingsAccount : public BankAccount {
public:
void addInterest() {
// balance -= 10; // Ошибка: private
logTransaction(); // OK: protected
}
};
int main() {
SavingsAccount acc;
acc.deposit(1000); // OK: public
// acc.logTransaction(); // Ошибка: protected
}
Когда что использовать:
public
: Интерфейс класса (что могут использовать все)protected
: Для расширения класса в наследникахprivate
: Внутренняя реализация (инкапсуляция)
Эти спецификаторы — основа инкапсуляции в C++, позволяющая скрывать детали реализации и предоставлять четкие интерфейсы.
39. Что такое конструктор и когда он вызывается?
Конструктор
Конструктор — это специальная не статическая функция-элемент класса, которая используется для инициализации объектов своего классового типа.
- Имя функции совпадает с именем класса.
- При создании объекта класса всегда вызывается один конструктор.
- Конструктор нельзя вызвать явно.
- У класса может быть произвольное число конструкторов (в том числе ноль).
Основные особенности:
- Имя совпадает с именем класса
- Не имеет возвращаемого типа (даже void)
- Может быть перегружен (несколько версий с разными параметрами)
Когда вызывается конструктор:
При создании объекта:
MyClass obj; // Вызывается конструктор по умолчанию
MyClass obj2(42); // Вызывается параметризованный конструктор
При динамическом выделении памяти:
MyClass* ptr = new MyClass(); // Вызывается конструктор
При создании временных объектов:
MyClass func() {
return MyClass(); // Вызывается конструктор
}
В составе других объектов:
class Container {
MyClass member; // Конструктор MyClass вызовется при создании Container
};
При копировании (если не используется move-семантика):
MyClass obj1;
MyClass obj2 = obj1; // Вызывается копирующий конструктор
Типы конструкторов:
По умолчанию (без параметров):
class MyClass {
public:
MyClass() { /* инициализация */ }
};
Параметризованный:
class MyClass {
public:
MyClass(int value) { /* использование value */ }
};
Копирующий:
class MyClass {
public:
MyClass(const MyClass& other) { /* копирование из other */ }
};
Перемещающий (C++11):
class MyClass {
public:
MyClass(MyClass&& other) { /* перемещение ресурсов из other */ }
};
Важные нюансы:
- Автоматическая генерация: Если не объявить ни одного конструктора, компилятор создаст конструктор по умолчанию
- Если объявить любой конструктор, конструктор по умолчанию не генерируется
40. Что такое деструктор и когда он вызывается?
Деструктор — это специальная функция-элемент, которая вызывается, когда заканчивается время жизни объекта. Цель деструктора освободить ресурсы, которые объект мог получить за время своего существования (или сделать какую-то финализацию).
- Имя функции
~ClassName
. - У деструктора не может быть аргументов.
- По умолчанию noexcept.
- При любом удалении объекта класса вызывается его деструктор.
- У класса может быть только один деструктор.
- Деструктор можно вызвать явно.
Основные особенности:
- Имя: ~ИмяКласса() (тильда + имя класса)
- Не имеет параметров и возвращаемого типа
- Не может быть перегружен (только один деструктор на класс)
- Виртуальный, если класс предназначен для наследования
Когда вызывается деструктор:
При выходе объекта из области видимости:
{
MyClass obj; // Конструктор
} // Деструктор (при выходе из блока)
При явном удалении динамических объектов:
MyClass* ptr = new MyClass();
delete ptr; // Вызывается деструктор
Для элементов массива:
MyClass* arr = new MyClass[5];
delete[] arr; // Деструкторы всех 5 элементов
Для членов класса:
class Container {
MyClass member;
public:
~Container() { /* Деструктор member вызовется автоматически после этого */ }
};
При исключениях (для локальных объектов в стеке)
41. Можно ли явно вызвать конструктор?
Конструктор нельзя вызвать явно. (см. 39 билет)
42. Можно ли явно вызвать деструктор?
Деструктор можно вызвать явно. (см. 40 билет)
43. В чем разница между оператором присваивания и конструктором копирования?
Это не шаблонный конструктор, принимающий первым аргументом lvalue ссылку на объект того же типа, что и класс конструктора, который может быть вызван с одним аргументом. Конструктор копирования вызывается всегда, когда объект класса инициализируется из lvalue выражения того же типа (класса).
Ключевые различия:
Характеристика | Конструктор копирования | Оператор присваивания |
---|---|---|
Когда вызывается | При создании объекта | Для существующего объекта |
Тип операции | Инициализация | Присваивание |
Возвращаемое значение | Нет | MyClass& (ссылка на объект) |
Проверка самокопирования | Не требуется | Обязательна |
Ответственность | Создание новой копии | Освобождение старых ресурсов + копирование |
class String {
char* data;
size_t size;
public:
// Конструктор копирования
String(const String& other) :
size(other.size),
data(new char[other.size + 1])
{
std::copy(other.data, other.data + size + 1, data);
}
// Оператор присваивания
String& operator=(const String& other) {
if (this != &other) { // Защита от самоприсваивания
delete[] data; // Освобождаем старые данные
size = other.size;
data = new char[size + 1];
std::copy(other.data, other.data + size + 1, data);
}
return *this;
}
~String() { delete[] data; }
};
Билеты 44 - 54
44. Чем отличается new int(10)
от new int[10]
?
Кратки ответ
Первый аллоцирует число
int{10}
и возвращает указатель на этот элемент, второй вариант сложнее, сначала он записывает размер массива, который вы пытаетесь разместить, затем все элементы этого массива поочередно.
operator new !пытается!** аллоцировать какую-то память в динамической куче (в оперативке). Зачем это нужно: выделяя память на куче с помощью new
, вы берете ответственность за ее освобождение, так как время жизни этого объекта контролируется вами. Соответственно существует два способа освободить эту память (в рамках C++), а именно operator delete
и operator delete[]
, соответственно каждый из них нужно вызывать в соответствии с тем, какой оператор вы использовали для выделения памяти. Так как оператор delete
освобождает указанную ему по указателю память и больше ничего, а delete[]
ищет тот самый размер до указателя, который мы туда должны были записать оператором new[]
, и только по нему далее удаляет столько памяти, сколько нужно.
Вывод:
Для new
используйте delete
, для new[]
используйте delete[]
, иначе пупа получит за лупу.
45. Можно ли делать delete ptr;
после auto ptr = new T[5];
и почему?
Кратки ответ
Нельзя, это утечка памяти и UB.
Смотри выше! Если так сделать, то это UB. Такой код конечно может запуститься и ничего страшного не произойдет, ведь после завершения программы вся память, которую она выделила, очищается автоматически, но в целом такое поведение вызывает утечку памяти, которая плоха тем, что бывает трудно обнаружима и может влечь внезапные ошибки в рандомный момент.
Offtop: В том числе для решения такой проблемы придумали умные указатели.
46. Что такое placement new (размещающее new)?
Кратки ответ
Аналогично обычному оператору new позволяет выделять объект в куске памяти, но на этот раз не только на куче, но и на любом буфере, в том числе статическом.
int main() {
void* buffer = ...;
T* obj = new (buffer) T();
obj->~T();
return 0;
}
Память которую мы используем для буфера должна быть достаточно неинициализированной памяти для размещения объектов; также она должна быть выровнена. Стоит заметить, что мы должны самостоятельно вызывать деструктор объекта, который выделили, так как он не будет вызван автоматически из-за специфики хранения. Бывает полезно, если мы не хотим много раз ходить к ядру и просить память или хотите использовать статическую память для данных манипуляций, что может отразиться на производительности.
47. Как обеспечить корректное удаление объекта, созданного при помощи размещающего new?
Кратки ответ
Явно вызвать детсрутктор размещенного объекта.
Смотри выше! Вырезка из базы: You must manually call the object's destructor if its side effects are depended by the program.
48. Undefined, unspecified and implementation-defined behavior
Кратки ответ
В сущности эти три понятия характеризуют поведение программы в случаях, когда стандарт языка не дает прямого ответа. Undefined behavior - может произойти все что угодно. Unspecified behavior - стандарт не заставляет документировать как делать, но органичивает возможные варианты. Implementation-defined behavior - компилятор должен явно задокументировать что он делает.
Примеры соответствующих поведений описаны в ссылках в названии:
-
Undefined behavior
Стандарт не несет ответственности за то, что произойдет; произойти может все что угодно. Чаще всего конечно происходит далеко не все что угодно и никто форматировать ваш диск не будет; но в сущности поведение программы в данных ситуациях бывает крайне непредсказуемым. -
Unspecified behavior
Ситуация когда компилятор может выбирать что делать; так как стандарт определил возможные варианты но не заставил указать как именно — например передача аргументов в функцию когда порядок вычисления аргументов не установлен стандартом; однако все аргументы должны быть вычислены; соответственно разные компиляторы выбирают разные способы. -
Implementation-defined behavior
Здесь в отличие от пункта выше компилятор должен четко указать как именно он реализует это поведение; разработчики могут реализовывать платформопереносимые программы.
49. Что такое наблюдаемое поведение программы (observable behaviour), зачем нужно это понятие?
Кратки ответ
Класс поведения, не состоящий в классах поведения, таких как: Undefined behavior, Unspecified behavior и Implementation-defined behavior. Нужно для возможности остлеживания сторонними программами выполнения нашей программы.
Итак, в предыдущем билете затрагивались классы поведения программ; соответственно если программа не находится ни в одном из этих классов она является программой с наблюдаемым поведением (так в стандарте определено это понятие). Основной смысл и польза состоит в том что выполнение программы достаточно корректно и предсказуемо; мы можем анализировать ее выполнение с помощью других программ что дает нам большие возможности для анализа кода как для разработчика.
50. Что такое this
? Можно ли делать присваивание в this
и почему?
this — это константный указатель на экземпляр класса метод которого был вызван. Так как указатель константный то присвоить ему другой объект невозможно; но можно получать доступ ко всем его методам и полям как у указателя.
51. Можно ли делать delete this
, почему?
Это выражение вполне возможно использовать; если объект был выделен с помощью new
, оно будет освобождать текущий экземпляр this
. После выполнения этой операции любые обращения будут недействительными. Такое даже используется, например в shared_ptr.
52. Что такое явное/неявное преобразование типов?
Кратки ответ
В C++ преобразование типов — это процесс, при котором значение одного типа данных преобразуется в значение другого типа. Это может происходить явно(самим человеком, например static_cast) или неявно (автоматически компилятором).
Неявное преобразование типов:
Компилятор сам выполняет преобразование, если считает это безопасным и логичным.
int a = 10;
double b = a; // неявное преобразование int → double
int
значение a
автоматически преобразуется в double
, потому что double
может без потерь представить любое int
.
Когда обычно происходит неявное преобразование типов:
Присваиваем один тип другому:
double d = 3; // int → double
В арифметике:
int + double → double
В аргументах функции:
void foo(double); foo(5); // int → double
Явное преобразование типов(Ручное):
Указываешь сам, во что преобразовать.
double b = (double)10; // C-style
double b = static_cast<double>(10); // C++-style (лучше)
Главные отличия в том, что явное ты делаешь сам, а неявное делает за тебя компилятор, но он может сделать приведение типо не так как тебе хочется -> сломается логика твеой программы. Так что с неявными приведениями нужно быть осторожнее.
53. Какие есть варианты явного приведения типов (cast) в C++ и чем отличаются?
Кратки ответ_
Все варианты приведения типов в С++ являются частями просиходящего в C - style касте и разбиваются на const_cast, static_cast, reinterpret_cast. Также начиная с С++11 существет dynamic_cast для динамеческого приведения типов по иерархии наследования "вверх, вниз и вбок".
Итак, В C++ есть 4 основных вида явного приведения типов (кастов). Каждый имеет своё назначение и уровень безопасности.
Виды кастов в C++
📋 Таблица кастов в C++
Каст | Назначение | Безопасность | Применение | Отличие от других кастов |
---|---|---|---|---|
static_cast | Стандартные преобразования типов | Безопасен | int → float, базовый класс → производный (вверх по иерархии) | Компилируется без runtime-проверок, используется для логичных преобразований |
dynamic_cast | Преобразование в иерархии классов (с virtual ) | Проверка в runtime | Только для указателей/ссылок с полиморфизмом | Проверяет тип в runtime, безопасен для downcast в иерархии с виртуальными функциями |
const_cast | Удаление или добавление const | Осторожно | Убрать const , передать в неконстантную функцию | Единственный каст, работающий только с const /volatile |
reinterpret_cast | Побитовое преобразование между типами | Опасен | Указатели, чтение/запись памяти через другой тип | Позволяет «обмануть» компилятор, интерпретируя данные как другой тип (может привести к UB) |
Подробные примеры кода:
static_cast
int a = 10;
double b = static_cast<double>(a); // int → double
dynamic_cast
class Base { public: virtual ~Base() {} };
class Derived : public Base {};
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // безопасный downcast
const_cast
void print(char* msg) {}
const char* text = "hello";
print(const_cast<char*>(text)); // убираем const
reinterpret_cast
int x = 65;
char* c = reinterpret_cast<char*>(&x); // читаем int как char
54. Что такое макросы? В чем отличия от функций (для макросов в духе #define MAX(a, b)
)?
Кратки ответ
Макросы — это текстовые подстановки, которые обрабатываются препроцессором до компиляции. Объявляются с помощью
#define
. Отличаются от функций тем, что просто подставляются в месте вызова и не являются объектами.
Например:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
При компиляции,MAX(a, b)
заменяется во всех местах на ((a) > (b) ? (a) : (b))
.
Макросы vs Функции в C++
Характеристика | Макрос (#define ) | Функция |
---|---|---|
Обрабатывается на стадии | Препроцессора (до компиляции) | Компиляции |
Типы данных | ❌ Нет проверки типов | ✅ Есть — зависит от сигнатуры |
Отладка | ❌ Нельзя отлаживать (нет кода в дебаге) | ✅ Можно пошагово пройти |
Побочные эффекты | ❌ Возможны (например, двойной вызов арг.) | ✅ Аргументы вызываются один раз |
Инлайнинг | ✅ По сути всегда inline (подстановка текста) | ⚠ Может быть inline, но не обязан |
Безопасность | ❌ Небезопасны, легко допустить ошибку | ✅ Безопаснее, соблюдают правила C++ |
Гибкость | ❌ Не поддерживает перегрузку, шаблоны и т.п. | ✅ Поддерживает перегрузку, шаблоны и ООП |
Вывод: нужно быть аккуратным с использованием макросов, так как код макроса легко можно сломать из-за приоритетов (оборачивайте макрос в скобочки).
Билеты 55-64
55. Что делает препроцессор?
Краткий ответ:
Препроцессор обрабатывает код перед его передачей компилятору: он подставляет макросы, включает заголовочные файлы, выполняет условную компиляцию и удаляет комментарии. Упрощает и оптимизирует дальнейшую компиляцию.
Препроцессор - это отдельный этап обработки исходного кода перед компиляцией. Он выполняет текстовые преобразования исходного кода на основе специальных директив (команды препроцессора). Они начинаются с символа
#
и не являются частью языка программирования в строгом смысле, а скорее указывают компилятору, как предварительно обработать код перед его компиляцией.
Основные характеристики:
- Работает на текстовом уровне (не анализирует синтаксис C++)
- Выполняется до компиляции
- Обрабатывает директивы, начинающиеся с символа
#
Обработка директив (#...
)
Препроцессор выполняет инструкции, начинающиеся с #
:
#include
- вставляет содержимое файла (библиотеки или заголовочные файлы:.h
)#define
- создаёт макроподстановки#ifdef
/#ifndef
- условная компиляция#pragma
- специфичные для компилятора инструкции
Удаление комментариев и пробелов
-
Удаляет
//
и/* ... */
комментарии -
Сокращает лишние пробелы и табы
-
Препроцессор не знает cинтаксиса
C++
— он работает с текстом. -
Без препроцессора компилятор получил бы "сырой" код с директивами, которые он не понимает!
56. Что такое ODR?
Краткий ответ:
ODR - правило единственного определения в
C++
, которое требует, чтобы у каждого объекта, функции или типа было не более одного определения в пределах программы, за исключениемinline
-функций,constexpr
-переменных и шаблонов.
One Definition Rule (ODR) - Правило Одного Определения
В любой единице трансляции допустимо только одно определение любой переменной, функции, типа класса, типа перечисления, концепта (начиная с C++20) или шаблона.
Одно и только одно определение каждой невстраиваемой функции или переменной, которая использует odr
,должно присутствовать во всей программе.
ODR-использование
- Объект ODR-используется, если его значение читается (если только это не константа с известным на момент компиляции значением), записывается, его адрес берётся или с ним связывается ссылка
- Функция ODR-используется, если она вызывается или её адрес берётся
- Объект или функция НЕ считаются ODR-используемыми, если их можно удалить из программы без изменения её поведения.
Пример нарушения ODR
// file1.cpp
int helper() { return 1; }
// file2.cpp
int helper() { return 2; } // Ошибка линковки: multiple definition
Исключения
- Inline-сущности могут определяться многократно
- Шаблоны (т.к. не являются готовым кодом, а лишь инструкцией для компилятора)
Нарушение ODR для классов/шаблонов часто приводит к трудноуловимым ошибкам, так как компилятор не всегда может их обнаружить.
57. В каких случаях допустимо размещать определение функции в заголовочном файле, а в каких - нет?
Краткий ответ:
✅
inline
,template
,constexpr
❌ обычные функции
Размещать можно, если:
- Функция inline
- Это шаблонная (template) функция
- Это static-функция
- Это метод внутри class
❌ Размещать нельзя, если:
- Это обычная функция (не inline) (если определить обычную функцию в заголовочном файле, а затем подключить его в нескольких
.cpp
файлах, то при линковке - ODR) - Функция меняет глобальное состояние (если функция изменяет глобальные переменные, её размещение в заголовочном файле может вызвать непредсказуемое поведени)
- Функция слишком сложная для заголовочного файла (во избежание перегрузки компиляции)
58. Что такое предварительное объявление (forward declaration) и для каких элементов языка это актуально?
Краткий ответ:
forward declaration
- это объявление сущности без её полного определения.
Актуально для: функций и классов, невозможно для переменных
Forward declaration - это предварительное объявление синтаксиса или сигнатуры идентификатора, переменной, функции, класса и т.д. до их использования (в более поздней части программы).
Что это вообще? пример
void func(); // предварительное объявление функции
// без этой строчки был бы Compile Error
int main() {
func(); // вызов функции, которая будет определена позже
return 0;
}
void func() { // определение функции
std::cout << "Hello, World!" << std::endl;
}
Актуальность
Разрешение циклических зависимостей: В случаях, когда два класса или функции ссылаются друг на друга, предварительное объявление помогает избежать ошибок компиляции. Например, один класс может использовать указатель на второй класс, а второй класс — на первый.
class B; // предварительное объявление класса B
class A {
public:
B* b; // указатель на B
};
class B {
public:
A* a; // указатель на A
};
❌ Нельзя использовать для переменных, так как компилятор должен знать их размер.
Итог
Forward declaration
— это инструмент для работы с зависимостями между различными элементами программы, позволяющий избежать ошибок компиляции, если полные определения этих элементов ещё не представлены в коде.
59. Когда начнётся и когда закончится время жизни объекта с именем a
: void f(X a) {}
?
Краткий ответ:
- начнётся: с момента вызова функции
f
- закончится: в конце функции
f
Пояснение для хомячков:
Объект a
создаётся в момент вызова функции и уничтожается в конце функции, поскольку он передан по значению. В случае передачи объекта по ссылке (например, void f(X& a)
), время жизни объекта будет зависеть от времени жизни переданного объекта.
Итог:
a
живёт, пока выполняется функция f
.
60. Когда начнётся и когда закончится время жизни объекта доступного по указателю pa
: void f() { auto pa = new X; }
?
Краткий ответ:
Начнётся при выполнении
new X
, закончится при окончании работы программы
- Начнётся: Время жизни объекта начнётся с момента выделения памяти с помощью оператора
new
. Когда вызываетсяnew X
, место под объект типаX
выделяется в динамической памяти, и этот объект существует до тех пор, пока не будет удалён. - закончится: когда закончится программа (т.к. нет
delete
, объект остаётся в памяти даже после выхода изf()
(как итог: утечка памяти))
61. В каком порядке будут созданы, а в каком разрушены объекты с именами a
и b
: void f() { X a; { Y b; } }
?
Краткий ответ:
Время жизни объекта начинается с его создания и заканчивается, когда объект выходит из области видимости. В случае с объектами
a
иb
в примере, они следуют правилу: сначала создаются, затем разрушаются в обратном порядке их создания, с учётом области видимости.
Порядок:
X constructed
Y constructed
Y destructed
X destructed
Программа для проверки:
#include <iostream>
using namespace std;
struct Y {
Y() { std::cout << "Y constructed\n"; }
~Y() { std::cout << "Y destructed\n"; }
};
struct X {
X() { std::cout << "X constructed\n"; }
~X() { std::cout << "X destructed\n"; }
};
void foo() {
X a;
{Y b;}
}
int main() {
foo();
return 0;
}
62. Какие подобъекты будут присутствовать в объекте класса X
: struct X : Y, Z { W w; V v; };
, каков порядок их создания и разрушения?
Краткий ответ:
В объекте класса X будут присутствовать следующие подобъекты:
- Подобъект базового класса Y.
- Подобъект базового класса Z.
- Подобъект нестатического члена w типа W.
- Подобъект нестатического члена v типа V.
Разбор порядка:
- Создание:
- Y -> Z -> w -> v -> X
- Разрушение (в обратном порядке):
- X -> v -> w -> Z -> Y
Убедиться в этом можно, запустив нижеприложенную программу
#include <iostream>
struct Y {
Y() { std::cout << "Y constructed\n"; }
~Y() { std::cout << "Y destructed\n"; }
};
struct Z {
Z() { std::cout << "Z constructed\n"; }
~Z() { std::cout << "Z destructed\n"; }
};
struct W {
W() { std::cout << "W constructed\n"; }
~W() { std::cout << "W destructed\n"; }
};
struct V {
V() { std::cout << "V constructed\n"; }
~V() { std::cout << "V destructed\n"; }
};
struct X : Y, Z {
W w;
V v;
X() { std::cout << "X constructed\n"; }
~X() { std::cout << "X destructed\n"; }
};
int main() {
X x;
return 0;
}
63. Что такое временный объект и чем ограничено его время жизни?
Краткий ответ:
Временный объект — это безымянный объект, созданный компилятором при вычислении выражения.
Время жизни: ✅ До конца текущего выражения, если не продлён. ✅ Может быть продлён, если привязан к
const
-ссылке. ❌ Уничтожается сразу после использования, если не сохранён.
Временные объекты и их время жизни
Временный объект — это объект, который создаётся на месте и существует только в рамках одного выражения.
Когда создаются временные объекты?
- При возвращении значения из функции без привязки к переменной.
- При передаче результата выражения в функцию.
- При создании анонимного объекта (
X()
вместоX obj;
).
Как долго живёт временный объект?
- По умолчанию до конца выражения, в котором он создан.
- Если временный объект привязывается к
const&
, то живет до конца области видимости ссылки.
Примеры:
#include <iostream>
struct X {
X() { std::cout << "Created X\n"; }
~X() { std::cout << "Deleted X\n"; }
};
struct Y {
Y() { std::cout << "Created Y\n"; }
~Y() { std::cout << "Deleted Y\n"; }
};
X fx() { return X(); } // Создаётся временный объект
Y fy() { return Y(); } // Создаётся временный объект
int main() {
const Y& xr = fy(); // Здесь объект будет жить до конца main
fx(); // Временный объект уничтожится сразу после вызова f()
}
Будет выведено:
Created Y
Created X
Deleted X
Deleted Y
(поч так? см. комменты кода выше)
Ошибка использования временного объекта:
int& f(int& a) {
return a;
}
int main(int argc, char** argv) {
return 11 + f(argc * 2); // Error: передаётся временное значение, а ожидается ссылка
}
Итог:
-
Временный объект живёт до конца выражения, если не привязан к const&.
-
Использование временных объектов в функциях, ожидающих ссылку &, может привести к недействительным ссылкам и ошибкам.
64. Чем отличается инициализация по умолчанию для базовых и пользовательских типов (классов)?
Краткий ответ: Базовые типы (int, double, etc.):
✅ Не инициализируются по умолчанию
Пользовательские типы:
✅ Если есть конструктор по умолчанию — он вызывается.
❌ Без конструктора — поля остаются неинициализированными.
Базовые vs пользовательские
Базовые
- Базовые типы не содержат методов, конструктора или деструктора и хранят только данные.
- Базовый тип — это встроенный тип данных, без методов (int, double, char и т. д.), у них фиксированное поведение.
- Базовый класс — это именно родительский класс, от которого можно наследоваться.
Пользовательские
- Пользовательский класс — это любой класс, который создаёт программист, независимо от его роли.
- Пользовательский тип — это тип данных, определённый программистом (поведение можно менять).
Различия инициализации (примеры)
P.S. у меня Clion умный, поэтому запускать скрипты ниже можно здесь, 100% будет видно то, что хотел передать автор
Базированный нормис
#include <iostream>
int y; // 0 (т.к. глобально)
int main() {
int x; // мусор - случайное число (т.к. локально)
std::cout << y << ' ' << x;
}
Конструктор по умолчанию у пользовательского типа (класса)
#include <iostream>
class Car {
public:
int speed; // Поле остается неинициализированным
Car() { speed = 100; } // Конструктор по умолчанию
};
int main() {
Car myCar;
std::cout << myCar.speed << std::endl; // Выведет 100
}
Пример с мусором
class Bike {
public:
int speed; // Неинициализированная переменная
};
int main() {
Bike myBike;
std::cout << myBike.speed << std::endl; // Мусорное значение
}
Вывод
Пользовательские типы (классы) автоматически вызывают конструктор по умолчанию, если он есть, инициализируя поля. Если конструктора нет, поведение такое же, как у базовых типов.
Билеты 65-74
65. Чем отличаются методы std::vector
push_back
и emplace_back
?
Краткий ответ
emplace_back
- более новая версияpush_back
, она способна эффективнее работать с rvalue значениями и допускает передачу аргументов для конструктора объекта без явного создания
Пример использования
class T {
public:
T(int, int) {
std::cout << "ctor called" << std::endl;
}
T(int) {
std::cout << "ctor called" << std::endl;
}
T(T&&) {
std::cout << "move ctor called" << std::endl;
}
T(const T&) {
std::cout << "copy ctor called" << std::endl;
}
};
void emplace() {
std::cout << "emplace rvalue" << std::endl;
std::vector<T> vector;
vector.emplace_back(1);
}
void push() {
std::cout << "push rvalue" << std::endl;
std::vector<T> vector;
vector.push_back(1);
}
int main() {
push();
emplace();
std::vector<T> v;
v.push_back(1, 2); // Error
v.push_back({1, 2}); // Ok
v.emplace_back(1, 2); // Ok
}
Вывод программы
push rvalue
ctor called
move ctor called
emplace rvalue
ctor called
- Итак, новая версия добавления элемента способна внутри себя создать объект из переданных ей аргументов и добавить в вектор без лишних перемещений. Старая же способна только принять в себя объект типа
T
, а значит будет вызван конструктор временного объекта, а затем перемещающий конструктор для добавления в вектор (более того,push_back
используетemplace_back
, если к нему приходит объект типаT&&
, что позволяет ему не делатьcopy ctor
, а толькоmove ctor
). - К тому же благодаря новой концепции,
emplace_back
может принять в себя несколько аргументов без вызова конструктора, что может быть полезно.
Важно отметить, что различные методы emplace
и push
это методы не только вектора, а и многих других контейнеров и адаптеров.
Вывод
Не используйте версию push
.
``
66. Ссылки на какие объекты уместно возвращать из функции?
Не стоит возвращать из функции ссылки на локальные объекты, иначе это приведет к висячим ссылкам. В каких случаях вернуть ссылку из функции уместно:
- Если функция позволяет получить доступ к элементу какого-то контейнера, тогда передача по ссылке — это альтернатива методам
get
,set
(в случаях, когда вы не хотите, чтобы кто-то мог менять объект, либо возвращайте по значению, либо по константной ссылке (первое безопасно, второе может вам навредить с помощьюconst_cast
, так что все же лучше по значению)). - Если вы хотите делать что-то типа
x.f().f().g().size()
, и при этом все манипуляции происходили над одним и тем же объектом (примерoperator=
). - *Наверняка есть что-то еще типа Синглтона или что-то с использованием динамической памяти (зачем если есть указатели), можно дополнить, но я думаю этого достаточно.
67. Что такое std::move
(вариант, принимающий один аргумент) и зачем она нужна?
Кратки ответ
Глобально, данная функция позволяет вам гарантировать, что дальнейшее использование переменной типа lvalue не будет, и его ресурсы можно использовать для нужд других объектов. В особенности
std::move
возвращает xvalue, ее вызов эквивалентенstatic_cast<T&&>
.
Буквально в type_traits.cpp
написано:
constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept {
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
Наиболее понятное и частое использование std::move
состоит в том, чтобы избежать лишнего копирования и сделать относительное дешевое перемещение для объекта, который нам больше не нужен при передаче его в конструктор.
68. Что такое std::forward
и зачем она нужна?
Задачка со звездочкой.
Кратки ответ
Для чего нужен
std::forward
? Чтобы передать аргументы ровно такого value category, какой был у них изначально.
Такая концепция передачи аргументов называется perfect forwarding (ранее это было использовано emplace'ом). Такое поведение позволяет нам избежать лишних копирований и перемещений.
#include <iostream>
#include <utility>
void f(int& x) {
std::cout << "&: " << x << "\n";
}
void f(int&& x) {
std::cout << "&&: " << x << "\n";
}
template<typename T>
void wrapper(T&& arg) {
// !!!Передаем arg дальше с сохранением его типа!!
f(std::forward<T>(arg));
}
int main() {
int a = 10;
wrapper(a); // lvalue
wrapper(20); // rvalue
}
output:
&: 10
&&: 20
Пример perfect forwarding с помощью std::forward
. Важно отметить конструкцию шаблона функции wrapper
, из-за правил вывода шаблона и схлопывания ссылок аргумент функции arg
является универсальной ссылкой (или forwarding reference), т.е для lvalue arg будет lvalue и для rvalue arg будет rvalue.
P.S что std::move
, что std::forward
можно заменить (и фактически они заменяются) на static_cast<T&/T&&>
, но как и для любого велосипеда, это может привести к ошибкам, и встроенные функции позволяют нам обработать например std::move(std::forward<int&>(7));
. К тому же такие функции гораздо легче читать и понимать, что намного важнее чем сэкономить на вызове функции.
69. Какие конструкторы (и сколько раз) класса T
будут вызваны:
T f(int n) { return n; }
auto x = f(101);
Кратки ответ
Только конструктор Т
class T {
public:
T(int) {
std::cout << "ctor called" << std::endl;
}
T(T&&) {
std::cout << "move ctor called" << std::endl;
}
T(const T&) {
std::cout << "copy ctor called" << std::endl;
}
};
T f(int n) {
return n;
}
int main() {
auto x = f(101);
}
output:
ctor called
В данном случае кажется, что при возврате функции f будет создан объект типа T, а затем вызовется перемещающий конструктор типа T для переменной x от возвращенного значения функции f. Однако вывод программы другой и все благодаря RVO (return value optimization), которая благодаря компилятору не стала создавать временный объект при возврате из функции, а создала его прямо для х.
70. Какие конструкторы (и сколько раз) класса T
будут вызваны:
T f(T x) { return x; }
auto y = f(101);
Кратки ответ
Конструктор Т и перемещающий контруктор Т
(Код для этого билета смотри выше и поменяй Int на T)
output:
ctor called
move ctor called
Что же поменялось в данном случае? Давайте разберем: мы создаем rvalue объект типа T при передаче функции f значения типа int, соответственно получаем ctor called. Далее RVO и мы создаем х уже не из int, а из T&&, который был сконструирован при вызове — вот и вся разница.
71. Что произойдёт в
T x;
x = T();
Кратки ответ
Два раза вызовется конструктор по умолчанию и перемещающее присваивание.
struct T {
T() {
std::cout << "default" << std::endl;
}
T& operator=(T&&) {
std::cout << "moved assignment" << std::endl;
return *this;
}
};
int main() {
T x;
x = T();
}
output:
default
default
moved assignment
Что произошло: T t
- вызов пустого конструктора класса T (Note: в С++ огромное множество способов инициировать переменную, и это не объявление переменной, а именно ее инициализация и определение с помощью умолчательного конструктора от 0 аргументов). Х = Т() - вызов пустого конструктора класса Т и создание rvalue значения T, затем вызов operator= класса Т от T&&.
72. Корректно ли выражение
f(x.get_value(), std::move(x));
Кратки ответ
Нет, это UB
После того как был использован std::move(x)
мы утверждаем, что в дальнейшем х мы использовать не будем; соответственно его можно переместить. Соответственно если мы будем использовать х после std::move
, то у нас UB. Исходя из этого нам важно чтобы x.get_value()
вызвался раньше чем std::move(x)
, но стандарт C++ не гарантирует порядок исполнения аргументов при передаче функции (и на практике действительно разные компиляторы в разном порядке вызывают аргументы). Соответственно данное выражение не является корректным.
73. Какие операции над объектами типа T
будут вызваны в коде и сколько раз?
void f(std::vector<T> & v) { v.resize(100); }
Кратки ответ
В данном коде может быть вызвано только 2 операции над объектами типа Т: конструктор по умолчанию и деструктор.
Давайте разберемся сколько и чего будет вызвано в зависимости от чего: пусть в функцию f передается вектор размером start, и делается resize размером resize.
static int n{ 0 };
static int m{ 0 };
struct T {
T() {
n++;
std::cout<< "constructor called " << n << " times" << std::endl;
}
~T() {
m++;
std::cout << "destructor called " << m << " times" << std::endl;
}
};
void f(std::vector<T>& vector) {
int resize = 10;
vector.resize(resize);
}
int main() {
int start = 15;
std::vector<T> v(start);
std::cout << "f start" << std::endl;
f(v);
std::cout << "f end" << std::endl;
}
Если размер start < resize, то очевидно конструктор по умолчанию вызовется resize - size раз; если start > resize то для сохранения размера вектора вызовется деструктор resize - size раз; если size = resize то не вызовется ничего.
74. Что будет в если класс Y уже содержит метод
class X : public Y { public: void f(); }
void f();
Кратки ответ
Вызовется функция X::f.
class Y {
public:
void f() {
std::cout << "f: Y" << std::endl;
}
void virtual f_v() {
std::cout << "f_v: Y" << std::endl;
}
};
class X : public Y {
public:
void f() {
std::cout << "f: X" << std::endl;
}
void f_v() override {
std::cout << "f_v: X" << std::endl;
}
};
int main() {
X x;
Y y;
x.f();
y.f();
x.f_v();
y.f_v();
Y* y_ptr = &x;
y_ptr->f();
y_ptr->f_v();
}
output:
f: X
f: Y
f_v: X
f_v: Y
f: Y
f_v: X
В данном случае класс Х наследуется от Y; при вызове x.f() класс X скроет метод Y и вызовет X::f. Если же функции в базовом классе не будет переопределения в наследуемом классе, то будет вызвана функция базового (пример функция g). Дальше приведены примеры как это влияет на указатель базового класса от производного и зачем нужны виртуальные функции (в билете не спрашивается, для общего развития).