Классы и объекты: сколько и какие типы файлов мне действительно нужны для их использования?

20

У меня нет предыдущего опыта работы с C ++ или C, но я знаю, как программировать на C #, и я изучаю Arduino. Я просто хочу организовать свои наброски, и мне вполне комфортно с языком Arduino даже с его ограничениями, но я действительно хотел бы иметь объектно-ориентированный подход к моему программированию на Arduino.

Итак, я видел, что у вас могут быть следующие способы (не исчерпывающий список) для организации кода:

  1. Один файл .ino;
  2. Несколько файлов .ino в одной папке (то, что IDE вызывает и отображает как «вкладки»);
  3. Файл .ino с включенным файлом .h и .cpp в одной папке;
  4. То же, что и выше, но файлы являются установленной библиотекой внутри папки программы Arduino.

Я также слышал о следующих способах, но пока не получил их работы:

  • Объявление класса в стиле C ++ в одном и том же файле .ino (слышал, но никогда не видел работающим - это вообще возможно?);
  • [предпочтительный подход] Включая файл .cpp, в котором объявлен класс, но без использования файла .h (хотелось бы, чтобы этот подход сработал?);

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

heltonbiker
источник
Для тех, кто интересуется, здесь есть интересная дискуссия об определениях классов без заголовка (только cpp): programmers.stackexchange.com/a/35391/35959
heltonbiker

Ответы:

31

Как IDE организует вещи

Во-первых, вот как IDE организует ваш «эскиз»:

  • Основной .inoфайл является одним из того же имени, что и папки он находится в Итак, для. foobar.inoВ foobarпапке - основной файл foobar.ino.
  • Любые другие .inoфайлы в этой папке объединяются вместе в алфавитном порядке в конце основного файла (независимо от того, где находится основной файл в алфавитном порядке).
  • Этот объединенный файл становится .cppфайлом (например, foobar.cpp) - он помещается во временную папку компиляции.
  • Препроцессор «услужливо» генерирует прототипы функций для функций, которые он находит в этом файле.
  • Основной файл проверяется на наличие #include <libraryname>директив. Это запускает среду IDE, чтобы также скопировать все соответствующие файлы из каждой (упомянутой) библиотеки во временную папку и сгенерировать инструкции для их компиляции.
  • Любые .c, .cppили .asmфайлы в папке эскиза добавляются в процессе сборки в виде отдельных единиц компиляции (то есть, они составлены обычным способом в виде отдельных файлов)
  • Любые .hфайлы также копируются во временную папку компиляции, поэтому к ним могут обращаться ваши файлы .c или .cpp.
  • Компилятор добавляет в процесс сборки стандартные файлы (вроде main.cpp)
  • Затем процесс сборки компилирует все вышеуказанные файлы в объектные файлы.
  • Если фаза компиляции проходит успешно, они связаны вместе со стандартными библиотеками AVR (например, предоставление вам strcpyи т. Д.)

Побочным эффектом всего этого является то, что вы можете рассматривать основной эскиз (файлы .ino) как C ++ для всех намерений и целей. Однако создание прототипа функции может привести к неясным сообщениям об ошибках, если вы не будете осторожны.


Избежание причуд препроцессора

Самый простой способ избежать этих идиосинкразий - оставить основной эскиз пустым (и не использовать другие .inoфайлы). Затем создайте еще одну вкладку ( .cppфайл) и поместите в нее свои вещи так:

#include <Arduino.h>

// put your sketch here ...

void setup ()
  {

  }  // end of setup

void loop ()
  {

  }  // end of loop

Обратите внимание, что вам нужно включить Arduino.h. Среда IDE делает это автоматически для основного эскиза, но для других модулей компиляции это необходимо сделать. В противном случае он не будет знать о таких вещах, как String, аппаратные регистры и т. Д.


Как избежать установки / основной парадигмы

Вам не нужно работать с концепцией настройки / цикла. Например, ваш файл .cpp может быть:

#include <Arduino.h>

int main ()
  {
  init ();  // initialize timers
  Serial.begin (115200);
  Serial.println ("Hello, world");
  Serial.flush (); // let serial printing finish
  }  // end of main

Принудительное включение библиотеки

Если вы работаете с концепцией «пустой эскиз», вам все равно нужно включить библиотеки, используемые в других местах проекта, например, в ваш основной .inoфайл:

#include <Wire.h>
#include <SPI.h>
#include <EEPROM.h>

Это связано с тем, что среда IDE сканирует только основной файл на предмет использования библиотеки. По сути, вы можете рассматривать основной файл как файл проекта, который указывает, какие внешние библиотеки используются.


