Klassen und Objekte: Wie viele und welche Dateitypen benötige ich tatsächlich, um sie zu verwenden?

20

Ich habe keine Vorkenntnisse in C ++ oder C, weiß aber, wie man C # programmiert und lerne Arduino. Ich möchte nur meine Skizzen organisieren und bin mit der Arduino-Sprache trotz ihrer Einschränkungen ziemlich vertraut, aber ich möchte wirklich einen objektorientierten Ansatz für meine Arduino-Programmierung haben.

So habe ich gesehen, dass Sie die folgenden Möglichkeiten (nicht vollständige Liste) haben können, um Code zu organisieren:

  1. Eine einzelne .ino-Datei;
  2. Mehrere .ino-Dateien im selben Ordner (was die IDE als "Tabs" bezeichnet und anzeigt);
  3. Eine .ino-Datei mit einer enthaltenen .h- und .cpp-Datei im selben Ordner.
  4. Wie oben, aber die Dateien sind eine installierte Bibliothek im Arduino-Programmordner.

Ich habe auch von den folgenden Möglichkeiten gehört, habe sie aber noch nicht zum Laufen gebracht:

  • Deklarieren einer Klasse im C ++ - Stil in derselben einzelnen .ino-Datei (habe von funktionierenden Klassen gehört, diese aber noch nie gesehen - ist das überhaupt möglich?);
  • [bevorzugter Ansatz] Einschließen einer CPP-Datei, in der eine Klasse deklariert ist, jedoch ohne Verwendung einer H-Datei (würde dieser Ansatz gefallen, sollte das funktionieren?);

Beachten Sie, dass ich nur Klassen verwenden möchte, damit der Code stärker partitioniert ist. Meine Anwendungen sollten sehr einfach sein und hauptsächlich Schaltflächen, LEDs und Summer enthalten.

Heltonbiker
quelle
Für Interessierte gibt es hier eine interessante Diskussion über die Definitionen von Klassen ohne Header (nur CPP): programmers.stackexchange.com/a/35391/35959
heltonbiker 07.07.15

Antworten:

31

Wie die IDE die Dinge organisiert

Das erste ist, wie die IDE Ihre "Skizze" organisiert:

  • Die .inoHauptdatei ist diejenige , mit dem gleichen Namen wie der Ordner ist es in. Also, für foobar.inoinfoobar Ordner - die Hauptdatei foobar.ino ist.
  • Jede andere .ino Dateien in diesem Ordner werden am Ende der Hauptdatei in alphabetischer Reihenfolge miteinander verknüpft (unabhängig davon, wo sich die Hauptdatei in alphabetischer Reihenfolge befindet).
  • Diese verkettete Datei wird zu einer .cppDatei (zB foobar.cpp) - sie wird in einem temporären Zusammenstellungsordner abgelegt.
  • Der Präprozessor generiert "hilfreich" Funktionsprototypen für Funktionen, die er in dieser Datei findet.
  • Die Hauptdatei wird gesucht #include <libraryname> Direktiven . Dadurch kopiert die IDE auch alle relevanten Dateien aus jeder (genannten) Bibliothek in den temporären Ordner und generiert Anweisungen zum Kompilieren.
  • Beliebig .c, .cppoder.asm Dateien in der Skizze Ordnern hinzugefügt werden, um den Build - Prozess als separate Kompilierungseinheiten (das heißt, sie werden in der üblichen Art und Weise zusammengestellt als separate Dateien)
  • Irgendein .h Dateien werden auch in den temporären Kompilierungsordner kopiert, sodass Ihre C- oder CPP-Dateien auf sie verweisen können.
  • Der Compiler fügt dem Buildprozess Standarddateien hinzu (like main.cpp)
  • Der Erstellungsprozess kompiliert dann alle oben genannten Dateien in Objektdateien.
  • Wenn die Kompilierungsphase erfolgreich ist, werden sie zusammen mit den AVR-Standardbibliotheken verknüpft (z. B. geben Sie strcpyusw.).

