Введение в ООП

Крайне рекомендую перед выполнения практических заданий ознакомиться с хорошей лекцией от преподавателя мехмата МГУ по ссылке. После этого можете ознакомиться с его другими лекциями по С++, информацию преподносит достаточно кратко и ёмко, зачастую упоминает интересные нюансы и особенности, на скорости x1,5 смотрится вполне комфортно.

1. Инкапсуляция

Теоретическая справка:

Инкапсуляция — это принцип ООП, который предполагает сокрытие данных и предоставление контролируемого доступа к ним через специальные методы. В C++ это достигается с помощью модификаторов доступа (private, public, protected). Приватные данные защищены от прямого изменения, и для работы с ними используются геттеры и сеттеры — специальные методы для получения и изменения значений приватных переменных.

  • Геттеры предоставляют доступ к приватным данным.
  • Сеттеры позволяют изменять приватные данные с дополнительной проверкой.

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

Синтаксис:

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

class Warehouse {
private:
    int itemCount;

public:
    Warehouse(int initialCount) : itemCount(initialCount) {}

    // Геттер для получения количества товаров
    int getItemCount() const {
        return itemCount;
    }

    // Сеттер для добавления товаров с проверкой
    void addItem(int count) {
        if (count > 0) {
            itemCount += count;
        }
    }

    // Сеттер для удаления товаров с проверкой
    void removeItem(int count) {
        if (count > 0 && count <= itemCount) {
            itemCount -= count;
        }
    }
};

int main() {
    Warehouse warehouse(100);  // Создание склада с 100 единицами товара

    warehouse.addItem(50);     // Добавление товаров на склад
    warehouse.removeItem(30);  // Удаление товаров со склада

    std::cout << "Current item count: " << warehouse.getItemCount() << std::endl;  // Вывод текущего количества товаров

    return 0;
}

В этом примере класс Warehouse инкапсулирует переменную itemCount, предоставляя доступ к ней только через методы getItemCount(), addItem(), и removeItem(), что позволяет контролировать изменение количества товаров на складе.

Задача: Моделирование банковского счета

Необходимо создать класс BankAccount, который инкапсулирует данные о счете: владелец и баланс. Методы класса:

  • deposit(double amount) — пополнение счета.
  • withdraw(double amount) — снятие средств с проверкой на достаточность баланса.
  • getBalance() — геттер для доступа к балансу.
  • setOwner(std::string owner) — сеттер для изменения владельца счета.
  • displayInfo() — вывод информации о счете.

Пример работы программы:

Входные данные:

  • Создание счета для "Иван Иванов" с балансом 1000 рублей.
  • Пополнение на 500 рублей.
  • Снятие 300 рублей.
  • Снятие 1500 рублей (превышение доступного баланса).

Выходные данные:

Владелец: Иван Иванов, Баланс: 1000 руб.
На счет зачислено: 500 руб. Баланс: 1500 руб.
Со счета снято: 300 руб. Баланс: 1200 руб.
Ошибка: Недостаточно средств на счете!
Владелец: Иван Иванов, Баланс: 1200 руб.
Вариант реализации основных требований:
#include <iostream>
#include <string>

class BankAccount {
private:
    std::string owner;  // Владелец счета
    double balance;     // Баланс счета

public:
    // Конструктор для инициализации счета
    BankAccount(std::string ownerName, double initialBalance) {
        owner = ownerName;
        balance = initialBalance;
    }

    // Метод для пополнения счета
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << "На счет зачислено: " << amount << " руб. Баланс: " << balance << " руб.\n";
        } else {
            std::cout << "Ошибка: Некорректная сумма пополнения!\n";
        }
    }

    // Метод для снятия средств со счета
    void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            std::cout << "Со счета снято: " << amount << " руб. Баланс: " << balance << " руб.\n";
        } else {
            std::cout << "Ошибка: Недостаточно средств на счете!\n";
        }
    }

    // Геттер для получения текущего баланса
    double getBalance() {
        return balance;
    }

    // Сеттер для изменения владельца счета
    void setOwner(std::string newOwner) {
        owner = newOwner;
    }

    // Метод для вывода информации о счете
    void displayInfo() {
        std::cout << "Владелец: " << owner << ", Баланс: " << balance << " руб.\n";
    }
};

int main() {
    BankAccount account("Иван Иванов", 1000);
    account.displayInfo();  // Владелец: Иван Иванов, Баланс: 1000 руб.
    account.deposit(500);  // На счет зачислено: 500 руб. Баланс: 1500 руб.
    account.withdraw(300);  // Со счета снято: 300 руб. Баланс: 1200 руб.
    account.withdraw(1500);  // Ошибка: Недостаточно средств на счете!
    account.displayInfo();  // Владелец: Иван Иванов, Баланс: 1200 руб.

    return 0;
}

