Ах, эти страшные, сложные, милые, простые классы.

Тема в разделе "Флудилка", создана пользователем DetSimen, 16 янв 2018.

  1. DetSimen

    DetSimen Guest

    Эпиграф: Не бойтесь писать классами... (ударение каждый поставит где нраица)

    Это типа эссе для новичков, зеленых новичков и новичков вапще с ног до головы облитых зелёнкой. Главное в новичке - любознательность и желание задумываться и учиться, а не уровень громкости вопля "ПАМАГИТИ!!!!" на форуме.
    Сразу скажу, я вообще ни разу не профессиональный С++ программист, по образованию я - физик, по профессии - олкаголег, по хобби - дачник, по призванию и по жизни- лентяй. Последнее я умею делать профессионально (сам удивляюсь, как до полтинника дожил и жена не убила).

    Это просто попытка такого же, как и вы, полудилетанта, рассказать простыми словами, что не надо бояться классов, не так страшен квадрат, как его Малевич. (на самом деле надо бояться классов, но только таких, как у quonе). Не претендуя на глубину изложения классовой модели С++, я постараюсь рассказать только то, что может быть полезно для простых и не очень поделок начинающего Дуниста.

    Класс это очень удобная, самодостаточная, законченная модель программного (и не только) объекта. Аналог в реальной жизни - да любая вещь, сделанная человеком: автомобиль, настольная лампа, часы, телевизор, да даже родной/любимый собутыльник стол-книшка, и тот является объектом. У каждой вещи есть функция, которую она исполняет, для чего её сопсно сделали, а потом и купили, и есть какой-то, где-то более простой, как в часах, где-то сложный, как в автомобиле, интерфейс взаимодействия с человеком. Каждый класс, как и вещь, должен быть самодостаточным, законченным изделием. Думаю, было бы не очень удобно, допустим, для того, чтобы подкорректировать время в настенных часах, идти, выдирать блок кнопок из стиралки, чтобы подключить их к часам для настройки. А для того, чтобы стиралка знала, сколько времени стирать, разбирать часы и вставлять оттуда в нее блок подсчета времени. А потом - обратно. Так что первое правило класса - хранить в себе все данные своего обьекта, не завися ни от чего снаружи.

    Второе правило класса (и вещи) - скрытность. Все то, что не желательно давать настраивать шаловливым ручкам пользователей - должно быть скрыто. Согласитесь, немногие из нас знают, что в телевизоре есть приемный тракт, усилительный тракт, блок разверток и всякие остальные лампы подсветки, нам это не интересно, у нас есть пульт и мы хотим нажать на нем кнопку и смотреть любимых телепузиков под пиво, лёжа на диване, не задумываясь, как там идет сигнал, откуда он берется и как выводится на экран. Всё это от нас скрыто. Если б мне каждый раз перед просмотром телека приходилось по полчаса вручную настраивать частоту строк, частоту кадров, размер экрана и (не дай Бог) подстраивать трепецеидальные искажения, я б на третий раз просто расколотил бы его нахрен без всякого сожаления. Ну, может быть всплакнул бы потом и, скорее всего, напился. И всё. Наружу у телека должно торчать только несколько кнопок и ИК датчик, через которые я буду им командовать. Так и в классе - пользователю должны быть доступны ровно те данные, которые требуются для правильной работы самого класса, для исполнения им своего предназначения, ну еще может быть чучуть для тонкой подстройки.

    Теперь рассмотрим что такое свойства и что такое методы. Возьмем, например, тот же любимый стол-книшку, за которым так удобно сидеть на веранде, когда придут гости и вести задушевные разговоры после бани под ледяную водочку. У стола есть физические свойства, длина, высота, ширина и т.д, некоторые из которых зависят от того, разложен стол или нет. У класса такие свойства называются поля и, обычно, называются именами существительными, отвечающим на вопрос "Что?" - "длина", "номер пина", "время" и т.д.
    Так же у класса должны быть "методы" для работы с полями. Например, приходят гости, мы выкатываем из дальнего угла запыленный стол, применяем к нему методы "Протереть", "Разложить" и "Накрыть" и хоба!, через некоторое время уже можно за ним спать, уютненько устроившись буйной головушкой в мягенькой, вкуснющей селедке под шубой. Стол свою функцию выполнил. Можно применять к нему методы "Сложить" и "Убрать". Заметим, что в методах "Сложить" и "Разложить" внутри автоматически меняется свойство (поле) "Длина". То бишь, методы - это глаголы, означающие действия ("Что делать?/что сделать?") над полями класса, открытыми, так и спрятанными глубоко унутре (а там еще и неонка).

    Но хватит демагогии, допустим, у нас есть какой-нибудь аналоговый сенсор, это может быть даччик влажности в цветочном горшке, или простой фоторезистор, а может быть вершина китайского сумасшедшего гения - датчик серии MQ-2 - 135. Суть аналогового датчика проста и понятна, измерять на определенном аналоговом пине напряжение и отдавать его программе, поэтому плодить каждый раз классы отдельно для фоторезистора, отдельно для MQ-2 или датчика влажности, смысла, я думаю, никакого нет, можно просто написать класс "Аналоговый сенсор" и использовать его спокойно потом со всеми шилдами или самопальными платами на своё усмотрение. Главное, работа с этим типом сенсоров будет единообразной и упорядоченной. Дак давайте этим и займемся.

    Чтобы создать свой класс перво-наперво надо создать 2 файла, заголовочный .h файл и файл реализации .срр (мы ж крутые, будем писать классы на лживом С++ по взрослому). .h файл в С++ рассматривается как своего рода "Протокол о намерениях" и содержит только ОБЪЯВЛЕНИЕ класса, то бишь описание всех его полей и методов скрытых и открытых, каэ это мы видим и которые хотим использовать в своем классе. Еще там иногда объявляются всякие вспомогательные типы, которые в этом классе могут понадобиться и которые будут видны снаружи, мы и до них дойдём, но потом. Директива #include "имяфайла.h" несколько туповата, и просто вставляет весь текст файла целиком в то место где она написана, поэтому, если h файл используется в разных частях проекта, то он будет вставлен в итоговый текст программы ровно столько раз, сколько встретился в директиве #include, что приведет к ошибке множественных деклараций. Чтоб этого не случилось, настоящие C программеры пишут в каждом h файле примерно следующее

    Код (C++):
    #ifndef имяфайла_H      // если не определен символ имяфайла_H
    #define имяфайла_H      // перво наперво определить его

       потом здесь пишут сопсно все свои обьявления и заканчивают файл так

    #endif             // конец ifndef
    Это позволяет включить h файл в результирующий текст программы всего один раз, когда встретилась директива #include. Вторая и последующая директивы #include не возымеют никакого действия, ибо символ "имяфайла_H" уже определен и все последующие строчки до #endif тупо пропускаются. Как по мне, это очень громоздкая конструкция, я ее не люблю и не использую, и выкидываю из всех библиотек, которые иногда приходится править, а чтобы h файл несколько раз не включался в текст программы добрые люди придумали директиву

    #pragma once

    которая делает всё тоже самое, что и предыдущая громоздкая конструкция, только не в пример легче запоминается. Используем её.

    Ну вот, настало время определиться с именем нашего класса и с именами файлов. Я, еще со времен Турбо-Паскаля, привык именовать все свои классы начиная с буквы T (type), а указатели на них - начиная с буквы P (pointer), так сложнее запутаться, ну для меня конечно. Я вас, естесственно, ни к чему не принуждаю, каждый волен сам выбирать систему имен для своих программ, но здесь я буду делать как привык. Итак, мы делаем класс для аналогового даччика, поэтому логично обозвать его TSensor, что мы и сделаем. Соответственно, заголовочный файл у нас будет TSensor.h а файл реализации TSensor.cpp.

    Создадим их в любом текстовом редакторе, хоть в Arduino IDE, потом откроем заголовочный h файл и прям первыми строками напишем:

    Код (C++):
    #pragma once
    #include "Arduino.h"

    потом поместим гордо строчку

    class TSensor
    {
    }
    и глубоко задумаемся. На самом деле нас уже есть с чем поздравить, мы только что обьявили реально работающий, синтаксически верный, хоть и пустой класс. Но в этом виде он абсолютно бессмысленен и умный (не чета мне) компилятор скорее всего даже выкинет его из результирующего текста программы. Чтоб класс стал полезен, пришло время подумать, что же мы хотим видеть у нашего сенсора, какие у него должны быть поля (свойства) и какие методы (действия над ними). Первое, что характеризует сенсор - номер пина, куда он подключен. Номер пина достаточно задать 1 раз при создании класса и не давать пользователю его изменять во время выполнения программы, ибо я считаю, что никто, обладающий хотя бы спинным мозгом, не полезет на-лету перетыкать сенсор в другой пин на включенной Ардуине. Да и ардуина скорее всего на такое абидица и перестанет отвечать. Значить, по правилам хорошего класса, номер пина должен быть скрытым полем. В С++ скрытые поля создаются директивами private и protected. Я буду использовать protected, пока без объяснений, потом, когда вы более плотно начнете разбираться с классами, вы поймете чем они отличаются, но на сейчас, нам достаточно знать, что protected менее строго ограничивает доступ к полям и методам, а private - более строго, но нам сойдёт и protected.
    Интересующимся: гугаль пока в Роисси не заблокировали.
     
    Последнее редактирование модератором: 16 янв 2018
    Kolyn, IvanUA, CYITEP_BAC9I и 5 другим нравится это.
  2. DetSimen

    DetSimen Guest

    Итак:

    Код (C++):
    class TSensor
    {
    protected:
      byte fpin;   // поле размером байт "номер пина", скрытый от пользователя
    }
    так как пинов в ардуине существенно меньше 255, то все их номера прекрасно помещаются в переменную типа байт (еще и место останется). Тем более, байт есть существо беззлобное и беззнаковое, а отрицательных пинов в AVR я отродясь не видел, поэтому byte (ну или uint8_t в терминах GCC С++) нам для хранения номера пина подойдет более чем полностью. Што? Почему называется fpin? Ну так я же родом из Турбо Паскаля, я честно предупреждал, а там защищенные поля принято начинать с F, а параметры, передаваемые в функцию - с А. Let it be, я так привык.

    Теперь во весь рост встает вопрос, ежели мы создали скрытое поле, которое снаружи класса не видно, как же запхать-то в него этот пресловутый номер пина, к которому без всякой уже надежды подключен наш грусно висящий "на полшестого" даччик? И тут со сверкающей вершины стандарта С++ нам на помощь летят конструкторы. Нет. Не те, которые в КБ на ватманах всяких очередной, приговоренный в Туве разбица, спутник чертют, те в такой ситуации способны только расшибиться в мясо. Конструктор - это специальная функция, которая вызывается единственный раз, именно при, и, самое главное, ДЛЯ создания класса и предназначена как раз для задания начального состояния класса, типа значения полей и таблиц. На самом деле, хотя мы в нашем классе еще не написали конструктор, но компилятор С++ неявно и заботливо вставил их в описание класса целых два, пустой и копирующий. Не надо протирать глаза водкой, в нашем тексте мы их не увидим, но они там есть. Только скрытые даже от нас. И даже если мы не напишем свой конструктор, то при создании класса по умолчанию вызовется пустой констуктор, в задачу которого входит присвоить начальные нули всем полям класса и инициализировать таблицу виртуальных функций (и не спрашивайте ачоэтотакое?, это для взрослых). Тоись, если мы в своей программе объявим переменную

    TSensor PhotoSensor;

    то ошибки не будет, компилятор вставит здесь для инициализации вызов пустого конструктора, который тупо заполнит нулями значения всех простых полей (которое у нас аш целое одно), и вызовет конструкторы по умолчанию для полей-классов, если они не обьявлены указателями (да, да поля могут быть не только int, byte, float или long, но и классами и структурами и массивами, да любыми сложными типами, но это тоже оставим пока для взрослых) соответственно наш fpin тоже будет равен нулю по умолчанию, всегда и постоянно, ведь снаружи мы его изменить не можем, он защищен от подглядывания директивой protected. Но это же ваще никуда не годитсяа! Нам же надо чтобы pin был вполне конкретно-определенный. Ну штош, придётся писать свой конструктор. На самом деле, конструктор - это простая функция с именем, ровно таким же, как у самого класса и с числом параметров, ограниченным только больной фантазией его создателя. И еще, директива видимости protected, начинаясь от момента обьявления, простирается до тех пор, пока не встретится более другая директива видимости, а конструктор наш должен быть видим снаружи, т.е. должен быть публичным, доступным для широкой общественности, поэтому напишем перед ним новую директиву, public. Вот что примерно получится:

    Код (C++):
    class TSensor
    {
    protected:
      byte fpin;   // поле "номер пина" типа байт, скрытый от пользователя

    public:
      TSensor(byte apin); // конструктор, открытый все ветрам и пользователям
    }
    функция-конструктор никада не отдает никаких значений, поэтому имя типа перед ней ставить не надо.

    сразу же открываем свой .срр файл (на этот момент он должен быть еще пустой) и пишем

    Код (C++):
    //-------------------------------------
    // срр файл

    #include "TSensor.h"        // перво-наперво подключаем свой же файл определений
                    // чтоб знать, реализацию для чего мы пишем
                    // срр файл мы не включаем в проект через #include,
                    // поэтому #pragma once писать не надо

    // Реализация нашего класса. Конструктор. В конструкторе мы запоминаем номер пина, который мы ему
    // передали в качестве параметра, во внутреннем поле нашего класса.

    TSensor::TSensor(byte apin)
    {
      fpin = apin;
    }

    // капец cpp файла
    //--------------------------------------
     
    из-за префиксов f и a перед pin мне сразу видно, что слева стоит переменная-поле а справа - параметр функции. Но это только вопрос удобства. Два странных двоеточия в TSensor::TSensor это не я два раза по клавиатуре промазал, это специальная конструкция языка, которая показывает, что конструктор TSеnsor относится к классу TSensor и ни к чему более. Префикс TSensor:: ставится перед любой функцией, обьявленной в классе.

    Идём дальше. Ничо не забыли? Ну ититькалатить, конечно забыли. У нас же это датчик, он же должен читать данные с pin-a, а на вход мы его настроить забыли. Сделаем это в конструкторе.

    Код (C++):
    //-------------------------------------
    // срр файл

    #include "TSensor.h"        // перво-наперво подключаем свой же файл определений
                    // чтоб знать, реализацию для чего мы пишем
                    // срр файл мы не включаем в проект через #include,
                    // поэтому #pragma once писать не надо

    // Реализация нашего класса. Конструктор. В конструкторе мы запоминаем номер пина, который мы ему
    // передали в качестве параметра, во внутреннем поле нашего класса.

    TSensor::TSensor(byte apin)
    {
      fpin = apin;            // запомнили номер pin-а во внутреннем поле класса
      pinMode(fpin,INPUT);        // настроили pin как вход
    }

    // капец cpp файла
    //--------------------------------------
    Блин. А как же подтягивающий резистор? Ведь некоторым датчикам, например фоторезистору, подтяжка нужна, а некоторым - нет, а другой класс городить из-за наличия/отсутствия подтяжки совсем не хочется. Вот если бы можно было написать еще один конструктор, в котором передавать классу, кроме номера pin-а еще и необходимость включить подтяжку, было бы здорово. Ну так такая возможность есть!!!! У класса может быть сколько угодно конструкторов, отличающихся друг от друга числом и типом параметров им передающихся. По научному это называется перегрузка функций (а конструктор у нас это просто специальная функция класса, но функция же). Объяснять это я не буду, интересующиеся набирают в гугале "перегрузка функций С++" и читают до просветления, нирваны или запоя. Главное, что мы можем создать в нашем классе еще один конструктор, с нужными нам параметрами. Ну и дак сделаем это.

    Код (C++):
    // --------------------------
    // .h файл

    #pragma once
    #include "Arduino.h"

    class TSensor
    {
    protected:
      byte fpin;   // поле "номер пина" типа байт, скрытый от пользователя

    public:
      TSensor(byte apin);               // конструктор, принимающий только номер пина
      TSensor(byte apin, bool apullup); // а этот еще и включает или нет подтягивающий резистор на pin
    }

    // --------------------------
    // капец .h файла
    как видим, новый конструктор называется точно так-же, только параметров уже 2. Так как подтяжка у нас может быть только включена или выключена, все ее значения прекрасно поместятся в тип bool, который может хранить в себе как раз только два значения, "да" и "нет". Переводя на корявый английский - "true" и "false". А больше нам и не нужно. Да и вообще, памяти у ардуины не так чтобы много, поэтому надо привыкать хранить значения в минимально подходящем для этого типе. Например, совершенно нехрен хранить, как некоторые, номер pin-а в переменной типа int. Во первых она знаковая (найдите мне отрицательный номер пина в Дуне - и я немедленно повешусь на осине, не в силах терпеть этот безумный мир), во-вторых, занимает целых два байта, хотя номер пина прекрасно поместится и в младших 6 битах целого 8-битного байта (в 328 камне). Поэтому, для хранения номера pin-а мы выбрали байт, а для подтяжки - bool. Ээээ, мы же не создали поле, которое будет хранить состояние подтяжки, включена она или выключена. Глубоко наморщим мозг и подумаем, а оно нам надо, знать в каком состоянии подтяжка? По-моему - нет, мы должны знать ПЕРЕД созданием класса, нужна она нам или нет, создать класс с нужным значением подтяжки и забыть про нее до конца времен, не будем же мы её дергать туда-сюда. Поэтому поле для хранения подтяжки нам не нужно, мы его и заводить не будем, сразу пишем:

    Код (C++):
    //-------------------------------------
    // срр файл

    #include "TSensor.h"        // перво-наперво подключаем свой же файл определений
                    // чтоб знать, реализацию чего мы пишем
                    // срр файл мы не включаем в проект через #include,
                    // поэтому #pragma once писать не надо

    // Реализация нашего класса.

    // Конструктор. В конструкторе мы запоминаем номер пина, который мы ему
    // передали в качестве параметра, во внутреннем поле нашего класса и настраиваем его на вход

    TSensor::TSensor(byte apin)
    {
      fpin = apin;            // запомнили номер pin-а во внутреннем поле класса
      pinMode(fpin,INPUT);        // настроили pin как вход
    }


    // Конструктор. В конструкторе мы запоминаем номер пина, который мы ему передали в качестве
    // параметра во внутреннем поле нашего класса и настраиваем его на вход с подтяжкой или без
    // в зависимости от параметра apullup

    TSensor::TSensor(byte apin, bool apullup)
    {
    fpin = apin;
    if (apullup) pinMode(fpin,INPUT_PULLUP); else pinMode(fpin, INPUT);
    }

    // капец cpp файла
    //--------------------------------------
     
    Последнее редактирование модератором: 16 янв 2018
    CYITEP_BAC9I, Radius, Сусемьбек и 3 другим нравится это.
  3. DetSimen

    DetSimen Guest

    Странная конструкция if (apullup) , вы не находите? Ведь ручки прям сами тянутся, особенно у новичков, написать if (apullup==true) ... А я ехидно спрашиваю, а зачем? У оператора if уши торчат из чистого-пречистого (чесного до последней запятой) С, в начале времен там не было типа bool и выражение в скобках должно было быть типа int и просто проверялось на 0 и не 0. К счастью очередной стандарт ввел тип bool в основные типы языка и гарантировал, что false этта гарантированный 0, а true - гарантированный не 0. Ну а так как в конструктор мы уже передали true или false, то в if дополнительную проверку городить нет никаго здравого смысла. Поэтому смело пишем:
    if (apullup) ...

    Теперь посмотрим на наши конструкторы попристальнее, как на кота, который присел ссать в тапки. Видно, что фактически, код наших конструкторов повторяется на 80%. И там и там мы устанавливаем внутреннее поле fpin и вызываем pinMode, разница только в константе INPUT и INPUT_PULLUP. Это не есть хорошо, потому что памяти в камнях AVR не так уж и много, и раздувать код класса мы позволить себе не можем. Этак вся флэш переполнится, микросхема распухнет, лопнет и нас байтами забрызгает. Как бы сделать так, чтоб код был один а конструкторов - два (или три, или over 9000)? К великому щастью, и так сделать можно. Можно из одного конструктора вызывать другой конструктор. То есть можно написать один более общий конструктор, который устанавливает большинство полей нужными значениями, а в более специфических конструкторах вызвать сначала общий, а потом уже эти специфические поля заполнить своими специфическими значениями. В нашем случае, более общим конструктором является второй, потому что в первом устанавливается только значение поля fpin и по умолчанию вход настраивается без подтяжки, а во втором делается все тоже самое, только есть еще выбор, устанавливать подтяжку или нет. Значит в первом конструкторе мы можем запросто вызвать второй с параметрами TSensor(apin, false), результат будет идентичен, а кода меньше. Но как это сделать? А щас посмотрим.

    Код (C++):
    //-------------------------------------
    // срр файл

    #include "TSensor.h"        // перво-наперво подключаем свой же файл определений
                    // чтоб знать, реализацию чего мы пишем
                    // срр файл мы не включаем в проект через #include,
                    // поэтому #pragma once писать не надо

    // Реализация нашего класса.

    // Конструктор. В конструкторе мы просто вызываем другой конструктор, более общий

    TSensor::TSensor(byte apin): TSensor(apin, false) {}


    // Конструктор. В конструкторе мы запоминаем номер пина, который мы ему передали в качестве
    // параметра во внутреннем поле нашего класса и настраиваем его на вход с подтяжкой или без

    TSensor::TSensor(byte apin, bool apullup)
    {
    fpin = apin;
    if (apullup) pinMode(fpin,INPUT_PULLUP); else pinMode(fpin, INPUT);
    }

    // капец cpp файла
    //--------------------------------------
    а просто оттак. После первого конструктора мы вместо открывающей фигурной скобки пишем двоеточие, а после него вызываем второй конструктор как обычную функцию, но с нужными нам параметрами. Вместо первого параметра мы подставляем apin, который нам передали, а вместо второго, константу false. Само тело конструктора с непонятными символами на С++ больше не нужно, поэтому дальше стоят просто фигурные скобки, показывающие, что функция пустая, ее задача просто передать управление другому конструктору.
    А в своей программе мы можем теперь обьявить датчик по-всякому

    TSensor PhotoSensor(10) // датчик на десятом pin, подтяжка не нужна
    TSensor PhotoSensor(10, false) // датчик на десятом pin, подтяжка не нужна
    TSensor PhotoSensor(10,true) // датчик на десятом pin, подтяжка нужна


    заметьте, что первые 2 обьявления одинаковы по результату, потому что, на самом деле неявно для пользователя класса всё равно вызывается конструктор TSensor(10, false).


    В продолжении, если местные гуру не закидают меня калом, мы двинемся дальше, будем датчик читать, да попутно millis()-ом крутить да размахивать.

    Я понимаю, что написано сумбурно, далеко от идеала, мои сентенции иногда наерна очень далеки от истины и, мягко выражаясь, спорны, вы уж простите великодушно мой атравленный олкаголем мозг, 80% клеток там уже благополучно померло. Если несогласны с написанным - пишите гневные комментарии, жмите кнопку "Пожаловаться", да в ООН пишите в конце концов. Будет много негатива, думаю модераторы помогут мне тему удалить .

    Продолжать? Плюсуем. Забросать калом - жалуйтесь. У нас же дерьмократия же в конце-то концов. А я пока протрезвею и буду писать (или не писать) про to be continued....
     
    Последнее редактирование модератором: 16 янв 2018
    Radius, AlexU, Сусемьбек и 2 другим нравится это.
  4. ИгорьК

    ИгорьК Гуру

    Афтар, пеши исчо. Тока добавь нирваны - вставляй код куда нада!
    А так - мед.
     
  5. Кроссаучег!
     
  6. AlexU

    AlexU Гуру

    Можно
    Код (C++):
    if (apullup) pinMode(fpin,INPUT_PULLUP); else pinMode(fpin, INPUT);
    заменить на более короткое:
    Код (C++):
    pinMode(fpin, apullup ? INPUT_PULLUP : INPUT);
    ЗЫ: и про 'this' наверно стоит что-то сказать
     
  7. DetSimen

    DetSimen Guest

    Итак, теперь мы научились хранить нужные нам данные внутри класса и пользоваться конструкторами.

    У функций-конструкторов есть полная противоположность - деструкторы. Эти злобные функции, как неистовые демоны ада, призваны расправляться с любым созданным классом, невзирая на его супер-полезность. Их функция - убивать, не щадя ни стар ни млад, гасить любому классу неонку, которая унутре. Вышел из зоны видимости - кончилось твое время жизни, класс!!. Добро пожаловать в Ад... Ну то-есть, для начала, в заботливые руки патологоанатома - деструктора. Деструкторы нужны, чтобы правильно освободить память выделенную классу. И если в простом до неприличия классе, таком как наш, деструктор тривиален (мы на него вообще забьём, он сам создастся, по умолчанию), то в более сложных классах, активно работающих с памятью, деструктор придется писать самим,в настоящую, кровавую врукопашную, чтоб не было потом мучительно больно за бесцельно потраченную и не освобожденную память, которая из-за этого сурово наехала на стек и Луноход-17, поэтому, улетел не на Марс, а на Солнце, где и сгорел благополучно. Ну, вопщем-то, канеш, не очень то и хотелось, денежки-то уже папилили, но осадочек остался, а вдруг бы он назад повернул, да наше супер-КБ расколотил в щебенку? Деструктор класса всегда вызывается либо автоматически, когда переменная-класс выходит из области видимости, либо вручную пограммистом, которому класс больше стал не нужен. В терминах Ардуино это означает, что если класс глобальный, ну то есть обьявлен где-то выше всех этих ваших loop() и setup(), то время жизни класса - ровно до отключения питания. Ну или до сгорания в Солнце, а тогда вообще пофиг, освободили мы память от класса или нет. Деструктор для таких объектов автоматически нигде не вызывается. А так как наш класс-сенсор планирует жить с Ардуиной долго и, может быть, даже сначала счастливо, и умереть в один день, то и деструктор ему не нужен. Поэтому, чисто в познавательных целях приведу его синтаксис:

    Код (C++):
    TSensor::~TSensor();
    как видим, деструктор от конструктора отличается только тильдой в начале названия "~", да полным отсутствием параметров ().

    Идём дальше. Что там нам еще надо в этом нашем нехитром классе аналогового сенсора? Ааааа, значения же надо читать с пина, для которого мы так заботливо выстраивали в секции protected собственную колыбельку типа байт. Дак мало того, что читать, его ж ведь и отдавать надо вызывающей класс основной программе, чтоб по результату опроса сумашедшего сенсора MQ-9 она решала, спать мне дальше, или зажав зубах паспорт, а под мышкой - спящего, как всегда, кота, немедленно спасаться бегством от бытового газа в запредельной концентрации. Так вставим же в обьявление класса функцию, которая будет читать значение с пина нашего датчика и отдавать всем желающим считанное значение. И незамысловато назовем её Read(). Так как аналоговый вход отдает значения от 0 до 1023, то байтом мы не отделаемся, в него всего 0-255 влазит, придется отдавать int. Хотя, отрицательных значений с аналогового пина я тоже отродясь не читал, и можно было бы обойтись типом word (ака uint16_t в терминах GCC), он как раз беззнаковый, делать этого я не буду и вот почему. Во-первых int более привычен и понятен, во вторых наше значение туда гарантиированно влезет, правду вам говорю, 10 бит как нибуть в 16 точно поместятся. А еще как-то Оккам мне шептал в изрядном подпитии, не плодить сущностей сверх необходимости. Или это не он был? Но похож... Бонусом, мы получаем возможность сигнализировать программе, что-то пошло не так, просто отдав из функции отрицательное значение, которое с аналогового pin-а в принципе прочитать нельзя. Например, я люблю отдавать -1, но это по вкусу. Итак, наша функция должна отдавать int, а принимать что? А ничего. Все, что ей нужно для работы, а именно, номер пина, с которого читать, всё уже есть у класса унутре. (и неонка).
    Поэтому, объявление функции будет выглядеть так:

    Код (C++):
    // --------------------------
    // .h файл

    #pragma once
    #include "Arduino.h"

    class TSensor
    {
    protected:
      byte fpin;                  // поле "номер пина" типа байт, скрытый от пользователя

    public:
      TSensor(byte apin);               // конструктор, принимающий только номер пина
      TSensor(byte apin, bool apullup); // а этот еще и включает или нет подтягивающий резистор на pin

      int Read();
    }

    // --------------------------
    // капец .h файла
    Естесственно, функция стоит в секции public, ведь она должна быть видима в основной программе. Посмотрим на реализацию.

    Код (C++):
    //-------------------------------------
    // срр файл

    #include "TSensor.h"    // перво-наперво подключаем свой же файл определений
                        // чтоб знать, реализацию чего мы пишем
                        // срр файл мы не включаем в проект через #include,
                        // поэтому #pragma once писать не надо

    // Реализация нашего класса.

    // Конструктор. В конструкторе мы просто вызываем другой конструктор, более общий

    TSensor::TSensor(byte apin): TSensor(apin, false) {}


    // Конструктор. В конструкторе мы запоминаем номер пина, который мы ему передали в качестве
    // параметра во внутреннем поле нашего класса и настраиваем его на вход с подтяжкой или без

    TSensor::TSensor(byte apin, bool apullup)
    {
    fpin = apin;
    if (apullup) pinMode(fpin,INPUT_PULLUP); else pinMode(fpin, INPUT);
    }

    int TSensor::Read(void)
    {
      return analogRead(fpin);
    }

    // капец cpp файла
    //--------------------------------------
    всё. Пин прочитан, оператором return значение отдано. Но теперь подумаем, правильно ли так делать? В принципе, это ничему не противоречит, все будет работать, но от случайных помех или флюктуаций самого датчика, считанные и отданные значения могут колебаться. Бывает, датчик читает-читает значение 200, а потом ба-бах, в момент чтения приходит по шине случайная иголка, значение подскакивает до 800, сирена включается и наступает время брать под мышку сонного кота и лихорадочно думать "Куды бечь?". Чтоб этого не случалось, я всегда читаю пин несколько раз и усредняю полученные значения. Читаю ровно степеньдвойки раз, конкретно 8 или 16, чтоб среднее значение получалось простым сдвигом вправо на 3 или 4 бита. И еще, я не колдую над битами в порту, не читаю напрямую АЦП, я знаю, что это круто, но для моих задач достаточно штатных Ардуиновских функций digitalWrite(), digitalRead(), analogRead(). В самом деле, если фоторезистор опрашивается раз в 5 секунд, какая мне разница, сколько времени будет работать analogRead() 130 микросекунд или 2000 (цифра от фонаря), в любом случае, разность измеряется тысячами раз, мне этого хватает. Значить, мы будем читать analogRead()-ом 8 или 16 раз, по вкусу, а потом просто отдавать это значение, сдвинутое на 3 или 4 бита вправо (то есть деленное на 8 или на 16). Ибо, сдвиги выполняются быстрее самого оптимизированного целочисленного деления на 10, допустим. Примерно так:

    Код (C++):
    int TSensor::Read(void)
    {
    // заводим переменную с результатом, инициализируя ее нулем

      int result = 0;

    // в цикле считываем с пина 16 значений, складывая с результатом

      for (byte i=0; i<16; i++) result += analogRead(fpin);

    // делим result на 16, (получаем среднее), отдаем результат вызывающему

      return (result >> 4);    
    }
    теперь результат более надежен и предсказуем. Можно конечно выдумать и более сложный лагаритм усреднения, но мне реально достаточно и такого. Про то какие здесь могут возникнуть ошибки, и подводные валуны, мы поговорим позже, когда захочем, чтобы код работал надежно. Итог всего этого:
     
    Последнее редактирование модератором: 17 янв 2018
  8. DetSimen

    DetSimen Guest

    Код (C++):
    // --------------------------
    // .h файл

    #pragma once
    #include "Arduino.h"

    class TSensor
    {
    protected:
      byte fpin;                  // поле "номер пина" типа байт, скрытый от пользователя

    public:
      TSensor(byte apin);               // конструктор, принимающий только номер пина
      TSensor(byte apin, bool apullup); // а этот еще и включает или нет подтягивающий резистор на pin

      int Read();                // читать данные с сенсора
    }

    // --------------------------
    // капец .h файла

    //-------------------------------------
    // срр файл

    #include "TSensor.h"    // перво-наперво подключаем свой же файл определений
                        // чтоб знать, реализацию чего мы пишем
                        // срр файл мы не включаем в проект через #include,
                        // поэтому #pragma once писать не надо

    // Реализация нашего класса.

    // Конструктор. В конструкторе мы просто вызываем другой конструктор, более общий

    TSensor::TSensor(byte apin): TSensor(apin, false) {}


    // Конструктор. В конструкторе мы запоминаем номер пина, который мы ему передали в качестве
    // параметра во внутреннем поле нашего класса и настраиваем его на вход с подтяжкой или без

    TSensor::TSensor(byte apin, bool apullup)
    {
    fpin = apin;
    if (apullup) pinMode(fpin,INPUT_PULLUP); else pinMode(fpin, INPUT);
    }

    int TSensor::Read(void)
    {
      int result = 0;
     
      for (byte i=0; i<16; i++) result += analogRead(fpin);

      return (result >> 4);    
    }

    // капец cpp файла
    //--------------------------------------
    проверьте, кому не лень, это сейчас уже компилируется и реально работает. Всего то нужно в макетку вонзить фоторезистор, подключить его к аналоговому pin, а своем проекте вставить
    #include "TSensor.h", и обьявить глобально

    TSensor MyPhoto(пин в куда он подключен, нужна/не нужна подтяжка true или false);

    для фоторезистора, если он подключен с входа прямо на землю - нужна однозначно.
    потом в лупе почитать, но не слишком часто,

    value = MyPhoto.Read();

    и повыводить это в сериал

    Serial.print("Sensor value = "); Serial.println(value);
     
    Последнее редактирование модератором: 17 янв 2018
  9. DetSimen

    DetSimen Guest

    to be continued
     
  10. AlexU

    AlexU Гуру

    Деструктор программистом не вызывается (хотя это и можно сделать). Это чревато. Компилятор сам вставляет вызов деструктора в правильное место. Программист может только явно удалить объект -- оператор delete.
    С чего это вдруг прерывания должны испортить жизнь в этой функции? Если с АЦП будете работать только через функцию 'analogRead()', то ни каких проблем не будет.
    А вот это решение:
    очень даже вредное.
    Честно говоря сам не мерял время выполнения функции 'analogRead()', но злые языки утверждают, что в районе 110 мксек. При 16-ти замерах прерывания будут запрещены в течении ~1.76 мсек -- привет потерянные пакеты при приёме через Serial и похожие интерфейсы, прощай корректная работа функций millis(), micros(), delay() (всё что завязано на встроенные RTC) и т.п. головная боль. Оно Вам нужно???
     
    arkadyf и SergeiL нравится это.
  11. DetSimen

    DetSimen Guest

    на самом деле 2 мс при 16 замерах, 1 мс при 8.
     
  12. AlexU

    AlexU Гуру

    Значит злые языки правду говорят...
    Поэтому запрет прерываний лучше убрать, "от греха подальше".
     
  13. DetSimen

    DetSimen Guest

    убрал.
    На самом деле, я столкнулся со странным поведением одного датчика всего один раз на Pro Mini, еще на 168 Атмеге, которая включала у меня подсветку дорожек на даче. Это была ее основная и единственная задача, которую она исполняла из рук вон плохо. Хотя, нет, там еще часы были 3231, их еще опрашивала. Подсветка могла включиться днем, могла выключиться ночью раньше означенного времени, вопщем, работала безобразно. Тогда у меня не было большого Arduino опыта, да и осциллографа не было, поэтому методом мониторинга сериала я выяснил, что фотосенсор иногда отдает данные, скажем так, сильно выбивающиеся из предыдущей последовательности, чего быть никак не должно. Тогда я попробовал запретить прерывания на время чтения и о чудо! все заработало штатно. Именно с того момента всё и идёт.
    Но та Мини была вся косячная насквозь, у нее был битый EEPROM, заливалась она не с первого раза, поэтому, когда она по осени выпустила из себя белый дым и приказала долго программировать после нее, я жалеть не стал. Но так как у меня уже собрана своя нехитрая библиотечка классов, а класс TSensor - из старых, то запрет прерывания в нем так и остался. :)
     
  14. DetSimen

    DetSimen Guest

    на ардуино.ру тапками закидали, буду тут графоманить. Как приснопамятный Фоба. :)
    Только сёдня некада. А в следующий раз поговорим, как не опрашивать датчик слишком часто. С millis().

    Самое страшное, что новички не плюсуют. Для них это как козе баянъ, наерна.

    Ау, новички? Вам это надо????
     
    Последнее редактирование модератором: 18 янв 2018
    acos нравится это.
  15. DetSimen

    DetSimen Guest

    Пока на улице мороз, работать уже лень, а квасить еще рано, продолжим.

    В этом безумном мире всегда найдутся очень простые люди, которых 3 раза из ПТУ выгоняли за неуспеваемость и которые прям в loop() напишут:

    Код (C++):
    void loop()
    {
       int i = MyPhoto.Read();
    }
    И будут читать значение с этого несчастного датчика несколько тысяч раз в секунду. В какой-то мере и данный подход оправдан, чо париться-то лишний раз в этом сложном мире, да вот (бида!, бида!), не все датчики допускают такое вольное с собой обращение. Хорошо, если он просто будет отдавать данные с потолка, а если возьмет да и подохнет? И что мы потом скажем над гробом безвременно почившего даччика? Что покойный жил в лупе, вел беспорядочную жизнь и часто болел? Потому и молодым и помер, не успев толком обжиться в доме? Да что не скажи, а перспектива безрадостная. Не хочется закапывать наши деньги в помойке, надо придумать механизм, чтоб не опрашивать датчик слишком часто. Люди которых не выгоняли из ПТУ скажут, а давайте читать датчик только если прошло достаточное количество времени, ну и они-таки будут правы. А чем нам померить время? А нам на помощь придёт функция millis(). Эта функция, как знатный бухгалтер, скурпулёзно подсчитывает, сколько миллисекунд проработал процессор с момента включения (наерна, чтоб знать, когда настанет пора начислять ему пенсию). Мы можем вызвать millis() один раз, потом другой и по разности полученных значений примерно-точно сказать, сколько миллисекунд программа проработала между вызовами. Значение, которое отдает millis() имеет тип unsigned long int, занимает целых 4 байта в памяти, зато может хранить значения от 0 до чуть больше 4 миллиардов. 4 лярда миллисекунд, в переводе на русский, это почти 50 суток, у кого папа калькулятор не отбрал могут сами проверить. Кто собирается читать датчик реже чем раз в 50 суток - дальше не читайте, там для вас ничего не написано.

    Можно, канеш, и самим в loop() проверять интервал чтения датчика, как то так

    Код (C++):
    unsigned long previous = 0;
    unsigned long interval = 2000;  // 2 секунды


    void loop()
    {
       unsigned long now = millis();  
       int value;

       if (now - previous > interval)
       {
        value = MyPhoto.Read();
        previous = now;
       }
    }
     
    работать будет и так, мы добились того, чтоб наш датчик не истёрся до дыр от частого использования, но как то это некрасиво. Для чего тогда класс городить? Мы и в if-е тогда analogRead() можем вызвать. Класс должен же ведь упрощать жизнь, а не делать ее сложнее. Мошт, пусть он сам решает, часто его читают или нет? Ну а почему бы и нет? Надо сделать так, чтобы класс помнил, когда он опрашивал датчик последний раз, и если время между вызовами Read() меньше заданного интервала, то можно просто отдать последнее, прочитанное ранее значение, а если времени прошло больше - то снова опросить датчик и запомнить. Для этого нам и понадобится-то совсем немного.

    Поле для запоминания последнего значения, считанного с pin-а, оно должно быть скрыто, нехрен пользователю в него лезть, мы сами его отдавать будем, когда надо. Назовем его int fvalue.

    Поле для запоминания времени последнего реального чтения датчика. Оно тоже должно быть скрыто, ведь никому, кроме самого класса это неинтересно. Назовем его unsigned long flasttime.

    Не бойтесь, кстати, давать переменным длинные осмысленные имена (хотя, если с английским туго, о какой осмысленности речь?) в эпоху intellisence набирать их ну чуть помедленнее, канешна, чем i, но код читается потом вами же не в пример легче. А если придется через год переделывать прошивку? Немногие вспомнят, что означает переменная v1 или s2 в своей же собственной программе, посмотрят-посмотрят на текст, как новые ворота на стаю баранов, почешут репу "Какой идиот это писал?", да пойдут с друзьями пиво пить. А с осмысленными именами даже через пять лет жизнь будет существенно проще. (там будет другая проблема, вспомнить лагаритм и поржать над собой, неучем, пятилетней давности).

    Кстати. В конструкторе можно присвоить начальные нулевые значения этим полям. Можно этого и не делать, нулями они и сами заполнятся при создании класса, это гарантировано стандартом, но мой вам совет, не полагайтесь на стандарт, присваивайте начальные значения полям сами. Стандарты С++ придумывают, как мне кажется, не очень адекватные люди, и я даже не удивлюсь, если узнаю, что все они содержатся в одной клинике на попечении государства(слава богу, не нашего). А завтра из этой психушки выйдет другой стандарт, предписывающий заполнять все поля единицами при создании класса, или пхать туда случайные числа, и что тогда? Вы должны чётко понимать в каком состоянии находится ваш класс после создания, поэтому вся инициализация - только сами, и не лениться. Не нравица - вот лопата, вот обьявление "Требуется кочегар", вам туда.

    Код (C++):
    // --------------------------
    // .h файл

    #pragma once
    #include "Arduino.h"

    class TSensor
    {
    protected:
      byte fpin;              // поле "номер пина" типа байт, скрытый от пользователя
      int  fvalue;            // последнее считанное с pin-а значение
      unsigned long flasttime;    // время последнего обращения к датчику

    public:
      TSensor(byte apin);               // конструктор, принимающий только номер пина
      TSensor(byte apin, bool apullup); // а этот еще и включает или нет подтягивающий резистор на pin

      int Read();                // читать данные с сенсора  
    }

    // --------------------------
    // капец .h файла


    //-------------------------------------
    // срр файл

    #include "TSensor.h"    // перво-наперво подключаем свой же файл определений
                        // чтоб знать, реализацию чего мы пишем
                        // срр файл мы не включаем в проект через #include,
                        // поэтому #pragma once писать не надо

    // Реализация нашего класса.

    // Конструктор. В конструкторе мы просто вызываем другой конструктор, более общий

    TSensor::TSensor(byte apin): TSensor(apin, false) {}


    // Конструктор. В конструкторе мы запоминаем номер пина, который мы ему передали в качестве
    // параметра во внутреннем поле нашего класса и настраиваем его на вход с подтяжкой или без

    TSensor::TSensor(byte apin, bool apullup)
    {
    fpin = apin;
    if (apullup) pinMode(fpin,INPUT_PULLUP); else pinMode(fpin, INPUT);
    fvalue = 0;    // инициализация ручками
    flasttime = 0;  
    }

    int TSensor::Read(void)
    {

      unsigned long now = millis();  // прочитаем время на сейчас

    // Если с момента последнего обращения прошло меньше 2 секунд, просто отдадим
    // наружу, то что прочитали последний раз, не дергая пин лишний раз

      if (now - flasttime < 2000) return fvalue;

    // Ну а уж если времени прошло больше, куда деваца, надо читать

      int result = 0;

      for (byte i=0; i<16; i++) result += analogRead(fpin);

      flasttime = now;        // запомним время, когда мы последний раз читали
      fvalue    = result >> 4;    // и запомним, что прочитали при этом

      return fvalue;        // ну и то, что прочитали, то и отдадим.
    }

    // капец cpp файла
    //--------------------------------------

    Теперь можно в loop() вставлять чтение нашего датчика хоть как и хоть куда, все равно реально читаться значение с pin-а будет раз в 2 секунды, это намайна, нас не посадят за жестокое обращение с даччиками, да и сам он не устанет и не будет просица на пенсию по здоровью.
    Кстати, а откуда взялись эти 2 секунды? Нам чо, судом присудили опрашивать датчик именно через 2 секунды? А если надо читать быстрее? А если по даташиту датчик нада опрашивать не чаще раза в 5 секунд, а у нас жёска настроено на 2? Что же делать-то? Да просто надо завести еще одно поле, обозвать его ReadInterval и дать возможность пользователю класса менять его значение. Так как оно должно быть видимо снаружи, то обьявить его надо в секции public, ну а в конструкторе присвоить ему некое начальное значение по умолчанию, ни большое, ни маленькое, да те же 2 секунды. Для большинства датчиков нет никакой необходимости читаться чаще раза в две секунды, а те, кто хочут, легко могут интервал изменить прямо после создания класса и лезть в исходник для этого не придётся. Платой за это будут лишних 4 байта на каждый датчик, которые придётся со слезами выкраивать из оперативки. Но, слава богу у Дуни всего 6 аналоговых пинов, много датчиков не подключишь, поэтому 24 байта за возможность индивидуальной настройки чтения каждого датчика, я думаю, это нормально. Падытожым.
     
    Securbond, AlexU, arkadyf и ещё 1-му нравится это.
  16. DetSimen

    DetSimen Guest

    Код (C++):
    // --------------------------
    // .h файл

    #pragma once
    #include "Arduino.h"

    class TSensor
    {
    protected:
      byte fpin;              // поле "номер пина" типа байт, скрытый от пользователя
      int  fvalue;            // последнее считанное с pin-а значение
      unsigned long flasttime;    // время последнего обращения к датчику

    public:
      unsigned long ReadInterval;       // частота реального чтения даччика в миллисекундах

      TSensor(byte apin);               // конструктор, принимающий только номер пина
      TSensor(byte apin, bool apullup); // а этот еще и включает или нет подтягивающий резистор на pin

      int Read();                // читать данные с сенсора  
    }

    // --------------------------
    // капец .h файла

    //-------------------------------------
    // срр файл

    #include "TSensor.h"    // перво-наперво подключаем свой же файл определений
                        // чтоб знать, реализацию чего мы пишем
                        // срр файл мы не включаем в проект через #include,
                        // поэтому #pragma once писать не надо

    // Реализация нашего класса.

    // Конструктор. В конструкторе мы просто вызываем другой конструктор, более общий

    TSensor::TSensor(byte apin): TSensor(apin, false) {}


    // Конструктор. В конструкторе мы запоминаем номер пина, который мы ему передали в качестве
    // параметра во внутреннем поле нашего класса и настраиваем его на вход с подтяжкой или без

    TSensor::TSensor(byte apin, bool apullup)
    {
    fpin = apin;
    if (apullup) pinMode(fpin,INPUT_PULLUP); else pinMode(fpin, INPUT);
    fvalue = 0;
    flasttime = 0;  
    ReadInterval = 2000;     // по умолчанию чтение с pin-а раз в две секунды
    }

    int TSensor::Read(void)
    {

      unsigned long now = millis();  // прочитаем время на сейчас

    // Если с момента последнего обращения прошло меньше ReadInterval секунд, просто отдадим
    // наружу, то что прочитали последний раз, не дергая пин лишний раз

      if (now - flasttime < ReadInterval) return fvalue;

    // Ну а уж если времени прошло больше, куда деваца, надо читать

      int result = 0;

      for (byte i=0; i<16; i++) result += analogRead(fpin);

      flasttime = now;        // запомним время, когда мы последний раз читали
      fvalue    = result >> 4;    // и запомним, что прочитали при этом

      return fvalue;        // ну и то, что прочитали, то и отдадим.
    }

    // капец cpp файла
    //--------------------------------------

    в основной программе

    Код (C++):
    //--------------------------------------
    //  Файл Test.ino

    #include "TSensor.h"

    TSensor MyPhoto(A0, true);   // обьявление и вызов конструктора для пина А0 с подтяжкой

    void setup()
    {
      MyPhoto.ReadInterval = 5000;   // читать не чаще раза в 5 сек.
    }

    void loop()
    {
      int value = MyPhoto.Read();
      Serial.print("Value = "); Serial.println(value);
      delay(1000);
    }

    //  капец файла Test.ino
    //--------------------------------------
    Пробоваем.
     
    AlexU, arkadyf и ИгорьК нравится это.
  17. AlexU

    AlexU Гуру

    Смотрел, смотрел, но так и не нашёл, где в стандарте написано, что поля класса инициализируются нулями -- может не там смотрел. Да к тому же реализация компилятора для AVR -- avr-g++ -- ничего не инициализирует. Если явно поля в конструкторе (или другим способом) не проинициализировать, то значения полей будут случайными. Поэтому инициализация нужна всегда.
    Можно конечно и в секции public объявить, страшного ни чего нет. Но всё-таки обычно поле делают private, а доступ к нему реализуют через getter/setter методы. Но это уже кому как нравится.
     
  18. Нормально :) только я одно не пойму. Вот писал я классами и тп на java, в принципе не сложно :)
    Одно не пойму. Вот никак. Зачем????? Разве не удобней писать функциями????
    Не вижу смысла в ООП. Наверное не дорос пока что :)
     
  19. DIYMan

    DIYMan Guest

    В точку ;)
     
  20. DetSimen

    DetSimen Guest

    при выделении памяти под класс, делается memset. Где читал, сразу не вспомню.