Задания.
Нарисовать интеллект-карту, отражающую все темы и основные определения, что мы прошли до сегодняшнего занятия. Центральное понятие – программа, исходящие ветки – среда программирования, переменные и константы, базовые алгоритмы.
Нарисовать схему программы, с учетом структур, прототипов и определений функций.
Знакомство с объектно-ориентированным программированием
ООП — это такой подход к программированию, где на первом месте стоят объекты. На самом деле там всё немного сложнее, но мы до этого ещё доберёмся. Для начала поговорим про ООП вообще и разберём, с чего оно начинается.
Обычное программирование (процедурное)
Чаще всего под обычным понимают процедурное программирование, в основе которого — процедуры и функции. Функция — это мини-программа, которая получает на вход какие-то данные, что-то делает внутри себя и может отдавать какие-то данные в результате вычислений. Представьте, что это такой конвейер, который упакован в коробочку.
Например, в интернет-магазине может быть функция «Проверить email». Она получает на вход какой-то текст, сопоставляет со своими правилами и выдаёт ответ: это правильный электронный адрес или нет. Если правильный, то true, если нет — то false.
Функции полезны, когда нужно упаковать много команд в одну. Например, проверка электронного адреса может состоять из одной проверки на регулярные выражения, а может содержать множество команд: запросы в словари, проверку по базам спамеров и даже сопоставление с уже известными электронными адресами. В функцию можно упаковать любой комбайн из действий и потом просто вызывать их все одним движением.
Что не так с процедурным программированием
Процедурное программирование идеально работает в простых программах, где все задачи можно решить, грубо говоря, десятком функций. Функции аккуратно вложены друг в друга, взаимодействуют друг с другом, можно передать данные из одной функции в другую.
Например, вы пишете функцию «Зарегистрировать пользователя интернет-магазина». Внутри неё вам нужно проверить его электронный адрес. Вы вызываете функцию «Проверить email» внутри функции «Зарегистрировать пользователя», и в зависимости от ответа функции вы либо регистрируете пользователя, либо выводите ошибку. И у вас эта функция встречается ещё в десяти местах. Функции как бы переплетены.
Тут приходит продакт-менеджер и говорит: «Хочу, чтобы пользователь точно знал, в чём ошибка при вводе электронного адреса». Теперь вам нужно научить функцию выдавать не просто true — false, а ещё и код ошибки: например, если в адресе опечатка, то код 01, если адрес спамерский — код 02 и так далее. Это несложно реализовать.
Вы залезаете внутрь этой функции и меняете её поведение: теперь она вместо true — false выдаёт код ошибки, а если ошибки нет — пишет «ОК».
И тут ваш код ломается: все десять мест, которые ожидали от проверяльщика true или false, теперь получают «ОК» и из-за этого ломаются.
Теперь вам нужно:
либо переписывать все функции, чтобы научить их понимать новые ответы проверяльщика адресов;
либо переделать сам проверяльщик адресов, чтобы он остался совместимым со старыми местами, но в нужном вам месте как-то ещё выдавал коды ошибок;
либо написать новый проверяльщик, который выдаёт коды ошибок, а в старых местах использовать старый проверяльщик.
Задача, конечно, решаемая за час-другой.
Но теперь представьте, что у вас этих функций — сотни. И изменений в них нужно делать десятки в день. И каждое изменение, как правило, заставляет функции вести себя более сложным образом и выдавать более сложный результат. И каждое изменение в одном месте ломает три других места. В итоге у вас будут нарождаться десятки клонированных функций, в которых вы сначала будете разбираться, а потом уже нет.
Это называется спагетти-код, и для борьбы с ним как раз придумали объектно-ориентированное программирование.
Объектно-ориентированное программирование
Основная задача ООП — сделать сложный код проще. Для этого программу разбивают на независимые блоки, которые мы называем объектами.
Объект — это не какая-то космическая сущность. Это всего лишь набор данных и функций — таких же, как в традиционном функциональном программировании. Можно представить, что просто взяли кусок программы и положили его в коробку и закрыли крышку. Вот эта коробка с крышками — это объект.
Программисты договорились, что данные внутри объекта будут называться свойствами, а функции — методами. Но это просто слова, по сути это те же переменные и функции.
Объект можно представить как независимый электроприбор у вас на кухне. Чайник кипятит воду, плита греет, блендер взбивает, мясорубка делает фарш. Внутри каждого устройства куча всего: моторы, контроллеры, кнопки, пружины, предохранители — но вы о них не думаете. Вы нажимаете кнопки на панели каждого прибора, и он делает то, что от него ожидается. И благодаря совместной работе этих приборов у вас получается ужин.
Объекты характеризуются четырьмя словами: инкапсуляция, абстракция, наследование и полиморфизм.
Инкапсуляция, абстракция, наследование, полиморфизм
Инкапсуляция — объект независим: каждый объект устроен так, что нужные для него данные живут внутри этого объекта, а не где-то снаружи в программе. Например, если у меня есть объект «Пользователь», то у меня в нём будут все данные о пользователе: и имя, и адрес, и всё остальное. И в нём же будут методы «Проверить адрес» или «Подписать на рассылку».
Абстракция — у объекта есть «интерфейс»: у объекта есть методы и свойства, к которым мы можем обратиться извне этого объекта. Так же, как мы можем нажать кнопку на блендере. У блендера есть много всего внутри, что заставляет его работать, но на главной панели есть только кнопка. Вот эта кнопка и есть абстрактный интерфейс.
В программе мы можем сказать: «Удалить пользователя». На языке ООП это будет «пользователь.удалить()» — то есть мы обращаемся к объекту «пользователь» и вызываем метод «удалить». Кайф в том, что нам не так важно, как именно будет происходить удаление: ООП позволяет нам не думать об этом в момент обращения.
Например, над магазином работают два программиста: один пишет модуль заказа, а второй — модуль доставки. У первого в объекте «заказ» есть метод «отменить». И вот второму нужно из-за доставки отменить заказ. И он спокойно пишет: «заказ.отменить()». Ему неважно, как другой программист будет реализовывать отмену: какие он отправит письма, что запишет в базу данных, какие выведет предупреждения.
Наследование — способность к копированию. ООП позволяет создавать много объектов по образу и подобию другого объекта. Это позволяет не копипастить код по двести раз, а один раз нормально написать и потом много раз использовать.
Например, у вас может быть некий идеальный объект «Пользователь»: в нём вы прописываете всё, что может происходить с пользователем. У вас могут быть свойства: имя, возраст, адрес, номер карты. И могут быть методы «Дать скидку», «Проверить заказ», «Найти заказы», «Позвонить».
На основе этого идеального пользователя вы можете создать реального «Покупателя Ивана». У него при создании будут все свойства и методы, которые вы задали у идеального покупателя, плюс могут быть какие-то свои, если захотите.
Идеальные объекты программисты называют классами.
Полиморфизм — единый язык общения. В ООП важно, чтобы все объекты общались друг с другом на понятном им языке. И если у разных объектов есть метод «Удалить», то он должен делать именно это и писаться везде одинаково. Нельзя, чтобы у одного объекта это было «Удалить», а у другого «Стереть».
При этом внутри объекта методы могут быть реализованы по-разному. Например, удалить товар — это выдать предупреждение, а потом пометить товар в базе данных как удалённый. А удалить пользователя — это отменить его покупки, отписать от рассылки и заархивировать историю его покупок. События разные, но для программиста это неважно. У него просто есть метод «Удалить()», и он ему доверяет.
Такой подход позволяет программировать каждый модуль независимо от остальных. Главное — заранее продумать, как модули будут общаться друг с другом и по каким правилам. При таком подходе вы можете улучшить работу одного модуля, не затрагивая остальные — для всей программы неважно, что внутри каждого блока, если правила работы с ним остались прежними.
Плюсы и минусы ООП
У объектно-ориентированного программирования много плюсов, и именно поэтому этот подход использует большинство современных программистов.
Визуально код становится проще, и его легче читать. Когда всё разбито на объекты и у них есть понятный набор правил, можно сразу понять, за что отвечает каждый объект и из чего он состоит.
Меньше одинакового кода. Если в обычном программировании одна функция считает повторяющиеся символы в одномерном массиве, а другая — в двумерном, то у них большая часть кода будет одинаковой. В ООП это решается наследованием.
Сложные программы пишутся проще. Каждую большую программу можно разложить на несколько блоков, сделать им минимальное наполнение, а потом раз за разом подробно наполнить каждый блок.
Увеличивается скорость написания. На старте можно быстро создать нужные компоненты внутри программы, чтобы получить минимально работающий прототип.
А теперь про минусы:
Сложно понять и начать работать. Подход ООП намного сложнее обычного процедурного программирования — нужно знать много теории, прежде чем будет написана хоть одна строчка кода.
Требует больше памяти. Объекты в ООП состоят из данных, интерфейсов, методов и много другого, а это занимает намного больше памяти, чем простая переменная.
Иногда производительность кода будет ниже. Из-за особенностей подхода часть вещей может быть реализована сложнее, чем могла бы быть. Поэтому бывает такое, что ООП-программа работает медленнее, чем процедурная (хотя с современными мощностями процессоров это мало кого волнует).
Одно из преимуществ ООП — не нужно много раз писать один и тот же код. Можно однажды придумать какую-то красивую штуку и потом заново её использовать буквально одной строкой. Для этого и нужны классы.
Что есть класс
Вы можете изготовить объект вручную из любых функций и данных, которые вам нужны. Но если вам для работы программы нужны десятки похожих друг на друга объектов, имеет смысл как-то их стандартизировать.
Если по-простому, то класс — это «чертёж», по которому вы можете изготовить объекты. Вы прописываете один класс, определяете его поведение и свойства, а потом даёте команду создать на основе этого класса нужное число объектов.
Например:
У вас может быть класс для пользователя интернет-магазина. Класс будет подсказывать, что у пользователя есть имя, адрес, почта и пароль, а также он может совершать покупки с помощью таких-то функций. На основании этого класса вы будете регистрировать множество пользователей.
Может быть класс для кнопки в интерфейсе. Вы знаете, что на кнопку можно нажимать и её можно отключать при определённых условиях. Вы описываете это поведение в классе, потом создаёте много кнопок в интерфейсе.
Пример: объекты, классы и функции в игре
Допустим, вы пишете компьютерную игру. У вас есть герой, которым управляет игрок, и много врагов, которыми управляет компьютер. И герой, и враги могут стрелять и лечиться.
Если бы вы писали всё на функциях, у вас были бы такие функции:
Герой_выстрелить();
Герой_подлечиться();
Враг1_выстрелить();
…
Враг99_выстрелить();
Враг1_подлечиться();
…
Враг99_подлечиться();
Это довольно большой зоопарк из функций, делать так не надо. Гораздо лучше создать объекты, а внутрь к ним положить нужные функции:
Герой.выстрелить();
Герой.подлечиться();
Враг1.выстрелить();
…
Враг99.подлечиться();
Представим, что каждый персонаж игры — это объект. Внутри объекта что-то лежит
Чтобы не прописывать все эти функции и объекты вручную, мы создадим класс «Персонаж»:
Класс «Персонаж»{
имя: тут будет имя;
тип: герой или враг;
здоровье: 100;
функция «Выстрелить» {тут описываем, как стрелять};
функция «Подлечиться» {тут описываем, как лечиться};
}
Теперь мы можем создавать сколько угодно персонажей, например, так:
Новый Персонаж (имя:Герой);
Новый Персонаж (имя:Враг1, здоровье: 10);
Новый Персонаж (имя:Враг2, здоровье: 20);
Допустим, мы решили добавить в нашу игру систему инвентаря. Чтобы не ходить по всем нашим врагам и героям и не копипастить в них код, мы пропишем эту систему внутри класса:
Класс «Персонаж»{
имя: тут будет имя;
тип: герой или враг;
здоровье: 100;
функция «Выстрелить» {тут описываем, как стрелять};
функция «Подлечиться» {тут описываем, как лечиться};
инвентарь: [сапоги-скороходы, меч-кладенец];
функция «Сбросить_инвентарь» {как сбрасывать};
функция «Подобрать_предмет» {как подбирать};
}
Теперь у всех персонажей появился инвентарь. по умолчанию у всех там лежат сапоги и меч, но при создании персонажа это можно переопределить. Также у всех персонажей появилась возможность сбросить весь инвентарь или подобрать предмет с земли.
Заметьте, что всё это мы добавили в одном месте, а появилось всё сразу у всех. В этом сила класса.
Где применять классы и ООП, а где — функции
Если вы делаете простую программу, которую можно сделать тремя функциями — делайте. Или даже если программа станет сложнее, в ней будет много функций, но все они логично связаны и понятно, почему сделано именно так, — тоже хорошо. Нет ничего плохого в том, что вы не используете объектно-ориентированное программирование там, где можно обойтись без него.
А вот если у вас проект со множеством абстракций, взаимосвязей внутри и вы заранее не можете предсказать, когда что с чем будет взаимодействовать, то лучше использовать классы и все преимущества ООП. Возможно, код станет сложнее, но это не всегда так. С другой стороны, вы сможете реализовать такую логику, которую на функциях было бы сделать непросто.
Классы
Класс – это тип данных, который создается пользователем на основе существующих типов. Одновременно с данными для каждого типа вводится набор операций для обработки этих данных, то есть определяется набор функций, которые задают поведение типа.
В общем виде определение класса записывается так:
class имя
{
public:
protected:
private:
};
Имя класса становится именем нового типа данных, которые называются объектами класса.
Объявление объектов класса:
имя_класса имя_объекта;
После того, как объект объявлен появляется возможность доступа к полям и методам объекта:
С помощью квалифицированных имен
имя объекта.имя класса::имя/поле/метод
. – операция выбора
:: - операция разрешения области видимости
С помощью уточненных имен.
имя объекта.имя поля/метода
Когда метод класса определяется вне класса, то перед именем метода ставится приставка из имени класса и операции разрешения области видимости.
Каждый объект класса имеет точную копию полей, методы у всех объектов общие.
При определении методов класса можно использовать имена полей и методов этого класса, не уточняя их с помощью операции выбора.
Свойства методов класса
Существенное свойство методов класса состоит в том, что им не нужно передавать в качестве формальных параметров поля того же класса.
Метки доступа
Метка | Значение |
public: (общедоступные) | Поля и методы класса доступны для методов данного класса и для всех функций программы, включая главную. |
private: (закрытый) | Поля и методы класса доступны только для методов данного класса и недоступны для других функций программы, включая главную. |
protected: (защищенный) | Поля и методы класса доступны для методов данного класса и классов, производных от него. |
Пример. Создадим класс, в открытой части которого будут 2 поля целого типа, которые можно рассматривать как номер месяца и число в этом месяце и метод для вывода даты на печать.
В главной функции должны быть объявлены 2 объекта класса, объекты должны быть введены с клавиатуры и необходимо выполнить сравнение этих объектов.
#include
using namespace std;
class beta
{
public:
int d; //число
inr m; //месяц
//прототип метода для вывода даты
void inputd();
};
//определение метода для печати даты
void beta::inputd()
{
cout
cout
}
int main()
{
setlocale(LC_ALL, "RUS");
//объявляем 2 объекта класса beta
beta x, y;
//ввод объекта x
coutn Ввести объект x\n";
coutn Ввести число: \n";
cinx.beta::d;
coutn Ввести месяц: \n";
cinx.beta::m;
//ввод объекта y
coutn Ввести объект y\n";
coutn Ввести число: \n";
ciny.beta::d;
coutn Ввести месяц: \n";
ciny.beta::m;
//вывести объект x
coutn Объект x\n";
x.beta::inputd();
//вывести объект y
coutn Объект y\n";
y.beta::inputd();
//сравнение объектов
if (x.d==y.d && x.m==y.m)
coutn Даты одинаковые";
else coutn Даты разные"
coutendl;
}
Открытые и закрытые элементы
При идеальном определении класса все поля классы должны быть закрытыми. Это означает, что в программе они могут использоваться только в определениях методов класса и больше нигде.
Если закрыть поля класса, то для того, чтобы обратиться к ним в главной функции и других функциях программы, в программу обязательно следует добавить методы доступа к закрытым переменным.
Метод доступа возвращает либо само закрытое поле, либо что-то эквивалентное ему.
Пример. Переопределим класс beta, сделав поля класса закрытыми.
Если мы поля класса закроем, то в главной функции мы их с клавиатуры ввести не сможем, поэтому в класс необходимо добавить методы для инициализации закрытых полей. Это может быть один метод для инициализации сразу двух полей либо 2 метода для инициализации каждого поля.
#include
using namespace std;
class beta
{
private:
int d; //число
int m; //месяц
public:
//прототип метода доступа к полю m
int getm();
//прототип метода доступа к полю d
int getd();
//прототип метода инициализации полей
void set(int nm, int nd);
//прототип метода для печати объекта
void outputd();
};
//определение метода доступа к полю m
int beta::getm()
{
return m;
}
//определение метода доступа к полю d
int beta::getd()
{
return d;
}
//определение метода для инициализации полей
void beta::set(int nm, int nd)
{
m=nm;
d=nd;
}
//определение метода вывода объекта
void beta::outputd()
{
coutnЧисло "dm;
}
int main()
{
setlocale(LC_ALL, "RUS");
//объявляем 2 объекта класса beta
beta x, y;
//инициализируем объект x
x.set(7, 8);
coutОбъект x\n";
x.outputd();
//инициализируем объект y
y.set(10, 10);
coutОбъект y\n";
y.outputd();
//сравнение
if (x.getm()==y.getm() && x.getd()==y.getd())
cout
else cout
cout
return 0;
}
Домашнее задание:
Описать любой класс, на Ваш выбор (машина, компьютер, телефон и т.п.) на естественном языке, указать свойства и методы класса.
Описать этот же класс на C++.