Класс


Класс является одним из самых крупных и важных понятий и инструментов языка C++, именно он делает язык объектно-ориентированным и очень мощным. Мы очень часто пользуемся объектами и методами, ведь 99% библиотек являются просто классами! Объекты, методы, что это? Я могу привести несколько официальных определений (хотя вы сами можете их загуглить), но не буду, потому что они очень абстрактные и ещё сильнее вас запутают. Давайте рассмотрим всё на примере библиотеки, пусть это будет стандартная и всем знакомая библиотека Servo. Возьмём из неё пример Knob и разберёмся, где кто и как называется.

#include <Servo.h>  // подключаем заголовочный ФАЙЛ библиотеки, Servo.h

Servo myservo;  // создаём ОБЪЕКТ myservo КЛАССА Servo

int potpin = 0;  // analog pin used to connect the potentiometer
int val;    // variable to read the value from the analog pin

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);
}

Итак, мы можем создать объект класса, и применять к нему методы. Какую первую мысль вызывает использование объекта? Правильно, “ой, как удобно“! Ведь мы можем создать несколько объектов Servo, и управлять каждым в отдельности при помощи одинаковых методов, но каждый объект будет обладать индивидуальным набором настроек, которые хранятся где-то внутри него. Это и есть объектно-ориентированный подход, позволяющий создавать очень сложные многоуровневые программы, не испытывая особых трудностей.

Классы очень похожи на структуры, как по объявлению, так и по использованию, но класс является гораздо более мощной единицей языка: если в структуре мы под одним именем храним переменные разных типов, то в классе мы храним не только переменные, но и собственные функции класса. Функции внутри класса кстати и называются методами.

Внутри класса


Ну чтож, давайте заглянем во внутрь класса и посмотрим, как там всё устроено! Начнём с того, как объявляется класс: при помощи ключевого слова class

class /*имя класса*/  // имя класса принято писать с Большой Буквы
{
  private:
  // список свойств и методов для использования внутри класса
  public:
  // список методов доступных другим функциям и объектам программы
  protected:
  // список средств, доступных при наследовании
};

Очень похоже на структуру (struct), правда? Так и есть, и создаётся объект точно так же – по “ярлыку”:

<название класса> <имя объекта>;      // создать объект
<название класса> <имя объекта>[10];  // создать массив объектов

Вы не поверите, обращение к методам и параметрам класса осуществляется точно так же, как в структурах: через оператор точка  . :

<имя_объекта>.<метод>();     // вызов метода
<имя_объекта>[<номер>].<метод>();  // вызов метода для объекта из массива объектов

Теперь давайте заглянем “под капот” класса Servo и сравним теорию с практикой:

Что такое public и private? Это спецификаторы доступа к членам класса. Открытые члены (public) — это члены структуры или класса, к которым можно получить доступ извне этой же структуры или класса (из скетча например). Закрытые члены (private) — это члены класса, доступ к которым имеют только другие члены этого же класса (только внутри класса). Есть ещё protected, но мы его рассматривать не будем, т.к. он вам вряд ли пригодится. Если освоите всё остальное – читайте про наследование классов, тема обширная и очень сложная.

Собственно мы видим в “публичном доступе” все те методы, которыми можно пользоваться при работе с Servo. Это очень удобно, потому что не нужно гуглить искать документацию – всё написано в описании класса. Методы объявляются точно так же, как обыкновенные функции, мы подробно обсуждали это в предыдущем уроке. Закрытые члены в классе серво – переменные, из их названий можно понять, что они в себе хранят. Доступа к этим переменным “из скетча” мы не имеем, читать эти переменные могут только методы класса.

Также на картинке выше вы могли увидеть слово конструктор – это ещё одна штука, которая может находиться в классе. Конструктор позволяет инициализировать закрытые параметры при создании объекта, и вообще по сути: конструктор – это функция, вызываемая в момент создания объекта.

Давайте создадим свой класс, и на его примере разберём некоторые особенности.

Пишем класс


