DNK_C_C++_Go_Rust
45 subscribers
14 photos
45 links
DNK - дневник кодера С и С++
Download Telegram
#95_PkS_PPPO_TP

Что такое ПАТТЕРНЫ ПРОЕКТИРОВАНИЯ и для чего их используют?

ПАТТЕРНЫ ПРОЕКТИРОВАНИЯ (или шаблоны проектирования) — это проверенные временем решения часто встречающихся задач в разработке программного обеспечения.
Они представляют собой общие подходы к решению проблем, возникающие при проектировании архитектуры программы, структуры классов и взаимодействия объектов.

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

Существует три основных типа паттернов проектирования:

1. ПОРОЖДАЮЩИЕ ПАТТЕРНЫ (Creational Patterns) — эти паттерны связаны с созданием объектов.
Их основная цель — обеспечить гибкость и контроль над процессом создания объектов.

Примеры порождающих паттернов:

Singleton (Одиночка) — гарантирует наличие только одного экземпляра класса и предоставляет глобальную точку доступа к нему;

Factory Method (Фабричный метод)определяет интерфейс для создания объекта, но оставляет подклассам решение о том, какой класс инстанцировать;

Abstract Factory (Абстрактная фабрика) — создает семейства связанных объектов без указания конкретных классов;

Builder (Строитель) — отделяет конструирование сложного объекта от его представления, позволяя пошагово создавать объекты.


2. СТРУКТУРНЫЕ ПАТТЕРНЫ (Structural Patterns) описывают способы организации классов и объектов таким образом, чтобы они легко взаимодействовали друг с другом.

Примеры структурных паттернов:


Adapter (Адаптер) — преобразует интерфейс одного класса в другой интерфейс, ожидаемый клиентом;

Decorator (Декоратор) — динамически добавляет объекту новые обязанности, оборачивая его другим объектом;

Facade (Фасад) — предоставляет упрощенный интерфейс к сложной системе, скрывая её внутреннюю сложность;

Proxy (Прокси) — представляет собой суррогат другого объекта, контролируя доступ к нему.

3. ПОВЕДЕНЧЕСКИЕ ПАТТЕРНЫ (Behavioral Patterns) — определяют взаимодействие между объектами и отвечают за распределение обязанностей между ними.

Примеры поведенческих паттернов:

Observer (Наблюдатель) — определяет зависимость "один ко многим" между объектами так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и автоматически обновляются;

Strategy (Стратегия) — определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми;

Command (Команда) — инкапсулирует запрос как объект, позволяющий параметризовать клиенты с различными запросами, ставить запросы в очередь или логировать их, а также поддерживать отмену операций;

Iterator (Итератор) — обеспечивает последовательный доступ к элементам коллекции, не раскрывая её внутренней реализации.


Преимущества использования паттернов проектирования:

- ПОВЫШЕНИЕ КАЧЕСТВА КОДА — использование проверенных решений помогает избежать типичных ошибок и создать более качественный и надежный код;
- УЛУЧШЕННАЯ ПОДДЕРЖКА и СОПРОВОЖДЕНИЕкод становится более понятным и предсказуемым, что облегчает его поддержку и внесение изменений;
- ГИБКОСТЬ и РАСШИРЯЕМОСТЬ — паттерны делают архитектуру системы более гибкой, позволяя легче адаптироваться под изменения требований;
- СНИЖЕНИЕ ВРЕМЕНИ РАЗРАБОТКИ — разработчики могут быстрее решать стандартные задачи благодаря использованию готовых решений;
- ОБМЕН ОПЫТОМ — общее понимание паттернов позволяет команде разработчиков говорить на одном языке и лучше понимать друг друга.


Когда использовать паттерны?

Не всегда стоит применять паттерн просто потому, что он существует. Важно учитывать контекст задачи и возможные последствия применения того или иного подхода.
Иногда использование паттерна может усложнить код там, где можно было обойтись простым решением. Поэтому важно оценивать необходимость применения каждого конкретного паттерна в зависимости от конкретной ситуации.
#96_Cp_PkS_PPPO_TP

Паттерн SINGLETON ("Одиночка")гарантирует наличие только одного экземпляра класса и предоставляет глобальную точку доступа к этому экземпляру.

Пример:
Представим, что у нас есть класс Logger, который отвечает за логирование сообщений. Мы хотим, чтобы существовал только один экземпляр этого класса для всего приложения.

#include <iostream>

class Logger {
public:
static Logger& getInstance() {
// Гарантируем создание объекта только один раз
static Logger instance;
return instance;
}

void log(const std::string& message) {
std::cout << "LOG: " << message << std::endl;
}

private:
// Конструктор скрыт, чтобы предотвратить создание объектов извне
Logger() {}
};

int main() {
Logger& logger = Logger::getInstance();
logger.log("Hello, world!");
return 0;
}


В этом примере мы гарантируем, что существует только один объект класса Logger благодаря статическому методу getInstance. Этот метод возвращает ссылку на единственный созданный экземпляр класса.
#97_Cpp_PkS_PPPO_TP

Паттерн STRATEGY ("Стратегия") — позволяет изменять поведение алгоритма во время выполнения программы.
Вместо того чтобы жестко закодировать алгоритм, он определяется через интерфейс, а конкретные реализации передаются как объекты.

Пример:

Допустим, у нас есть приложение, которое может обрабатывать текст разными способами: сортировкой по алфавиту, переворотом строки и т.д. Мы можем использовать стратегию для динамического выбора способа обработки текста.

#include <iostream>
#include <vector>
#include <algorithm>

// Интерфейс стратегии
class TextProcessor {
public:
virtual ~TextProcessor() {}
virtual void process(std::vector<std::string>& text) const = 0;
};

// Стратегия для сортировки по алфавиту
class SortByAlphabet : public TextProcessor {
public:
void process(std::vector<std::string>& text) const override {
std::sort(text.begin(), text.end());
}
};

// Стратегия для переворота строк
class ReverseString : public TextProcessor {
public:
void process(std::vector<std::string>& text) const override {
for (auto& line : text) {
std::reverse(line.begin(), line.end());
}
}
};