Дополнительные задания:

  1. Добавьте проверку при изменении владельца счета через сеттер (владелец не может быть пустым).
  2. Добавьте ограничение на количество операций пополнения/снятия в сутки (например, не более 5 операций).
  3. Добавьте поддержку мультивалютных счетов, чтобы можно было хранить баланс в разных валютах.

2. Наследование

Теоретическая справка:

Наследование — это механизм ООП, который позволяет создавать новые классы на основе существующих. Базовый класс содержит общие свойства и методы, которые могут быть унаследованы производными классами. Наследники могут добавлять новые функции или переопределять существующие методы для изменения поведения.

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

Синтаксис:

Пример наследования:

class Vehicle {
public:
    void start() const {
        std::cout << "Vehicle is starting.";
    }

    void stop() const {
        std::cout << "Vehicle is stopping.";
    }
};

class Car : public Vehicle {
public:
    void honk() const {
        std::cout << "Car is honking.";
    }
};

class Bicycle : public Vehicle {
public:
    void ringBell() const {
        std::cout << "Bicycle is ringing the bell.";
    }
};

int main() {
    Car myCar;
    Bicycle myBike;

    myCar.start();
    myCar.honk();
    myCar.stop();

    myBike.start();
    myBike.ringBell(); 
    myBike.stop();

    return 0;
}

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

Для чего используется:

  • Повторное использование кода: Наследование позволяет избежать дублирования кода, создавая общий базовый класс для связанных объектов. В системе управления заказами можно создать базовый класс Order, от которого наследуются классы OnlineOrder и OfflineOrder, добавляющие специфичные для их типов данные.
  • Создание специализированных классов: Наследование позволяет расширять функциональность базовых классов, создавая специализированные классы с добавлением новых свойств и методов. В системе учета сотрудников можно создать базовый класс Employee и производные классы Manager и Intern, у каждого из которых будут свои особенности.

Когда лучше избегать:

  • Чрезмерное наследование: Если слишком глубоко использовать наследование, это может усложнить поддержку кода и снизить его читаемость. В системе управления проектами чрезмерное использование наследования для каждого типа задач может усложнить архитектуру и затруднить внесение изменений.
  • Нарушение принципа слабой связанности: При использовании наследования слишком часто можно нарушить принцип слабой связанности, когда классы становятся слишком зависимыми друг от друга. В системе управления продуктами создание слишком связанных классов, где изменение в одном классе требует изменений в других, может усложнить поддержку системы.

Задача: Наследование для модели животных

Создайте базовый класс Animal с методами move() и makeSound(), где move() будет одинаковым для всех животных, а метод makeSound() заменяется специфическими для каждого наследника методами: bark() для собак и meow() для кошек. Затем создайте классы-наследники Dog и Cat. Для класса Dog добавьте метод fetchStick(), чтобы собака могла приносить палку.

Пример работы программы:

Входные данные:

  • Создание объектов Dog и Cat.
  • Вызов методов move(), bark(), и fetchStick() для собаки.
  • Вызов методов move() и meow() для кошки.

Выходные данные:

Животное двигается.
Собака лает.
Собака приносит палку.

Животное двигается.
Кошка мяукает.
Вариант реализации основных требований:
#include <iostream>
#include <string>

// Базовый класс Animal
class Animal {
public:
    // Метод для звуков, общий для всех животных
    void makeSound() const {
        std::cout << "Животное издает звук.\n";
    }

    // Метод для движения, общий для всех животных
    void move() const {
        std::cout << "Животное двигается.\n";
    }
};

// Класс Dog (Собака), наследник Animal
class Dog : public Animal {
public:
    // Специфический метод для собаки - принести палку
    void fetchStick() const {
        std::cout << "Собака приносит палку.\n";
    }

    // Специфический звук собаки
    void bark() const {
        std::cout << "Собака лает.\n";
    }
};

// Класс Cat (Кошка), наследник Animal
class Cat : public Animal {
public:
    // Специфический звук кошки
    void meow() const {
        std::cout << "Кошка мяукает.\n";
    }
};

int main() {
    Dog myDog;
    Cat myCat;

    myDog.move();         // Животное двигается.
    myDog.bark();         // Собака лает.
    myDog.fetchStick();   // Собака приносит палку.

    myCat.move();        // Животное двигается.
    myCat.meow();        // Кошка мяукает.

    return 0;
}