Я стараюсь соблюдать последовательность изложения материала, поэтому в этом уроке мы постараемся не использовать того, чего ещё не было в предыдущих. Полноценный пример по созданию класса-библиотеки выйдет отдельно, и там мы уже не будем себя ограничивать. Так что давайте сделаем класс, который хранит в себе цвет и яркость как закрытые переменные, и позволяет при помощи методов этот цвет/яркость получать или изменять.

class Color {     // класс Color
  public:
    Color();      // конструктор
  private:
    byte _color;  // переменная цвета
    byte _bright; // переменная яркости
};

Мы создали класс под названием Color, в котором есть конструктор и переменные _color и _bright. Что важно знать:

  • Имя класса принято писать с большой буквы, чтобы отделить от переменных (которые, напомню, принято писать с маленькой)
  • Имя конструктора должно совпадать с именем его класса (регистр важен!)
  • Закрытые переменные принято называть, начиная с нижнего подчёркивания, _color у нас
  • Конструктора может не быть, тогда компилятор сам его создаст, название будет совпадать с названием класса

Продолжаем. Давайте сделаем так, чтобы при создании объекта можно было указать значение _color. Для этого нам понадобится конструктор, который принимает параметры. То есть, как обычная функция:

class Color {   // класс Color
  public:
    Color(byte color) { // конструктор
      _color = color;   // запоминаем
    }
  private:
    byte _color;  // переменная цвета
    byte _bright; // переменная яркости
};

Color myColor(10);  // создаём объект myColor, указав значение

Мы сделали конструктор функцией, которая принимает параметр типа byte, и присваивает его в переменную класса _color. Мы написали реализацию функции внутри класса, это важно, потому что это можно сделать и снаружи. После вызова конструктора (создания объекта) переменная color удаляется из памяти (она была локальная), но её значение остаётся в нашей _color. Хорошо. Давайте позволим пользователю установить также яркость при создании объекта. Здесь нам поможет знание о перегруженных функциях из прошлого урока.

class Color {   // класс Color
  public:
    Color(byte color) { // конструктор
      _color = color;   // запоминаем
    }
    Color(byte color, byte bright) { // конструктор
      _color = color;   // запоминаем
      _bright = bright;
    }
  private:
    byte _color;  // переменная цвета
    byte _bright; // переменная яркости
};

Color myColor(10);  // создаём объект myColor, указав значение
Color myColor2(10, 20);  // указываем цвет и яркость!

Теперь у нас два конструктора, и при создании объекта программа сама выберет, какой использовать. Ну и раз пошла такая пьянка, вернём наш пустой конструктор, чтобы можно было создать объект без инициализации параметров:

class Color {   // класс Color
  public:
    Color();
    Color(byte color) { // конструктор
      _color = color;   // запоминаем
    }
    Color(byte color, byte bright) { // конструктор
      _color = color;   // запоминаем
      _bright = bright;
    }
  private:
    byte _color;  // переменная цвета
    byte _bright; // переменная яркости
};

Color myColor(10);  // создаём объект myColor, указав значение
Color myColor2(10, 20);  // указываем цвет и яркость!
Color myColor3();  // без инициализации (скобки нужны!)

И создадим сразу объект, нам не жалко. А вот такую кучу конструкторов наплодили – жалко! И тут нам поможет такая штука, как инициализация внутри конструктора. Смотрите, как это работает:

class Color {   // класс Color
  public:
    Color(byte color = 5, byte bright = 30) { // конструктор
      _color = color;   // запоминаем
      _bright = bright;
    }
  private:
    byte _color;  // переменная цвета
    byte _bright; // переменная яркости
};

Color myColor(10);  // создаём объект myColor, указав _color (получим 10, 30)
Color myColor2(10, 20);  // указываем цвет и яркость! (получим 10, 20)
Color myColor3;  // без инициализации (получим 5, 30)