// Контекст, который использует стратегию
class TextEditor {
public:
explicit TextEditor(TextProcessor* strategy) : m_strategy(strategy) {}

void setStrategy(TextProcessor* strategy) {
// Уничтожаем старую стратегию
delete m_strategy;
m_strategy = strategy;
}

void editText(std::vector<std::string>& text) {
m_strategy->process(text);
}

private:
TextProcessor* m_strategy;
};

int main() {
std::vector<std::string> text {"hello", "world"};

// Начальная стратегия
TextEditor editor(new SortByAlphabet());

// Сортирует текст по алфавиту
editor.editText(text);
for (const auto& word : text) {
std::cout << word << ' ';
}
// Выведет: hello world

// Меняем стратегию
editor.setStrategy(new ReverseString());

// Переворачивает строки
editor.editText(text);
for (const auto& word : text) {
std::cout << word << ' ';
}
// Выведет: olleh dlrow

// Не забываем освободить память
delete editor.getStrategy();
return 0;
}


Здесь у нас есть абстрактный класс TextProcessor, от которого наследуются конкретные стратегии (SortByAlphabet и ReverseString). Класс TextEditor использует эти стратегии для обработки текста. В зависимости от выбранной стратегии текст будет либо отсортирован, либо перевернут.
#98_Cpp_PkS_PPPO_TP

Паттерн TEMPLATE METHOD ("Шаблонный Метод") — определяет скелет алгоритма в базовом классе, позволяя подклассам переопределять некоторые шаги алгоритма без изменения его структуры.

Пример:
Предположим, у нас есть программа, которая обрабатывает файлы разных форматов (например, XML и JSON). Обработка файлов включает несколько шагов, но общий порядок этих шагов одинаков.

#include <iostream>

class FileProcessor {
public:
void processFile() {
openFile();
readData();
parseData();
closeFile();
}

protected:
virtual void openFile() { std::cout << "Открытие файла..." << std::endl; }
virtual void readData() { std::cout << "Чтение данных..." << std::endl; }
virtual void parseData() { std::cout << "Парсинг данных..." << std::endl; }
virtual void closeFile() { std::cout << "Закрытие файла..." << std::endl; }
};

class XmlFileProcessor : public FileProcessor {
protected:
void parseData() override { std::cout << "Парсинг XML-данных..." << std::endl; }
};

class JsonFileProcessor : public FileProcessor {
protected:
void parseData() override { std::cout << "Парсинг JSON-данных..." << std::endl; }
};

int main() {
FileProcessor* processor = new XmlFileProcessor();
processor->processFile();

delete processor;

processor = new JsonFileProcessor();
processor->processFile();

delete processor;
return 0;
}


Класс FileProcessor определяет общую последовательность действий при обработке файла: открытие, чтение, парсинг и закрытие. Подклассы XmlFileProcessor и JsonFileProcessor могут переопределить конкретный шаг (в данном случае парсинг), не изменяя остальной процесс.
#99_Cpp_PkS_PPPO_TP

Паттерн DECORATOR ("Декоратор") — добавляет новые функции объектам динамически, оборачивая их другими объектами.
Это альтернатива созданию подклассов для расширения функциональности.

Пример:
Рассмотрим программу, которая выводит сообщения на экран. Мы можем добавить дополнительные возможности, такие как добавление времени или подчеркивание текста, используя декораторы.

#include <iostream>
#include <ctime>

class Message {
public:
virtual void display() = 0;
};

class SimpleMessage : public Message {
public:
void display() override {
std::cout << "Простое сообщение." << std::endl;
}
};

class Decorator : public Message {
protected:
Message* m_message;

public:
explicit Decorator(Message* message) : m_message(message) {}

void display() override {
m_message->display();
}
};

class TimestampDecorator : public Decorator {
public:
explicit TimestampDecorator(Message* message) : Decorator(message) {}

void display() override {
time_t now = time(0);
char buffer[80];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localtime(&now));
std::cout << "Время: " << buffer << std::endl;
Decorator::display();
}
};

class UnderlineDecorator : public Decorator {
public:
explicit UnderlineDecorator(Message* message) : Decorator(message) {}

void display() override {
Decorator::display();
std::cout << "______________________" << std::endl;
}
};

int main() {
Message* simpleMessage = new SimpleMessage();
// Простое сообщение.
simpleMessage->display();

Message* timestampedMessage = new TimestampDecorator(simpleMessage);
timestampedMessage->display();

// Время: 2023-10-01 12:34:56
// Простое сообщение.

Message* underlinedMessage = new UnderlineDecorator(timestampedMessage);
underlinedMessage->display();

// Время: 2023-10-01 12:34:56
// Простое сообщение.
// ______________________

delete simpleMessage;
delete timestampedMessage;
delete underlinedMessage;
return 0;
}


Класс SimpleMessage является простым сообщением. Декораторы TimestampDecorator и UnderlineDecorator добавляют соответственно вывод времени и подчеркивание к исходному сообщению. Таким образом, можно комбинировать различные декораторы для создания сложных поведений.
#100_PkS_TP

Для чего нужны МОДУЛЬНЫЕ ТЕСТЫ?

Модульные тесты необходимы для проверки корректной работы отдельных компонентов программного обеспечения.
Они помогают убедиться, что каждый отдельный модуль (функция, класс, метод) работает так, как ожидается, до интеграции с остальными частями системы.

Ключевые причины использования модульных тестов:

обеспечение качества кода - модульные тесты позволяют разработчикам выявлять ошибки на ранних этапах разработки.
Когда код проходит через серию автоматических проверок, вероятность появления ошибок значительно снижается. Это помогает поддерживать высокое качество продукта;

документирование поведения кода - хорошо написанные модульные тесты служат своего рода документацией для других разработчиков.
Они показывают, каким образом должен работать тот или иной компонент, какие входные данные ожидаются и какой результат должен быть получен;

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

ускорение процесса разработки - автоматизация тестирования ускоряет цикл разработки.
Разработчики могут быстро проверять работу новых функций и исправлений, не тратя много времени на ручное тестирование;

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

