Klasy i obiekty: ile i jakich typów plików faktycznie muszę ich używać?

20

Nie mam wcześniejszego doświadczenia z C ++ lub C, ale wiem, jak programować w C # i uczę się Arduino. Chcę tylko organizować swoje szkice i czuję się swobodnie z językiem Arduino, nawet z jego ograniczeniami, ale naprawdę chciałbym mieć podejście obiektowe do mojego programowania Arduino.

Widziałem więc, że możesz organizować kod na następujące sposoby (niewyczerpująca lista):

  1. Pojedynczy plik .ino;
  2. Wiele plików .ino w tym samym folderze (to, co IDE wywołuje i wyświetla jak „tabs”);
  3. Plik .ino z dołączonym plikiem .h i .cpp w tym samym folderze;
  4. Tak jak powyżej, ale pliki są zainstalowaną biblioteką w folderze programu Arduino.

Słyszałem również o następujących sposobach, ale jeszcze ich nie uruchomiłem:

  • Zadeklarowanie klasy w stylu C ++ w tym samym pojedynczym pliku .ino (słyszałem o działaniu, ale nigdy nie widziałem - czy to w ogóle możliwe);
  • [preferowane podejście] Dołączenie pliku .cpp, w którym zadeklarowana jest klasa, ale bez użycia pliku .h (czy to podejście, czy to powinno działać?);

Zauważ, że chcę używać klas, aby kod był bardziej podzielony na partycje, moje aplikacje powinny być bardzo proste, głównie z przyciskami, diodami LED i brzęczykami.

heltonbiker
źródło
Dla zainteresowanych ciekawa dyskusja na temat definicji klas bez nagłówków (tylko CPP) tutaj: programmers.stackexchange.com/a/35391/35959
heltonbiker

Odpowiedzi:

31

Jak IDE organizuje rzeczy

Po pierwsze, w ten sposób IDE organizuje „szkic”:

  • Plik główny .inoma taką samą nazwę jak folder, w którym się znajduje. Tak więc, foobar.inow foobarfolderze - głównym plikiem jest foobar.ino.
  • Wszelkie inne .inopliki w tym folderze są łączone razem, w kolejności alfabetycznej, na końcu pliku głównego (niezależnie od tego, gdzie znajduje się plik główny, alfabetycznie).
  • Ten skonkatenowany plik staje się .cppplikiem (np. foobar.cpp) - jest umieszczany w tymczasowym folderze kompilacji.
  • Preprocesor „pomocnie” generuje prototypy funkcji dla funkcji znalezionych w tym pliku.
  • Główny plik jest skanowany w poszukiwaniu #include <libraryname>dyrektyw. To powoduje, że IDE kopiuje również wszystkie odpowiednie pliki z każdej (wspomnianej) biblioteki do folderu tymczasowego i generuje instrukcje, aby je skompilować.
  • Wszelkie .c, .cppczy .asmpliki w folderze szkicu są dodawane do procesu budowania jako oddzielne jednostki kompilacji (to znaczy, że są kompilowane w zwykły sposób jako osobne pliki)
  • Wszelkie .hpliki są również kopiowane do tymczasowego folderu kompilacji, aby można było do nich odwoływać pliki .c lub .cpp.
  • Kompilator dodaje do standardowych plików procesu kompilacji (jak main.cpp)
  • Proces kompilacji następnie kompiluje wszystkie powyższe pliki do plików obiektowych.
  • Jeśli faza kompilacji się powiedzie, są one łączone razem ze standardowymi bibliotekami AVR (np. Dając ci strcpyitp.)

Efektem ubocznym tego wszystkiego jest to, że można uznać główny szkic (pliki .ino) za C ++ do wszystkich celów i celów. Generowanie prototypu funkcji może jednak prowadzić do niejasnych komunikatów o błędach, jeśli nie będziesz ostrożny.


Unikanie dziwactwa preprocesora

Najprostszym sposobem uniknięcia tych osobliwości jest pozostawienie głównego szkicu pustego (i nie używanie innych .inoplików). Następnie utwórz kolejną kartę ( .cppplik) i umieść w niej swoje rzeczy w następujący sposób:

#include <Arduino.h>

// put your sketch here ...

void setup ()
  {

  }  // end of setup

void loop ()
  {

  }  // end of loop

Pamiętaj, że musisz to uwzględnić Arduino.h. IDE robi to automatycznie dla głównego szkicu, ale w przypadku innych jednostek kompilacji musisz to zrobić. W przeciwnym razie nie będzie wiedział o takich rzeczach jak String, rejestry sprzętu itp.


Unikanie paradygmatu setup / main

Nie musisz biegać z koncepcją setup / loop. Na przykład plik .cpp może być:

#include <Arduino.h>

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

Wymuś włączenie biblioteki