Дополнительные задания:

  1. Добавьте класс Bird, который наследует класс Animal, и добавьте для него метод fly(), который будет выводить сообщение о полете птицы.
  2. Реализуйте методы для типичных действий животных в базовом классе Animal, такие как eat() или sleep(), и добавьте для каждого наследника специфические действия (например, кошка охотится).
  3. Добавьте проверку состояния животного (например, усталость или голод), чтобы некоторые действия (например, движение или игра) могли быть недоступны, если животное устало или голодно.

3. Полиморфизм

Теоретическая справка:

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

В С++ полиморфизм реализуется при помощи двух ключевых слов:

  • virtual — используется для указания того, что метод в базовом классе может быть переопределён в производных классах.
  • override — применяется в производных классах для явного указания того, что метод переопределяет метод базового класса. Это помогает избежать ошибок, если метод базового класса не является виртуальным или если в сигнатуре метода допущена ошибка.

Синтаксис:

Пример полиморфизма с использованием виртуальных методов:

class Animal {
public:
    virtual void sound() {
        std::cout << "Animal makes a sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void sound() override {
        std::cout << "Dog barks" << std::endl;
    }
};

class Cat : public Animal {
public:
    void sound() override {
        std::cout << "Cat meows" << std::endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->sound();  // Вызывает метод Dog::sound()
    animal2->sound();  // Вызывает метод Cat::sound()

    delete animal1;
    delete animal2;
    return 0;
}

В этом примере базовый класс Animal имеет виртуальный метод sound, который переопределяется в классах-наследниках Dog и Cat. При вызове метода через указатель на базовый класс вызывается метод конкретного объекта.

Для чего используется:

  • Унификация интерфейса: Полиморфизм позволяет создавать системы, где объекты разных типов могут использоваться через общий интерфейс. В системе обработки данных разных типов отчетов (финансовые, аналитические) можно использовать общий интерфейс Report, с помощью которого можно обрабатывать все отчеты, не заботясь о конкретной реализации.
  • Упрощение поддержки и расширяемости: Полиморфизм позволяет легко добавлять новые классы без изменения существующего кода. В системе управления персоналом можно добавить новый класс Intern, унаследованный от Employee, с переопределением методов расчета заработной платы или рабочего времени.

Когда лучше избегать:

  • Сложность понимания: Если слишком активно использовать полиморфизм, код может стать сложным для понимания, особенно для новых разработчиков. В системе управления заказами чрезмерное использование полиморфизма для разных типов продуктов может затруднить понимание и сопровождение кода.
  • Неоправданная сложность: В проектах с четко определенными типами объектов может не требоваться полиморфизм. В небольшом приложении для учета задач полиморфизм может быть излишним, если все задачи имеют одну и ту же структуру и функционал.

Задача: Полиморфизм для геометрических фигур

Создайте базовый класс Shape с виртуальным методом getArea(). Далее создайте два производных класса: Circle и Rectangle, которые будут реализовывать метод getArea() для вычисления площади. Ваша задача — создать массив фигур и вычислить их площади через указатель на базовый класс.

Пример работы программы:

Входные данные:

  • Создание объектов Circle с радиусом 5 и Rectangle с размерами 4x6.
  • Вычисление и вывод площади для каждой фигуры.

Выходные данные:

Площадь круга: 78.54 кв. ед.
Площадь прямоугольника: 24 кв. ед.
Вариант реализации основных требований:
#include <iostream>
#include <cmath>

// Базовый класс Shape
class Shape {
public:
    // Виртуальный метод для вычисления площади
    virtual double getArea() const = 0;  // Чисто виртуальная функция

    // Виртуальный деструктор
    virtual ~Shape() {}
};

// Класс Circle (Круг), наследник Shape
class Circle : public Shape {
private:
    double radius;  // Радиус круга

public:
    // Конструктор для инициализации радиуса
    Circle(double r) : radius(r) {}

    // Переопределение метода getArea() для вычисления площади круга
    double getArea() const override {
        return M_PI * radius * radius;
    }
};

// Класс Rectangle (Прямоугольник), наследник Shape
class Rectangle : public Shape {
private:
    double width, height;  // Ширина и высота прямоугольника

public:
    // Конструктор для инициализации ширины и высоты
    Rectangle(double w, double h) : width(w), height(h) {}

    // Переопределение метода getArea() для вычисления площади прямоугольника
    double getArea() const override {
        return width * height;
    }
};

int main() {
    Shape* shapes[2];

    shapes[0] = new Circle(5);           // Круг с радиусом 5
    shapes[1] = new Rectangle(4, 6);     // Прямоугольник с размерами 4x6

    for (int i = 0; i < 2; ++i) {
        std::cout << "Площадь фигуры: " << shapes[i]->getArea() << " кв. ед.\n";
    }

    for (int i = 0; i < 2; ++i) {
        delete shapes[i];  // Вызов виртуального деструктора
    }

    return 0;
}

Дополнительные задания:

