C++ Курс

C++ ООП: классы, наследование, полиморфизм

Самоучитель Python 3, собранный из материалов данного сайта.Предназначен в основном для тех, кто хочет изучить язык программирования Python с нуля.

Введение: Собираем всё вместе — настоящее программирование

Привет, юный программист! 👋 Сегодня у нас особенный урок. Мы уже изучили классы, конструкторы, методы и даже инкапсуляцию. Но до сих пор мы создавали простые классы, которые не связаны друг с другом. А что, если я скажу тебе, что программисты создают целые миры из классов? Миры, где одни объекты управляют другими, где есть начальники и подчинённые, где можно переиспользовать старый код для новых задач.

Представь, что ты строишь город в компьютерной игре. Ты не можешь каждый раз заново рисовать каждый дом — ты используешь готовые блоки. В программировании то же самое! Сегодня мы научимся собирать программы как конструктор LEGO: брать готовые детали (классы) и соединять их в сложные механизмы.

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

Часть 1: Композиция — когда деталь не может жить без целого

Что такое композиция?

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

Композиция — это когда один класс (назовём его "контейнер") содержит внутри себя объекты другого класса, и эти объекты не могут существовать без контейнера.

Пример: Автомобиль и двигатель

Представь, что мы создаём класс Двигатель. Сам по себе двигатель — просто кусок металла. Но когда он внутри Автомобиля, он становится частью системы.

#include <iostream>
#include <string>
using namespace std;

// Класс "Двигатель" — он не может существовать без машины
class Engine {
private:
    int horsepower;  // мощность в лошадиных силах
    bool isRunning;  // работает ли двигатель
    
public:
    // Конструктор: создаём двигатель с заданной мощностью
    Engine(int hp) : horsepower(hp), isRunning(false) {
        cout << "Создан двигатель мощностью " << horsepower << " л.с." << endl;
    }
    
    // Запустить двигатель
    void start() {
        if (!isRunning) {
            isRunning = true;
            cout << "Двигатель запущен! Врум-врум!" << endl;
        }
    }
    
    // Остановить двигатель
    void stop() {
        if (isRunning) {
            isRunning = false;
            cout << "Двигатель остановлен." << endl;
        }
    }
    
    // Получить мощность
    int getHorsepower() const {
        return horsepower;
    }
    
    // Проверить, работает ли
    bool isEngineRunning() const {
        return isRunning;
    }
};

// Класс "Автомобиль" — он содержит двигатель
class Car {
private:
    string brand;        // марка
    string model;        // модель
    Engine engine;       // композиция! Двигатель создаётся внутри машины
    
public:
    // Конструктор: сначала создаём двигатель, потом настраиваем машину
    Car(string b, string m, int hp) : brand(b), model(m), engine(hp) {
        cout << "Создан автомобиль " << brand << " " << model << endl;
    }
    
    // Завести машину
    void startCar() {
        cout << "Пытаемся завести " << brand << "..." << endl;
        engine.start();  // запускаем двигатель
    }
    
    // Поехать
    void drive() {
        if (engine.isEngineRunning()) {
            cout << "Поехали! Мощность: " << engine.getHorsepower() << " л.с." << endl;
        } else {
            cout << "Сначала заведи двигатель!" << endl;
        }
    }
    
    // Остановить машину
    void stopCar() {
        engine.stop();
        cout << "Машина остановлена." << endl;
    }
};

int main() {
    // Создаём машину — и двигатель создаётся автоматически!
    Car myCar("Toyota", "Camry", 200);
    
    // Пробуем ехать без запуска
    myCar.drive();  // Вывод: "Сначала заведи двигатель!"
    
    // Заводим и едем
    myCar.startCar();  // Вывод: "Двигатель запущен!"
    myCar.drive();     // Вывод: "Поехали! Мощность: 200 л.с."
    
    // Останавливаем
    myCar.stopCar();
    
    return 0;
}

Важно! Когда машина уничтожается, двигатель тоже уничтожается. Мы не можем создать двигатель отдельно — он живёт только внутри машины. Это и есть композиция.

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