Проблемы с именами

  • Не называйте ваш главный набросок "main.cpp" - в среде IDE есть собственный main.cpp, поэтому у вас будет дубликат, если вы это сделаете.

  • Не называйте ваш .cpp файл тем же именем, что и ваш основной .ino файл. Поскольку файл .ino фактически превращается в файл .cpp, это также даст вам конфликт имен.


Объявление класса в стиле C ++ в одном и том же файле .ino (слышал, но никогда не видел работающим - это вообще возможно?);

Да, это компилируется нормально:

class foo {
  public:
};

foo bar;

void setup () { }
void loop () { }

Однако вам, вероятно, лучше всего следовать обычной практике: поместите ваши декларации в .hфайлы, а ваши определения (реализации) в .cpp(или .c) файлы.

Почему "наверное"?

Как показывает мой пример, вы можете собрать все в один файл. Для более крупных проектов лучше быть более организованным. В конце концов вы попадаете на сцену в средне-крупномасштабном проекте, где вы хотите разделить вещи на «черные ящики», то есть класс, который делает одно, делает это хорошо, тестируется и самодостаточен ( настолько далеко, насколько возможно).

Если этот класс затем используется в нескольких других файлах в вашем проекте, то здесь отдельные .hи .cppфайлы вступают в игру.

  • .hФайл объявляет класс - то есть, он обеспечивает достаточно деталей для других файлов , чтобы знать , что он делает, какие функции у него есть, и как они называются.

  • В .cppфайл определяет (реализует) класс - то есть, он на самом деле обеспечивает функции и статические члены класса, которые делают класс делать свое дело. Поскольку вы хотите реализовать его только один раз, это находится в отдельном файле.

  • .hФайл , что входит в другие файлы. .cppФайл компилируется один раз в IDE для реализации функций класса.

Библиотеки

Если вы следуете этой парадигме, то вы готовы очень легко переместить весь класс ( файлы .hи .cppфайлы) в библиотеку. Затем его можно разделить между несколькими проектами. Все , что требуется , чтобы сделать папку (например. myLibrary) И поставить .hи .cppфайлы в ней (например, myLibrary.hа myLibrary.cpp) , а затем поместить эту папку внутри librariesпапки в папку , в которой хранятся ваши эскизы (этюдник папку).

Перезапустите IDE, и теперь она знает об этой библиотеке. Это действительно тривиально просто, и теперь вы можете поделиться этой библиотекой с несколькими проектами. Я делаю это много.


Чуть подробнее здесь .

Ник Гаммон
источник
Хороший ответ. Однако одна наиболее важная тема для меня еще не ясна: почему все говорят: « Возможно, вам лучше всего следовать обычной практике: .h + .cpp»? Почему лучше? Почему, вероятно, часть? И самое главное: как я могу этого не делать, то есть иметь интерфейс и реализацию (то есть весь код класса) в одном и том же файле .cpp? Большое спасибо сейчас! : o)
Хелтонбайкер
Добавил еще пару абзацев, чтобы ответить, почему «вероятно» у вас должны быть отдельные файлы.
Ник Гэммон
1
Как ты этого не делаешь? Просто сложите их вместе, как показано в моем ответе, однако вы можете обнаружить, что препроцессор работает против вас. Некоторые совершенно правильные определения класса C ++ не работают, если они помещены в основной файл .ino.
Ник Гэммон
Они также потерпят неудачу, если вы включите файл .H в два ваших файла .cpp, и этот файл .h содержит код, который является обычной привычкой некоторых. Это открытый исходный код, просто исправьте это самостоятельно. Если вам неудобно это делать, вам, вероятно, не следует использовать открытый исходный код. Прекрасное объяснение @Nick Gammon, лучше всего, что я видел до сих пор.
@ Spiked3 Вопрос не столько в том, чтобы выбрать то, с чем мне удобнее всего, на данный момент это вопрос знания того, из чего я могу выбрать. Как я могу сделать разумный выбор, если я даже не знаю, какие у меня есть варианты, и почему каждый вариант такой, какой он есть? Как я уже сказал, у меня нет предыдущего опыта работы с C ++, и похоже, что C ++ в Arduino может потребовать дополнительной осторожности, как показано в этом самом ответе. Но я уверен, что в конце концов я
пойму это и сделаю
6

Мой совет - придерживаться типичного для C ++ способа работы: разделять интерфейс и реализацию в файлах .h и .cpp для каждого класса.

Есть несколько уловов:

  • Вам нужен по крайней мере один файл .ino - я использую символическую ссылку на файл .cpp, где я создаю экземпляры классов.
  • Вы должны предоставить обратные вызовы, которые ожидает среда Arduino (setu, loop и т. д.)
  • в некоторых случаях вы будете удивлены нестандартными странными вещами, которые отличают IDE Arduino от обычной, такими как автоматическое включение определенных библиотек, но не других.