Jeśli korzystasz z koncepcji „pustego szkicu”, nadal musisz uwzględnić biblioteki używane w innym miejscu projektu, na przykład w .inopliku głównym :

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

Wynika to z faktu, że IDE skanuje tylko główny plik w celu użycia biblioteki. W rzeczywistości można uznać główny plik za plik „projektu”, który określa, które biblioteki zewnętrzne są w użyciu.


Problemy z nazewnictwem

  • Nie nazywaj głównego szkicu „main.cpp” - IDE zawiera własny main.cpp, więc będziesz miał duplikat, jeśli to zrobisz.

  • Nie nazywaj pliku .cpp taką samą nazwą jak główny plik .ino. Ponieważ plik .ino faktycznie staje się plikiem .cpp, również spowodowałoby to konflikt nazw.


Zadeklarowanie klasy w stylu C ++ w tym samym pojedynczym pliku .ino (słyszałem o działaniu, ale nigdy nie widziałem - czy to w ogóle możliwe);

Tak, kompiluje się OK:

class foo {
  public:
};

foo bar;

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

Jednak prawdopodobnie najlepiej jest postępować zgodnie ze zwykłą praktyką: Umieść swoje deklaracje w .hplikach, a definicje (implementacje) w .cpp(lub .c) plikach.

Dlaczego „prawdopodobnie”?

Jak pokazuje mój przykład, możesz złożyć wszystko w jednym pliku. W przypadku większych projektów lepiej być bardziej zorganizowanym. W końcu dostajesz się na scenę w średnim i dużym projekcie, w którym chcesz podzielić rzeczy na „czarne skrzynki” - to znaczy, klasę, która robi jedną rzecz, robi to dobrze, jest testowana i jest niezależna ( tak daleko jak to możliwe).

Jeśli ta klasa zostanie następnie użyta w wielu innych plikach w twoim projekcie, to tutaj oddzielne .hi .cpppliki wchodzą w grę.

  • .hPlik deklaruje klasę - to znaczy, że zapewnia wystarczającą ilość szczegółów dla innych plików wiedzieć, co robi, jakie funkcje to ma, a jak są one nazywane.

  • W .cpppliku definiuje (narzędzia) klasa - to znaczy, że faktycznie zapewnia funkcje i statycznych członków klasy, które sprawiają, że klasy, co robi. Ponieważ chcesz go zaimplementować tylko raz, jest to osobny plik.

  • .hPlik jest tym, co zostaje włączone do innych plików. .cppPlik jest skompilowany raz IDE do realizacji funkcji klasy.

Biblioteki

Jeśli zastosujesz się do tego paradygmatu, możesz bardzo łatwo przenieść całą klasę ( pliki .hi .cpp) do biblioteki. Następnie można go współdzielić między wieloma projektami. Wszystko, co jest wymagane, to utworzenie folderu (np. myLibrary) I umieszczenie w nim plików .horaz .cpp(np. myLibrary.hI myLibrary.cpp), a następnie umieszczenie tego folderu w librariesfolderze, w którym przechowywane są szkice (folder szkicownika).

Uruchom ponownie IDE i teraz wie o tej bibliotece. Jest to naprawdę banalnie proste i teraz możesz udostępniać tę bibliotekę w wielu projektach. Często to robię.


Trochę więcej szczegółów tutaj .

Nick Gammon
źródło
Niezła odpowiedź. Jednak jeden najważniejszy temat nie stał się dla mnie jasny: dlaczego wszyscy mówią „ prawdopodobnie najlepiej jest postępować zgodnie z normalną praktyką: .h + .cpp”? Dlaczego to jest lepsze? Dlaczego prawdopodobnie część? I najważniejsze: jak tego nie zrobić, to znaczy mieć interfejs i implementację (czyli cały kod klasy) w tym samym, pojedynczym pliku .cpp? Na razie bardzo dziękuję! : o)
heltonbiker
Dodano kolejną parę akapitów, aby odpowiedzieć, dlaczego „prawdopodobnie” powinieneś mieć osobne pliki.
Nick Gammon
1
Jak tego nie robisz? Po prostu złóż je wszystkie, jak pokazano w mojej odpowiedzi, jednak może się okazać, że preprocesor działa przeciwko tobie. Niektóre doskonale poprawne definicje klas C ++ zawodzą, jeśli zostaną umieszczone w głównym pliku .ino.
Nick Gammon
Nie powiodą się również, jeśli umieścisz plik .H w dwóch plikach .cpp, a ten plik .h zawiera kod, co jest powszechnym zwyczajem. Jest open source, po prostu napraw go sam. Jeśli nie czujesz się komfortowo, prawdopodobnie nie powinieneś używać open source. Piękne wyjaśnienie @Nick Gammon, lepsze niż cokolwiek, co do tej pory widziałem.
@ Spiked3 Nie chodzi tylko o to, aby wybrać to, co jest dla mnie najwygodniejsze, na razie chodzi o to, aby wiedzieć, co jest dla mnie dostępne. Jak mogę dokonać rozsądnego wyboru, jeśli nawet nie wiem, jakie są moje opcje i dlaczego każda z nich jest taka, jaka jest? Tak jak powiedziałem, nie mam wcześniejszego doświadczenia z C ++ i wygląda na to, że C ++ w Arduino może wymagać dodatkowej opieki, jak pokazano w tej samej odpowiedzi. Ale jestem pewien, że w końcu to rozumiem i
załatwiam
6