Давай создадим ещё один пример. Книга не может существовать без библиотеки? В реальности может, но мы сделаем так, чтобы книга была частью библиотеки.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Класс "Книга"
class Book {
private:
    string title;
    string author;
    int pages;
    
public:
    Book(string t, string a, int p) : title(t), author(a), pages(p) {}
    
    void displayInfo() const {
        cout << "Книга: \"" << title << "\" by " << author 
             << ", " << pages << " стр." << endl;
    }
    
    string getTitle() const { return title; }
};

// Класс "Библиотека" — содержит книги (композиция)
class Library {
private:
    string name;
    vector<Book> books;  // вектор книг — все книги принадлежат библиотеке
    
public:
    Library(string n) : name(n) {
        cout << "Библиотека \"" << name << "\" открыта!" << endl;
    }
    
    // Добавить новую книгу
    void addBook(string title, string author, int pages) {
        books.push_back(Book(title, author, pages));
        cout << "Добавлена книга: " << title << endl;
    }
    
    // Показать все книги
    void showAllBooks() const {
        cout << "Книги в библиотеке \"" << name << "\":" << endl;
        for (const auto& book : books) {
            book.displayInfo();
        }
    }
    
    // Найти книгу по названию
    void findBook(string title) const {
        for (const auto& book : books) {
            if (book.getTitle() == title) {
                cout << "Книга найдена!" << endl;
                book.displayInfo();
                return;
            }
        }
        cout << "Книга \"" << title << "\" не найдена." << endl;
    }
};

int main() {
    // Создаём библиотеку
    Library myLib("Школьная библиотека");
    
    // Добавляем книги
    myLib.addBook("Гарри Поттер", "Дж.К. Роулинг", 500);
    myLib.addBook("Властелин колец", "Дж.Р.Р. Толкин", 1200);
    myLib.addBook("Маленький принц", "Антуан де Сент-Экзюпери", 100);
    
    // Показываем все книги
    myLib.showAllBooks();
    
    // Ищем книгу
    myLib.findBook("Гарри Поттер");
    
    return 0;
}

Когда библиотека будет уничтожена (например, программа завершится), все книги тоже исчезнут. Они не могут существовать отдельно — это и есть композиция.

Часть 2: Агрегация — когда детали могут жить сами по себе

Чем агрегация отличается от композиции?

Агрегация — это более слабая связь. Представь школьный класс и учеников. Ученики могут переходить из класса в класс, а могут вообще не учиться. Класс без учеников существует? Да, просто пустой класс. А ученик без класса? Тоже может — он же человек, а не функция.

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

Пример: Команда и игроки

Создадим футбольную команду. Игроки могут переходить в другие команды или завершать карьеру, а команда может набирать новых.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Класс "Игрок" — может существовать сам по себе
class Player {
private:
    string name;
    int number;
    string position;  // вратарь, защитник, нападающий
    
public:
    Player(string n, int num, string pos) 
        : name(n), number(num), position(pos) {}
    
    void play() const {
        cout << "Игрок " << name << " (№" << number 
             << ", " << position << ") играет!" << endl;
    }
    
    string getName() const { return name; }
    int getNumber() const { return number; }
};

// Класс "Команда" — содержит игроков (агрегация)
class Team {
private:
    string teamName;
    vector<Player*> players;  // указатели на игроков — они не принадлежат команде
    
public:
    Team(string name) : teamName(name) {}
    
    // Добавить игрока в команду (игрок уже существует)
    void addPlayer(Player* player) {
        players.push_back(player);
        cout << "Игрок " << player->getName() << " присоединился к команде \"" 
             << teamName << "\"" << endl;
    }
    
    // Убрать игрока из команды
    void removePlayer(string name) {
        for (auto it = players.begin(); it != players.end(); ++it) {
            if ((*it)->getName() == name) {
                cout << "Игрок " << name << " покинул команду." << endl;
                players.erase(it);
                return;
            }
        }
        cout << "Игрок " << name << " не найден в команде." << endl;
    }
    