Ein Nebeneffekt all dessen ist, dass Sie die Hauptskizze (die .ino-Dateien) in jeder Hinsicht als C ++ betrachten können. Die Generierung von Funktionsprototypen kann jedoch zu undurchsichtigen Fehlermeldungen führen, wenn Sie nicht vorsichtig sind.


Vermeiden Sie die Macken des Vorprozessors

Die einfachste Möglichkeit, diese Eigenheiten zu vermeiden, besteht darin, die Hauptskizze leer zu lassen (und keine anderen .inoDateien zu verwenden). Dann mache einen weiteren Tab (eine .cppDatei) und füge deine Sachen so ein:

#include <Arduino.h>

// put your sketch here ...

void setup ()
  {

  }  // end of setup

void loop ()
  {

  }  // end of loop

Beachten Sie, dass Sie einschließen müssen Arduino.h. Die IDE macht das automatisch für die Hauptskizze, aber für andere Kompilierungseinheiten müssen Sie es tun. Andernfalls werden Dinge wie String, die Hardware-Register usw. nicht bekannt.


Vermeidung des Setup- / Hauptparadigmas

Sie müssen nicht mit dem Setup / Loop-Konzept arbeiten. Beispielsweise kann Ihre CPP-Datei sein:

#include <Arduino.h>

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

Bibliothekseinschluss erzwingen

Wenn Sie mit dem Konzept "Leere Skizze" arbeiten, müssen Sie die an anderer Stelle im Projekt verwendeten Bibliotheken einbeziehen, beispielsweise in Ihre Hauptdatei .ino:

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

Dies liegt daran, dass die IDE nur die Hauptdatei auf Bibliotheksnutzung überprüft. Tatsächlich können Sie die Hauptdatei als "Projekt" -Datei betrachten, die angibt, welche externen Bibliotheken verwendet werden.


Probleme beim Benennen

  • Nennen Sie Ihre Hauptskizze nicht "main.cpp" - die IDE enthält ihre eigene main.cpp, sodass Sie in diesem Fall ein Duplikat erhalten.

  • Benennen Sie Ihre CPP-Datei nicht mit demselben Namen wie Ihre Haupt-INO-Datei. Da die .ino-Datei effektiv zu einer .cpp-Datei wird, erhalten Sie auch einen Namenskonflikt.


Deklarieren einer Klasse im C ++ - Stil in derselben einzelnen .ino-Datei (habe von funktionierenden Klassen gehört, diese aber noch nie gesehen - ist das überhaupt möglich?);

Ja, dies wird in Ordnung kompiliert:

class foo {
  public:
};

foo bar;

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

Es ist jedoch wahrscheinlich am besten, wenn Sie der normalen Praxis folgen: Schreiben Sie Ihre Deklarationen in .hDateien und Ihre Definitionen (Implementierungen) in .cpp(oder .c) Dateien.

Warum "wahrscheinlich"?

Wie mein Beispiel zeigt, können Sie alles in einer Datei zusammenfassen. Für größere Projekte ist es besser, organisierter zu sein. Schließlich kommen Sie in einem mittleren bis großen Projekt auf die Bühne, in dem Sie Dinge in "Black Boxes" aufteilen möchten - eine Klasse, die eines tut, es gut macht, getestet wird und in sich geschlossen ist ( so weit wie möglich).

Wenn diese Klasse dann in mehreren anderen Dateien in Ihrem Projekt verwendet wird, kommen hier die separaten .hund .cppDateien ins Spiel.

  • Die .hDatei deklariert die Klasse - das heißt, sie enthält genügend Details, damit andere Dateien wissen, was sie tut, welche Funktionen sie hat und wie sie aufgerufen werden.

  • Die .cppDatei definiert (implementiert) die Klasse - das heißt, sie stellt tatsächlich die Funktionen und statischen Klassenmitglieder bereit, die die Klasse dazu bringen, ihre Sache zu tun. Da Sie es nur einmal implementieren möchten, befindet es sich in einer separaten Datei.

  • Die .hDatei ist das, was in anderen Dateien enthalten ist. Die .cppDatei wird einmal von der IDE kompiliert, um die Klassenfunktionen zu implementieren.

