Объекты и классы
Класс является одним из самых крупных и важных инструментов языка 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 секунда. Нужно продублировать все имеющиеся переменные и выполняемый код. Чтобы не запутаться в именах переменных, нам придётся их пронумеровать.
[su_spoiler title="Мигаем двумя светодиодами" open="no" style="fancy" icon="arrow"]
byte pin1 = 13, pin2 = 12; uint32_t tmr1, tmr2; uint16_t prd1 = 500, prd2 = 1000; bool flag1, flag2; void setup() { pinMode(pin1, OUTPUT); pinMode(pin2, OUTPUT); } void loop() { if (millis() - tmr1 >= prd1) { tmr1 = millis(); flag1 = !flag1; digitalWrite(pin1, flag1); } if (millis() - tmr2 >= prd2) { tmr2 = millis(); flag2 = !flag2; digitalWrite(pin2, flag2); } }
[/su_spoiler]
Отлично! А если нужно добавить ещё 10 светодиодов? Да, можно оптимизировать программу в таком виде как она есть, добавить массив таймеров и использовать циклы, но этот урок - про классы. Поэтому давайте обернём всю подпрограмму "мигающего светодиода" в отдельный независимый класс.
Напишем заготовку и сразу разместим все переменные в приватной области: прямой доступ к ним из программы нам не нужен.
class LED { public: private: byte _pin; uint32_t _tmr; uint16_t _prd; bool _flag; };
Разместим данный код в самом верху скетча, до блоков setup()
и loop()
.
На данный момент мы уже можем создать объекты двух наших светодиодов:
LED led1; LED led2;
[su_spoiler title="Полный код программы" open="no" style="fancy" icon="arrow"]
class LED { public: private: byte pin = 13; uint32_t tmr; uint16_t prd = 500; bool flag; }; LED led1; LED led2; void setup() { } void loop() { }
[/su_spoiler]
Теперь в нашей программе есть два объекта, 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(); }
[su_spoiler title="Полный код программы" open="no" style="fancy" icon="arrow"]
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; }; LED led1(13, 500); LED led2(12, 1000); void setup() { } void loop() { led1.blink(); led2.blink(); }
[/su_spoiler]
Что делать, если захочется изменить период мигания во время работы? Есть два варианта:
- Сделать переменную периода публичной и обращаться к ней из программы напрямую
- Сделать отдельный метод для изменения этой переменной
Чаще всего используют второй вариант, чтобы вообще не оперировать в программе переменными, а использовать только методы. Добавим в класс функцию:
void setPrd(uint16_t prd) { _prd = prd; }
И сможем менять период мигания, например так:
void setup() { led1.setPrd(200); }
Ну вот, мы с вами на практике разобрали создание класса. Теперь можно переместить весь код класса в отдельный файл, назвать его например LED.h, поместить рядом со скетчем, а затем подключить в программу как #include "LED.h"
. Наша программа стала компактнее, в ней нет кучи глобальных переменных и повторяющихся блоков кода. Более того, программа стала занимать меньше памяти, потому что одинаковые конструкции теперь представлены одним блоком кода, который работает для всех объектов одинаково.
[su_spoiler title="LED.h" open="no" style="fancy" icon="arrow"]
class LED { public: LED(byte pin, uint16_t prd) { _pin = pin; _prd = prd; pinMode(_pin, OUTPUT); } void setPrd(uint16_t prd) { _prd = prd; } void blink() { if (millis() - _tmr >= _prd) { _tmr = millis(); _flag = !_flag; digitalWrite(_pin, _flag); } } private: byte _pin; uint32_t _tmr; uint16_t _prd; bool _flag; };
[/su_spoiler][su_spoiler title="Скетч" open="no" style="fancy" icon="arrow"]
#include "LED.h" LED led1(13, 500); LED led2(12, 1000); void setup() { led1.setPrd(200); } void loop() { led1.blink(); led2.blink(); }
[/su_spoiler]
Да, фактически мы сделали свою собственную библиотеку! Оформление библиотек и некоторые другие трюки с классами мы разберём в следующем уроке про написание библиотек.
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])