  1. Добавьте метод для вычисления периметра в каждом классе.
  2. Реализуйте класс для треугольника и добавьте его в массив фигур.
  3. Добавьте метод для вычисления и сравнения площадей между фигурами (например, сравнение площади круга и прямоугольника).

4. Различия между ООП и процедурным подходом

Теоретическая справка:

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

Задача: Система управления транспортными средствами

Создайте две версии программы для управления транспортными средствами:

  1. Процедурный подход: создайте функции для управления машиной и велосипедом, которые выполняют действия, такие как движение и заправка.
Пример решения задачи с использованием процедурного подхода
#include <iostream>

// Процедура для управления машиной
void driveCar(double& fuel, double distance) {
    double fuelConsumptionPerKm = 0.1;
    double requiredFuel = distance * fuelConsumptionPerKm;

    if (fuel >= requiredFuel) {
        fuel -= requiredFuel;
        std::cout << "Машина проехала " << distance << " км. Осталось топлива: " << fuel << " л.\n";
    } else {
        std::cout << "Недостаточно топлива для поездки на " << distance << " км.\n";
    }
}

// Процедура для заправки машины
void refuelCar(double& fuel, double amount) {
    fuel += amount;
    std::cout << "Машина заправлена на " << amount << " л. Осталось топлива: " << fuel << " л.\n";
}

// Процедура для управления велосипедом
void rideBicycle(double distance) {
    std::cout << "Велосипед проехал " << distance << " км.\n";
}

int main() {
    double carFuel = 5.0;
    driveCar(carFuel, 50);  // Машина двигается на 50 км
    refuelCar(carFuel, 10); // Машина заправляется на 10 л топлива

    rideBicycle(20);  // Велосипед двигается на 20 км

    return 0;
}
  1. ООП подход: создайте базовый класс Vehicle с общими методами и классы-наследники Car и Bicycle с их специфическими методами.
Пример решения задачи с использованием ООП подхода
#include <iostream>

// Базовый класс для транспортных средств
class Vehicle {
public:
    virtual void move(double distance) const = 0;
    virtual ~Vehicle() {}
};

// Класс Car (Машина), наследует класс Vehicle
class Car : public Vehicle {
private:
    double fuel;  // Текущий уровень топлива
    double fuelConsumptionPerKm;  // Расход топлива на 1 км

public:
    // Конструктор для инициализации машины
    Car(double initialFuel) : fuel(initialFuel), fuelConsumptionPerKm(0.1) {}

    // Метод для движения
    void move(double distance) const override {
        double requiredFuel = distance * fuelConsumptionPerKm;
        if (fuel >= requiredFuel) {
            std::cout << "Машина проехала " << distance << " км. Осталось топлива: " << fuel - requiredFuel << " л.\n";
        } else {
            std::cout << "Недостаточно топлива для поездки на " << distance << " км.\n";
        }
    }

    // Метод для заправки машины
    void refuel(double amount) {
        fuel += amount;
        std::cout << "Машина заправлена на " << amount << " л. Осталось топлива: " << fuel << " л.\n";
    }

    // Деструктор
    ~Car() override = default;
};

// Класс Bicycle (Велосипед), наследует класс Vehicle
class Bicycle : public Vehicle {
public:
    // Метод для движения велосипеда
    void move(double distance) const override {
        std::cout << "Велосипед проехал " << distance << " км.\n";
    }
};

int main() {
    Car myCar(5);
    myCar.move(50);
    myCar.refuel(10);

    Bicycle myBicycle;
    myBicycle.move(20);

    return 0;
}

Пример работы программы:

Входные данные:

  • Машина двигается на 50 км, велосипед на 20 км.
  • Машина заправляется на 10 литров топлива.

Выходные данные:

Машина проехала 50 км. Осталось топлива: 5 л.
Машина заправлена на 10 л. Осталось топлива: 15 л.
Велосипед проехал 20 км.

Дополнительные задания:

  1. В ООП версии добавьте новый вид транспорта (например, мотоцикл) и продемонстрируйте, как легко его интегрировать в систему.
  2. Добавьте возможность учета пройденного времени для каждого типа транспорта.
  3. Попробуйте внести аналогичные изменения в процедурный подход, проанализируйте насколько это было удобнее / сложнее, чем с использованием ООП.

5. Перегрузка функций и операторов

Теоретическая справка:

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

Перегрузка операторов позволяет изменять поведение стандартных операторов (например, +, -, *, ==) для пользовательских типов данных (классов). Это полезно, когда вы хотите, чтобы стандартные операторы могли работать с вашими объектами, например, для арифметических операций или сравнения объектов.

Синтаксис:

Пример перегрузки функции:

class MathOperation {
public:
    int add(int a, int b) {
        return a + b;
    }

