Objektorientierte Programmiersprachen
Ausgangspunkt für das Verstehen einer objektorientierten Programmiersprache ist immer das objektorientierte Basiskonzept (Paradigma). Das Basiskonzept stellt sozusagen das SOLL und die konkrete Sprache das IST dar. Gehen Sie davon aus, dass eigentlich in keiner derzeit verfügbaren objektorientierten Sprache auch alle in der Theorie formulierten Konzepte bereits komplett umgesetzt sind. Man sollte sich auf jeden Fall davor hüten von den Möglichkeiten und den Einschränkungen einer konkreten Sprache auf das Konzept zu schließen. Wesentliche Aspekte objektorientierter Sprachen sollen im Folgenden anhand der Sprache C++ aufgezeigt werden. Für das Verständnis von C++ ist weiterhin wichtig zu wissen, das C++ die Sprache C beinhaltet. C++ ist die objektorientierte Erweiterung der Sprache C.
Der Sprachumfang von C (Auszug)
// Schlüsselworte .......................................... break double int struct case else long switch char extern return unsigned const float short signed continue for void sizeof default if sizeof volatile do while main // Operatoren .............................................. + - * / = ++ -- << >> ! & | ^ ~ % == > < <= >= && || !=
Wesentliche C-Steuerflusskonstrukte
C folgt dem Basiskonzept der Strukturierung was nichts weiter bedeutet, als dass auf die Sprunganweisung (vgl. Assembler, GOTO, JUMP, usw.) verzichtet wird, um einen Algorithmus zu erstellen. Es werden nur drei algorithmische Grundstrukturen benötigt, um jede Programmlogik zu realisieren. Das sind:
- die Sequenz, Anweisungen werden nacheinander ausgeführt
- die Iteration (Wiederholung, Schleife), Anweisungen werden wiederholt ausgeführt
- die Alternative, bestimmte Anweisungen werden entsprechend einer Bedingung anstatt anderer Anweisungen ausgeführt
In den strukturierten Sprachen gibt es zwar noch weitere Spielarten dieser drei Grundstrukturen, aber dem Wesen nach gibt es tatsächlich nur diese drei. Im Folgenden soll dargestellt werden wie diese Algorithmusbausteine in C und dann natürlich auch in C++ notiert werden. Beachten Sie die geschweiften Klammern! Diese begrenzen einen Anweisungsblock und sollten immer besonders am Anfang diszipliniert gesetzt werden.
// Blöcke in C und C++ { // BEGIN // Blockinhalt } // END
Das Weglassen von Klammern in bestimmten Fällen ist eben genau das, Sonderfälle… und diese sollte man sich als Einsteiger erst mal sparen. Übrigens gibt es die Blockbildung in allen strukturierten Sprachen. Manchmal sind das statt Klammern Schlüsselworte wie BEGIN oder END. Das gleiche gilt für das Semikolon. Ein Semikolon ist in C das Endekennzeichen einer Anweisung und für den Compiler extrem wichtig. Notieren Sie eine Anweisung immer in einer eigenen Zeile und schließen Sie diese immer mit einem Semikolon ab. Das Einrücken der Anweisungen mit einem TAB innerhalb eines Blocks gewährleistet den Überblick zu bewahren.
int A,B,C; // eine Sequenz .............................................. { A = 1; // führe zuerst A B = 2; // dann B C = A+B; // danach C aus } // eine Wiederholung ......................................... while ( A < 100 ) // wiederhole ( solange ) { A = A + 1; // diese Anweisungen } // eine Alternative .......................................... if ( A == 0) // wenn ( das wahr ist ) { C = B; // dann führe diese Anweisungen aus } else // sonst { C = A; // führe diese Anweisungen aus } // ----------------------------------------------------------- // ausgewählte Spielarten // ----------------------------------------------------------- // Zählschleife .............................................. for ( uint8_t i = 0; i<100; i++) { // was auch immer genau 100 mal getan werden muss } // fußgesteuerte Schleife .................................... do { // etwas was mindestens einmal oder öfter nötig ist } while ( notReady ); // bedingte Anweisung ....................................... if ( ok ) { // etwas alternativloses } // Fallunterscheidung ............. switch (fall) { case 1: { // Fall 1 } break; case 2: { // Fall 2 } break; default: { // Standardfall } break; }
Zusätzlicher Sprachumfang von C++ (Auszug)
bool catch false enum class new delete public template virtual operator private protected this namespace using true throw try
Wie wir sehen, ist der eigentliche Sprachumfang von C übersichtlich und die Erweiterung, um zur Sprache C++ zu kommen vergleichsweise gering. Was dem Lernenden als umfangreich erscheint, sind die hinzukommenden Bibliotheken, welche vorgefertigte Funktionen anbieten. Des Weiteren ist ersichtlich, dass eine objektorientierte Programmiersprache ihre strukturierten Vorgänger nicht ablöst, sondern auf deren Konzepten und Schlüsselworten aufbaut und diese auch übernimmt. Die Botschaft lautet also, alles was man mit AVR C machen kann funktioniert nach wie vor auch in myAVR C++ . Eine objektorientierte Programmiersprache muss also zusätzlich nur die Ausdrucksmittel zur Realisierung der objektorientierten Basiskonzepte anbieten. Zu einem objektorientierten Programm kommt man nicht einfach durch Verwendung eines bestimmten Compilers, sondern einzig durch die Art und Weise, wie man das Programm aufbaut. Im Folgenden vorab wichtige C++ Konstrukte, welche ausgewählte besondere Merkmale einer objektorientierten Sprache aufzeigen. Im Verlauf des Tutorials werden wesentliche Aspekte des Gezeigten Schritt für Schritt besprochen.
Deklarieren von Klassen in C++
Es ist ein Anwendungsprogramm mit dem Namen Applikation (englisch: Application) zu entwickeln. Die Anwendung soll zunächst geplant und dann programmiert werden und letztlich benötigen wir eine Instanz von dem Programm.
// Klasse Name { Bauplan } Instanz; class Application { } app;
Vererbung in C++
Die Applikation ist eine Mikrocontrolleranwendung. Diese soll alle Möglichkeiten der vorhandenen Klasse Controller besitzen. Somit erbt die Applikation am besten alle Merkmale vom Controller.
// Klasse Name : Sichtbarkeit Basisklasse { Bauplanerweiterung } Instanz; class Application : public Controller { } app;
Operationen in C++
Der Controller wird eingeschaltet und arbeitet dann fortlaufend taktgesteuert. Oh ja, wir erinnern uns dunkel. Subjekt und Prädikat. WER (der Controller) macht WAS (wird eingeschaltet, arbeitet)… Dafür sollte es jetzt Operationen in der Klasse geben.
// Klasse Name : Sichtbarkeit Basisklasse { Bauplanerweiterung } Instanz; class Application : public Controller { // Sichtbarkeit : RückgabeTyp name (Parameter) { Code; } public: void onStart() { // alles was beim Hochfahren getan werden muss // dann gehe zur Mainloop } // Sichtbarkeit : RückgabeTyp name (Parameter) { Code; } public: void onWork() { // alles was fortlaufend getan werden muss // die Mainloop liegt in der Controllerklasse // von dort aus wird onWork fortlaufend aufgerufen (getriggert) // hier also KEINE Unendlichschleife !!! } } app;
Aggregationen und Kapselung in C++
Es soll eine LED angeschlossen werden. An diese LED wollen wir niemand anderes heran lassen. Wir schützen diese vor unberechtigtem Zugriff.
// Klasse Name : Sichtbarkeit Basisklasse { Bauplanerweiterung } Instanz; class Application : public Controller { // Sichtbarkeit : Typ name; protected: LED led; public: void onStart() { // ... } public: void onWork() { // ... } } app;
Nachrichten in C++
Die LED ist eine fertige Klasse aus dem Framework. Wir müssen der LED mitteilen, an welchem Port-Pin sie angeschlossen ist und wir wollen sie einschalten.
// Klasse Name : Sichtbarkeit Basisklasse { Bauplanerweiterung } Instanz; class Application : public Controller { // Sichtbarkeit : Typ name; protected: LED led; public: onStart() { // instanzName . nachricht ( Parameter ); led.config(portB,bit0); } public: onWork() { // instanzName . nachricht ( ); led.on(); } } app;
Zwischenfazit
Bei diesem kurzen Ausflug in die objektorientierte Art und Weise Programme zu schreiben ist wohl deutlich geworden, dass es sehr darauf ankommt, sich ein bestimmtes Muster anzugewöhnen, Systeme zu betrachten und darüber nachzudenken. Objektorientierung beginnt im Kopf! Übrigens ist es hilfreich, das zu programmierende System in kurzen einfachen Sätzen zu beschreiben oder diese laut vor sich hin zu sagen.
Zwischenbilanz
In einem kleinen Vergleich zwischen einer Lösung in C und dem gegenüber die selbe Lösung in C++ sollen die Unterschiede nochmals verdeutlicht werden. Dabei soll dieser Vergleich auch der Hypothese nachgehen dass C++ Lösungen für eingebettete Systeme viel zu viel Ressourcen fressen vor allem zu viel Speicher verbrauchen.
//----------------------------------------------------------------------------- // Taste an Pin D2 schaltet LED an Pin B0 // Klassisches Embedded C //----------------------------------------------------------------------------- #define F_CPU 3686400 #include <avr\io.h> //----------------------------------------------------------------------------- int main (void) { DDRB |= (1<<DDB0); DDRB |= (1<<DDB1); PORTD|= (1<<PD2); while (true) { if (!(PIND&(1<<PD2))) PORTB |= (1<<PB0); else PORTB &= ~(1<<PB0); } return 0; } //-----------------------------------------------------------------------------
Der klassischen C Lösung mit den direkten Registerzugriffen in der gesamten Applikation stellen wir eine einfache C++ Lösung gegenüber. In der C++ Lösung werden die controllerspezifischen Registerzugriffe in anwendungsorientierten Klassen gekapselt. Die eigentliche „Problemlösung“ hier im Hauptprogramm ist völlig frei von controllerspezifischen Statements und damit vollständig portabel.
//----------------------------------------------------------------------------- // Taste an Pin D2 schaltet LED an Pin B0 // Embedded C++ //----------------------------------------------------------------------------- #define F_CPU 3686400 #include <avr\io.h> class Button { public: Button() {PORTD|=(1<<PD2);} public: bool isPressed() {return !(PIND&(1<<PD2));} }; class Led { public: Led() {DDRB|=(1<<DDB0);} public: void on() {PORTB|=(1<<PB0);} public: void off() {PORTB&=~(1<<PB0);} }; //----------------------------------------------------------------------------- int main (void) { // Instanzen anlegen Button button; Led led; while (true) { if (button.isPressed()) led.on(); else led.off(); } return 0; } //-----------------------------------------------------------------------------
Ein weiterer wichtiger Aspekt ist, dass die Objektorientierte Lösung in C++ durchaus erheblich mehr Schreibaufwand sprich Codezeilen bedeutet ABER kein einziges Byte mehr FLASH verbraucht. Es ist sogar so, dass beide Anwendungen binär identisch sind. Das spricht für die Leistungsfähigkeit moderner C++ Compiler.
Der enorme Schreibaufwand bei der objektorientierten Lösung reduziert sich wiederum bei der Verwendung einer objektorientierten Bibliothek. Die entsprechende Bibliothek muss dann natürlich etwas flexibler und professioneller gebaut werden. Die Klassen in einer Klassenbibliothek sollten zum Beispiel auf beliebige Pins des Controllers angewendet werden können. Der folgende Code zeigt wie die für das Beispiel benötigten Bibliotheksklassen aussehen könnten.
//---------------------------------------------------------------- // simple C++ Library, Datei "simpleLIB.h" //---------------------------------------------------------------- typedef volatile uint8_t& port_t; //................................................................ class Pin { protected: port_t port; protected: uint8_t bit; public: Pin( port_t p,uint8_t b) : port(p), bit(b) { } public: bool getState() { return *((&port)-2)&(1<<bit);} }; //................................................................ class InputPinPullUp : public Pin { public: InputPinPullUp( port_t p,uint8_t b) : Pin(p, b) { port |= (1<<bit); } }; //................................................................. class PushButton : public InputPinPullUp { public: PushButton( port_t p,uint8_t b) : InputPinPullUp(p, b) {} public: bool isPressed() { return !getState();} }; //................................................................. class OutputPin : public Pin { public: OutputPin( port_t p,uint8_t b) : Pin(p, b) { *((&port)-1) |= (1<<bit); } public: void on() { port |= (1<<bit); } public: void off() { port &= ~(1<<bit); } public: void toggle(){ port ^= (1<<bit); } }; //.................................................................
Wir gehen also davon aus, dass die obigen Klassen in einer schicken Bibliothek zur Verfügung stehen. Das ist letztlich ja auch der tiefere Sinn von Bibliotheken. Die sind verfügbar und bieten schicke Bausteine um es dem Anwendungsentwickler leichter zu machen. Was dem Anwednungsentwickler noch obliegt, ist sich mit der Bibliothek zu beschäftigen und diese korrekt anzuwenden.
//------------------------------------------------------------------ // Button und LED Beispile unter Verwendung der obigen simpleLIB.h //------------------------------------------------------------------ #define F_CPU 3686400 #include <avr\io.h> #include "simpleLIB.h" int main (void) { // Bibliotheksklassen anwenden OutputPin statusLED(PORTB,0); PushButton funktionsTaste(PORTD,2); while (true) // Mainloop { if (funktionsTaste.isPressed()) statusLED.on(); else statusLED.off(); } return 0; } //-----------------------------------------------------------------------------
Zum Abschluss dieser Betrachtung vergleichen wir noch mal die Quelltexte der Anwendungen in C und C++ mit Bibliothek sowie den daraus resultierenden Maschinencode den wir auf den Controller übertragen.
Wer das nicht glaubt findet das Beispiel im SiSy LibStore unter der Suche „C Vergleich“.
Das Ergebnis sollte den einen oder anderen vielleicht nachdenklich machen. Objektorientierte Programmiersprachen sind nicht per se einfach so fetter und träger. Das scheinbar Fette und Träge moderner Programmiersprachen gegenüber alten Sprachen ist der Komfort für den Anwender mit wenigen Statements viel zu erreichen. Komfort verbraucht mehr Ressourcen. Das ist wie im echten Leben. In den nächsten Abschnitten werden wir eine solche objektorientierte Bibliothek für eingebettete Systeme kennenlernen und wir werden die objektorientierten Anwendungen nicht von Hand schreiben sondern die grafische Modellierungssprache UML nutzen, um dann den C++ Code für unsere Anwendungen automatisch generieren zu lassen.