Класи та об'єкти: скільки та які типи файлів мені потрібно фактично їх використовувати?


20

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

Тому я бачив, що ви можете мати наступні способи (не вичерпний список) для організації коду:

  1. Один .ino файл;
  2. Кілька файлів .ino в одній папці (те, що IDE викликає і відображається як "вкладки");
  3. Файл .ino із включеним .h та .cpp-файлом у одній папці;
  4. Як і вище, але файли - це встановлена ​​бібліотека всередині папки програм Arduino.

Я також чув про такі способи, але ще не змусив їх працювати:

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

Зауважте, що я хочу використовувати класи лише для того, щоб код був більш розділеним, мої програми повинні бути дуже простими, в основному включаючи в себе переважно кнопки, світлодіоди та зумери.


Для тих, хто цікавиться, тут є цікава дискусія щодо визначень класів без заголовків (лише для 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)
heltonbiker

Додано ще пару абзаців, щоб відповісти, чому "ймовірно" у вас повинні бути окремі файли.
Нік Гаммон

1
Як цього не зробити? Просто складіть їх разом, як показано у моїй відповіді, проте ви можете виявити, що препроцесор працює проти вас. Деякі ідеально правильні визначення класу C ++ виходять з ладу, якщо вони розміщені у головному файлі .ino
Нік Гаммон

Вони також вийдуть з ладу, якщо ви включите .H файл у два ваших .cpp-файли, а цей .h-файл містить код, який є звичною для деяких. Його відкритий код, просто виправте його самостійно. Якщо вам це не зручно, ви, ймовірно, не повинні користуватися відкритим кодом. Гарне пояснення @ Нік Гаммон, краще за все, що я бачив досі.

@ Spiked3 - це не стільки питання вибору того, що мені найбільше подобається, бо зараз питання про те, що мені в першу чергу можна вибрати. Як я міг зробити розумний вибір, якщо навіть не знаю, що таке мої варіанти, і чому кожен варіант є таким, яким він є? Як я вже говорив, у мене немає попереднього досвіду роботи з C ++, і схоже, що C ++ в Arduino може зажадати додаткового догляду, як показано в цій самій відповіді. Але я впевнений, що в кінцевому підсумку я розумію це і буду робити свої речі, не вигадуючи колесо (принаймні, на це я сподіваюся) :)
heltonbiker

6

Моя порада - дотримуватися типового C ++ способу роботи: окремий інтерфейс та реалізація у .h та .cpp-файли для кожного класу.

Є кілька уловів:

  • вам потрібен принаймні один .ino файл - я використовую симпосилання на файл .cpp, де інстанціюю класи.
  • ви повинні надати зворотні дзвінки, яких очікує середовище Arduino (налаштування, цикл тощо)
  • в деяких випадках вас здивують нестандартні дивні речі, які відрізняють ID Arduino від нормальної, наприклад автоматичне включення певних бібліотек, але не інших.

Або ви можете вирвати Arduino IDE і спробувати 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;
    }
};

1
Це схоже на java і прищеплює реалізацію методів у декларації класу. Окрім зменшеної читабельності - заголовок дає вам декларування методів у стислій формі, - мені цікаво, чи ще незвичні декларації класу (наприклад, зі статикою, друзями тощо) все-таки спрацюють. Але більшість із цього прикладу насправді не дуже добре, оскільки він включає файл лише один раз, коли включення просто об'єднується. Справжні проблеми починаються, коли ви включаєте один і той же файл у декількох місцях, і ви починаєте отримувати суперечливі декларації об’єктів від лінкера.
Ігор Стоппа
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.