изоляция дефектов - модульные тесты работают с отдельными модулями, изолируя их от остальных частей системы.
Это упрощает поиск и устранение ошибок, поскольку они локализованы в конкретном модуле;

поддержание тестируемости кода - написание модульных тестов требует, чтобы код был хорошо структурированным и легко тестируемым.
Это способствует улучшению архитектуры проекта и снижению сложности кода.

Примеры применения модульных тестов:
— проверка корректности вычислений в математической библиотеке;
— тестирование методов классов на соответствие ожидаемому поведению
;
— проверка обработки исключительных ситуаций;
— тестирование API-интерфейсов.

Таким образом, модульные тесты являются важным инструментом в арсенале любого разработчика, стремящегося создать надежное и качественное программное обеспечение.
#101_PkS_TP

TDD (Test-Driven Development) – это методология разработки программного обеспечения, которая предполагает написание тестов до написания самого кода.
Этот подход позволяет разработчикам сосредоточиться на создании минимально необходимого функционала и гарантирует, что каждый фрагмент кода покрывается тестами.

Основные этапы TDD:
— НАПИСАНИЕ ТЕСТА
— сначала пишется тест для новой функциональности, который пока не будет проходить, так как код еще не реализован;
— РЕАЛИЗАЦИЯ МИНИМАЛЬНОЙ ВЕРСИИ ФУНКЦИОНАЛА — создается минимальный объем кода, чтобы тест прошел;
— РЕФАКТОРИНГ — после того как тесты проходят успешно, можно улучшить структуру кода без изменения его поведения, сохраняя при этом все тесты "зелеными";
— ПОВТОРЕНИЕ ЦИКЛА — процесс повторяется для каждой новой функции или улучшения существующего функционала.

Преимущества использования TDD:
— ПОВЫШЕНИЕ КАЧЕСТВА КОДА за счет раннего выявления ошибок;
— УМЕНЬШЕНИЕ КОЛИЧЕСТВА БАГОВ благодаря покрытию кода тестами;
— УЛУЧШЕННАЯ АРХИТЕКТУРА СИСТЕМЫ, поскольку разработчики вынуждены думать о том, как сделать код легко тестируемым;
— БОЛЕЕ ЧЕТКОЕ ПОНИМАНИЕ ТРЕБОВАНИЙ К СИСТЕМЕ через создание тестов.


Этот подход особенно популярен среди разработчиков, практикующих Agile-методологии и экстремальное программирование (XP).
#102_PkS_TP

AGILE-методология — это гибкий подход к управлению проектами, основанный на принципах быстрой адаптации к изменениям и тесного взаимодействия между всеми участниками проекта.
В отличие от традиционных методов управления проектами, таких как водопадная модель, где весь процесс разбит на последовательные стадии, Agile предлагает более итеративный и инкрементальный подход.

Основные принципы Agile:
гибкость и адаптивность проект развивается через короткие циклы (итерации), которые позволяют быстро реагировать на изменения требований или условий;
командное взаимодействие — важную роль играют регулярные встречи команды, обсуждения и совместная работа над задачами;
постоянное улучшение продуктапродукт разрабатывается постепенно, с возможностью вносить изменения по мере получения обратной связи от пользователей или заказчиков;
клиентоориентированность — основной фокус делается на удовлетворение потребностей клиента, а не на строгое следование заранее утвержденному плану;
минимизация бюрократии — процессы упрощены, чтобы минимизировать время на документирование и максимизировать эффективность работы команды;
тесное сотрудничество — постоянное общение внутри команды и с заказчиком помогает быстрее выявлять проблемы и находить решения.

Популярные фреймворки Agile:

SCRUM — один из самых популярных подходов, включающий роли Scrum-мастера, владельца продукта и команды разработки.
Работа разбивается на спринты (обычно длительностью 2–4 недели).

KANBAN — фокусируется на визуализации задач и управлении потоком работ.
Используется доска Kanban, где задачи перемещаются по этапам выполнения.

EXTREME PROGRAMMING (XP) — подход, акцентирующий внимание на разработке через тестирование (TDD), парном программировании и частых релизах.


Преимущества Agile:
- быстрая реакция на изменения;
- высокое качество продукта благодаря постоянному улучшению;
- открытое и прозрачное взаимодействие внутри команды;
- высокая мотивация участников проекта.

Недостатки Agile:
- требует высокой дисциплины и вовлеченности всех членов команды;
- может быть сложно масштабировать на большие проекты;
- необходимость постоянного участия заказчика может создавать дополнительную нагрузку.

Agile-методология стала популярной в IT-индустрии, но также находит применение в других сферах, где требуется гибкость и быстрая адаптация к изменяющимся условиям.
#103_Cpp_PkS_PPPO_TP

Что такое ШАБЛОННЫЙ КЛАСС и ШАБЛОННАЯ ФУНКЦИЯ?

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

ШАБЛОННЫй КЛАСС — это класс, параметры которого задаются во время компиляции.
Это позволяет создать один общий класс, который может работать с разными типами данных.

Пример простого шаблонного класса:

template <typename T>
class MyArray {
private:
T* arr;
int size;
public:
MyArray(int s) : size(s) {
arr = new T[size];
}

~MyArray() {
delete[] arr;
}

void setValue(int index, const T& value) {
if (index >= 0 && index < size)
arr[index] = value;
}

T getValue(int index) const {
return arr[index];
}
};

Здесь MyArray — это шаблонный класс, который принимает параметр T, представляющий собой любой тип данных. Мы можем использовать этот класс для создания массивов различных типов:

int main() {
// Создаем массив целых чисел
MyArray<int> intArray(10);
intArray.setValue(0, 42);
std::cout << intArray.getValue(0) << std::endl;
// Выведет 42

// Создаем массив строк
MyArray<std::string> stringArray(5);
stringArray.setValue(0, "Hello");
std::cout << stringArray.getValue(0) << std::endl;
// Выведет Hello

return 0;
}



ШАБЛОННАЯ ФУНКЦИЯ — это функция, параметры которой задаются во время компиляции. Она позволяет написать одну функцию, которая может работать с аргументами разных типов.

Пример простой шаблонной функции:

template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}

Эта шаблонная функция возвращает максимальное значение из двух переданных аргументов. Тип данных определяется параметром шаблона T.

Мы можем вызывать эту функцию с разными типами данных:

int main() {
int x = 5, y = 7;
double m = 3.14, n = 9.99;

std::cout << max(x, y) << std::endl;
// Выведет 7

std::cout << max(m, n) << std::endl;
// Выведет 9.99

return 0;
}

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


Шаблонные классы и функции — мощные инструменты языка C++, позволяющие создавать универсальные и эффективные программы. Они широко используются в стандартной библиотеке C++ (например, контейнеры STL) и в сторонних библиотеках.
#104_Cpp_PkS_TP

Что такое КОНСТРУКТОРЫ?
Какие типы конструкторов существуют в С++?


КОНСТРУКТОР в C++ — это специальный метод класса, который имеет такое же имя как имя класса и у которого не указывается тип возвращаемого значения.
Конструктор автоматически вызывается при создании объекта этого класса.
Основная функция конструктора
—инициализация членов данных и приведение объекта к состоянию, пригодному для использования.

Основные типы конструкторов:

КОНСТРУКТОР ПО УМОЛЧАНИЮ (Default Constructor) - конструктор без параметров или с параметрами, у которых есть значения по умолчанию.
Используется, когда необходимо создать объект без явной передачи значений членам данных. Если не определить конструктор явно, компилятор создаст его автоматически.

class MyClass {
public:
// Конструктор по умолчанию
MyClass() { /* Инициализация */ }
};


ПАРАМЕТРИЧЕСКИЙ КОНСТРУКТОР (Parameterized Constructor) - тип конструктора принимает параметры, которые используются для инициализации членов данных объекта.

class MyClass {
private:
int value;
public:
// Параметрический конструктор
MyClass(int v) : value(v) {}
};



КОНСТРУКТОР КОПИРОВАНИЯ (Copy Constructor) - создает новый объект как копию уже существующего объекта того же типа.

class MyClass {
private:
int value;
public:
// Конструктор копирования
MyClass(const MyClass& other) : value(other.value) {}
};


ПЕРЕГРУЖЕННЫЦ КОНСТРУКТОР - класс может иметь несколько конструкторов с разными списками параметров.
Это позволяет создавать объекты различными способами.

class MyClass {
private:
int a, b;
public:
// Первый перегруженный конструктор
MyClass(int x) : a(x), b(0) {}

// Второй перегруженный конструктор
MyClass(int x, int y) : a(x), b(y) {}
};


КОНСТРУКТОР ПЕРЕМЕЩЕНИЯ (Move Constructor) - введен в стандарт C++11. Позволяет эффективно перемещать ресурсы между объектами, избегая глубокого копирования.

class MyClass {
private:
std::unique_ptr<int> ptr;
public:
// Конструктор перемещения
MyClass(MyClass&& other) noexcept : ptr(std::move(other.ptr)) {}
};


КОНКСТРУКТОР ПРЕОБРАЗОВАНИЯ (Conversion Constructor) - используется для автоматического преобразования одного типа в другой.
Например, если класс имеет конструктор, принимающий один параметр другого типа, этот конструктор будет считаться конструктором преобразования.

class MyClass {
private:
int value;
public:
// Конструктор преобразования
MyClass(int v) : value(v) {}
};


ПУСТОЙ КОНСТРУКТОР (Empty Constructor) - конструктор, который ничего не делает.
Иногда полезен для классов, где требуется только определение конструктора по умолчанию, но нет необходимости выполнять какие-либо действия при инициализации.

class MyClass {
public:
// Пустой конструктор
MyClass() {}
};


ДЕСТРУКТОР - хотя деструктор не является конструктором, он также играет важную роль в управлении жизненным циклом объектов.
Деструктор - метод класса так же как и конструктор имеет имя класса перед которым вводится символ '~' - тильда и у которого так же не указывается тип возвращемого значения.
Вызывается автоматически при уничтожении объекта и обычно используется для освобождения ресурсов, выделенных объектом.

class MyClass {
public:
~MyClass() {
// Освобождение ресурсов

}
};

Каждый из этих типов конструкторов служит своей цели и помогает разработчику управлять созданием и инициализацией объектов класса.
#105_Cpp_PkS_TP

Может ли конструктор быть шаблонной функцией?

Да, конструктор может быть шаблонным.

Шаблонный конструктор
позволяет создавать объекты различных типов на основе передаваемых аргументов. Это особенно полезно, когда нужно работать с обобщенными типами данных или контейнерными структурами.

Пример использования шаблонного конструктора:

#include <iostream>

template<typename T>
class MyTemplateClass {
private:
T data;
public:
// Шаблонный конструктор
template<typename U>
MyTemplateClass(U &&value) : data(std::forward<U>(value)) {
std::cout << "Шаблонный конструктор вызван" << std::endl;
}
};

int main() {
// Тип T = int, аргумент int
MyTemplateClass<int> obj1(42);

// Тип T = double, аргумент double
MyTemplateClass<double> obj2(3.14);

return 0;
}

В этом примере конструктор принимает любой тип данных через универсальный шаблонный параметр U, а затем использует std::forward для правильного продвижения аргумента в член данных data.
Такой подход обеспечивает гибкость и возможность работы с различными типами данных без необходимости вручную писать отдельные конструкторы для каждого конкретного случая.
Стоит отметить, что использование шаблонных конструкторов требует осторожности, так как они могут привести к неожиданным вызовам при передаче неправильных типов данных.
#106_Cpp_PkS_TP

Может ли виртуальная функция быть шаблонной?

НЕТ, виртуальные функции НЕ МОГУТ быть шаблонными.

Причина кроется в природе обоих механизмов:

Механизм вызова виртуальных функций - ВИРТУАЛЬНЫЕ ФУНКЦИИ предназначены для полиморфизма во время выполнения программы.
Когда вы вызываете виртуальную функцию через указатель или ссылку базового класса, выбор конкретной реализации происходит динамически, основываясь на реальном типе объекта. Для этого компилятор создает таблицу виртуальных методов (vtable), которая содержит адреса всех виртуальных функций данного класса.