    double add(double a, double b) {
        return a + b;
    }

    int add(int a, int b, int c) {
        return a + b + c;
    }
};

int main() {
    MathOperation math;
    int sum1 = math.add(3, 4);        // Перегрузка для целых чисел
    double sum2 = math.add(2.5, 3.5); // Перегрузка для вещественных чисел
    int sum3 = math.add(1, 2, 3);     // Перегрузка для трех аргументов
    return 0;
}

Пример перегрузки оператора:

class ComplexNumber {
private:
    double real, imag;

public:
    ComplexNumber(double r = 0, double i = 0) : real(r), imag(i) {}

    // Перегрузка оператора +
    ComplexNumber operator+(const ComplexNumber& other) {
        return ComplexNumber(real + other.real, imag + other.imag);
    }

    void display() {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

int main() {
    ComplexNumber num1(3.0, 4.0), num2(1.0, 2.0);
    ComplexNumber sum = num1 + num2;  // Использование перегруженного оператора +
    sum.display(); // Вывод: 4.0 + 6.0i
    return 0;
}

В этом примере перегружается оператор +, чтобы его можно было использовать для сложения комплексных чисел.

Для чего используются:

  • Обеспечение удобного интерфейса для пользователя: Перегрузка функций позволяет пользователям кода вызывать одну и ту же функцию для разных типов данных, что делает API более интуитивным. Пример: В системе управления учебными курсами перегруженная функция может использоваться для обработки различных типов данных студента (целочисленные идентификаторы, строки с именем и фамилией).
  • Работа с пользовательскими типами данных: Перегрузка операторов позволяет пользователям определять естественное поведение операторов для объектов. Пример: В системе учета финансов перегруженный оператор + может использоваться для сложения объектов класса Money, который содержит информацию о валюте и сумме.

Когда лучше избегать:

  • Чрезмерное использование перегрузки: Если перегружать слишком много операторов или функций, это может запутать пользователей кода и привести к сложностям в его понимании. Пример: В приложении для управления инвентарем слишком частое использование перегрузки операторов может привести к путанице, так как пользователи могут не понимать, как операторы работают с объектами инвентаря.
  • Сложность отладки: Перегрузка операторов может затруднить отладку программы, так как она изменяет стандартное поведение операторов. Пример: В системе для управления проектами перегрузка операторов сравнения (== или <) может привести к непредсказуемым результатам, если не реализована корректно.

Задача: Перегрузка операторов для работы с комплексными числами

Необходимо создать класс Complex, который будет представлять комплексные числа. Класс должен содержать:

  • Приватные члены: действительная и мнимая часть числа.
  • Публичные методы для получения и изменения частей комплексного числа.
  • Перегруженные операторы:
    • + для сложения двух комплексных чисел.
    • - для вычитания.
    • == для сравнения двух комплексных чисел.
    • << для вывода комплексного числа.

Пример работы программы:

Входные данные:

  • Создание двух комплексных чисел 3 + 4i и 1 + 2i.
  • Сложение и вычитание этих чисел.
  • Сравнение двух чисел.

Выходные данные:

Первое число: 3 + 4i
Второе число: 1 + 2i
Результат сложения: 4 + 6i
Результат вычитания: 2 + 2i
Вариант реализации основных требований:
#include <iostream>

class Complex {
private:
    double real;  // Действительная часть
    double imag;  // Мнимая часть

public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

    // Метод для получения действительной части
    double getReal() const {
        return real;
    }

    // Метод для получения мнимой части
    double getImag() const {
        return imag;
    }

    // Метод для изменения действительной части
    void setReal(double r) {
        real = r;
    }

    // Метод для изменения мнимой части
    void setImag(double i) {
        imag = i;
    }

    // Перегрузка оператора + для сложения двух комплексных чисел
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    // Перегрузка оператора - для вычитания двух комплексных чисел
    Complex operator-(const Complex& other) const {
        return Complex(real - other.real, imag - other.imag);
    }

    // Перегрузка оператора == для сравнения двух комплексных чисел
    bool operator==(const Complex& other) const {
        return (real == other.real) && (imag == other.imag);
    }

    // Перегрузка оператора << для вывода комплексного числа
    friend std::ostream& operator<<(std::ostream& out, const Complex& c) {
        out << c.real;
        if (c.imag >= 0) {
            out << " + " << c.imag << "i";
        } else {
            out << " - " << -c.imag << "i";
        }
        return out;
    }
};

int main() {
    Complex num1(3, 4);
    Complex num2(1, 2);

    std::cout << "Первое число: " << num1 << "\n";
    std::cout << "Второе число: " << num2 << "\n";

    Complex sum = num1 + num2;
    std::cout << "Результат сложения: " << sum << "\n";

    Complex difference = num1 - num2;
    std::cout << "Результат вычитания: " << difference << "\n";

    if (num1 == num2) {
        std::cout << "Числа равны.\n";
    } else {
        std::cout << "Числа не равны.\n";
    }

    return 0;
}

Дополнительные задания:

  1. Реализуйте перегрузку оператора * для умножения комплексных чисел.
  2. Добавьте оператор != для проверки неравенства.
  3. Реализуйте оператор [], который позволяет обращаться к действительной и мнимой частям числа по индексу (например, c[0] для действительной и c[1] для мнимой).

6. Шаблоны (Templates)

Теоретическая справка:

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

Синтаксис:

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

template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    int resultInt = add(3, 4);        // Работает с целыми числами
    double resultDouble = add(3.5, 2.1);  // Работает с числами с плавающей запятой
    return 0;
}

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

Шаблоны также могут применяться к классам. Рассмотрим пример класса Vector, который может работать с различными типами данных:

template <typename T>
class Vector {
private:
    T* data;
    int size;

public:
    Vector(int s) : size(s) {
        data = new T[size];
    }

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

