Hier gibt es eine Anleitung für C++ Klassen:
http://www.cplusplus.com/doc/tutorial/classes/
Was spricht eigentlich dagegen, nur *.h-Files anstatt *.cpp files zu
verwenden ? BTW die Methodendeklaration zusammen zu halten?
Hier mein Beispiel:
Cornelius schrieb:> Was spricht eigentlich dagegen, nur *.h-Files anstatt *.cpp files zu> verwenden
Man kann so kein Makefile vernünfitg einsetzen.
Denn ein "normales" Makefile kann mit wenig Aufriss alle *.cpp durch den
Compiler jagen und alle *.h ignorieren. Und ja, das gehört genau so.
Bei IDEs ist es ähnlich, sobald sie Features wie Syntax Highlighting
haben. Auch da muss man IMO *.h und *.cpp unterschiedlich behandeln -
sonst hagelt es Phantomfehler.
Übersichtlichkeit und noch weit mehr
Ich verwende den STM32 mit C++ und benutze Sachen wie virtuelle
abstrakte Klassen (Interfaces) und andere Späße.
manchmal ändert sich die Implementierung der Klasse während die
Funktionsnamen und Parameter die Selben geblieben sind.
Beispielsweise habe ich eine virtuelle Klasse "externer ADC". Das heisst
wenn es einen neuen ADC gibt welcher besser geeignet ist, dann muss ich
nur eine .cpp neu schreiben und das wars.
Wenn ich mir C# oder Java anschaue, brauch ich da auch keine getrennten
Declarations- und Implementationsfiles.
Es erscheint mir umständlich.
Hier mein minimalistisches LED-Experiment ohne Trennung:
1
classLed
2
{
3
booleanledState;
4
unsignedcharledPin;
5
6
public:
7
Led(unsignedchar_ledPin)
8
{
9
ledState=false;
10
ledPin=_ledPin;
11
pinMode(ledPin,OUTPUT);
12
}
13
14
voidtoogle()
15
{
16
ledState=!ledState;
17
digitalWrite(ledPin,ledState);
18
}
19
};
20
21
Ledled1(LED_BUILTIN);
22
23
voidsetup()
24
{
25
26
}
27
28
voidloop()
29
{
30
led1.toogle();
31
delay(100);
32
}
Als will ich ein Led-Array daraus machen. Alledings klapts leider noch
nicht ganz.
Stell Dir vor, du hast eine hardware.h mit der Funktion led_an(). Nun
bindest du diese Header Datei in zwei c oder cpp Dateien ein. Dann
existiert die Funktion doppelt, und das darf nicht sein.
Der Sinn Header Dateien ist, dem Compiler anzukündigen, wie eine
Struktur oder eine Funktion von außen betrachtet aussieht. Du kannst sie
mit Spezifikationen von Bauteilen vergleichen.
Mit Hilfe der Spezifikation eines Mikrochips kannst du eine Platine
bauen, in die der Mikrochip später eingesetzt werden kann. Die
Spezifikation beschreibt die Abmessungen, die Eingänge und die Ausgänge
des Mikrochips.
Später bestückst du die Platine mit dem Mikrochip. Du brauchst seinen
Innenschaltplan nicht zu kennen, Hauptsache er erfüllt die
Spezifikation.
So ähnlich ist das mit dem C/C++ Compiler. Wenn er eine C/C++ Datei
compiliert kann er Funktionsaufrufe einplanen, ohne dass der Code für
die Funktion zu diesem Zeitpunkt vorliegt. Hauptsache, Aufrufparameter
und Rückgabewert passen. Und die Header Datei sagt dem Compiler, wie
diese aussehen sollen, das ist die Spezifikation.
Wenn der Linker am Ende die Fragmente deines Programmes (*.o Dateien)
zusammenfügt, muss alles vollständig sein. Wenn nach dem Zusammenfügen
noch irgendeine Funktion fehlt, meldet sich der Linker entsprechend.
Außerhalb der Mikrocontroller Welt werden Libraries typischerweise in
bereits compilierter Form ausgeliefert. Bei Linux sind das entweder *.o
oder *.so Dateien, bei Windows sind es normalerweise *.dll Dateien.
Die *.o Dateien sind Code-Fragmente, die der C Compiler zu einem ganzen
Programm zusammenfügen kann. Die *.so und *.dll Dateien werden hingegen
zur Laufzeit vom Betriebssystem nachgeladen - sind daher für µC eher
irrelevant.
In der Regel werden Libraries zusammen mit Header Dateien ausgeliefert,
damit man sie in selbst geschriebene C/C++ Quelltexte einbinden kann.
Der C-Compiler braucht sie, sonst kann er nicht wissen, welche
"Signatur" (Eingabe- und Ausgabeparameter) die Funktion hat.
Also: *.h Dateien enthalten Spezifikationen von Funktionen und
Datentypen, aber keinen konkreten Quelltext der Speicher belegen würden.
*.c Dateien Enthalten konkreten Quelltext und konkrete Variablen, die
Speicher belegen. Der Compiler erzeugt daraus *.o Dateien.
Viele *.o Dateien werden durch den Linker zu einem lauffähigen Programm
zusammengefügt.
Cornelius schrieb:> Wenn ich mir C# oder Java anschaue, brauch ich da auch keine getrennten> Declarations- und Implementationsfiles.> Es erscheint mir umständlich.
Tja, Äpfel,Birnen und so.
Ich persönlich finde es auch besser, wenn man diesen include quatsch
nicht brauchen würde. C# und Java verfolgen aber ein Grundlegend anderes
Konzept im Hintergrund (beide werden eigentlich eher interpretiert als
compiliert. Gut, so einfach ist dass dann auch wieder nicht).
Bei C/C++ ist es nun mal so, dass die Deklaration im Headerfile erfolgt
und die Implementation im Codefile. Das hat den Vorteil, dass man die
Codefiles nur einmal Compilieren muss, wenn sich was ändert. Ansonsten
wird das einfach nur vom Linker hinzugefügt.
Wenn du die Implementation im Headerfile machst, dann muss der Compiler
jedes mal alles compilieren. Das würde ewig dauern und echt schnell
nerven.
Cornelius schrieb:> Wenn ich mir C# oder Java anschaue, brauch ich da auch keine> getrennten Declarations- und Implementationsfiles.
Das ist richtig. Der offensichtliche Vorteil ist, dass du weniger
Dateien brauchst. Mir fallen dazu folgende Nachteile ein:
1) Um die Klassen einer Bibliothek in eigenen Programmen nutzen zu
können, brauchst du zwangsläufig deren Quelltext. Da aber nicht alle
Anbieter auf Open-Source stehen, gibt es in Java die Interface, welche
prinzipiell den *.h Dateien von C/C++ entsprechen.
Der Hersteller kann in Form eines Interface die Signaturen der
Funktionen beschreiben. Der eigentliche Code wird in compilierter Form
(einzelne *.class Dateien oder *.jar Packete) geliefert.
2) Wenn du in einer Java Quelltext-Datei irgend etwas änderst, muss der
Compiler alle anderen Java Quelltexte neu compilieren, die davon
abhängen. Dies führt zu unnötig aufwändigen Compiliervorgängen, denn oft
ändert man nur den Inhalt einer Funktion, nicht deren Signatur. Diesen
Nachteil kann man durch den Einsatz von Interfacen umgehen.
Bei Mikrocontroller Programmen ist es nicht mehr so wichtig, ob jedesmal
alle Quelltexte compiliert werden, oder nur wenige ausgewählte Dateien.
Denn unsere Desktop Computer sind sehr schnell geworden und µC Programme
sind meistens eher klein.
In den 90er Jahren sah das noch ganz anders aus, da benötigte der
Compiler für jede einzelne Datei sehr viel mehr Zeit. Jede unnötige
Datei war da ein Ärgernis.
Deswegen hat man Build Tools wie Ant (Java) und Make (C/C++) erfunden.
Diese Programme finden anhand der Zeitstempel heraus, welche Dateien
verändert wurden und compilieren nur die Dateien neu, die davon
abhängen. Solange ich in C keine Header Datei ändere, müssen nur die C
Dateien neu compiliert werden, die ich verändere. Das konnte damals eine
Menge Zeit einsparen.
Heute kannst du den Effekt am Linux Kerne nachvollziehen. Compiliere mal
einen kompletten Linux Kernel. Das dauert vielleicht eine Stunde. Dann
editiere eine einzige *.c Datei und compiliere ihn nochmal. Dieses mal
dauert es nur Sekunden, denn es wird nur diese eine *.c Datei neu
compiliert.
Stefanus F. schrieb:> Heute kannst du den Effekt am Linux Kerne nachvollziehen. Compiliere mal> einen kompletten Linux Kernel. Das dauert vielleicht eine Stunde. Dann> editiere eine einzige *.c Datei und compiliere ihn nochmal. Dieses mal> dauert es nur Sekunden, denn es wird nur diese eine *.c Datei neu> compiliert.
Und jetzt stelle man sich mal vor, der gesamte Kernel bestünde aus einer
einzelnen C-Datei und sonst nur aus Headern. Es würde dann nicht nur
genauso lang wie beim ersten mal dauern, sondern vermutlich auch 50 GB
RAM zum Compilieren brauchen, weil der Compiler dann den gesamten Code
an einem Stück parsen und verarbeiten muss.
Irgendwo hatte ich mal gelesen, dass es modern sei, nur mit *.h Files zu
programmieren.
Stefanus F. (stefanus) schrieb:
>Stell Dir vor, du hast eine hardware.h mit der Funktion led_an(). Nun>bindest du diese Header Datei in zwei c oder cpp Dateien ein. Dann>existiert die Funktion doppelt, und das darf nicht sein.
Dagegen gibt es in den Header-Files die bekannte #ifndef-Klammer:
1
#ifndef __LED__
2
#define __LED__
3
4
classLed
5
{
6
booleanledState;
7
unsignedcharledPin;
8
9
public:
10
Led(unsignedchar_ledPin)
11
{
12
ledState=false;
13
ledPin=_ledPin;
14
pinMode(ledPin,OUTPUT);
15
}
16
17
voidtoogle()
18
{
19
ledState=!ledState;
20
digitalWrite(ledPin,ledState);
21
}
22
};
23
24
#endif // __LED__
Eigentlich ist eine Klasse ja eine Definition wie "struct" in C und
diese finden sich üblicherweise in den Header-Files. Die Instantiierung
von Objekten ( also das Anlegen von Variablen in C ) findet dann in den
*.cpp Dateien statt.
Es ist also die Frage, ob es Beispielprojekte gibt, die versuchen auf
*.cpp -Files zu verzichten.
In der Mozzi-Library wird fast alles mit Header-Files gemacht:
https://github.com/sensorium/Mozzi
Cornelius schrieb:> Dagegen gibt es in den Header-Files die bekannte #ifndef-Klammer:
Die aber überhaupt nicht gegen dieses Problem hilft, weil Makros nicht
über einzelne Compiler Läufe erhalten bleiben. Wenn man wie gezeigt alle
Funktionen direkt in der Klasse definiert, sind sie automatisch Inline.
Dadurch hat man am Ende eine riesige Code Duplikation. Wenn man sie
außerhalb der Klasse im Header definiert, bekommt man vom Linker
multiple Definition Errors. Man sollte längere Funktionen also auf jeden
Fall in eigene .cpp Dateien packen, kürzere kann man Inline machen.
Übrigens ist __ LED __ ein verbotener Bezeichner, da er mit zwei
Unterstrichen anfängt. Der bool Typ heißt bool in C++, nicht boolean.
Der Grund für separate Header Dateien kommt letzendlich aus der Frühzeit
der Compiler in den 80ern, als die Computer nicht genug Leistung hatten
um ein komplettes Programm gleichzeitig zu betrachten. Das wurde in
nahezu allen moderneren Sprachen anders gemacht, diese wären aber auf
damaligen Rechnern nicht nutzbar. Leider hat sich noch keiner wirklich
die Mühe gemacht, separate Header Files abzuschaffen...
Cornelius schrieb:> In der Mozzi-Library wird fast alles mit Header-Files gemacht:
Weil das Templates sind und es da nicht anders geht. Hier muss man drauf
hoffen dass der Optimizer Duplikationen entfernt.
Cornelius schrieb:> Irgendwo hatte ich mal gelesen, dass es modern sei,> mit *.h Files zu programmieren.
Vergiss das besser wieder ganz schnell.
>>Stell Dir vor, du hast eine hardware.h mit der Funktion led_an(). Nun>>bindest du diese Header Datei in zwei c oder cpp Dateien ein. Dann>>existiert die Funktion doppelt, und das darf nicht sein.> Dagegen gibt es in den Header-Files die bekannte #ifndef-Klammer:
Das hilft nicht, denn der C Compiler compiliert jede *.c/*.cpp Datei
einzeln. Als Ergebnis hast du dann zwei *.o Dateien und beide enthalten
die gleichnamige Funktion led_an(). Diese beiden *.o Dateien kann der
Linker nicht zusammenfügen, denn er verlangt eindeutige Namen. Die
Funktion led_an() darf und muss nur in einer der beiden *.o Dateien
existieren.
(kein Arduino)
**************
In meinen uC-c++ Programmen ist die ganze Funktionalität in den Klassen
und damit in der *.h(pp)-Datei, Objekt-Deklarationen und int main() in
*.c(pp). In den dabei verwendeten Bibliotheken ebenfalls.
-
IDE AtmelStudio + Editor NP++, Toolchain GCC 8.2, std=c++2a.
>In der Mozzi-Library wird fast alles mit Header-Files gemacht:>https://github.com/sensorium/Mozzi
Die Mozzi-Library habe ich aus folgenden Gründen als Beispiel gewählt:
- ist in C++ geschrieben
- Läuft auf Atmegas
- Sound Erzeugung ist extrem zeitkritisch
- Atmegas habe wenig Speicher
- 95% des Code sind *.h files
- die Library ist extrem erfolgreich
Mann kann also viel für die Verwendung von C++ auf kleinen MC-Systemen
lernen.
>Cornelius schrieb:>> In der Mozzi-Library wird fast alles mit Header-Files gemacht:
Dr.Sommer schrieb:
>Weil das Templates sind und es da nicht anders geht. Hier muss man drauf>hoffen dass der Optimizer Duplikationen entfernt.
Das habe ich übersehen. Danke für den Hinweis. Die Frage ist: wie wird
die Verdoppelung von Code bei der Verwendung von Templates verhindert?
>Der bool Typ heißt bool in C++, nicht boolean.
In der Arduino API gibt's boolean. Das ist aus historischen Gründen der
Kompatibilität zu Java geschuldet.
Die Auteilung in Schnittstellen Deklaration und Implementierung wird bei
C/C++ auch in der Kombination Header + kompilierte Module (Objectfiles,
Archivfiles oder DLL) genutzt. Damit muss man eine Implementierung nicht
veröffentlichen. Das ist zu open source Zeiten zwar aus der Mode
gekommen, gibt es aber immer noch.
Für Klassen haben gute Editoren auch Unterstützung, bei Eclipse schreibe
ich erstmal die Klassendefinition in den Header, Eclipse übernimmt dann
den stupiden Teil und generiert die Methodenrümpfe in der .cpp Datei. Da
sehe ich keinen Grund darin an dem System zu rütteln oder das wie bei
Arduino durch .ino um zusätzliche Regeln zu erweitern.
Cornelius schrieb:> Durch die Trennung von Header und Methoden
Da ist keine Trennung von "Header und Methoden" zu sehen. Man könnte die
Klassendeklaration in eine Headerdatei (*.h oder *.hpp, je nach
Geschmack) auslagern, und die Definitionen der Memberfunktionen wiederum
in einer Sourcedatei (*.cpp) unterbringen.
Dazu müsste man unterhalb der ersten schließenden geschweiften Klammer
eine neue Datei anfangen.
Im übrigen:
Der Begriff "Methode" ist eher im Java-Umfeld gebräuclich.
Rufus Τ. F. schrieb:> Im übrigen:> Der Begriff "Methode" ist eher im Java-Umfeld gebräuclich.
Es ist ein Begriff aus der allgemeinen objektorientierten
Programmierung. Im C++-Umfeld ist er aber nicht genau definiert, und
jeder versteht was anderes darunter. Ich hab in der Praxis schon die
Verwendung des Begriffs in mindestens drei verschiedenen Bedeutungen
erlebt. Daher halte ich es für besser, ihn in C++ nicht zu verwenden.
Die Begriffe in C++ sind ohnehin ziemlich gaga.
Ein Objekt soll gemäß Lehrbuch ein Ding mit seinen Eigenschaften (Daten)
und Funktionen repräsentieren.
In C++ verwendet man den Begriff Objekt aber für die Definition.
Speicherplatz wird durch die Deklaration belegt und das tatsächliche
Objekt mit Daten heißt dann Objekt-Instanz.
An anderen Stellen werden die Begriffe "Definition" und "Deklaration"
synonym verwendet oder genau umgekehrt als oben Beschrieben. Funktionen
heißen plötzlich Methoden und was "statisch" bedeutet ist nicht einmal
innerhalb der Sprache einheitlich.
Da soll man als Programmierer, der in zahlreichen Programmiersprachen
entwickeln muss, nicht bekloppt werden?
Mein Tipp: Legt die Begriffe nicht in die Waagschale, solange erkennbar
ist, was der Schreiber meint. Was heute gilt, kann morgen schon obsolet
sein.
Vermutlich kommt ein Teil der Verwirrung auch aus der (üblicherweise
sehr schlechten) Übersetzung ins Deutsche.
Das soll jetzt nicht bedeuten, daß englischsprachige Literatur
prinzipiell überlegen und fehlerfrei wäre, aber in der Übersetzung kann
halt viel verloren gehen.
Auch sind deutschprachige Akademiker nach wie vor oft der Ansicht,
einfache Sachverhalte so komplex wie nur möglich darstellen zu müssen,
andernfalls wäre das ja nicht akademisch.
Und je nach Lebensalter des Informatik-Akademikers werden auch noch die
deutschsprachigen Koteteletten-und-Hornbrille-Fachbegriffe der 60er
Jahre exhumiert; noch gar nicht lange her, und jemand verwendete so ein
Fossil, der "Kellerspeicher" war es.
Obendrein gibt es auch tatsächlich schlechte deutschsprachige Literatur,
wie z.B. den "Schellong" ("Moderne C-Programmierung").
Gut geschriebene, fachlich korrekte Informatik-Literatur ist jedenfalls
sehr, sehr rar.
Ok, so weit zu *.h und *.cpp
Jetzt mal was Praktisches. Ein sehr einfaches Programm, aber der
Compiler wirft 2 Fehler:
error: variable or field 'test' declared void
error: 'Base' was not declared in this scope
Cornelius schrieb:> Jetzt mal was Praktisches. Ein sehr einfaches Programm, aber der> Compiler wirft 2 Fehler:
Und das steht so, wie Du es zwischen die [ c ] / [ /c ]-Tags gesetzt
hast, auch in einer Datei?
Ja, in der *.ino.
Allerdings compiliere ich für den ESP8266.
esp8266/tools/xtensa-lx106-elf-gcc/1.20.0-26-gb404fb9-2/bin/xtensa-lx106
-elf-g++" ...
../sloeber.ino.cpp:9:11: error: variable or field 'test' declared void
Anscheinend erkennt er aus irgendeinem Grund die Definition der Klasse
Base nicht. Das würde beide Fehler erklären.
Der Code sieht aber in der Hinsicht soweit richtig aus (auch wenn er
wohl nicht das machen wird, was du eigentlich willst).
/home/daten/arduino-1.8.7/portable/packages/esp8266/tools/xtensa-lx106-elf-gcc/1.20.0-26-gb404fb9-2/bin/xtensa-lx106-elf-size -A /tmp/arduino_build_289241/sketch_nov25b.ino.elf
117
Der Sketch verwendet 224177 Bytes (51%) des Programmspeicherplatzes. Das Maximum sind 434160 Bytes.
118
Globale Variablen verwenden 31664 Bytes (38%) des dynamischen Speichers, 50256 Bytes für lokale Variablen verbleiben. Das Maximum sind 81920 Bytes.
Danke.
Ich glaube der Fehler liegt ganz wo anders: Ich verwende den ESP8266 und
habe das Ganze mit der Eclipse-Sloeber-IDE compiliert.
Wenn ich das in der Arduino-IDE mache, verschwindet der Fehler. Ich
glaub's nicht ...
>Schön zu lesen, dass du der Problemursache näher gekommen bist.
Ja, mühsam ernährt sich das Eichhörnchen. Nicht dass ich nur mit C++
kämpfen muss, jetzt hat mir auch noch die Sloeber-IDE dazwischen
gefunkt.
Sie hat wohl ein Problem damit, wenn Klassen und Funktionen im *.ino
File gemischt sind.
Jetzt bin ich wieder bei der Arduino-IDE.
Hier ein neues Programm. Ich will verschiedene Objekte, die alle von
einer Basisklasse abstammen in eine Liste eintragen und deren
Eigenschaften drucken.
Aus irgend welchen Gründen wird aber nur die Basisklasse gedruckt:
Cornelius schrieb:> Aus irgend welchen Gründen wird aber nur die Basisklasse gedruckt:
Und da kommen wir dann zu dem Teil:
Rolf M. schrieb:> (auch wenn er wohl nicht das machen wird, was du eigentlich willst)Cornelius schrieb:> std::vector<Base> objects;
Hier definierst du einen Vektor aus Instanzen der Klasse Base. Der kann
genau das speichern. Derived kann er nicht speichern.
> void add(Base a)
Und hier definierst du eine Funktion, die ein Base als Parameter per
Kopie übergeben bekommt. Auch hier: Die Klasse kann ausschließlich
Instanzen von Base annehmen und von nichts anderem.
Cornelius schrieb:> Derived d;> v.add(d);
Was hier passiert, nennt sich "Slicing". (
https://stackoverflow.com/questions/274626/what-is-object-slicing ). Da
v.add einen Base per Kopie haben will, bekommt es auch nur eine Kopie
des Base-Teils des Objekts. Der Derived-Teil wird abgeschnitten.
Das sind aber recht grundlegende Dinge in C++. Mach dich darüber schlau
wie Polymorphie in C++ funktioniert und schau dir an, was eine Referenz
ist.
Wobei du hier eher Zeiger brauchst, denn einen std::vector<Base> kannst
du dafür nicht nehmen. Du brauchst einen std::vector<Base*>.
Stefanus F. schrieb:> Wenn du Funktionen einer Basisklasse überschrieben willst, müssen sie> als "virtual" deklariert werden.
Ja, das kommt noch dazu - hatte ich übersehen.
OK, damit funktioniert's.
Aber so richtig gefallen tut es mir nicht, weil ich gerne hätte, dass
Objekte nah am Ort der Verwendung definiert werden.
Wie ist es damit?:
Cornelius schrieb:> OK, damit funktioniert's.>> Aber so richtig gefallen tut es mir nicht, weil ich gerne hätte, dass> Objekte nah am Ort der Verwendung definiert werden.
Naja, wenn sie in setup definiert sind und in loop verwendet werden, ist
das in dem Sinne nicht nah.
> Wie ist es damit?:
Ja, das wäre der klassische Weg, wie man es auf dem PC macht. Du musst
dann natürlich daran denken, dass du die Objekte wieder explizit mit
delete löschen musst, wenn du sie nicht mehr brauchst. Wenn sie aber eh
"für immmer" existieren, könntest du sie alternativ in setup als static
definieren. Das ist allerdings auch nicht gerade elegant, aber man spart
sich die dynamische Allokation.
Mit new müsste es gehen, weil die Objekte dann auf dem Heap wohnen.
By the way: Die Grundlagen von C/C++ lernt man auf dem PC viel leichter.
Lade Dir QT Creator herunter, das verwendet auch den GCC Compiler (in
der Variante für deinen PC). Mit dem Debugger kannst du gut
nachvollziehen, was passiert. Der eingebaute Lint Checker weist schon
während der Eingabe auf mögliche Fehler hin.
Wenn Dir QT Creator gefällt, kannst du den avr-gcc und auch den arm-gcc
für deine Arduino Projekte hinzufügen und auch diese damit editieren.
Ich nutze QT Creator als "externer Editor" zusammen mit der Arduino IDE
zum Compilieren und Flashen.
Die Arduino-String-Klasse ist für Mikrocontroller optimiert. Auf dem PC
verwendet man std::string. uC und PC Programme lassen sich nicht 1:1
übertragen, besonders wenn es um AVR mit den diversen Sonderlocken
(PROGMEM und so) geht.
>Die Arduino-String-Klasse ist für Mikrocontroller optimiert. Auf dem PC>verwendet man std::string.
Die habe ich schon probiert, passt aber leider ziemlich schlecht. Wenn
man sich bemüht, sollte man die Kompatibilität schaffen.
>(PROGMEM und so)
Auf dem ESP:
https://github.com/esp8266/Arduino/blob/master/cores/esp8266/pgmspace.h
Cornelius schrieb:> Die habe ich schon probiert, passt aber leider ziemlich schlecht. Wenn> man sich bemüht, sollte man die Kompatibilität schaffen.
Dann viel Spaß - dafür ist Arduino halt gar nicht gemacht.
Du kannst Standard C++ programmieren, dann funktionieren deine Programme
garantiert auf allen Standard-konformen Compilern, und damit auf allen
gängigen PC-Betriebssystemen. Leider sind Embedded-Targets, insbesondere
für AVR, nicht standardkonform.
Dr. Sommer schrieb:> Du kannst Standard C++ programmieren
Sicher? Ich glaube die std:: Klassen stehen wiederum auf Arduino nicht
zur Verfügung.
Ehrlich gesagt meide ich die String Klasse von Arduino ohnehin wie der
teufel das Weihwasser, weil sie dazu verleitet, den Heap zu
fragmentieren. Wenn ich ständig nachdenken muss, was unter der Haube der
Klasse passiert, kann ich gleich char[] und die Funktionen der
<string.h> verwenden.
Stefanus F. schrieb:> Sicher? Ich glaube die std:: Klassen stehen wiederum auf Arduino nicht> zur Verfügung.
Sag ich doch.
Stefanus F. schrieb:> weil sie dazu verleitet, den Heap zu> fragmentieren.
Ich vermeide die Nutzung des Heaps gleich ganz... Mit ein bisschen
Planung braucht man den nicht, und spart sich manche böse Überraschung.
Cornelius schrieb:> Aber r sollte 16 sein !!
Dann solltest Du um 16 Bits und nicht um 24 Bits schieben, schließlich
sieht Deine Konstante so aus:
0x00RRGGBB
Mit dem Schieben um 24 Bits erhältst Du folgerichtig die 0.
Cornelius schrieb:> Die Objekte löschen und den Speicher freigeben?
Dazu dient die delete Anweisung.
Object* o=new Object();
o->doSomething();
delete o;
Cornelius schrieb:> Kann ich mitobjects.erase();>> Die Objekte löschen und den Speicher freigeben?
erase() erwartet einen oder zwei Parameter, die angeben, welche Elemente
gelöscht werden sollen. Wenn du den ganzen Container leeren willst,
musst du clear() verwenden.
Gelöscht werden dann aber nur die Zeiger, nicht die Objekte, die du mit
new angelegt hast.
erase() entfernt die Objekte aus dem Vektor und löscht sie auch.
Wenn man sie allerdings - wie im Code oben - auf dem Stack angelegt hat,
ist das keine gute Idee.
Markus F. schrieb:> erase() entfernt die Objekte aus dem Vektor und löscht sie auch.
Richtig. In diesem Fall die Pointer, die im Vektor gespeichert sind.
Markus, Rolf: Eure Aussage widersprechen sich. Was stimmt jetzt?
Mir scheint das Thema Speicherverwaltung recht schwierig. In Java gibt
es ja den "Garbage Collector", der immer wieder mal aufräumt.
Wer räumt in C++ auf, wenn abwechselnd Objekte im Speicher liegen und
jedes zweite gelöscht wird. Dann entstehen ja Löcher und man müsste
quasi "defragmentieren" wie die Festplatte bei Windows früher.
Die Frage ist, welche Methoden gibt es, um das Problem zu lösen?
Kann man den Objekten, die man löschen und wieder erzeugen möchte einen
bestimmten Speicherbereich zuweisen?
Cornelius schrieb:> Markus, Rolf: Eure Aussage widersprechen sich. Was stimmt jetzt?
Ein vector enthält Objekte von dem Typ, mit dem er angelegt wurde. Diese
werden in den vector hinein kopiert. Bei einem std::vector<Base*> also
ein Base*.
Wenn du also schreibst:
1
Derivedd;
2
v.add(&d);
Dann wird mit &d ein Zeiger auf d gebildet, in einen Zeiger auf Base
konvertiert, und dieser Zeiger wird dann in den Vektor kopiert. Wenn du
ein Element (oder den ganzen Inhalt) des vectors löschst, wird der
Pointer wieder aus dem vector entfernt. Auf d hat das aber keinerlei
Einfluss, ganz egal, wo es gespeichert ist.
> Mir scheint das Thema Speicherverwaltung recht schwierig. In Java gibt> es ja den "Garbage Collector", der immer wieder mal aufräumt.
Dafür gibt's da keine vernünftig funktionierenden Destruktoren, d.h. um
alle anderen Ressourcen muss man sich von Hand kümmern.
> Wer räumt in C++ auf, wenn abwechselnd Objekte im Speicher liegen und> jedes zweite gelöscht wird. Dann entstehen ja Löcher und man müsste quasi> "defragmentieren" wie die Festplatte bei Windows früher.
Speicherfragmentierung gibt es, kann auf kleinen µCs auch schnell zum
Problem werden. Das ist mit ein Grund, weshalb man dort möglichst auf
dynamischen Speicher verzichtet.
Auf PCs ist der Speichermanager recht ausgeklügelt (und hat durch den
großen Speicher viel mehr Möglichkeiten), um das zu vermeiden. Ein
Garbage Collector hat tatsächlich etwas bessere Möglichkeiten, weil er
sowieso alle Referenzen auf ein bestimmtes Objekt kennen muss und es
daher im Speicher auch verschieben kann, solange er alle Referenzen
darauf entsprechend anpasst. Wie weit sowas in der Praxis auch gemacht
wird, weiß ich aber nicht.
> Die Frage ist, welche Methoden gibt es, um das Problem zu lösen?> Kann man den Objekten, die man löschen und wieder erzeugen möchte einen> bestimmten Speicherbereich zuweisen?
Nein, der wird automatisch vergeben.
Bedenke, dass der vector selbst auch wieder dynamischen Speicher
braucht, denn irgendwo müssen die darin gespeicherten Elemente ja auch
hin. Wenn du da immer wieder neue Elemente hinzufügst, muss er
regelmäßig reallokieren, was gerade besonders zu Fragmentierung führt.
Cornelius schrieb:> Wer räumt in C++ auf, wenn abwechselnd Objekte im Speicher liegen und> jedes zweite gelöscht wird.
Niemand, das musst du notfalls selbst Implementieren. oder besser von
vorne herein vermeiden.
Das Problem bei C ist: Man kann nicht einfach Objekte im Speicher
verschieben, ohne dass Zeiger und Referenzen ungültig werden. Bei Java
werden diese tatsächlich vom Garbage Collector gefunden und korrigiert,
aber dazu muss das ganze Programm zeitweise angehalten werden. Das
willst du auf einem Mikrocontroller sicher nicht tun.
Rolf M. schrieb:> Cornelius schrieb:>> Die Frage ist, welche Methoden gibt es, um das Problem zu lösen?>> Kann man den Objekten, die man löschen und wieder erzeugen möchte einen>> bestimmten Speicherbereich zuweisen?>> Nein, der wird automatisch vergeben.> Bedenke, dass der vector selbst auch wieder dynamischen Speicher> braucht, denn irgendwo müssen die darin gespeicherten Elemente ja auch> hin. Wenn du da immer wieder neue Elemente hinzufügst, muss er> regelmäßig reallokieren, was gerade besonders zu Fragmentierung führt.
Doch, kann man. Man kann den Operator new überschreiben.
Aber wer sowas macht, der weiß auch warum.
merciless
man kann in eigenen Speicherverwaltungsroutinen auch mit Memory Pools
arbeiten und so Speicherbereiche für wichtige und weniger wichtige
Sachen aufteilen. Man sollte sich dann aber Online Diagnosemöglichkeiten
einbauen und viel testen.
@ Cornelius
Arduino meint Mikrokontroller mit c++.
Das was ich in diesem Thread bisher lese hat mit Mikrokontroller mit c++
nicht wirklich zu tun, new, delete, heap ect. braucht man dazu nicht.
-
Die "Objekte" müssen nicht im Speicher erzeugt werden.
-
Es werden nur die Register des Mikrokontroller gelesen und beschrieben
und dazu reichen struct oder class als typedef.
-
Falls das interessiert, dann kann der interessierte Leser seine Zeit
z.B. in Mcucpp investieren. Die Bibliothek nutzt c++03 und hat auch
Beispiele. Die Softwarestruktur von Mcucpp ist für verschiedene
uC-Familien geeignet.
Wilhelm M. kann dasselbe auch mit c++2a in deutlich komplexeren
Strukturen.
>Das was ich in diesem Thread bisher lese hat mit Mikrokontroller mit c++>nicht wirklich zu tun, new, delete, heap ect. braucht man dazu nicht.
Aber klar doch, jedes Code-Stück was Du hier siehst läuft auf dem
ESP8266 mit der Arduino IDE.
Da ich keinen Fehler erkennen kann, habe ich das mal auf meinem PC (ohne
Arduino) ausprobiert:
1
#include<iostream>
2
#include<string>
3
#include<vector>
4
5
usingnamespacestd;
6
7
classTier
8
{
9
stringname;
10
11
public:
12
Tier(string_name);
13
virtualvector<Tier*>getObjects(intget);
14
voidshow();
15
voidshowAll();
16
};
17
18
vector<Tier*>tiere;
19
20
Tier::Tier(string_name)
21
{
22
name=_name;
23
tiere.push_back(this);
24
}
25
26
vector<Tier*>Tier::getObjects(intget)
27
{
28
returntiere;
29
}
30
31
voidTier::show()
32
{
33
cout<<name<<endl;
34
}
35
36
voidTier::showAll()
37
{
38
for(intn=0;n<tiere.size();n++)
39
{
40
cout<<n<<":";
41
tiere[n]->show();
42
}
43
}
44
45
Tierhund("bello");
46
Tierkatze("miau");
47
48
intmain()
49
{
50
hund.showAll();
51
return0;
52
}
Ausgabe:
1
0:bello
2
1:miau
Läuft also. Dein Problem muss irgendwie mit dem Arduino Framework zu
tun haben. Programmieren übt man besser auf einem PC.
Eine Anmerkung dazu: Objekte mit virtuellen Funktionen sollten auch
einen virtuellen Destruktor haben, selbst wenn der leer ist. Ich kann
Dir nur empfehlen, die Qt Creator IDE als Editor zu verwenden, die weist
auf solche Fehler hin. Anleitung dazu:
http://stefanfrings.de/esp8266/index.html#qtcreatorhttp://stefanfrings.de/avr_tools/index.html#qtcreator
>Läuft also. Dein Problem muss irgendwie mit dem Arduino Framework zu>tun haben.
Danke, da muss ich wohl noch ein wenig forschen.
Mit dem ESP8266 Framework ist es alive, kommen aber keine Tiernamen.
Mit dem Arduino-Uno kann ich nicht kompilieren, weil der "vector" nicht
kennt.
Cornelius schrieb:> Mit dem Arduino-Uno kann ich nicht kompilieren, weil der "vector" nicht> kennt.
Der avr-gcc hat leider keine Standard C++ Libraries.
Ich bin einen Schritt weiter: Es funktioniert, wenn ich die beiden
Objectinstanzierungen vom *.ino in das Tier.cpp File schiebe.
Es scheint an der Besonderheit des *.ino-Files zu liegen.
Die Reihenfolge, in der globale Variablen aus unterschiedlich Source
Files initialisiert werden, ist in C++ nicht festgelegt. Wenn die
einzelnen Tiere vor dem Vektor initialisiert werden, wird auf einen
Vektor im ungültigen Zustand zugegriffen... dann passiert beliebiger
Unfug.
Dr. Sommer schrieb:> Die Reihenfolge, in der globale Variablen aus unterschiedlich Source> Files initialisiert werden, ist in C++ nicht festgelegt.
Wow, das leuchtet ein. Bei Java ist das einfacher, da muss man die
Reihenfolge nicht beachten.
>Was haben diese Namen jetzt mit Java zu tun?
Ich denke, Stefanus bezieht sich hier auf das C++ Programm.
Wenn ich die Definition des C++-Vectors vor die Tierinstanzierung
schreibe, funktioniert es:
1
#include"Tier.h"
2
3
std::vector<Tier*>tiere;
4
5
Tierhund("bello");
6
Tierkatze("miau");
7
8
voidsetup()
9
{
10
Serial.begin(115200);
11
delay(10);
12
Serial.println("go");
13
Serial.print("Anzahl Tiere:");
14
Serial.println(tiere.size());
15
}
16
17
voidloop()
18
{
19
Serial.println("alive");
20
delay(1000);
21
showAll();
22
}
>Die Reihenfolge, in der globale Variablen aus unterschiedlich Source>Files initialisiert werden, ist in C++ nicht festgelegt. Wenn die>einzelnen Tiere vor dem Vektor initialisiert werden, wird auf einen>Vektor im ungültigen Zustand zugegriffen... dann passiert beliebiger>Unfug.
Das finde ich höchst erstaunlich, weil ja bei der Instanzierung die
Konstrukturen auf jeden Fall aufgerufen werden. Dort wird "vector"
verwendet und es ist seltsam, dass das möglich ist, obwohl "vector"
nicht initialisiert ist.
Allerdings spricht für die These der nicht vollständigen
Initilaisierung, dass das Serial.print im Konstruktor scheinbar noch
nicht initialisiert ist, weil "new animal born" nicht im Terminal
erscheint.
( Bezogen auf ESP8266 )
Rolf M. schrieb:> Gibt's da überhaupt globale Variablen?
Es gibt da statische Variablen. Davon gibt's, genau wie bei globalen,
genau 1 Instanz pro Programmlauf; anders als globale Variablen sind sie
aber nicht überall, sondern nur in einer Klasse sichtbar. Wie Java
sicherstellt dass diese Variablen in der richtigen Reihenfolge
initialisiert werden - insbesondere bei zirkulären Abhängigkeiten - weiß
ich auch nicht...
Cornelius schrieb:> Das finde ich höchst erstaunlich, weil ja bei der Instanzierung die> Konstrukturen auf jeden Fall aufgerufen werden
Ja, aber in welcher Reihenfolge! Es wird bei dir wohl erst der
Konstruktor der Tiere aufgerufen, und dann der vom vector. Die
Reihenfolge ist hier nicht definiert.
Cornelius schrieb:> Das finde ich höchst erstaunlich
Es ist aber so. Steht im Standard und ist auch zu ergoogeln. Überleg
mal, wie der Linker wissen soll, dass er den vector-Konstruktor vor dem
Tier-Konstruktor aufrufen soll...
Ok, Ich versuche "vector" los zu werden und die Tiere in einem Array zu
halten.
Das Array sollte ja von Anfang an da sein und ohne "vector" sollte es
auch auf dem Uno compilieren.
Cornelius schrieb:> extern std::vector<Tier*> tiere;Cornelius schrieb:> Tier tiere[10];
Bitte den Unterschied zwischen Zeiger und "direkter" Variable lernen.
Cornelius schrieb:> tiere[tierPointer++]=*this;
Hier wird eine Kopie angelegt. String kann möglicherweise nicht
kopiert werden.
Bloss mal als Anregung: Du greifst innerhalb der Klasse
1
Tier
munter auf die globale Variable
1
tiere
zu. Ich kann dir nur dringend raten, die Kapitel
über objektorientierte Programmierung (insbesondere
Kapselung etc) in deinem C++-Buch zu lesen, die
du ausgelassen hast. Sonst wirst du mit C++ keinen
Spass haben.
merciless
Wenn man das std::cout auf dem Arduino durch Serial.print ersetzt,
erhält man einen Code der komplett ohne dynamische Speicherverwaltung
auskommt. Dank constexpr braucht man nicht einmal RAM für das Array (für
AVR brauchts noch PROGMEM). Beim Start müssen dann auch keinerlei
Konstruktoren laufen, die umständlich Speicher initialisieren.
>Eine einfachere und effizientere Möglichkeit wäre ganz schlicht:
Ich weiß nicht, ob das einfacher zu nennen ist.
Mein Ziel ist eher "optischer" Natur.
Ich will, dass die Klassenerzeugung im *.ino File genau so aussieht:
1
Tierhund("bello");
2
Tierkatze("miau");
3
4
voidsetup()
5
{
6
Serial.begin(115200);
7
}
8
9
voidloop()
10
{
11
Serial.println("alive");
12
delay(1000);
13
showAll();
14
}
Es geht mir darum, für den Benutzer eine möglichst einfache API zu
schaffen.
Ob es da grundsätzliche Hindernisse gibt, weiß ich nicht. Ich vermute
aber, dass es ein Möglichkeit zur Lösung gibt.
Ich werde weiter forschen ...
Autor: Dirk K. (merciless)
>zu. Ich kann dir nur dringend raten, die Kapitel>über objektorientierte Programmierung (insbesondere>Kapselung etc) in deinem C++-Buch zu lesen, die>du ausgelassen hast. Sonst wirst du mit C++ keinen>Spass haben.
Das Du mir Ratschläge geben kannst, zeigst Du am ehesten durch ein
Beispiel, welches das Problem löst.
So weit ich sehen kann, hat hier bis jetzt noch niemand eine Lösung.
Cornelius schrieb:> Ich will, dass die Klassenerzeugung im *.ino File genau so aussieht:
Und was, wenn der Nutzer mehrere Listen an Tieren haben will? Vielleicht
gibt es ja mehr als einen Zoo. Dass sich ein Objekt bei der Erstellung
automatisch in eine "versteckte" Liste/Array einfügt ist nicht
wartungsfreundlich oder sauber. Manchmal ist die kürzeste Lösung nicht
die Beste.
Dein einzige Art, den Code (fast) so kurz zu machen wie gewünscht, ist
es den vector in der selben Datei wie "hund" und "katze" anzulegen, und
zwar vor diesen beiden Variablen.
Johannes S. schrieb:> Oder die Liste im Konstruktor mitgeben, als Referenz, dann> existiert die> auf jeden Fall.
Nicht, wenn man die Referenz über eine Forward-Declaration mitgibt.
Cornelius schrieb:> Es geht mir darum, für den Benutzer eine möglichst einfache API zu> schaffen.
Dann macht man eine Klasse Zoo, die als Factory für Tiere benutzt wird.
Sprich: du erzeugst die Tiere nicht selber, sondern überlässt das dem
Zoo, dder die Tiere dann auch direkt in die Liste einträgt.
Cornelius schrieb:> Autor: Dirk K. (merciless)>>zu. Ich kann dir nur dringend raten, die Kapitel>>über objektorientierte Programmierung (insbesondere>>Kapselung etc) in deinem C++-Buch zu lesen, die>>du ausgelassen hast. Sonst wirst du mit C++ keinen>>Spass haben.>> Das Du mir Ratschläge geben kannst, zeigst Du am ehesten durch ein> Beispiel, welches das Problem löst.> So weit ich sehen kann, hat hier bis jetzt noch niemand eine Lösung.
Dr. Sommer hat doch direkt nach mir eine
saubere Lösung präsentiert, die sogar noch
auf die Besonderheiten der Speicherverwaltung
bei uCs eingeht.
Eine Klasse legt man an, damit man diese in
anderen Projekten wiederverwenden kann. Deine
Klasse Tier kann man nicht wiederverwenden,
ohne eine globale Variable tiere vom Typ
std::vector<Tier*> zu deklarieren. Solche
Abhängigkeiten löst man durch Dependency
Injection o.ä. Da dies aber den Rahmen hier
sprengen würde und ich nicht glaube, dass du
momentan mit solchen Erklärungen etwas anfangen
kannst, habe ich nur vorsichtig darauf hinweisen
wollen, dass dir Grundkenntnisse über OOP fehlen.
merciless
Stefanus F. schrieb:> Dr. Sommer schrieb:>> Die Reihenfolge, in der globale Variablen aus unterschiedlich Source>> Files initialisiert werden, ist in C++ nicht festgelegt.>> Wow, das leuchtet ein. Bei Java ist das einfacher, da muss man die> Reihenfolge nicht beachten.Rolf M. schrieb:> Was haben diese Namen jetzt mit Java zu tun?
Niemand hat gesagt, das die Namen mit Java zu tun haben.
Bei Java hat man automatisch keine Probleme mit der Reihenfolge der
Initialisierung. Bei C++ muss man etwas mehr mitdenken. Darum ging es.
>Dr. Sommer hat doch direkt nach mir eine>saubere Lösung präsentiert, die sogar noch>auf die Besonderheiten der Speicherverwaltung>bei uCs eingeht.
Aber falls ich mich nicht täusche, kann ich damit nicht im *.ino File
nach Arduino-Style anlegen.
Wie ich schon sagte, der Main-Code soll genau so aussehen:
Beitrag "Re: C++ Arduino Grundlagen"
Meines Erachtens ist das der Syntax mit der maximalen Klarheit und
Einfachheit.
Arduino heißt: Die Maschine dient dem Menschen. Der Syntax soll so
einfach sein, dass es für den Menschen einfach wird, nicht für die
Maschine.
So, ich habe jetzt die ultimative Lösung maximaler Einfachheit erreicht.
Besser wird's nicht gehen.
Der Code läuft jetzt auch auf einem Arduino Uno und zwar ohne "vector"
oder ähnlichen Schnick-Schnack zu benötigen.
Ich greife dazu in die Informatiker Trickkiste und verwende verkettete
Listen.
Cornelius schrieb:> Besser wird's nicht gehen.
Besser als falsch gehts immer. falsch, weil die statische Variable
nach dem Aufruf der Konstruktoren initialisiert werden könnte. Nur
weil es gerade zufällig den Anschein hat zu funktionieren, ist es noch
lange nicht richtig.
Cornelius schrieb:> Arduino heißt: Die Maschine dient dem Menschen. Der Syntax soll so> einfach sein, dass es für den Menschen einfach wird, nicht für die> Maschine.
Dann nimm Python oder Ruby. C und C++ haben eine viel kompliziertere
Syntax. Außerdem ging es den Einwänden darum, dass der Mensch (du) den
Code später sinnvoll wieder verwenden können soll. Du legst dir aber
selbst Steine in den Weg: Du hast nur genau 1 Liste von Tieren pro
Programm. Du kannst nie einen 2. Zoo hinzufügen. Oder Tiere komplett
unabhängig von der Liste anlegen. Das Tier trägt sich heimlich hinter
den Kulissen in eine Liste ein; wenn du eine Katze kaufst, möchtest du
dann auch dass sie eine unsichtbare Schnur hinter sich her zieht mit der
sie sich mit anderen Katzen verheddert? Ein Objekt, eine C++ Klasse,
sollte so gut wie möglich für sich alleine stehen können.
Erfahrene Programmierer wissen so etwas; du kannst solchen Rat entweder
direkt annehmen oder später durch Schmerz lernen, warum man so etwas
nicht macht...
Cornelius schrieb:> #ifndef _TIERH_
Bezeichner mit Unterstrich+Großbuchstabe sind verboten.
Cornelius schrieb:> int Tier::tierZaehler=0;
Wenn schon std::size_t für Array-Größen; "int" könnte zu klein sein.
Dirk K. schrieb:> list<Tier *> wäre wohl zu kompliziert gewesen O.o
Da er die list, vector oder was auch immer unbedingt in einer anderen
Translation Unit als die globalen Instanzen haben möchte, sind alle
Varianten falsch; nur äußert sich der Fehler der intrusiven Variante mit
Pointern in der Klasse selbst später. Symptom-Behandlung halt.
vector, list und co. möchte er gar nicht haben um kompatibel mit AVR zu
sein für die es das nicht gibt, aber da würde ich mir eher überlegen ob
man sich noch an die historische HW klammern muss. Die std container
bieten ja mehr Luxus als nur Elemente einzufügen, löschen, suchen,
indizierte Listen sind ja alles nur leichte Variationen im Code.
Das Factory Pattern würde ich auch bevorzugen, das das Tier seine
Verwaltung kennen muss ist nicht gut.
Johannes S. schrieb:> Das Factory Pattern würde ich auch bevorzugen, das das Tier seine> Verwaltung kennen muss ist nicht gut.
Sehe ich auch so. Die Tiere verwalten nicht den Zoo, sondern anders
herum.
Also kann ein Zoo gerne eine Methode wie nehmeTierAuf() haben, aber das
Tier sollte keinerlei Methoden enthalten, die irgend etwas mit dem Zoo
zu tun haben.
Tiere sollte es auch ohne Zoo geben.
Was machst du, wenn manche Tiere im Zoo, manche in der Wildnis und mache
im Haushalt sind?
Johannes S. schrieb:> vector, list und co. möchte er gar nicht haben um kompatibel mit AVR zu> sein für die es das nicht gibt, aber da würde ich mir eher überlegen ob> man sich noch an die historische HW klammern muss. Die std container> bieten ja mehr Luxus als nur Elemente einzufügen, löschen, suchen,> indizierte Listen sind ja alles nur leichte Variationen im Code.> Das Factory Pattern würde ich auch bevorzugen, das das Tier seine> Verwaltung kennen muss ist nicht gut.
Das meinte ich ja vorhin mit meinem Einwand
"Vielleicht erstmal das Kapitel über OOP lesen".
Cornelius schreibt schlecht strukturierten C-Code
im C++-Gewand. Aber wenn er sich nicht helfen lassen will...
merciless
Ich weiß, für euch ist das Design-Ziel schwer zu verstehen:
Es geht um die optimale User-API.
Das Optimierungkriterium heißt: "Einfachheit für den User"
Und die sieht nach meiner Meinung so aus:
1
#include"Tier.h"
2
3
Tierhund("Bello");
4
Tierkatze("Mia");
5
Tiervogel("Flippy");
6
7
voidsetup()
8
{
9
Serial.begin(115200);
10
delay(10);
11
}
12
13
voidloop()
14
{
15
Serial.println("animals alive!!");
16
delay(1000);
17
showAll();
18
Serial.println();
19
}
Wenn es bessere gibt, nur zu. Konstruktive Vorschläge werden gerne
untersucht.
>Dann nimm Python oder Ruby.
Nein, ich bin sozusagen Werkzeugmacher: Ich mache es für andere
einfacher.
> C und C++ haben eine viel kompliziertere>Syntax.
So ist es. Und deshalb hat Wiring den Syntax auf eine einfache Form
reduziert. Deshalb gibt es Arduino. Deshalb ist es so erfolgreich: Weil
es mit alten Weisheiten gebrochen hat.
>Außerdem ging es den Einwänden darum, dass der Mensch (du) den>Code später sinnvoll wieder verwenden können soll.
Er wird sinnvoll verwendet werden.
>Du legst dir aber>selbst Steine in den Weg: Du hast nur genau 1 Liste von Tieren pro>Programm. Du kannst nie einen 2. Zoo hinzufügen.
Ich will keinen zweiten Zoo. In meiner Stadt ist einer genug.
>Oder Tiere komplett>unabhängig von der Liste anlegen. Das Tier trägt sich heimlich hinter>den Kulissen in eine Liste ein; wenn du eine Katze kaufst, möchtest du>dann auch dass sie eine unsichtbare Schnur hinter sich her zieht mit der>sie sich mit anderen Katzen verheddert?
Kein Tier darf hier frei rumlaufen. Alle müssen in den Zoo.
> Ein Objekt, eine C++ Klasse,> sollte so gut wie möglich für sich alleine stehen können.
Versuch das mal mit der Arduino TwoWire-Klasse
>Erfahrene Programmierer wissen so etwas; du kannst solchen Rat entweder>direkt annehmen oder später durch Schmerz lernen, warum man so etwas>nicht macht...
Was C++ anbelangt, kann man mich wahrscheinlich nicht als erfahrenen
Programmierer bezeichnen. Was einige andere Programmiersprachen angeht
mit Sicherheit schon.
>Cornelius schrieb:>> #ifndef _TIERH_>Bezeichner mit Unterstrich+Großbuchstabe sind verboten.
Das hat wizziger Weise Eclipse-CDT automatisch beim Anlegen der Klasse
erzeugt. Vielleicht sind die nicht auf dem neusten Stand.
Cornelius schrieb:> Es geht um die optimale User-API.
Ein fehlerhaftes API welches beim nächsten Compiler Update nicht mehr
funktioniert ist bestimmt nicht optimal.
Cornelius schrieb:> Wenn es bessere gibt, nur zu
Wurden dir ausreichend genannt. Wenn du unbedingt fehlerhafte Software
machen möchtest, bitte schön.
Cornelius schrieb:> Und deshalb hat Wiring den Syntax auf eine einfache Form reduziert
Wiring ist immer noch C++ und hat genau die gleich komplizierte Syntax.
Cornelius schrieb:> Ich will keinen zweiten Zoo. In meiner Stadt ist einer genug.
Genau wie 64 KB RAM für alle genug sind, und IPv4-Adressen für alle
ausreichen? So ziemlich das wichtigste bei Software-Architekturen ist
es, zukunftssicher und flexibel zu bleiben. Das ist man mit einer fixen
versteckten Datenstruktur nicht. Früher oder später will man doch
irgendwann mal 2 Zoos haben. Globale Datenstrukturen von denen man nur 1
hat sind ein absolutes No-Go.
Cornelius schrieb:> Versuch das mal mit der Arduino TwoWire-Klasse
Arduino ist gewiss nicht Beispielhaft. Bei Peripherie-Ansteuerung kann
man vielleicht noch Ausnahmen machen, aber für eine Software-Only
Struktur wie deinen Zoo nicht.
Cornelius schrieb:> Was einige andere Programmiersprachen angeht mit Sicherheit schon.
Dann kannst du doch die hoffentlich da erlernten guten Vorgehensweisen
übernehmen.
Cornelius schrieb:> Vielleicht sind die nicht auf dem neusten Stand.
Diese Regel ist nicht neu. Bei mir macht CDT das nicht...
Cornelius schrieb:> Ich bin sozusagen Werkzeugmacher:> Ich mache es für andere einfacher.
Du machst dich lächerlich.
> Deshalb ist es so erfolgreich: Weil> es mit alten Weisheiten gebrochen hat.
Du hast doch keine Ahnung, warum Arduino erfolgreich ist. Diese Meinung
mag für Dich persönlich zutreffen, das glaube ich Dir gerne. Aber so
allgemein-gültig wie du das geäußert hast, kann man das nicht stehen
lassen.
Mein persönlicher Eindruckt ist, dass Arduino genau wie Youtube die
Leute verdummen lässt. Sie bilden sich ein, dass man mit ein paar Klicks
hier und ein paar Strippen da alles besser machen kann, als die
vorherige Generation. Und dann kommen sie angekrochen und stellen
Fragen, weil sie an den Grenzen ihres Flickwerks scheitern. Das kannst
du nicht abstreiten, denn es ist Dir gerade selbst auch passiert.
Sei mal ein bisschen demütiger. Dir fehlt Erfahrung.
Stefanus F. schrieb:> Rolf M. schrieb:>> Was haben diese Namen jetzt mit Java zu tun?>> Niemand hat gesagt, das die Namen mit Java zu tun haben.
Ich hatte gefragt, ob es in Java globale Variablen gibt, und als Antwort
darauf nanntest du die Tiernamen.
> Bei Java hat man automatisch keine Probleme mit der Reihenfolge der> Initialisierung. Bei C++ muss man etwas mehr mitdenken. Darum ging es.
Worum es ging war, dass bei C++ die Initialisierungsreihenfolge globaler
Variablen nicht immer definiert ist. Du meintest, dass man sich bei Java
im Gegensatz dazu keine Gedanken über die Reihenfolge machen muss.
Soweit ich mich erinnere, gibt's aber in Java gar keine globalen
Variablen, so dass es gar nicht erst eine Reihenfolge gibt, mit der man
Probleme bekommen könnte. Da mich diese Erinnerung aber trügen könnte,
habe ich nachgefragt.
>Cornelius schrieb:>> Vielleicht sind die nicht auf dem neusten Stand.>Dr.Sommer schrieb>Diese Regel ist nicht neu. Bei mir macht CDT das nicht...
Ich hab's grad noch mal nach kontrolliert: Du hast Recht. Es erzeugt die
Klassen ohne Unterstrich am Anfang. Da ist mir die Hand-geschriebene
wohl rein gerutscht.
1
intTier::tierZaehler=0;
>Besser als falsch gehts immer. falsch, weil die statische Variable>nach dem Aufruf der Konstruktoren initialisiert werden könnte. Nur>weil es gerade zufällig den Anschein hat zu funktionieren, ist es noch>lange nicht richtig.
Das ist ein guter Hinweis. Ich bin davon ausgegangen, dass statische
Variablen mit Vorinitialisierung genau wie in C zur Compilezeit
initialisiert werden.
Die Frage ist: Wie wäre eine sichere Implementierung?
Theoretisch könnte ich auf den Tierzähler verzichten, wenn ich eine
Funktion schreibe, die jedes mal die List durchiteriert und zählt. Das
würde den Speicher für die Variable sparen, dafür aber Zeit kosten.
Cornelius schrieb:> Die Frage ist: Wie wäre eine sichere Implementierung?Dr. Sommer schrieb:> Dein einzige Art, den Code (fast) so kurz zu machen wie gewünscht, ist> es den vector in der selben Datei wie "hund" und "katze" anzulegen, und> zwar vor diesen beiden Variablen.
Oder
Johannes S. schrieb:> Oder die Liste im Konstruktor mitgeben, als Referenz, dann existiert die> auf jeden Fall.
(Aber vorher nicht nur eine Forward declaration)
Oder
Dr. Sommer schrieb:> Eine einfachere und effizientere Möglichkeit wäre ganz schlicht:
Diese Variante geht natürlich auch mit vector, list, array, und ist
m.M.n. die sauberste - der Leser sieht sofort dass die Tiere in einen
Zoo gepackt werden, wie groß dieser ist, wie schnell der Zugriff ist...
Cornelius schrieb:>>falsch, weil die statische Variable nach dem Aufruf der Konstruktoren>>initialisiert werden könnte. Nur weil es gerade zufällig den Anschein hat>>zu funktionieren, ist es noch lange nicht richtig.>> Das ist ein guter Hinweis. Ich bin davon ausgegangen, dass statische> Variablen mit Vorinitialisierung genau wie in C zur Compilezeit> initialisiert werden.
Das kommt drauf an. Da es Konstruktoren gibt, kann es ja auch Code
geben, der zur Initialisierung ausgeführt werden muss. In C ist die
Initialisierung globaler Variablen immer statisch, in C++ kann sie je
nach Situation statisch oder dynamisch sein.
Rolf M. schrieb:> Ich hatte gefragt, ob es in Java globale Variablen gibt, und als Antwort> darauf nanntest du die Tiernamen.
Ja, so ähnlich. Es gibt keine Variablen außerhalb von Klassen, aber man
kann die Member der Klassen als "static" kennzeichnen, dann sind sie
immer und überall erreichbar, ohne eine Instanz von der Klasse erstellen
zu müssen.
In manchen Projekte lege ich dafür extra eine Dummy Klasse an, die
Globals heißt und als Sammelpunkt für alle quasi globalen Variablen
dient.
Ich möchte aber darauf hinweisen, das man globale Variablen in
objektorientierten Desktop Programmen nur spärlich und mit Bedacht
einsetzen sollte. Bei Mikrocontrollern benutze ich mehr globale
Variablen, wegen der beschränkten Speicherverwaltung.
Stefanus F. schrieb:> Es gibt keine Variablen außerhalb von Klassen, aber man> kann die Member der Klassen als "static" kennzeichnen, dann sind sie> immer und überall erreichbar, ohne eine Instanz von der Klasse erstellen> zu müssen.>> In manchen Projekte lege ich dafür extra eine Dummy Klasse an, die> Globals heißt und als Sammelpunkt für alle quasi globalen Variablen> dient.
Das war sowas, das mich an Java schon bei Funktionen gestört hat. Solche
Dummy-Klassen sind ein schlechter Work-Around für das Fehlen von
Sprachfeatures.
> Ich möchte aber darauf hinweisen, das man globale Variablen in> objektorientierten Desktop Programmen nur spärlich und mit Bedacht> einsetzen sollte. Bei Mikrocontrollern benutze ich mehr globale> Variablen, wegen der beschränkten Speicherverwaltung.
Da sind die Programme ja meist auch überschaubarer.
Rolf M. schrieb:>> In manchen Projekte lege ich dafür extra eine Dummy Klasse an, die>> Globals heißt und als Sammelpunkt für alle quasi globalen Variablen>> dient.>> Das war sowas, das mich an Java schon bei Funktionen gestört hat. Solche> Dummy-Klassen sind ein schlechter Work-Around für das Fehlen von> Sprachfeatures.
Kann man so sehen. Man muss halt Wege finden, mit den Arbeitsmitteln
klar zu kommen, die zur Verfügung stehen. Es gibt schlimmeres. Irgendwo
ist immer ein haken.
Stefanus F. schrieb:> Kann man so sehen. Man muss halt Wege finden, mit den Arbeitsmitteln> klar zu kommen, die zur Verfügung stehen.
Java ist halt dazu konzipiert, möglichst einfach und schnell erlernbar
zu sein. Dazu gehört, dass es für die meisten Dinge nur eine Möglichkeit
gibt sie umzusetzen, was Verwirrung insbesondere bei Anfängern
vermeidet. Für Funktionen ohne "this"-Instanz gibt es in Java nur
statische Funktionen, nicht statische und freie wie in C++. Zum
Weglassen von Parametern gibt es nur Overloads, keine Default-Parameter.
Für Callbacks gab es nur Interfaces, keine Funktionszeiger oder so
(mittlerweile auch Lambdas, aber das sind auch nur Interfaces). In C++
gibt es Funktionszeiger, Member-Funktionszeiger (in 12-Facher(!)
Ausfertigung für die verschiedenen Referenz-Kategorien), std::function,
Lambdas und Klassen mit operator(). Natürlich ist das mächtiger und
flexibler, aber auch komplizierter.
Dr. Sommer schrieb:> Java ist halt dazu konzipiert, möglichst einfach und schnell> erlernbar zu sein.> Natürlich ist das (C++) mächtiger und flexibler, aber auch komplizierter.
Ehrlich gesagt benutze ich sowohl von Java als auch von C++ immer noch
nur einen Bruchteil der Sprach-Features, obwohl ich beide Sprachen
beruflich seit über 10 Jahren einsetze. Und das finde ich so auch in
Ordnung. Man muss nicht alles machen, nur weil es geht.
Dr. Sommer schrieb:
>Diese Variante geht natürlich auch mit vector, list, array, und ist>m.M.n. die sauberste - der Leser sieht sofort dass die Tiere in einen>Zoo gepackt werden, wie groß dieser ist, wie schnell der Zugriff ist...
Wenn die Welt so konzipiert ist, dass alle Tiere in den Zoo müssen, ist
es nicht notwendig, dies extra zu erwähnen:
Der Löwe. Er muss in den Zoo.
Der Fuchs. Er muss in den Zoo.
Der Hamster. Er muss in den Zoo.
Der Emu. Er muss in den Zoo.
Die Katze. Sie muss in den Zoo.
Das sie in den Zoo müssen ist nur ein Tribut an die Maschine.
Unnötige Schreibarbeit und Syntaxkomplexität sollte man sich in einer
Arduino-Umgebung sparen ( und um die geht es hier laut der Überschrift
).
Cornelius schrieb:> Unnötige Schreibarbeit und Syntaxkomplexität sollte man sich in einer> Arduino-Umgebung sparen
ja, genauso wie gute Entwürfe und Lernbereitschaft. Gehört einfach nicht
in die Arduino Welt :-(
Es funktioniert, also ist es gut.
Und was ist, wenn du Unit-Tests schreiben möchtest, und für jeden Test
einen neuen Zoo brauchst?
Wenn du unbedingt nur den einen Zoo haben willst, dann definiere diesen
in der selben Datei wie die Tiere, und zwar vor diesen. Das ist 1 Zeile.
Diese 1 Zeile wird die Komplexität nicht dramatisch steigern. So wird
der Zoo korrekt vor den Tieren initialisiert.
Tier.h
1
#include<vector>
2
#include<string>
3
4
classTier{
5
public:
6
Tier(std::stringname);
7
voidshow();
8
private:
9
std::stringm_name;
10
};
11
12
externstd::vector<Tier*>zoo;
13
voidshowAll();
Tier.cpp
1
#include"Tier.h"
2
3
Tier::Tier(std::stringname):m_name(name){
4
zoo.push_back(this);
5
}
6
7
voidTier::show(){std::cout<<m_name;}
8
9
voidshowAll(){
10
for(auto&t:zoo)t.show();
11
}
Main.cpp
1
#include"Tier.h"
2
3
std::vector<Tier*>zoo;
4
5
Tierkatze("Katze"),
6
kuh("Kuh"),
7
tiger("Tiger");
8
9
intmain(){
10
showAll();
11
}
Jetzt kannst du Tiere natürlich nur noch in der Main.cpp global
definieren, Tiere nicht mehr entfernen, und keinen neuen Zoo anlegen.
Aber jetzt ist es so "einfach" wie du es haben wolltest, und immerhin
nicht fehlerhaft.
Cornelius schrieb:> Das sie in den Zoo müssen ist nur ein Tribut an die Maschine.
Das sie Speicher brauchen ist ein Tribut. Die Maschine braucht den Zoo
nicht. Du willst diese Datenstruktur haben, sie scheint mit dem
Problem zusammen zu hängen.