Механизм шаблонов - ШАБЛОНЫ, напротив, являются механизмом генерации кода на этапе компиляции.
Они позволяют создавать различные версии одной и той же функции или класса для разных типов данных. Однако для каждой комбинации типов данных, используемых в шаблоне, компилятор генерирует отдельный экземпляр функции или класса.

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


Если нужна функциональность, похожая на комбинацию виртуальности и шаблона, можно рассмотреть следующие подходы:

использование паттерна "Стратегия", который позволяет изменять поведение объекта во время выполнения путем замены алгоритмов, реализованных в виде отдельных классов;

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

Пример реализации стратегии:

#include <iostream>

// Базовая стратегия
struct BaseStrategy {
virtual void execute() const = 0;
};

// Реализация стратегии для int
template<typename T>
struct IntStrategy : public BaseStrategy {
void execute() const override {
std::cout << "Int strategy executed!" << std::endl;
}
};

// Реализация стратегии для double
template<typename T>
struct DoubleStrategy : public BaseStrategy {
void execute() const override {
std::cout << "Double strategy executed!" << std::endl;
}
};

// Контекст, использующий стратегию
class Context {
private:
BaseStrategy* strategy_;
public:
explicit Context(BaseStrategy* strategy) : strategy_(strategy) {}

// Убедитесь, что управление памятью правильно организовано!
void set_strategy(BaseStrategy* strategy) {
delete strategy_;
strategy_ = strategy;
}

void execute_strategy() const {
strategy_->execute();
}
};

int main() {
Context ctx(new IntStrategy<int>());
ctx.execute_strategy();
// Выведет: "Int strategy executed!"

ctx.set_strategy(new DoubleStrategy<double>());
ctx.execute_strategy();
// Выведет: "Double strategy executed!"

return 0;
}


Пример демонстрирует, как можно комбинировать полиморфизм и шаблоны, используя паттерн "Стратегия".
#107_Cpp_PkS_TP

Что такое ИНСТАНЦИАЦИЯ ШАБЛОНА?

Инстанцирование шаблона — это процесс создания конкретной реализации шаблона для заданного набора типов или значений.
Когда компилятор встречает использование шаблона с конкретными аргументами, он создает новую версию этого шаблона, заменяя формальные параметры шаблона на переданные аргументы.

Рассмотрим простой шаблон функции:

template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}


Когда мы используем эту функцию с конкретными типами данных, например:

int x = 10, y = 20;
auto result = max(x, y);

Компилятор создает конкретную реализацию функции max для типа int:

int max(int a, int b) {
return (a > b) ? a : b;
}

Теперь рассмотрим еще один случай:

double p = 3.14, q = 2.71;
auto result_double = max(p, q);

Для этого вызова компилятор создаст другую реализацию функции max, теперь уже для типа double:

double max(double a, double b) {
return (a > b) ? a : b;
}

Особенности инстанцирования шаблона:

автоматическое инстанцирование - компилятор автоматически инстанцирует шаблон каждый раз, когда встречается его использование с новыми типами или значениями.

явная специализация - разработчик может предоставить специализированные версии шаблона для конкретных типов данных.

template<>
char max(char a, char b) {
return (a > b) ? a : b;
}


явное инстанцирование - разработчик может явно указать компилятору, что нужно инстанцировать шаблон для определенного набора типов или значений:

template int max<int>(int, int);

компонентная инстанцирование - при использовании шаблонов классов инстанцирование может происходить поэтапно. Сначала инстанцируется объявление класса, затем — определения его методов.


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

Инстанцирование шаблонов
— ключевой механизм, обеспечивающий гибкость и эффективность программ на языке C++, позволяя разработчикам писать обобщенные алгоритмы и структуры данных, которые могут использоваться с любыми совместимыми типами данных.
#108_Cpp_PkS_TP

Что такое специализация шаблона?
Частичная специализация шаблона?


СПЕЦИАЛИЗАЦИЯ ШАБЛОНА — это способ предоставления специализированной реализации шаблона для определенных типов данных или наборов параметров.
Она позволяет разработчику адаптировать общий шаблон под конкретные случаи, обеспечивая более точное и эффективное поведение.

Специализация шаблона позволяет задать особую реализацию для определённого набора типов или значений, отличающуюся от общей реализации.

Существует две основные формы специализации:

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

Пример полной специализации функции-шаблона:

// Обобщенная версия функции max
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}

// Полная специализация для типа char*
template<>
const char* max<const char*>(const char* a, const char* b) {
return strcmp(a, b) > 0 ? a : b;
}

Здесь первая функция max — это общая реализация для любых типов данных. Вторая функция — это полная специализация для типа const char*, которая сравнивает строки с помощью strcmp.

частичная специализация - применяется, когда нужно специализироваться не для всех параметров шаблона, а лишь для некоторых из них.
Эта форма специализации доступна только для шаблонов классов.

Пример частичной специализации шаблона класса:

// Обобщённый шаблон класса Pair
template<typename T1, typename T2>
class Pair {
public:
T1 first;
T2 second;

Pair(const T1& f, const T2& s) : first(f), second(s) {}
};

// Частичная специализация для случаев, // когда оба типа одинаковы
template<typename T>
class Pair<T, T> {
public:
T first;
T second;

Pair(const T& f, const T& s) : first(f), second(s) {}
};

В данном случае основная версия шаблона Pair предназначена для хранения двух значений произвольных типов. Частично специализированная версия применяется, когда оба типа совпадают (T1 == T2), что может быть полезным для оптимизации или изменения поведения.

Различие между полной и частичной специализацией:
полная специализация - применяется ко всем параметрам шаблона. Подходит как для функций, так и для классов;
частичная специализация - применяется только к части параметров шаблона. Доступна только для шаблонов классов.

Специализация шаблонов — мощный инструмент языка C++, позволяющий разработчикам адаптировать общие решения под специфические ситуации.
Это улучшает читаемость кода, упрощает поддержку и способствует повышению производительности приложений.
#109_Cpp_PkS_TP

