Hier werden die Unterschiede zwischen zwei Versionen gezeigt.
Beide Seiten der vorigen Revision Vorhergehende Überarbeitung Nächste Überarbeitung | Vorhergehende Überarbeitung | ||
skript:klassen-arduino [2018/10/07 19:46] d.golovko |
skript:klassen-arduino [2021/06/11 11:28] (aktuell) d.golovko |
||
---|---|---|---|
Zeile 7: | Zeile 7: | ||
====1. Deklaration==== | ====1. Deklaration==== | ||
- | Jeder Song hat zwei Eigenschaften: die Liste von abzuspielenden Noten in Form eines ''int''-Arrays und der Pin, welcher mit dem Piezo verbunden ist. | + | Jeder Song soll zwei Eigenschaften haben: die Liste von abzuspielenden Noten in Form eines ''int''-Arrays und der Pin, welcher mit dem Piezo verbunden ist. |
Eine Klasse in C++ besteht in der Regel aus zwei Dateien: einer Header-Datei ''.h'', die alle Variablen, Konstruktoren und Methoden benannt werden, und einer ''.cpp''-Datei, die die eigentliche Impementieriung der Methoden beihnaltet. In unserem Beispiel kann die Klasse so aussehen: | Eine Klasse in C++ besteht in der Regel aus zwei Dateien: einer Header-Datei ''.h'', die alle Variablen, Konstruktoren und Methoden benannt werden, und einer ''.cpp''-Datei, die die eigentliche Impementieriung der Methoden beihnaltet. In unserem Beispiel kann die Klasse so aussehen: | ||
Zeile 14: | Zeile 14: | ||
class Song { | class Song { | ||
public: | public: | ||
- | Song(); // Konstruktor, muss noch mit Parametern versehen werden | + | Song(int noteList[], int arraySize, int pinNumber); // Konstruktor |
void play(); // Methode zum Abspielen der Melodie | void play(); // Methode zum Abspielen der Melodie | ||
private: | private: | ||
- | int notes[]; // Noten-Frequenzen in Hz | + | int *notes; // Noten-Frequenzen in Hz; ist ein Zeiger, da Array-Laenge noch nicht bekannt |
+ | int arraySize; // Laenge des Noten-Arrays | ||
int pin; // Piezo-Pin | int pin; // Piezo-Pin | ||
}; | }; | ||
Zeile 24: | Zeile 25: | ||
<file cpp Song.cpp> | <file cpp Song.cpp> | ||
#include "Song.h" | #include "Song.h" | ||
- | Song::Song() { | + | Song::Song(int noteList[], int arraySize, int pinNumber) { |
// TODO: Implementierung des Konstruktors fehlt | // TODO: Implementierung des Konstruktors fehlt | ||
} | } | ||
Zeile 36: | Zeile 37: | ||
Ihr sieht oben, dass die Header-Datei zwei Blöcke hat, die mit ''public'' und ''private'' anfangen. Diese Schlüsselworte weisen auf die [[https://de.wikipedia.org/wiki/Sichtbarkeit_(Programmierung)|Sichtbarkeit]] der Variablen und Funktionen hin. ''Public'' bedeutet, dass die Variable/Funktion nur innerhalb der Klasse sichtbar ist, ''protected'' innerhalb der Klasse und ihrer Unterklassen, ''public'' überall. (Ähnliche Ebenen gibt es übrigens auch in Java.) Der Faustregel ist, die Sichtbarkeit möglichst begrenzt zu wählen. Die Begründung ist die gleiche wie bei globalen und lokalen Variablen: Je mehr Stellen haben Zugriff auf die Variable, desto mehr fehleranfällig ist der Code. Wir machen den Konstruktor und die Funktion ''play()'' ''public'', damit wir darauf von unserem Haupt-Tab zugreifen können. Die beiden Variablen sind ''private'', sie sollen nur innerhalb der Klasse verfügbar sein. Es ist üblich, in der Header-Datei den ''public''-Block ganz oben zu platzieren. | Ihr sieht oben, dass die Header-Datei zwei Blöcke hat, die mit ''public'' und ''private'' anfangen. Diese Schlüsselworte weisen auf die [[https://de.wikipedia.org/wiki/Sichtbarkeit_(Programmierung)|Sichtbarkeit]] der Variablen und Funktionen hin. ''Public'' bedeutet, dass die Variable/Funktion nur innerhalb der Klasse sichtbar ist, ''protected'' innerhalb der Klasse und ihrer Unterklassen, ''public'' überall. (Ähnliche Ebenen gibt es übrigens auch in Java.) Der Faustregel ist, die Sichtbarkeit möglichst begrenzt zu wählen. Die Begründung ist die gleiche wie bei globalen und lokalen Variablen: Je mehr Stellen haben Zugriff auf die Variable, desto mehr fehleranfällig ist der Code. Wir machen den Konstruktor und die Funktion ''play()'' ''public'', damit wir darauf von unserem Haupt-Tab zugreifen können. Die beiden Variablen sind ''private'', sie sollen nur innerhalb der Klasse verfügbar sein. Es ist üblich, in der Header-Datei den ''public''-Block ganz oben zu platzieren. | ||
+ | |||
+ | Wenn in C++ Arrays als Parameter an eine Funktion übergeben werden, wird nur der Zeiger auf das erste Array-Element übergeben. D.h. die Funktion bekommt keine Kopie des ganzen Arrays, sondern nur die Adresse des ersten Elementes. Eine Lösung ist, die Array-Länge als einen weiteren Parameter (''arraySize'' in unserem Beispiel) zu übergeben. In der Liste der Membervariablen wird ebenso nur ein Zeiger ''int *notes'' verwendet, da die Deklaration eines Arrays ohne seiner Länge nicht möglich ist. | ||
====2. Implementierung==== | ====2. Implementierung==== | ||
Zeile 46: | Zeile 49: | ||
// Konstruktor; noteList: Notenabfolge, arraySize: Laenge des Noten-Arrays, pin: Piezo-Pin | // Konstruktor; noteList: Notenabfolge, arraySize: Laenge des Noten-Arrays, pin: Piezo-Pin | ||
Song::Song(int noteList[], int arraySize, int pinNumber) { | Song::Song(int noteList[], int arraySize, int pinNumber) { | ||
- | for (int i = 0; i < arraySize; i++) { | + | this->arraySize = arraySize; |
- | this->notes[i] = noteList[i]; | + | this->notes = noteList; |
- | } | + | |
this->pin = pinNumber; | this->pin = pinNumber; | ||
} | } | ||
+ | |||
//[...] | //[...] | ||
</file> | </file> | ||
- | |||
- | Dementsprechend müssen wir die Konstruktordeklaration auch in Zeile 3 der Header-Datei ändern: \\ | ||
- | ''Song(int noteList[], int arraySize, int pinNumber);'' | ||
Genauso wie in Java wird ''this'' benutzt, um auf die Instanz der Klasse (d.h. auf die gerade erzeugte Objekt vom Typ ''Song'') zu zeigen. Achtung: in C++ wird statt Punkt ein Pfeil verwendet. | Genauso wie in Java wird ''this'' benutzt, um auf die Instanz der Klasse (d.h. auf die gerade erzeugte Objekt vom Typ ''Song'') zu zeigen. Achtung: in C++ wird statt Punkt ein Pfeil verwendet. | ||
Zeile 63: | Zeile 63: | ||
#include "Arduino.h" | #include "Arduino.h" | ||
//[...] | //[...] | ||
+ | // Spielt die Melodie ab | ||
void Song::play() { | void Song::play() { | ||
- | for (int i = 0; i < sizeof(notes) / sizeof(notes[0]); i++) { | + | for (int i = 0; i < arraySize ; i++) { |
- | tone(pin, notes[i], 500); | + | tone(pin, notes[i]); |
+ | delay(500); | ||
} | } | ||
+ | noTone(pin); | ||
} | } | ||
//[...] | //[...] | ||
</file> | </file> | ||
+ | <hidden Komplette Datei Song.cpp> | ||
+ | <file cpp Song.cpp> | ||
+ | #include "Song.h" | ||
+ | #include "Arduino.h" | ||
+ | |||
+ | // Konstruktor; noteList: Notenabfolge, arraySize: Laenge des Noten-Arrays, pin: Piezo-Pin | ||
+ | Song::Song(int noteList[], int arraySize, int pinNumber) { | ||
+ | this->arraySize = arraySize; | ||
+ | this->notes = noteList; | ||
+ | this->pin = pinNumber; | ||
+ | } | ||
+ | |||
+ | // Spielt die Melodie ab | ||
+ | void Song::play() { | ||
+ | for (int i = 0; i < arraySize ; i++) { | ||
+ | tone(pin, notes[i]); | ||
+ | delay(500); | ||
+ | } | ||
+ | noTone(pin); | ||
+ | } | ||
+ | </file> | ||
+ | </hidden> | ||
+ | \\ | ||
====3. Aufruf==== | ====3. Aufruf==== | ||
- | In C++ gibt es die Unterscheidung zwischen Referenzen und Zeigern. Kurz gefasst sind Referenzen die Objekte an sich, und Zeiger sind ihre Adressen. Die beiden sind oft | + | Nun werden wir einen Song im Haupt-Tab erstellen und aufrufen, z.B. so (Piezo auf Pin 2): |
+ | |||
+ | <file cpp variante1> | ||
+ | #include "Song.h" | ||
+ | |||
+ | void setup() { | ||
+ | int notes[] = {262, 294, 330, 349, 392}; // Notenfrequenzen | ||
+ | Song mySong (notes, sizeof(notes) / sizeof(notes[0]), 2); // oder: Song mySong(notes, 5, 2); | ||
+ | mySong.play(); | ||
+ | } | ||
+ | |||
+ | void loop() { | ||
+ | } | ||
+ | </file> | ||
+ | |||
+ | Achtung: wir müssen dem Compiler mit ''#include Song.h'' Bescheid sagen, dass eine Klasse aus einer anderen Datei verwendet wird. Falls wir auf die Variable ''mySong'' sowohl von ''setup()'' als auch von ''loop()'' zugreifen wollen, können wir diese inkl. Konstruktoraufruf auch global haben: | ||
+ | <file cpp variante2> | ||
+ | #include "Song.h" | ||
+ | int notes[] = {262, 294, 330, 349, 392}; | ||
+ | Song mySong (notes, sizeof(notes) / sizeof(notes[0]), 2); // oder: Song mySong(notes, 5, 2); | ||
+ | |||
+ | void setup() { | ||
+ | mySong.play(); | ||
+ | } | ||
+ | |||
+ | void loop() { | ||
+ | mySong.play(); | ||
+ | } | ||
+ | </file> | ||
+ | |||
+ | Der Konstruktor hier wird aufgerufen, bevor ''setup()'' anfängt. Das kann ungünstig sein im Fall, wenn der Konstruktor Parameter hat, deren Werte am Anfang des Programms noch nicht bekannt sind (z.B. wenn sie von Sensormessungen abhängen, die in ''setup()'' gemacht werden). Anders als in Java, können wir nicht einfach ''Song mySong;'' in Zeile 3 schreiben. Das liegt daran, dass ''mySong'' eine Referenz ist, und Referenzen dürfen nicht gleich NULL sein. | ||
+ | |||
+ | <note> | ||
+ | Die Unterscheidung zwischen Referenzen und Zeigern (Englisch: pointers) in C++ ist ein separates großes Thema -- bei Interesse lesst [[https://www.geeksforgeeks.org/pointers-vs-references-cpp/|diesen Link]] oder sucht Informationen dazu online. Wir werden dieses Thema hier nicht angehen. | ||
+ | </note> | ||
+ | |||
+ | Wir werden stattdessen einen //Zeiger// nutzen -- man kann das so verstehen, dass, statt des Objektes an sich, haben wir nur einen Verweis darauf, wo dieses Objekt sich befindet. Ein Zeiger wird mit einem ''*'' markiert, und der Zugriff auf Funktionen und Variablen geht über ''->''. | ||
+ | |||
+ | <file cpp variante3> | ||
+ | #include "Song.h" | ||
+ | Song* mySong; // Zeiger | ||
+ | |||
+ | void setup() { | ||
+ | int notes[] = {262, 294, 330, 349, 392}; | ||
+ | mySong = new Song(notes, sizeof(notes) / sizeof(notes[0]), 2); // oder: Song mySong(notes, 5, 2); | ||
+ | mySong->play(); | ||
+ | } | ||
+ | |||
+ | void loop() { | ||
+ | mySong->play(); | ||
+ | } | ||
+ | </file> |