Один конструктор, в котором через оператор = поставлена инициализация значения локальной переменной, если она не передаётся как параметр. То есть, вызвав конструктор при создании myColor3() мы не передали параметров, и конструктор взял параметры по умолчанию, 5 и 30, и приравнял их в цвет и яркость. Также обратите внимание, что при создании myColor3 мы не ставим круглые скобки, т.к. конструктор у нас универсальный! При создании myColor(10) мы передали только цвет, 10, а яркость автоматически установилась в 30. А как не указать цвет, но указать яркость? А вот уже никак =) Только создавать новый конструктор.

Давайте добавим ещё два метода – для установки и чтения текущих значений. Тут всё просто – установка по аналогии с конструктором, а чтение – просто return:

class Color {   // класс Color
  public:
    Color(byte color = 5, byte bright = 30) { // конструктор
      _color = color;   // запоминаем
      _bright = bright;
    }
    void setColor(byte color) {_color = color;}
    void setBright(byte bright) {_bright = bright;}
    byte getColor() {return _color;}
    byte getBright() {return _bright;}

  private:
    byte _color;  // переменная цвета
    byte _bright; // переменная яркости
};

Color myColor(10);  // создаём объект myColor, указав _color (получим 10, 30)
Color myColor2(10, 20);  // указываем цвет и яркость! (получим 10, 20)
Color myColor3;  // без инициализации (получим 5, 30)

Теперь при вызове например myColor2.getColor() мы получим значение 10, как и установили при инициализации. Если вызовем myColor2.setColor(50), то присвоим закрытой переменной _color объекта myColor2 значение 50. При дальнейшем вызове myColor2.getColor() мы получим уже 50. И точно так же это работает с остальными объектами и методом set/get Bright, который мы написали. Можете поиграться и проверить, выводя данные в порт (об этом у меня как раз следующий урок).

Что тут ещё хочется добавить: не всегда удобно писать реализацию метода внутри класса, получается очень громоздко и класс перестаёт быть документацией к самому себе. Вспомните библиотеку Servo – методы объявлены, но расписаны где-то в другом месте! Это сделано в файле .cpp, об этом поговорим в уроке о создании библиотек. Сейчас давайте распишем реализацию методов вне класса – описание методов оставим внутри класса, а реализацию распишем “ниже”, в итоге класс останется чистеньким и наглядным:

// описание класса
class Color {   // класс Color
  public:
    Color(byte color = 5, byte bright = 30);
    void setColor(byte color);
    void setBright(byte bright);
    byte getColor();
    byte getBright();
  private:
    byte _color;  // переменная цвета
    byte _bright; // переменная яркости
};

// реализация методов
Color::Color(byte color = 5, byte bright = 30) { // конструктор
  _color = color;   // запоминаем
  _bright = bright;
}
void Color::setColor(byte color) {_color = color;}
void Color::setBright(byte bright) {_bright = bright;}
byte Color::getColor() {return _color;}
byte Color::getBright() {return _bright;}

Color myColor(10);  // создаём объект myColor, указав _color (получим 10, 30)
Color myColor2(10, 20);  // указываем цвет и яркость! (получим 10, 20)
Color myColor3;  // без инициализации (получим 5, 30)

Оу, что за двойное двоеточие? Двойное двоеточие :: является оператором, который уточняет область видимости имени, к которому применяется. Написав void Color::setColor… мы сообщили компилятору, что именно эта функция (метод) setColor относится к классу Color, и является собственно реализацией описанного там метода. Это означает, что у вас может быть и другая функция с таким же названием, но не относящаяся к классу Color, это очень удобно. Например вот такое пополнение не приведёт к ошибке, потому что мы объяснили компилятору, что к чему относится: первая функция – к классу Color, вторая – ни к чему, просто функция в этом документе:

void Color::setColor(byte color) {_color = color;}   // относится к классу Color
void setColor(byte color) {return color;}  // просто какая-то функция

Итак, мы с вами поэтапно создали класс и изучили большую часть особенностей работы с классами. На этом завершается раздел программирования, и начинается раздел базовых уроков Ардуино. А к классам мы ещё вернёмся, когда будем писать свою собственную библиотеку!

Важные страницы


  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
Последнее обновление Сентябрь 03, 2019
2019-09-03T21:09:47+03:00