Radzę trzymać się typowego dla C ++ sposobu robienia rzeczy: osobnego interfejsu i implementacji w plikach .h i .cpp dla każdej klasy.

Istnieje kilka połowów:

  • potrzebujesz co najmniej jednego pliku .ino - używam dowiązania symbolicznego do pliku .cpp, w którym tworzę instancję klas.
  • musisz podać wywołania zwrotne, których oczekuje środowisko Arduino (setu, loop itp.)
  • w niektórych przypadkach będziesz zaskoczony niestandardowymi dziwnymi rzeczami, które odróżniają Arduino IDE od normalnych, takich jak automatyczne dołączanie niektórych bibliotek, ale nie innych.

Możesz też porzucić Arduino IDE i spróbować z Eclipse . Jak już wspomniałem, niektóre rzeczy, które powinny pomóc początkującym, stają się przeszkodą dla bardziej doświadczonych programistów.

Igor Stoppa
źródło
Chociaż uważam, że rozdzielenie szkicu na wiele plików (tabulatorów lub dołączeń) pomaga wszystko znajdować się na swoim miejscu, mam ochotę mieć dwa pliki, aby zająć się tym samym (.h i .cpp) jest rodzajem niepotrzebna redundancja / powielanie. Wydaje się, że klasa jest definiowana dwa razy i za każdym razem, gdy muszę zmienić jedno miejsce, muszę zmienić drugie. Uwaga: dotyczy to tylko prostych przypadków, takich jak mój, w których będzie tylko jedna implementacja danego nagłówka i będą one używane tylko raz (w jednym szkicu).
heltonbiker,
Upraszcza to pracę kompilatora / linkera i pozwala mieć w plikach .cpp elementy, które nie są częścią klasy, ale są używane w niektórych metodach. A jeśli klasa ma statyczne elementy pamięci, nie można ich umieścić w pliku .h.
Igor Stoppa,
Oddzielanie plików .h i .cpp od dawna jest uznawane za niepotrzebne. Żadne Java, C #, JS nie wymagają plików nagłówkowych, a nawet standardy ISO procesora CPP starają się od nich odejść. Problem polega na tym, że istnieje zbyt wiele starszego kodu, który mógłby zerwać przy tak radykalnej zmianie. To dlatego mamy CPP po C, a nie tylko rozszerzone C. Oczekuję, że to samo się powtórzy, CPX po CPP?
Jasne, jeśli kolejna wersja wymyśli sposób wykonywania tych samych zadań, które są wykonywane przez nagłówki, bez nagłówków ... ale w międzyczasie istnieje wiele rzeczy, których nie można zrobić bez nagłówków: Chcę zobaczyć, jak kompilacja rozproszona może się zdarzyć bez nagłówków i bez ponoszenia dużych kosztów ogólnych.
Igor Stoppa,
6

Wysyłam odpowiedź tylko dla kompletności, po znalezieniu i przetestowaniu sposobu deklarowania i implementacji klasy w tym samym pliku .cpp, bez użycia nagłówka. Tak więc, jeśli chodzi o dokładne sformułowanie mojego pytania „ile typów plików muszę użyć klas”, w niniejszej odpowiedzi wykorzystano dwa pliki: jeden .ino z dołączeniem, instalacją i pętlą oraz .cpp zawierający całość (raczej minimalistyczny ), reprezentujących sygnały zwrotne pojazdu-zabawki.

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
źródło
1
Jest to podobne do języka Java i sprowadza implementację metod do deklaracji klasy. Oprócz zmniejszonej czytelności - nagłówek podaje deklarację metod w zwięzłej formie - zastanawiam się, czy bardziej nietypowe deklaracje klas (takie jak statyka, przyjaciele itp.) Nadal by działały. Ale większość tego przykładu nie jest naprawdę dobra, ponieważ zawiera plik tylko wtedy, gdy dołączenie po prostu się łączy. Prawdziwe problemy zaczynają się, gdy umieścisz ten sam plik w wielu miejscach i zaczniesz otrzymywać deklaracje o konfliktach obiektów od konsolidatora.
Igor Stoppa