Объекты и классы
Класс является одним из самых крупных и важных инструментов языка C++, именно он делает язык объектно-ориентированным и очень мощным. Именно благодаря добавлению классов в язык Си и появился язык C++, его даже называют "Си с классами". Вы уже сталкивались с классами, потому что 99% библиотек состоят из класса, описанного в отдельном файле! Некоторые встроенные инструменты Arduino также являются объектами, например Serial
.
Класс - это просто независимая подпрограмма, в которой есть свой набор переменных и функций (обязательно изучи урок про функции). В основной программе мы можем создать экземпляр класса (он же называется объект) и пользоваться теми инструментами, которые имеются в классе. Использование классов позволяет:
- Разделить сложную программу на отдельные независимые части
- Создавать удобные библиотеки
- Использовать свои "наработки" в другом проекте, не переписывая один и тот же код
- Облегчить и упростить программу, если в ней используются повторяющиеся конструкции и алгоритмы
Давайте рассмотрим пример из библиотеки, пусть это будет стандартная и всем знакомая библиотека Servo. Возьмём из неё пример Knob и разберёмся, кто и как называется:
#include <Servo.h> // подключаем заголовочный ФАЙЛ библиотеки, Servo.h Servo myservo; // создаём ОБЪЕКТ myservo КЛАССА Servo int potpin = 0; int val; void setup() { myservo.attach(9); // применяем МЕТОД attach к ОБЪЕКТУ myservo } void loop() { val = analogRead(potpin); val = map(val, 0, 1023, 0, 180); myservo.write(val); // применяем МЕТОД write к ОБЪЕКТУ myservo delay(15); }
Мы создали объект myservo
класса Servo
и можем управлять углом сервопривода при помощи функции write()
- она применяется к объекту через точку .
. Функция, применяемая к объекту, называется методом.
Класс является самодостаточной единицей программы - он содержит переменные и функции, которые с ними взаимодействуют. Все возможности класса и взаимодействие с его данными доступны в его функциях и не требуют дополнительного кода.
Создание класса
Классы очень похожи на структуры (читай урок по структурам), как по объявлению, так и по использованию, но класс является гораздо более мощной единицей языка благодаря механизмам наследования и прочим ООП-штукам.
Класс объявляется при помощи ключевого слова class
и содержит внутри себя члены класса - переменные и функции:
class Имя_класса { член1; член2; };
Важное отличие от структуры: содержимое класса делится на области: публичные и приватные. Они определяются при помощи ключевых слов public
и private
, область действует до начала следующей области или до закрывающей фигурной скобки класса:
public
- члены класса в этой области доступны для взаимодействия из основной программы (скетча), в которой будет создан объект. Напримерwrite()
у серво.private
- члены класса в этой области доступны только внутри класса, то есть из программы к ним обратиться нельзя.
class Имя_класса { public: // список членов, доступных в программе private: // список членов для использования внутри класса };
В классе должна быть указана хотя бы одна область. Например только public
.
Теперь давайте заглянем "под капот" класса Servo и сравним теорию с практикой:
Собственно мы видим в "публичном доступе" все те методы, которыми можно пользоваться при работе с Servo. Это очень удобно, потому что не нужно гуглить искать документацию - всё написано в описании класса. Методы объявляются точно так же, как обыкновенные функции (читай урок про функции).
Приватные члены в классе серво - переменные, из их названий можно понять, что они в себе хранят. Доступа к этим переменным "из скетча" мы не имеем, обращаться к этим переменным могут только методы класса.
Также на картинке выше вы могли увидеть слово конструктор - это функция, которая вызывается в момент создания объекта и имеет такое же имя, как сам класс. Может использоваться для инициализации переменных, передачи настроек и так далее.
Создание объекта
Создаётся объект точно так же, как структура - по имени класса:
название_класса имя_объекта; // создать объект название_класса имя_объекта[10]; // создать массив объектов
Обращение к членам класса осуществляется точно так же, как в структурах: через оператор точка .
:
имя_объекта.метод(); // вызов метода имя_объекта[номер].метод(); // вызов метода для объекта из массива объектов
Пишем свой класс
Рассмотрим абстрактный пример: мигающий светодиод. Мигать будем по таймеру на миллис (читай урок про многозадачность). Нам понадобятся следующие переменные:
- Переменная для хранения номера пина
- Переменная таймера
- Переменная для хранения периода
- Флаг текущего состояния светодиода
Необходимый код:
- Нужно сделать пин выходом при запуске программы, внутри
setup()
- В основном цикле программы
loop()
будет конструкция таймера, внутри которой светодиод переключается
Итоговая программа, которая асинхронно мигает светодиодом на 13 пине с периодом 500 миллисекунд:
byte pin = 13; uint32_t tmr; uint16_t prd = 500; bool flag; void setup() { pinMode(pin, OUTPUT); } void loop() { if (millis() - tmr >= prd) { tmr = millis(); flag = !flag; digitalWrite(pin, flag); } }
Всё хорошо, программа работает, светодиод мигает. Далее нам захотелось добавить второй светодиод на пин 12 и сделать с ним всё то же самое, но мигать с периодом 1 секунда. Нужно продублировать все имеющиеся переменные и выполняемый код. Чтобы не запутаться в именах переменных, нам придётся их пронумеровать.
Отлично! А если нужно добавить ещё 10 светодиодов? Да, можно оптимизировать программу в таком виде как она есть, добавить массив таймеров и использовать циклы, но этот урок - про классы. Поэтому давайте обернём всю подпрограмму "мигающего светодиода" в отдельный независимый класс.
Напишем заготовку и сразу разместим все переменные в приватной области: прямой доступ к ним из программы нам не нужен.
class LED { public: private: byte _pin; uint32_t _tmr; uint16_t _prd; bool _flag; };
Разместим данный код в самом верху скетча, до блоков setup()
и loop()
.
На данный момент мы уже можем создать объекты двух наших светодиодов:
LED led1; LED led2;
Теперь в нашей программе есть два объекта, led1
и led2
, каждый хранит в себе набор переменных из класса. Первым делом нужно присвоить значения жизненно необходимым переменным - это номер пина и период работы. Эти параметры являются основными для нашего мигающего светодиода и будут отличаться у разных светодиодов, поэтому было бы удобно задавать их сразу при создании объекта.
Здесь на помощь придёт конструктор - функция, которая будет вызвана при создании объекта. Она должна иметь такое же название, как и сам класс. Давайте передадим в неё номер пина и период, а затем запишем их во внутренние переменные. Также в конструкторе сразу можно сделать пин выходом!
class LED { public: LED(byte pin, uint16_t prd) { _pin = pin; _prd = prd; pinMode(_pin, OUTPUT); } private: byte _pin; uint32_t _tmr; uint16_t _prd; bool _flag; };
Теперь создание объектов можно переписать. Укажем параметры из первого примера:
LED led1(13, 500); LED led2(12, 1000);
Готово, значения записаны. Осталось реализовать функцию мигания: назовём её blink()
и поместим в неё конструкцию из самого первого примера:
class LED { public: LED(byte pin, uint16_t prd) { _pin = pin; _prd = prd; pinMode(_pin, OUTPUT); } void blink() { if (millis() - _tmr >= _prd) { _tmr = millis(); _flag = !_flag; digitalWrite(_pin, _flag); } } private: byte _pin; uint32_t _tmr; uint16_t _prd; bool _flag; };
Теперь в главном цикле программы нам достаточно вызывать эти методы у обоих объектов:
void loop() { led1.blink(); led2.blink(); }
Что делать, если захочется изменить период мигания во время работы? Есть два варианта:
- Сделать переменную периода публичной и обращаться к ней из программы напрямую
- Сделать отдельный метод для изменения этой переменной
Чаще всего используют второй вариант, чтобы вообще не оперировать в программе переменными, а использовать только методы. Добавим в класс функцию:
void setPrd(uint16_t prd) { _prd = prd; }
И сможем менять период мигания, например так:
void setup() { led1.setPrd(200); }
Ну вот, мы с вами на практике разобрали создание класса. Теперь можно переместить весь код класса в отдельный файл, назвать его например LED.h, поместить рядом со скетчем, а затем подключить в программу как #include "LED.h"
. Наша программа стала компактнее, в ней нет кучи глобальных переменных и повторяющихся блоков кода. Более того, программа стала занимать меньше памяти, потому что одинаковые конструкции теперь представлены одним блоком кода, который работает для всех объектов одинаково.
Да, фактически мы сделали свою собственную библиотеку! Оформление библиотек и некоторые другие трюки с классами мы разберём в следующем уроке про написание библиотек.
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])