    // Провести матч
    void playMatch() const {
        cout << "Команда \"" << teamName << "\" выходит на матч!" << endl;
        for (const auto& player : players) {
            player->play();
        }
        cout << "Матч окончен!" << endl;
    }
    
    // Показать состав
    void showSquad() const {
        cout << "Состав команды \"" << teamName << "\":" << endl;
        for (const auto& player : players) {
            cout << "  №" << player->getNumber() << " " 
                 << player->getName() << endl;
        }
    }
};

int main() {
    // Создаём игроков отдельно — они могут жить без команды
    Player player1("Иван Иванов", 10, "Нападающий");
    Player player2("Пётр Петров", 1, "Вратарь");
    Player player3("Сидор Сидоров", 7, "Защитник");
    
    cout << "Игроки созданы!" << endl;
    
    // Создаём команду
    Team team("Зенит");
    
    // Добавляем игроков в команду
    team.addPlayer(&player1);
    team.addPlayer(&player2);
    team.addPlayer(&player3);
    
    // Играем матч
    team.playMatch();
    
    // Удаляем одного игрока
    team.removePlayer("Пётр Петров");
    
    // Показываем состав
    team.showSquad();
    
    // Игроки всё ещё существуют, даже если команда удалится
    cout << "Игрок " << player1.getName() << " всё ещё существует!" << endl;
    
    return 0;
}

Обрати внимание: мы используем указатели (Player*), а не объекты. Это важно! Если бы мы хранили объекты, то при удалении команды удалились бы и игроки. А так — игроки продолжают жить своей жизнью.

Часть 3: Дружественные функции и классы

Зачем нужны "друзья"?

Помнишь, мы говорили про инкапсуляцию? Закрытые члены класса (private) нельзя трогать снаружи. Но иногда нужно, чтобы какой-то особый класс или функция могли получить доступ к этим секретам. Для этого существуют дружественные функции и дружественные классы.

Представь, что у тебя есть личный дневник (private). Ты не даёшь его читать никому. Но есть твой лучший друг — ему ты доверяешь. Ты делаешь его "дружественным" — и он может читать твой дневник.

Объявление дружественной функции

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

#include <iostream>
#include <string>
using namespace std;

// Класс "Секретный агент"
class SecretAgent {
private:
    string codeName;     // кодовое имя (секрет!)
    int missionCount;    // количество миссий (тоже секрет)
    
public:
    SecretAgent(string name, int missions) 
        : codeName(name), missionCount(missions) {}
    
    // Объявляем функцию другом — она сможет читать private-поля
    friend void revealSecret(const SecretAgent& agent);
    
    // Ещё один друг — функция, которая увеличивает счётчик миссий
    friend void completeMission(SecretAgent& agent);
};

// Дружественная функция — может читать private-данные
void revealSecret(const SecretAgent& agent) {
    cout << "Секретный агент \"" << agent.codeName 
         << "\" выполнил " << agent.missionCount << " миссий." << endl;
}

// Дружественная функция — может изменять private-данные
void completeMission(SecretAgent& agent) {
    agent.missionCount++;
    cout << "Миссия выполнена! Агент \"" << agent.codeName 
         << "\" теперь имеет " << agent.missionCount << " миссий." << endl;
}

int main() {
    SecretAgent bond("007", 20);
    
    // Обычная функция не может сделать так:
    // cout << bond.codeName;  // Ошибка! codeName — private
    
    // А друг может:
    revealSecret(bond);     // Вывод: Секретный агент "007" выполнил 20 миссий.
    completeMission(bond);  // Вывод: Миссия выполнена!...
    revealSecret(bond);     // Теперь 21 миссия
    
    return 0;
}

Дружественные классы

Иногда нужно, чтобы целый класс имел доступ к секретам другого класса. Например, класс "Директор" может смотреть зарплаты всех сотрудников (а обычные сотрудники — нет).

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Класс "Сотрудник"
class Employee {
private:
    string name;
    double salary;  // зарплата — секрет!
    
public:
    Employee(string n, double s) : name(n), salary(s) {}
    
