Билеты 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 при первом обращении в потоке

Порядок инициализации

  1. Глобальные переменные
  2. Статические переменные в функциях
  3. Статические поля классов

Деинициализация

  • Происходит в обратном порядке относительно инициализации.
  • Локальные 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
  1. Указатель на константу
const int* ptr = &x;  // Данные нельзя изменить через ptr
*ptr = 30;            // Ошибка!
  1. Константный указатель
int y = 5;
int* const ptr = &y;  // ptr нельзя перенаправить
ptr = &x;             // Ошибка!
*ptr = 10;            // OK, данные можно менять
  1. Константный указатель на константу
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() не константный
  1. mutable исключение Поле, помеченное mutable, можно изменять даже в константных методах

8. static и его использование

static для локальных переменных в функциях

  1. Переменная сохраняет своё значение между вызовами функции.
  2. Инициализируется только один раз (при первом вызове функции). Пример:
void counter() {
    static int count = 0;  // Инициализируется один раз
    count++;
    std::cout << "Count: " << count << "\n";
}

int main() {
    counter();  // Выведет "Count: 1"
    counter();  // Выведет "Count: 2"
    counter();  // Выведет "Count: 3"
}

static для глобальных переменных и функций

  1. Ограничивает область видимости переменной/функции текущим файлом (единицей трансляции).

  2. Предотвращает конфликты имён между разными .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;
}
  1. Метод makeSound() помечен как virtual.

  2. При вызове через указатель на базовый класс выполняется позднее связывание (динамический полиморфизм).

  3. Вызывается версия метода из реального типа объекта (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 и его использование

Краткий ответ

Нужно, чтобы другой класс либо внешняя для класса функция имела доступ к приватным полям нашего класса когда вам это необходимо.

Данное ключевое слово может использоваться в двух случаях:

  1. 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;
    }
    

    Такие функции могут иметь доступ к внутренним полям класса, хотя на самом деле внешние. Плюс для таких функций запрещено неявное приведение типов, что может быть активно использовано. Важно: функции внешиние - не являются методами класса.

  2. 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, так что мысли о применении данной эвристики лучше оставить компилятору.