Bibliotheken

Wenn Sie diesem Paradigma folgen, können Sie die gesamte Klasse (die .hund .cppDateien) ganz einfach in eine Bibliothek verschieben. Dann kann es zwischen mehreren Projekten geteilt werden. Alles , was erforderlich ist , um einen Ordner zu machen (z. B. myLibrary) und die setzt .hund .cppDateien hinein (z. B. myLibrary.hund myLibrary.cpp) und stellen Sie diesen Ordner in Ihrem dann librariesin dem Ordner Ordner , in dem Sie Ihre Skizzen (das Skizzenbuch Ordner) gehalten werden.

Starten Sie die IDE neu und sie kennt diese Bibliothek jetzt. Dies ist ganz einfach, und jetzt können Sie diese Bibliothek für mehrere Projekte freigeben. Ich mache das oft.


Ein bisschen mehr Details hier .

Nick Gammon
quelle
Gute Antwort. Ein wichtigstes Thema ist mir jedoch noch nicht klar geworden: Warum sagt jeder "Sie sind wahrscheinlich am besten dran, um der normalen Praxis zu folgen: .h + .cpp"? Warum ist es besser? Warum das wohl Teil? Und das Wichtigste: Wie kann ich das nicht tun, dh Interface und Implementierung (dh den gesamten Klassencode) in derselben einzigen CPP-Datei haben? Vielen Dank fürs erste! : o)
Heltonbiker
Es wurden weitere Absätze hinzugefügt, um zu beantworten, warum "wahrscheinlich" Sie separate Dateien haben sollten.
Nick Gammon
1
Wie machst du das nicht ? Fügen Sie einfach alle zusammen, wie in meiner Antwort dargestellt. Möglicherweise stellen Sie jedoch fest, dass der Präprozessor gegen Sie arbeitet. Einige perfekt gültige C ++ - Klassendefinitionen schlagen fehl, wenn sie in die .ino-Hauptdatei eingefügt werden.
Nick Gammon
Sie schlagen auch fehl, wenn Sie eine .H-Datei in zwei Ihrer .cpp-Dateien einfügen und diese .h-Datei Code enthält, was bei einigen häufig vorkommt. Es ist Open Source, reparieren Sie es einfach selbst. Wenn Sie sich damit nicht wohl fühlen, sollten Sie Open Source wahrscheinlich nicht verwenden. Schöne Erklärung @Nick Gammon, besser als alles, was ich bisher gesehen habe.
@ Spiked3 Es geht nicht so sehr darum, zu entscheiden, womit ich mich am wohlsten fühle. Im Moment geht es darum, zu wissen, aus welchen Optionen ich überhaupt auswählen kann. Wie könnte ich eine vernünftige Wahl treffen, wenn ich nicht einmal weiß, was meine Optionen sind und warum jede Option so ist, wie sie ist? Wie gesagt, ich habe noch keine Erfahrung mit C ++ und es sieht so aus, als ob C ++ in Arduino zusätzliche Sorgfalt erfordern könnte, wie in dieser Antwort gezeigt. Aber ich bin mir sicher, dass ich irgendwann ein Gefühl dafür bekomme und meine Arbeit erledige, ohne das Rad neu zu erfinden (zumindest hoffe ich das) :)
heltonbiker 07.07.15
6

Mein Rat ist, sich an die typische C ++ - Vorgehensweise zu halten: separate Schnittstelle und Implementierung in .h- und .cpp-Dateien für jede Klasse.

Es gibt ein paar Fänge:

  • Sie benötigen mindestens eine .ino-Datei. Ich verwende einen Symlink zu der .cpp-Datei, in der ich die Klassen instanziiere.
  • Sie müssen die Rückrufe bereitstellen, die die Arduino-Umgebung erwartet (setu, loop usw.)
  • In einigen Fällen werden Sie von den ungewöhnlichen Dingen überrascht sein, die die Arduino-IDE von einer normalen unterscheiden, wie dem automatischen Einbeziehen bestimmter Bibliotheken, aber nicht anderer.