    T getData(int index) {
        if (index >= 0 && index < size) {
            return data[index];
        }
        return T(); // Возвращаем значение по умолчанию для типа T
    }

    ~Vector() {
        delete[] data;
    }
};

int main() {
    Vector<int> intVector(5);
    intVector.setData(0, 10);

    Vector<double> doubleVector(3);
    doubleVector.setData(1, 5.5);

    return 0;
}

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

Для чего используются:

  • Универсальные контейнеры данных: Шаблонные классы, такие как Vector, можно использовать в небольших модулях для хранения и обработки данных (например, идентификаторы студентов или их оценки).
  • Гибкость с разными типами данных: В модуле вычислений шаблонные функции могут выполнять операции с различными типами данных (например, целые числа, вещественные числа, комплексные числа).
  • Повторное использование кода: Шаблоны позволяют создавать универсальные структуры данных (например, списки, очереди), что предотвращает дублирование кода.

Когда лучше избегать:

  • Переусложнение для конкретных задач: В модуле с четко определенными типами данных использование шаблонов может быть излишним. Конкретные типы данных проще использовать для решения задач.
  • Сложность отладки: В приложениях, где используются разные типы данных (например, обработка изображений), отладка шаблонов может усложниться из-за неочевидных ошибок.
  • Усложнение поддержки: Если использовать шаблоны для всех операций в одном модуле, это может усложнить поддержку кода, так как разработчики должны понимать взаимодействие шаблонов с разными типами данных.

Задача: Шаблон класса для работы с парой данных

Создайте шаблон класса Pair, который может хранить два объекта любого типа и предоставлять методы для доступа к этим объектам. Класс должен содержать:

  • Приватные члены для хранения пары данных.
  • Публичные методы для доступа к первому и второму элементам.
  • Метод для сравнения двух объектов Pair (пара считается равной, если оба элемента равны).

Пример работы программы:

Входные данные:

  • Создание пары целых чисел (5, 10).
  • Создание пары строк ("Привет", "Мир").
  • Сравнение двух пар целых чисел.

Выходные данные:

Первая пара: 5, 10
Вторая пара: Привет, Мир
Пары равны: Нет
Вариант реализации основных требований:
#include <iostream>
#include <string>

// Шаблонный класс Pair для работы с двумя объектами любого типа
template <typename T1, typename T2>
class Pair {
private:
    // Приватные члены для хранения двух объектов
    T1 first;
    T2 second;

public:
    Pair(T1 firstValue, T2 secondValue) : first(firstValue), second(secondValue) {}

    T1 getFirst() const {
        return first;
    }

    T2 getSecond() const {
        return second;
    }

    // Метод для сравнения двух пар
    bool isEqual(const Pair<T1, T2>& other) const {
        return (first == other.first && second == other.second);
    }

    friend std::ostream& operator<<(std::ostream& os, const Pair<T1, T2>& pair) {
        os << pair.first << ", " << pair.second;
        return os;
    }
};

int main() {
    // Создание пары целых чисел
    Pair<int, int> intPair(5, 10);
    std::cout << "Первая пара: " << intPair << std::endl;

    // Создание пары строк
    Pair<std::string, std::string> stringPair("Привет", "Мир");
    std::cout << "Вторая пара: " << stringPair << std::endl;

    // Сравнение двух пар целых чисел
    Pair<int, int> anotherIntPair(5, 10);
    bool areEqual = intPair.isEqual(anotherIntPair);
    std::cout << "Пары равны: " << (areEqual ? "Да" : "Нет") << std::endl;

    return 0;
}

Дополнительные задания:

