Введение: Собираем всё вместе — настоящее программирование
Привет, юный программист! 👋 Сегодня у нас особенный урок. Мы уже изучили классы, конструкторы, методы и даже инкапсуляцию. Но до сих пор мы создавали простые классы, которые не связаны друг с другом. А что, если я скажу тебе, что программисты создают целые миры из классов? Миры, где одни объекты управляют другими, где есть начальники и подчинённые, где можно переиспользовать старый код для новых задач.
Представь, что ты строишь город в компьютерной игре. Ты не можешь каждый раз заново рисовать каждый дом — ты используешь готовые блоки. В программировании то же самое! Сегодня мы научимся собирать программы как конструктор 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&