Чем отличается специализация шаблонов от инстанциации?

Специализация шаблонов и инстанцирование — это разные процессы, связанные с использованием шаблонов в C++.

Разберем их различия подробнее:

Инстанцирование шаблона
— это процесс создания конкретной реализации шаблона для заданного набора типов или значений. Оно происходит автоматически, когда компилятор сталкивается с использованием шаблона с конкретными аргументами.

Специализация шаблона — это предоставление особой реализации шаблона для определенных типов данных или наборов параметров. Она позволяет разработчику адаптировать общий шаблон под конкретные случаи, обеспечивая более точное и эффективное поведение.

Ключевые отличия.

Цель процесса:
Инстанцирование — автоматически создает конкретную реализацию шаблона для заданного набора типов или значений;
Специализация — предоставляет особую реализацию шаблона для определенных типов данных или наборов параметров.

Время выполнения:
Инстанцирование — происходит во время компиляции, когда компилятор встречает использование шаблона с конкретными аргументами;
Специализация — определяется программистом до этапа компиляции, предоставляя альтернативные реализации для конкретных случаев.

Область применения:
Инстанцирование — может применяться как к функциям, так и к классам;
Специализация — может быть полной (для функций и классов) и частичной (только для классов).

Инстанцирование и специализация шаблонов играют важные роли в разработке на C++.
Инстанцирование автоматизирует создание конкретных версий шаблонов, тогда как специализация позволяет программисту настраивать поведение шаблонов для особых случаев.
Оба подхода помогают улучшить производительность и читаемость кода, делая его более универсальным и гибким.
#110_Cpp_PkS_TP

Расскажите об имплементации шаблонных классов в срр-файле?


ИМПЛЕМЕНТАЦИЯ (реализация) ШАБЛОННЫХ КЛАССОВ в C++ традиционно размещается в заголовочных файлах (*.h или *.hpp).
Это связано с тем, что компилятор должен видеть полную реализацию шаблона, чтобы корректно произвести инстанцирование для каждого набора типов, с которыми используется данный шаблон. Однако иногда возникает необходимость отделить интерфейс и реализацию, разместив их в разных файлах, подобно тому, как это делается для обычных классов.
В таких случаях можно воспользоваться несколькими методами:

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

my_template_class.h
#ifndef MY_TEMPLATE_CLASS_H
#define MY_TEMPLATE_CLASS_H

template<typename T>
class MyTemplateClass {
public:
MyTemplateClass(T value);
T getValue() const;
private:
T m_value;
};

// Реализация методов класса
template<typename T>
MyTemplateClass<T>::MyTemplateClass(T value)
: m_value(value) {}

template<typename T>
T MyTemplateClass<T>::getValue() const {
return m_value;
}

#endif // MY_TEMPLATE_CLASS_H


main.cpp
#include "my_template_class.h"

int main() {
MyTemplateClass<int> obj(42);
std::cout << obj.getValue() << std::endl;
return 0;
}

Такой подход удобен и широко распространен, однако он увеличивает объем заголовочного файла и усложняет его чтение.


Использование отдельного файла реализации — чтобы разделить интерфейс и реализацию, можно использовать дополнительный файл с расширением *tpp (или иной аналог), содержащий реализацию шаблона. Затем этот файл включается в конце заголовочного файла.

my_template_class.h
#ifndef MY_TEMPLATE_CLASS_H
#define MY_TEMPLATE_CLASS_H

template<typename T>
class MyTemplateClass {
public:
MyTemplateClass(T value);
T getValue() const;
private:
T m_value;
};

#include "my_template_class.tpp"

#endif // MY_TEMPLATE_CLASS_H

my_template_class.tpp
template<typename T>
MyTemplateClass<T>::MyTemplateClass(T value)
: m_value(value) {}

template<typename T>
T MyTemplateClass<T>::getValue() const {
return m_value;
}

main.cpp
#include "my_template_class.h"

int main() {
MyTemplateClass<int> obj(42);
std::cout << obj.getValue() << std::endl;
return 0;
}

Этот подход сохраняет разделение интерфейса и реализации, но всё равно требует включения реализации в заголовочный файл.


Явное инстанцирование — еще одним способом разделения интерфейса и реализации является явное указание компилятору, для каких типов следует инстанцировать шаблон.
Это делается с помощью ключевого слова extern template.

my_template_class.h
#ifndef MY_TEMPLATE_CLASS_H
#define MY_TEMPLATE_CLASS_H

template<typename T>
class MyTemplateClass {
public:
MyTemplateClass(T value);
T getValue() const;
private:
T m_value;
};

#endif // MY_TEMPLATE_CLASS_H

my_template_class.cpp
#include "my_template_class.h"

template<typename T>
MyTemplateClass<T>::MyTemplateClass(T value)
: m_value(value) {}

template<typename T>
T MyTemplateClass<T>::getValue() const {
return m_value;
}

// Явное инстанцирование для типа int
template class MyTemplateClass<int>;

main.cpp
#include "my_template_class.h"

int main() {
MyTemplateClass<int> obj(42);
std::cout << obj.getValue() << std::endl;
return 0;
}

Этот метод работает, если известно, для каких типов будет использоваться шаблон. Однако он ограничивает возможности использования других типов, кроме тех, для которых было произведено явное инстанцирование.

Размещение реализации шаблонных классов в заголовочных файлах — наиболее распространенный и удобный подход. Однако в зависимости от требований проекта можно выбрать другие варианты, такие как использование файлов с расширением *tpp или явное инстанцирование.
Каждый из этих подходов имеет свои преимущества и недостатки, поэтому выбор зависит от конкретных задач и предпочтений разработчика.
#111_CMPL_Cpp_GCC_PkS

Как проходит процесс компиляции срр-файлов в бинарный файл?

Процесс компиляции cpp-файлов в бинарный файл включает несколько этапов, каждый из которых выполняет свою задачу.
Эти этапы включают препроцессинг, компиляцию, ассемблирование и компоновку (линковку).

Рассмотрим каждый этап подробно.