Oder Sie könnten die Arduino-IDE fallen lassen und es mit Eclipse versuchen . Wie ich bereits erwähnte, stehen einige Dinge, die Anfängern helfen sollen, eher erfahrenen Entwicklern im Weg.

Igor Stoppa
quelle
Obwohl ich der Meinung bin, dass das Aufteilen einer Skizze in mehrere Dateien (Tabs oder Includes) dazu beiträgt, dass sich alles an seinem Platz befindet, möchte ich zwei Dateien haben, um dasselbe zu erledigen (.h und .cpp) unnötige Redundanz / Vervielfältigung. Es fühlt sich an, als würde die Klasse zweimal definiert, und jedes Mal, wenn ich einen Ort wechseln muss, muss ich den anderen ändern. Beachten Sie, dass dies nur für einfache Fälle wie meinen gilt, in denen es nur eine Implementierung eines bestimmten Headers gibt und diese nur einmal verwendet werden (in einer einzelnen Skizze).
Heltonbiker
Es vereinfacht die Arbeit des Compilers / Linkers und ermöglicht es Ihnen, Elemente in den CPP-Dateien zu haben, die nicht Teil der Klasse sind, aber in einigen Methoden verwendet werden. Und falls die Klasse statische Meme r enthält, können Sie diese nicht in die .h-Datei einfügen.
Igor Stoppa
Das Trennen von .h- und .cpp-Dateien wurde lange als nicht mehr erforderlich erkannt. Für Java, C # und JS sind keine Header-Dateien erforderlich, und selbst CPP-ISO-Standards versuchen, sich von ihnen zu entfernen. Das Problem ist, dass es zu viel Legacy-Code gibt, der bei solch einer radikalen Änderung brechen könnte. Das ist der Grund, warum wir CPP nach C haben und nicht nur ein erweitertes C. Ich gehe davon aus, dass dasselbe wieder passieren wird, CPX nach CPP?
Klar, wenn in der nächsten Revision die gleichen Aufgaben ausgeführt werden, die von Headern ausgeführt werden, ohne Header ... Aber in der Zwischenzeit gibt es viele Dinge, die nicht ohne Header ausgeführt werden können: Ich möchte sehen, wie die Kompilierung verteilt ist Dies kann ohne Header und ohne größere Gemeinkosten geschehen.
Igor Stoppa
6

Der Vollständigkeit halber stelle ich eine Antwort bereit , nachdem ich herausgefunden und getestet habe, wie eine Klasse in derselben CPP-Datei deklariert und implementiert werden kann, ohne einen Header zu verwenden. In Bezug auf die genaue Formulierung meiner Frage "Wie viele Dateitypen benötige ich, um Klassen zu verwenden?" Werden in der vorliegenden Antwort zwei Dateien verwendet: eine .ino-Datei mit Include, Setup und Schleife und die .cpp-Datei, die das Ganze enthält (eher minimalistisch) ) Klasse, die die Blinker eines Spielzeugfahrzeugs darstellt.

Blinker.ino

#include <TurnSignals.cpp>

TurnSignals turnSignals(2, 4, 8);

void setup() { }

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

Blinker.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
quelle
1
Dies ist Java-ähnlich und schlägt die Implementierung von Methoden in die Deklaration der Klasse. Abgesehen von der eingeschränkten Lesbarkeit - der Header gibt Ihnen die Deklaration der Methoden in einer übersichtlichen Form - frage ich mich, ob ungewöhnlichere Klassendeklarationen (wie bei Statics, Friends usw.) noch funktionieren würden. Das meiste in diesem Beispiel ist jedoch nicht wirklich gut, da es die Datei nur dann enthält, wenn eine Aufnahme einfach verkettet wird. Die eigentlichen Probleme treten auf, wenn Sie dieselbe Datei an mehreren Stellen einfügen und vom Linker widersprüchliche Objektdeklarationen abrufen.
Igor Stoppa