    // Объявляем класс "Директор" другом
    friend class Director;
    
    // Обычный метод для получения имени (доступен всем)
    string getName() const { return name; }
};

// Класс "Директор" — может смотреть зарплаты
class Director {
private:
    string name;
    
public:
    Director(string n) : name(n) {}
    
    // Директор может посмотреть зарплату любого сотрудника
    void viewSalary(const Employee& emp) const {
        cout << "Директор " << name << " смотрит зарплату сотрудника \"" 
             << emp.getName() << "\": " << emp.salary << " руб." << endl;
    }
    
    // Директор может изменить зарплату
    void setSalary(Employee& emp, double newSalary) {
        cout << "Директор " << name << " меняет зарплату сотрудника \"" 
             << emp.getName() << "\" с " << emp.salary << " на " 
             << newSalary << " руб." << endl;
        emp.salary = newSalary;
    }
};

int main() {
    Employee emp1("Анна", 50000);
    Employee emp2("Борис", 60000);
    
    Director dir("Елена Петровна");
    
    // Директор смотрит зарплаты
    dir.viewSalary(emp1);  // Вывод: Директор Елена Петровна смотрит зарплату...
    dir.viewSalary(emp2);
    
    // Директор повышает зарплату
    dir.setSalary(emp1, 55000);
    dir.viewSalary(emp1);  // Теперь 55000
    
    return 0;
}

Часть 4: Собираем всё вместе — игра "Битва героев"

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

#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>  // для rand()
#include <ctime>    // для времени
using namespace std;

// ========== КЛАСС "ОРУЖИЕ" (композиция) ==========
class Weapon {
private:
    string name;
    int damage;
    int durability;  // прочность (сколько раз можно использовать)
    
public:
    Weapon(string n, int dmg, int dur) 
        : name(n), damage(dmg), durability(dur) {}
    
    // Использовать оружие — возвращает урон
    int use() {
        if (durability > 0) {
            durability--;
            cout << "Использовано оружие \"" << name << "\"! Урон: " 
                 << damage << " (осталось использований: " << durability << ")" << endl;
            return damage;
        } else {
            cout << "Оружие \"" << name << "\" сломано!" << endl;
            return 0;
        }
    }
    
    string getName() const { return name; }
    int getDamage() const { return damage; }
    int getDurability() const { return durability; }
};

// ========== КЛАСС "ГЕРОЙ" ==========
class Hero {
private:
    string name;
    int health;        // здоровье
    int maxHealth;     // максимальное здоровье
    Weapon weapon;     // композиция! Оружие — часть героя
    
public:
    Hero(string n, int hp, string weaponName, int weaponDmg, int weaponDur) 
        : name(n), health(hp), maxHealth(hp), 
          weapon(weaponName, weaponDmg, weaponDur) {
        cout << "Герой \"" << name << "\" создан! HP: " << health 
             << ", Оружие: " << weapon.getName() << endl;
    }
    
    // Атаковать другого героя
    void attack(Hero& target) {
        cout << name << " атакует " << target.getName() << "!" << endl;
        int dmg = weapon.use();
        target.takeDamage(dmg);
    }
    
    // Получить урон
    void takeDamage(int dmg) {
        health -= dmg;
        if (health < 0) health = 0;
        cout << name << " получает " << dmg << " урона! HP: " 
             << health << "/" << maxHealth << endl;
    }
    
    // Лечиться
    void heal(int amount) {
        health += amount;
        if (health > maxHealth) health = maxHealth;
        cout << name << " лечится на " << amount << " HP! Теперь: " 
             << health << "/" << maxHealth << endl;
    }
    
    // Проверить, жив ли герой
    bool isAlive() const {
        return health > 0;
    }
    
    string getName() const { return name; }
    int getHealth() const { return health; }
    
    // Дружественная функция — специальная атака
    friend void specialAttack(Hero& attacker, Hero& target);
};

// Дружественная функция — специальная атака (игнорирует оружие)
void specialAttack(Hero& attacker, Hero&