Билеты 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 будут присутствовать следующие подобъекты:

  1. Подобъект базового класса Y.
  2. Подобъект базового класса Z.
  3. Подобъект нестатического члена w типа W.
  4. Подобъект нестатического члена v типа V.

Разбор порядка:

  1. Создание:
  • Y -> Z -> w -> v -> X
  1. Разрушение (в обратном порядке):
  • 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-ссылке. ❌ Уничтожается сразу после использования, если не сохранён.


Временные объекты и их время жизни

Временный объект — это объект, который создаётся на месте и существует только в рамках одного выражения.

Когда создаются временные объекты?

  1. При возвращении значения из функции без привязки к переменной.
  2. При передаче результата выражения в функцию.
  3. При создании анонимного объекта (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: передаётся временное значение, а ожидается ссылка
}

Итог:

  1. Временный объект живёт до конца выражения, если не привязан к const&.

  2. Использование временных объектов в функциях, ожидающих ссылку &, может привести к недействительным ссылкам и ошибкам.

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; // Мусорное значение
}

Вывод

Пользовательские типы (классы) автоматически вызывают конструктор по умолчанию, если он есть, инициализируя поля. Если конструктора нет, поведение такое же, как у базовых типов.