  1. Реализуйте шаблон для сравнения пар разного типа (например, пара целых чисел и пара с плавающей точкой).
  2. Добавьте возможность работы с шаблоном для трех элементов (например, Triplet).
  3. Реализуйте шаблонный метод для вывода данных пары через перегрузку оператора <<.

7. Статические члены класса

Теоретическая справка:

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

Пример:

class Example {
public:
    static int staticVar;

    static void staticMethod() {
        std::cout << "This is a static method!" << std::endl;
    }
};

int Example::staticVar = 0;

int main() {
    // Вызов статического метода
    Example::staticMethod();

    // Доступ к статической переменной
    std::cout << "Static variable: " << Example::staticVar << std::endl;
    return 0;
}

Для создания статического класса и управления его экземпляром, можно использовать следующий подход:

class Singleton {
private:
    static Singleton* instance;

    // Закрытый конструктор
    Singleton() {}

public:
    // Статический метод для получения экземпляра класса
    static Singleton* getInstance() {
        if (!instance)
            instance = new Singleton();
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;

Для чего используются:

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

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

  3. Удобство в утилитарных функциях. Например, статические методы могут использоваться для валидации данных в системе электронного документооборота (СЭД). Например, метод для проверки формата email можно реализовать как статический метод, который вызывается без создания экземпляра класса пользователя.

Когда лучше избегать:

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

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

Задача: Учет количества сотрудников в компании

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

Класс должен содержать:

  • Приватные члены: имя сотрудника и идентификационный номер (ID).
  • Статический счетчик для подсчета сотрудников.
  • Статический метод для вывода общего количества сотрудников.
  • Конструктор, который увеличивает счетчик и присваивает сотруднику уникальный ID.

Пример работы программы:

Входные данные:

  • Создание трех сотрудников с именами "Иван", "Мария" и "Алексей".

Выходные данные:

Сотрудник Иван, ID: 1
Сотрудник Мария, ID: 2
Сотрудник Алексей, ID: 3
Общее количество сотрудников: 3
Вариант реализации основных требований:
#include <iostream>
#include <string>

class Employee {
private:
    std::string name;
    int id;

    // Статический счетчик для подсчета сотрудников
    static int employeeCount;
    
public:
    Employee(const std::string& employeeName) : name(employeeName) {
        employeeCount++;
        id = employeeCount;
    }

    void displayInfo() const {
        std::cout << "Сотрудник " << name << ", ID: " << id << std::endl;
    }

    // Статический метод для вывода общего количества сотрудников
    static void showEmployeeCount() {
        std::cout << "Общее количество сотрудников: " << employeeCount << std::endl;
    }
};

// Инициализация статической переменной
int Employee::employeeCount = 0;

int main() {
    Employee emp1("Иван");
    Employee emp2("Мария");
    Employee emp3("Алексей");

    emp1.displayInfo();
    emp2.displayInfo();
    emp3.displayInfo();

    Employee::showEmployeeCount();

    return 0;
}

Дополнительные задания:

  1. Реализуйте возможность удаления сотрудника. Не забудьте уменьшить общий счетчик сотрудников.
  2. Добавьте статический метод для вывода списка всех сотрудников.
  3. Добавьте методы для изменения данных сотрудника (например, изменение имени), чтобы проверить, как это влияет на работу статических методов.

8. Дружественные классы и функции

Теоретическая справка:

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

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

Синтаксис:

Чтобы сделать функцию дружественной классу, необходимо объявить её с ключевым словом friend внутри класса. Пример синтаксиса:

class ClassName {
private:
    int privateData;

public:
    // Дружественная функция имеет доступ к приватным членам этого класса
    friend void friendFunction(ClassName& obj);
};

// Определение дружественной функции
void friendFunction(ClassName& obj) {
    // Функция имеет доступ к приватным членам класса
    std::cout << "Private data: " << obj.privateData << std::endl;
}

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

class ClassA {
private:
    int privateDataA;

    // Класс ClassB является дружественным и имеет доступ ко всем приватным членам
    friend class ClassB;
};

class ClassB {
public:
    void accessClassA(ClassA& obj) {
        // ClassB может получить доступ к приватным членам ClassA
        std::cout << "Private data from ClassA: " << obj.privateDataA << std::endl;
    }
};

В этом примере класс ClassB может получить доступ к приватным данным класса ClassA напрямую, так как объявлен дружественным.

Зачем нужны дружественные функции и классы?

Для чего используются:

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

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

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

Когда лучше избегать:

  • Чрезмерная связанность.. Например, в системе бронирования авиабилетов использование дружественных классов для тесной связи между классами билетов и пассажиров может привести к сложному коду, который трудно сопровождать, особенно если потребуется добавить новые виды билетов или изменить структуру данных пассажиров.

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

Задача: Модель взаимодействия между классами "Компания" и "Сотрудник"

Создайте два класса: Company и Employee. Класс Company должен управлять сотрудниками, а класс Employee должен содержать информацию о сотрудниках. Класс Company должен быть дружественным к классу Employee, чтобы иметь возможность добавлять и удалять сотрудников.

Класс Employee содержит:

  • Приватные данные: имя и ID сотрудника.
  • Конструктор для создания сотрудника.
  • Метод для вывода информации о сотруднике.

Класс Company включает:

  • Приватные данные: массив сотрудников (или вектор).
  • Дружественные функции для добавления и удаления сотрудников.

Пример работы программы:

Входные данные:

  • Создание объекта компании.
  • Добавление двух сотрудников.
  • Вывод списка сотрудников.

Выходные данные:

Сотрудник Иван, ID: 1
Сотрудник Мария, ID: 2
Общее количество сотрудников: 2
Вариант реализации основных требований:
#include <iostream>
#include <vector>
#include <string>

class Employee {
private:
    std::string name;
    int id;
    static int employeeCount;

public:
    Employee(const std::string& employeeName) : name(employeeName) {
        employeeCount++;
        id = employeeCount;
    }

    void displayInfo() const {
        std::cout << "Сотрудник " << name << ", ID: " << id << std::endl;
    }

    static int getEmployeeCount() {
        return employeeCount;
    }

    // Делаем класс Company дружественным, чтобы он имел доступ к приватным членам Employee
    friend class Company;
};

// Инициализация статической переменной
int Employee::employeeCount = 0;

// Класс Company для управления сотрудниками
class Company {
private:
    std::vector<Employee> employees;

public:
    void addEmployee(const std::string& employeeName) {
        Employee newEmployee(employeeName);
        employees.push_back(newEmployee);
    }

    void removeLastEmployee() {
        if (!employees.empty()) {
            employees.pop_back();
        }
    }

    void listEmployees() const {
        for (const auto& employee : employees) {
            employee.displayInfo();
        }
    }

    void showEmployeeCount() const {
        std::cout << "Общее количество сотрудников: " << Employee::getEmployeeCount() << std::endl;
    }
};

int main() {
    Company myCompany;

    myCompany.addEmployee("Иван");
    myCompany.addEmployee("Мария");

    myCompany.listEmployees();
    myCompany.showEmployeeCount();

    return 0;
}

Дополнительные задания:

  1. Реализуйте возможность поиска сотрудника по имени или ID.
  2. Добавьте метод для изменения данных сотрудника через класс Company (например, изменение имени).
  3. Реализуйте дружественные функции для вывода всех сотрудников компании.

Контрольное задание: Магазин с товарами, клиентами и покупками

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

Задача: Разработка модели магазина

Необходимо создать систему, моделирующую работу магазина с клиентами и товарами. Ваша система должна включать следующие элементы:

  1. Класс Product для хранения информации о товаре:
    • Приватные данные: название товара, цена, количество на складе.
    • Публичные методы для изменения количества и получения информации о товаре.
    • Перегрузите оператор == для сравнения товаров.
  2. Класс Customer для хранения информации о клиентах:
    • Приватные данные: имя клиента, баланс счета.
    • Публичные методы для пополнения счета и покупки товара.
    • Дружественная функция для доступа к данным клиента из класса магазина.
  3. Класс Store для управления покупками и товарами:
    • Приватные данные: список товаров и клиентов.
    • Статический счетчик для учета количества товаров в магазине.
    • Публичные методы для добавления товаров, покупки товаров клиентами.
    • Шаблонный метод для возврата списка товаров или клиентов.

Пример работы программы:

Входные данные:

  • Добавление товаров в магазин: "Хлеб", "Молоко", "Мясо".
  • Создание клиента с балансом 1000 рублей.
  • Покупка товара клиентом.

Выходные данные:

Товар: Хлеб, Цена: 50 руб., Остаток: 98 шт.
Товар: Молоко, Цена: 60 руб., Остаток: 97 шт.
Клиент Иван Иванов: Баланс: 940 руб.

Дополнительные задания:

  1. Реализуйте шаблонный метод для возврата информации как о товарах, так и о клиентах (например, метод, который принимает тип объекта и возвращает соответствующую информацию).
  2. Реализуйте перегрузку оператора += для увеличения количества товара на складе и оператора -= для уменьшения.
  3. Добавьте шаблонный класс для управления учетными записями сотрудников магазина, который включает информацию о сотрудниках и позволяет управлять их статусом и зарплатой.