Препроцессингна этом этапе обрабатываются директивы препроцессора, такие как #include, #define, #ifdef и т.д.
Препроцессор вставляет содержимое включённых заголовочных файлов, раскрывает макросы и удаляет комментарии. В результате получается промежуточный файл, который отправляется на следующий этап.

example.cpp
#include <iostream>
using namespace std;

int main() {
cout << "Hello, World!" << endl;
return 0;
}


После препроцессинга получится примерно такой текст:

...
// Содержимое iostream
namespace std {
...
}
using namespace std;

int main() {
cout << "Hello, World!" << endl;
return 0;
}


Компиляцияна этом этапе исходный код преобразуется в ассемблерный язык. Компилятор проверяет синтаксис и семантику программы, переводит её в низкоуровневый машинный код, представленный в виде инструкций ассемблера.

Пример результата компиляции (упрощён):

main:
push ebp
mov ebp, esp
sub esp, 8
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
xor eax, eax
leave
ret



Ассемблирование ассемблер переводит ассемблерный код в объектный файл, состоящий из машинных инструкций. На этом этапе создаются файлы с расширением .o (в Unix-подобных системах) или .obj (в Windows).

Пример команды ассемблирования:
as example.s -o example.o


Компоновка (Линковка) — на этом этапе линкер объединяет объектные файлы и библиотеки в единый исполняемый файл. Линкер разрешает ссылки на внешние символы (например, функции из стандартных библиотек) и собирает итоговую программу.

Пример команды линковки:

ld example.o -lc -o example

Чаще всего, используется компилятор, который сам запускает линковщик:

g++ example.cpp -o example


Итоговый результат — в итоге получается исполняемый файл, который можно запустить на целевой платформе. В случае Unix-подобных систем это будет файл без расширения, а в Windows — файл с расширением .exe.

Таким образом, весь процесс компиляции C++-файлов можно представить следующим образом:

Исходный код → Препроцессор → Промежуточный файл
Промежуточный файл → Компилятор → Ассемблерный код
Ассемблерный код → Ассемблер → Объектный файл
Объектные файлы + Библиотеки → Линкер → Исполняемый файл


Этот процесс может варьироваться в зависимости от среды разработки, компиляторов и инструментов, но основные шаги остаются неизменными.
#112_CMPL_C_Cpp_GCC_PkS

Что такое ПРЕПРОЦЕССОР?

Препроцессор — это программа, которая обрабатывает исходный код перед передачей его компилятору. Её основная задача — выполнение директив препроцессора, таких как #include, #define, #if, #pragma и другие.
Препроцессор изменяет исходный код в соответствии с этими директивами, после чего полученный текст передается на дальнейшую обработку компилятором.

Основные задачи препроцессора:

Подключение заголовочных файлов — директива #include указывает препроцессору включить содержимое указанного файла в текущий исходный файл.
Это позволяет использовать стандартные библиотеки и повторно используемые модули кода.

#include <iostream>


Определение макросов — директива #define позволяет определять макросы, которые могут быть простыми константами или сложными выражениями.
Макросы заменяются своими значениями в процессе обработки препроцессором.

#define PI 3.14159


Условная компиляция — директивы #if, #ifdef, #ifndef и другие позволяют включать или исключать фрагменты кода в зависимости от условий.
Это удобно для управления различными конфигурациями программы.

#ifdef DEBUG
std::cerr << "Debug message" << std::endl;
#endif


Управление строковыми литералами — препроцессор объединяет соседние строковые литералы в одну строку.

std::string str = "This is a "
"single string";


Предварительная обработка ошибок и предупреждений — директивы #error и #warning позволяют сообщать о проблемах на стадии препроцессинга.

#if !defined(__cplusplus)
#error This code requires C++
#endif

Рассмотрим небольшой пример:

example.cpp
#include <iostream>

#define MAX_SIZE 100

int main() {
#ifdef DEBUG
std::cerr << "Debug mode enabled" << std::endl;
#endif

int array[MAX_SIZE];
for (int i = 0; i < MAX_SIZE; ++i) {
array[i] = i;
}

return 0;
}


После прохождения препроцессора код может выглядеть так:

// Содержимое iostream
namespace std {
...
}

int main() {
int array[100];
for (int i = 0; i < 100; ++i) {
array[i] = i;
}

return 0;
}

Обратите внимание, что макрос MAX_SIZE был заменен своим значением, а условная секция DEBUG была убрана, так как она не была определена.

Преимущества:
— упрощение повторного использования кода;
— возможность условной компиляции для поддержки различных платформ и конфигураций;
— удобство определения глобальных констант и макросов.


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


Препроцессор играет важную роль в процессе компиляции C/C++ программ, позволяя управлять структурой и содержимым исходного кода перед его дальнейшей обработкой компилятором. Правильное использование возможностей препроцессора может значительно упростить разработку и сопровождение программного обеспечения.
#113_CMPL_C_Cpp_GCC_PkS

Что такое ПРЕПРОЦЕССОР?

Препроцессор в C и C++ выполняет предварительную обработку исходного кода перед его компиляцией.
Его работа состоит из нескольких шагов, направленных на модификацию исходного текста программы согласно директивам препроцессора.

Этапы работы препроцессора:

чтение исходного файла — препроцессор начинает работу с чтения исходного файла (.cpp или .c) и обработки его построчно;

удаление комментариев — все комментарии, обозначаемые символами /* ... */ или //, удаляются из исходного кода;

замена макросов — препроцессор ищет и заменяет все макросы, определенные с помощью директивы #define.
Макросы могут быть простыми константами или сложными выражениями.

#define PI 3.14159

Во всех местах, где встречается PI, препроцессор подставит значение 3.14159;

подключение заголовочных файлов — директива #include указывает препроцессору включить содержимое указанного файла в текущий исходный файл.
Это позволяет использовать стандартные библиотеки и повторно используемые модули кода.

#include <iostream>

Препроцессор вставит содержимое файла iostream в место, где находится данная директива;

условная компиляция
— директивы #if, #ifdef, #ifndef и другие позволяют включать или исключать фрагменты кода в зависимости от условий.
Это удобно для управления различными конфигурациями программы.

