Объекты и классы


Класс является одним из самых крупных и важных инструментов языка 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 секунда. Нужно продублировать все имеющиеся переменные и выполняемый код. Чтобы не запутаться в именах переменных, нам придётся их пронумеровать.

Мигаем двумя светодиодами
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);
  }
}

Отлично! А если нужно добавить ещё 10 светодиодов? Да, можно оптимизировать программу в таком виде как она есть, добавить массив таймеров и использовать циклы, но этот урок – про классы. Поэтому давайте обернём всю подпрограмму “мигающего светодиода” в отдельный независимый класс.

Напишем заготовку и сразу разместим все переменные в приватной области: прямой доступ к ним из программы нам не нужен.

class LED {
  public:

  private:
    byte _pin;
    uint32_t _tmr;
    uint16_t _prd;
    bool _flag;
};

Разместим данный код в самом верху скетча, до блоков setup() и loop().

На данный момент мы уже можем создать объекты двух наших светодиодов:

LED led1;
LED led2;
Полный код программы
class LED {
  public:

  private:
    byte pin = 13;
    uint32_t tmr;
    uint16_t prd = 500;
    bool flag;
};

LED led1;
LED led2;

void setup() {
}

void loop() {
}

Теперь в нашей программе есть два объекта, 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();
}
Полный код программы
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();
}

Что делать, если захочется изменить период мигания во время работы? Есть два варианта:

  • Сделать переменную периода публичной и обращаться к ней из программы напрямую
  • Сделать отдельный метод для изменения этой переменной

Чаще всего используют второй вариант, чтобы вообще не оперировать в программе переменными, а использовать только методы. Добавим в класс функцию:

void setPrd(uint16_t prd) {
  _prd = prd;
}

И сможем менять период мигания, например так:

void setup() {
  led1.setPrd(200);
}

Ну вот, мы с вами на практике разобрали создание класса. Теперь можно переместить весь код класса в отдельный файл, назвать его например LED.h, поместить рядом со скетчем, а затем подключить в программу как #include "LED.h". Наша программа стала компактнее, в ней нет кучи глобальных переменных и повторяющихся блоков кода. Более того, программа стала занимать меньше памяти, потому что одинаковые конструкции теперь представлены одним блоком кода, который работает для всех объектов одинаково.

LED.h
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;
};
Скетч
#include "LED.h"

LED led1(13, 500);
LED led2(12, 1000);

void setup() {
  led1.setPrd(200);
}

void loop() {
  led1.blink();
  led2.blink();
}

Да, фактически мы сделали свою собственную библиотеку! Оформление библиотек и некоторые другие трюки с классами мы разберём в следующем уроке про написание библиотек.

Полезные страницы


  • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
  • Поддержать автора за работу над уроками
  • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту (alex@alexgyver.ru)
5/5 - (16 голосов)
Назад Структуры и перечисления
Вперёд Пишем свою библиотеку
Подписаться
Уведомить о
guest
18 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии