Билеты 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; // Мусорное значение
}
Вывод
Пользовательские типы (классы) автоматически вызывают конструктор по умолчанию, если он есть, инициализируя поля. Если конструктора нет, поведение такое же, как у базовых типов.