#ifdef DEBUG
std::cerr << "Debug message" << std::endl;
#endif

Если макрос DEBUG определен, то соответствующий фрагмент кода будет включен в результирующий текст;

объединение строковых литералов — препроцессор объединяет соседние строковые литералы в одну строку.

std::string str = "This is a "
"single string";

Препроцессор объединит две строки в одну: "This is a single string";

предварительные ошибки и предупреждениядирективы #error и #warning позволяют сообщить о проблемах на стадии препроцессинга.

#if !defined(__cplusplus)
#error This code requires C++
#endif

Если макрос __cplusplus не определен, препроцессор выдаст ошибку;

генерация выходного файла — после завершения всех вышеуказанных операций препроцессор формирует выходной файл, который передается компилятору для дальнейшего анализа и трансляции в объектный код.

Рассмотрим небольшой пример:

example.cpp
#include <iostream>

#define MAX_SIZE 100

int main() {
#ifdef DEBUG
std::cerr << "Debug mode enabled" << std::endl;
#endif

int array[MAX_SIZE];
for (int i = 0; i < MAX_SIZE; ++i) {
array[i] = i;
}

return 0;
}

После прохождения препроцессора код может выглядеть так:

// Содержимое iostream
namespace std {
...
}

int main() {
int array[100];
for (int i = 0; i < 100; ++i) {
array[i] = i;
}

return 0;
}

Обратите внимание, что макрос MAX_SIZE был заменен своим значением, а условная секция DEBUG была убрана, так как она не была определена.

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

#include <iostream>

Препроцессор вставит содержимое файла iostream в место, где находится данная директива.
Условная компиляция: Директивы #if, #ifdef, #ifndef и другие позволяют включать или исключать фрагменты кода в зависимости от условий. Это удобно для управления различными конфигурациями программы.Пример:

#ifdef DEBUG
std::cerr << "Debug message" << std::endl;
#endif

Если макрос DEBUG определен, то соответствующий фрагмент кода будет включен в результирующий текст.
Объединение строковых литералов: Препроцессор объединяет соседние строковые литералы в одну строку.Пример:

std::string str = "This is a "
"single string";

Препроцессор объединит две строки в одну: "This is a single string".
Предварительные ошибки и предупреждения: Директивы #error и #warning позволяют сообщить о проблемах на стадии препроцессинга.Пример:

#if !defined(__cplusplus)
#error This code requires C++
#endif

Если макрос __cplusplus не определен, препроцессор выдаст ошибку.
Генерация выходного файла: После завершения всех вышеуказанных операций препроцессор формирует выходной файл, который передается компилятору для дальнейшего анализа и трансляции в объектный код.

Пример работы препроцессора

Рассмотрим небольшой пример:

example.cpp

#include <iostream>

#define MAX_SIZE 100

int main() {
#ifdef DEBUG
std::cerr << "Debug mode enabled" << std::endl;
#endif

int array[MAX_SIZE];
for (int i = 0; i < MAX_SIZE; ++i) {
array[i] = i;
}

return 0;
}

После прохождения препроцессора код может выглядеть так:

// Содержимое iostream
namespace std {
...
}

int main() {
int array[100];
for (int i = 0; i < 100; ++i) {
array[i] = i;
}

return 0;
}

Обратите внимание, что макрос MAX_SIZE был заменен своим значением, а условная секция DEBUG была убрана, так как она не была определена.
Заключение

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

Какие знаете команды препроцессора?

Препроцессор в языках программирования C и C++ предоставляет множество команд, которые управляют предварительной обработкой исходного кода перед его компиляцией.

Основные команды препроцессора и их назначение:

#include — позволяет подключить содержимое внешнего файла в текущую точку исходного кода.
Чаще всего используется для подключения заголовочных файлов.

// Подключаем стандартный заголовок
#include <iostream>

// Подключаем пользовательский заголовок
#include "myheader.h"


#define — определяет макрос, который может быть простым именем-константой или сложным выражением.
Макросы заменяются соответствующими значениями в процессе препроцессинга.

// Простое имя-константа
#define PI 3.14159

// Сложное выражение
#define MAX(a, b) ((a) > (b) ? (a) : (b))


#undef — отменяет ранее определенное имя макроса.

// Отмена определения PI
#undef PI


#if, #elif, #else, #endif — эти команды обеспечивают условную компиляцию.
Код внутри блока будет включен в результирующий текст только при выполнении условия.

#if defined(DEBUG)
std::cerr << "Debug mode enabled" << std::endl;
#elif defined(RELEASE)
std::cout << "Release version" << std::endl;
#else
std::cout << "Unknown build type" << std::endl;
#endif


#ifdef, #ifndef — проверяют наличие определения макроса.
Если макрос определен (#ifdef) или не определен (#ifndef), выполняется соответствующий блок кода.

#ifdef DEBUG
std::cerr << "Debug mode enabled" << std::endl;
#endif

#ifndef RELEASE
std::cout << "Not in release mode" << std::endl;
#endif


#error — генерирует сообщение об ошибке на этапе препроцессинга.

#if !defined(__cplusplus)
#error This code requires C++
#endif


#warning — генерирует предупреждение на этапе препроцессинга.

#if defined(BETA_VERSION)
#warning Compiling beta version
#endif

#line — изменяет номер текущей строки и имя файла, которое отображается в сообщениях об ошибках компилятора.

#line 100 "newfile.cpp"


#pragma — используется для указания компилятору специальных инструкций, зависящих от платформы или компилятора.

// Предотвращает повторное
// включение заголовочного файла
#pragma once

// Отключает конкретное
// предупреждение
компилятора
#pragma warning(disable : 4996)


## — оператор конкатенации, который соединяет два лексических элемента в один.

#define CONCAT(a, b) a ## b
CONCAT(var, 123)
// Результат: var123


#оператор stringify, который превращает аргумент в строковый литерал.

#define TO_STRING(x) #x
TO_STRING(hello)

// Результат: "hello"


Эти команды препроцессора предоставляют мощные средства для управления структурой и содержанием исходного кода, позволяя разработчикам гибко настраивать компиляцию программы в зависимости от различных условий и окружений.