Или вы можете отказаться от IDE Arduino и попробовать Eclipse . Как я уже говорил, некоторые вещи, которые должны помочь начинающим, имеют тенденцию мешать более опытным разработчикам.

Игорь Стоппа
источник
Хотя я чувствую, что разделение эскиза на несколько файлов (вкладок или включений) помогает всему быть на своем месте, я чувствую, что мне нужно иметь два файла, чтобы заботиться об одном и том же (.h и .cpp), это своего рода ненужная избыточность / дублирование. Такое ощущение, что класс определяется дважды, и каждый раз, когда мне нужно поменять одно место, мне нужно поменять другое. Обратите внимание, что это применимо только к простым случаям, таким как мой, где будет только одна реализация данного заголовка, и они будут использоваться только один раз (в одном эскизе).
Heltonbiker
Это упрощает работу компилятора / компоновщика и позволяет иметь в файлах .cpp элементы, которые не являются частью класса, но используются в каком-либо методе. И если в классе есть статические метеры, вы не можете поместить их в файл .h.
Игорь Стоппа
Разделение файлов .h и .cpp давно признано ненужным. Java, C #, JS не требуют заголовочных файлов, и даже стандарты cpp iso пытаются от них отказаться. Проблема в том, что слишком много устаревшего кода может сломаться с таким радикальным изменением. Вот почему у нас CPP после C, а не просто расширенный C. Я ожидаю, что то же самое произойдет снова, CPX после CPP?
Конечно, если в следующей ревизии появится способ выполнять те же задачи, которые выполняются заголовками, без заголовков ... но в то же время есть много вещей, которые нельзя сделать без заголовков: я хочу посмотреть, как распределенная компиляция может происходить без заголовков и без больших накладных расходов.
Игорь Стоппа
6

Я выкладываю ответ только для полноты, после того, как выяснил и протестировал способ объявления и реализации класса в одном и том же файле .cpp без использования заголовка. Итак, что касается точной формулировки моего вопроса «сколько типов файлов мне нужно, чтобы использовать классы», в настоящем ответе используются два файла: один .ino с включением, настройкой и циклом, и .cpp, содержащий целое (довольно минималистично ) класс, представляющий поворотники игрушечного транспортного средства.

Blinker.ino

#include <TurnSignals.cpp>

TurnSignals turnSignals(2, 4, 8);

void setup() { }

void loop() {
  turnSignals.run();
}

TurnSignals.cpp

#include "Arduino.h"

class TurnSignals
{
    int 
        _left, 
        _right, 
        _buzzer;

    const int 
        amberPeriod = 300,

        beepInFrequency = 600,
        beepOutFrequency = 500,
        beepDuration = 20;    

    boolean
        lightsOn = false;

    public : TurnSignals(int leftPin, int rightPin, int buzzerPin)
    {
        _left = leftPin;
        _right = rightPin;
        _buzzer = buzzerPin;

        pinMode(_left, OUTPUT);
        pinMode(_right, OUTPUT);
        pinMode(_buzzer, OUTPUT);            
    }

    public : void run() 
    {        
        blinkAll();
    }

    void blinkAll() 
    {
        static long lastMillis = 0;
        long currentMillis = millis();
        long elapsed = currentMillis - lastMillis;
        if (elapsed > amberPeriod) {
            if (lightsOn)
                turnLightsOff();   
            else
                turnLightsOn();
            lastMillis = currentMillis;
        }
    }

    void turnLightsOn()
    {
        tone(_buzzer, beepInFrequency, beepDuration);
        digitalWrite(_left, HIGH);
        digitalWrite(_right, HIGH);
        lightsOn = true;
    }

    void turnLightsOff()
    {
        tone(_buzzer, beepOutFrequency, beepDuration);
        digitalWrite(_left, LOW);
        digitalWrite(_right, LOW);
        lightsOn = false;
    }
};
heltonbiker
источник
1
Это похоже на java и добавляет реализацию методов в объявление класса. Помимо ограниченной читабельности - заголовок дает вам объявление методов в сжатой форме - мне интересно, будут ли работать более необычные объявления классов (например, со статикой, друзьями и т. Д.). Но большая часть этого примера не очень хороша, потому что он включает файл только после того, как включение просто объединяется. Реальные проблемы начинаются, когда вы включаете один и тот же файл в нескольких местах и ​​начинаете получать объявления о конфликтующих объектах от компоновщика.
Игорь Стоппа