Grundlagen der Software-Entwicklung GSE Friedrich Haase Dieses Skript ist work in progress. Anregungen und Hinweise auf Fehler sind sehr willkommen. Vorlesung FH-Dortmund, Grundlagen der Software-Entwicklung © 2014-2017 Dr. Friedrich Haase http://fhd.61131.com/ Juli 2017 Haase, Grundlagen der Software-Entwicklung 2 Inhaltsverzeichnis 1. Einleitung..................................................................................................................................................5 1.1. Ziele...................................................................................................................................................5 1.2. Architektur eines Computer............................................................................................................6 1.3. Programmier-Werkzeuge................................................................................................................7 1.3.1. Windows Microsoft C++ IDEs...................................................................................................9 1.3.2. Linux Code::Blocks....................................................................................................................9 1.3.3. Arduino.....................................................................................................................................10 1.3.4. Android CppDroid....................................................................................................................10 1.3.5. Diverse Programmierplattformen.............................................................................................10 1.3.6. Programmbeispiele...................................................................................................................10 1.4. Zahl- und Zeichendarstellung.......................................................................................................11 1.4.1. Zahlen.......................................................................................................................................11 1.4.2. Zeichen.....................................................................................................................................13 1.4.3. C++ Datentypen........................................................................................................................15 1.5. Algorithmen....................................................................................................................................16 2. C / C++....................................................................................................................................................17 2.1. Variablen.........................................................................................................................................19 2.1.1. Initialisierung............................................................................................................................19 2.2. Einfache Anweisungen...................................................................................................................21 2.2.1. Operatoren................................................................................................................................21 2.2.2. Ausführungspriorität.................................................................................................................22 2.2.3. Typkonvertierung......................................................................................................................23 2.3. Felder...............................................................................................................................................24 2.3.1. Deklaration...............................................................................................................................24 2.3.2. Initialisierung............................................................................................................................24 2.3.3. Verwendung..............................................................................................................................25 2.3.4. Zeichenfelder............................................................................................................................25 2.4. Steueranweisungen.........................................................................................................................27 2.4.1. If-Anweisungen........................................................................................................................27 2.4.2. Switch-Anweisungen................................................................................................................29 2.4.3. Schleifen...................................................................................................................................29 2.4.4. Schleife mittels goto.................................................................................................................30 2.4.5. while-Schleife...........................................................................................................................30 2.4.6. for-Schleife...............................................................................................................................31 2.4.7. do-while-Schleife......................................................................................................................33 2.4.8. continue....................................................................................................................................34 2.5. Funktionen......................................................................................................................................35 2.5.1. Standardfunktionen...................................................................................................................36 2.5.2. Vom Anwender definierte Funktionen......................................................................................38 2.5.3. Rekursionen..............................................................................................................................38 2.5.4. Überladene Funktionen.............................................................................................................39 2.6. Strukturen.......................................................................................................................................40 2.6.1. Deklaration...............................................................................................................................40 2.6.2. Initialisierung............................................................................................................................40 2.6.3. Verwendung..............................................................................................................................40 2.7. Zeiger...............................................................................................................................................42 2.7.1. Zeiger und Adressen.................................................................................................................42 2.7.2. Verwendung von Zeigern..........................................................................................................43 2.7.3. Übergabe von Parameter an eine Funktion...............................................................................44 2.7.4. Zeiger und Felder......................................................................................................................45 Haase, Grundlagen der Software-Entwicklung 3 2.7.5 Dynamische allozierter Speicherplatz.......................................................................................46 2.7.6 Speicherplatzverwaltung...........................................................................................................47 2.8. Typedef............................................................................................................................................49 2.9. Ein-/Ausgabe...................................................................................................................................50 2.9.1. Standard-Ein-Ausgabe..............................................................................................................50 2.9.2. Formatierte Ausgabe.................................................................................................................50 2.9.3. Formatierte Eingabe.................................................................................................................51 2.9.4. Dateizugriff...............................................................................................................................51 2.10. Gültigkeitsbereiche von Variablen..............................................................................................53 2.10.1. Lokale Daten einer Funktion..................................................................................................53 2.10.2. Lokale Daten und Funktionen in einer Datei..........................................................................54 2.10.3. Weitere Speicherklassen.........................................................................................................54 2.11. Modulare Programmierung.........................................................................................................56 2.12. Multitasking und Echtzeit...........................................................................................................58 3. Technische Systeme................................................................................................................................60 3.1. Digitale Ein- und Ausgänge...........................................................................................................60 3.1.1. PNP- und NPN-Eingänge.........................................................................................................61 3.1.2. PNP- und NPN-Ausgänge........................................................................................................62 3.2. Analoge Ein- und Ausgänge...........................................................................................................62 3.3. Spezielle Interface..........................................................................................................................64 3.4. Bus-Systeme....................................................................................................................................64 4. C++..........................................................................................................................................................65 4.1. Klassen.............................................................................................................................................65 4.1.1. Konstruktoren und Destruktor..................................................................................................66 4.1.2. Methoden..................................................................................................................................66 4.1.3. Klassen und Header-Dateien....................................................................................................68 4.2. Vererbung........................................................................................................................................68 4.2.1. Basisklasse und abgeleitete Klassen.........................................................................................68 4.2.2 Initialisierung der Anteile der Basisklassen und Elemente.......................................................70 4.2.3 Virtuelle Funktionen..................................................................................................................70 4.3. Statische Variablen und Methoden...............................................................................................71 4.4. Automatisch erzeugte Methoden...................................................................................................72 4.5. Überladene Operatoren.................................................................................................................72 4.6. Standard C++ Klassen string und Containerklassen..................................................................73 5. Beliebte Fehler........................................................................................................................................74 5.1 Mangelhafte oder fehlende Kommentierung................................................................................74 5.2 Schlecht Wahl von Variablennamen.............................................................................................74 5.3 Warnungen des Compilers beachten.............................................................................................74 5.4 RYFM...........................................................................................................................................74 5.5 Schlechte Strukturierung und Formatierung von Quellcode........................................................74 5.6 Mangelnde Fehlerüberprüfung.....................................................................................................74 5.7 Nicht deklarierte Variablen...........................................................................................................74 5.8 Nicht definierte Funktionen..........................................................................................................75 5.9 Semikolon vergessen....................................................................................................................75 5.10 Überflüssiges Semikolon............................................................................................................75 5.11. Nicht initialisierte Variablendefinition.......................................................................................75 5.12 Einfaches Gleich-Zeichen für Vergleich verwendete.................................................................76 5.13 Semikolon an falscher Stellen....................................................................................................76 5.14 break in einer switch-Anweisung vergessen..............................................................................76 5.15 Feldgrenzen nicht beachten........................................................................................................76 5.16 Verwechseln der && und || Operatoren......................................................................................77 5.17 Ganzzahlige Division.................................................................................................................77 Haase, Grundlagen der Software-Entwicklung 4 5.18 Nicht initialisierte Zeiger............................................................................................................77 5.19 Zeichenfelder auf Gleichheit testen............................................................................................77 Anhang........................................................................................................................................................79 Haase, Grundlagen der Software-Entwicklung 1. Einleitung Wie lernt man Schwimmen? Wie lernt man Programmieren? Ein Buch lesen reicht nicht. Man muss es tun – learning by doing. 1.1. Ziele Mittels Programmierung kann man zwar mathematische Aufgaben lösen, aber die Schreibweise unterscheidet sich sehr von mathematischen Gleichungen. x=x+1 Ist keine mathematische Gleichung (wäre auch unsinnig), sondern eine Rechenanweisung. (rechte Seite vom Gleichheitszeichen) Nehme x und addiere 1 (linke Seite vom Gleichheitszeichen) speichere das Resultat als neues x Eine Umwandlung einer Temperatur von °C nach °F erfolgt in C durchführen tF = tC * 9 / 5 + 32 ; Programmieren in C und (etwas) C++ targets Embedded Controller C++ hier nur in geringem Umfang Ziel: programmieren einfacher, kleiner Programme für Berechnungen 5 Haase, Grundlagen der Software-Entwicklung 1.2. Architektur eines Computer CPU zentrale Recheneinheit ROM nur lesbarer Speicher RAM schreib und lesbarer Speicher In ROM und Massenspeicher bleiben Daten auch nach Abschalten der Spannungsversorgung erhalten. RAM-Speicher ohne Spannung verliert seine Daten. ROM-Speicher ist (normalerweise) nicht änderbar. 6 Haase, Grundlagen der Software-Entwicklung 1.3. Programmier-Werkzeuge Programmiersprache: eine Methode für den Menschen verständliche und handhabbare Darstellung eines Algorithmus in eine Form zu bringen, die von einem Computer „verstanden“ wird. Maschinensprache Sehr schwer verständlich. Für jeden CPU-Typ anders. Eigentlich sind es ja nur Bitmuster. Im Bild befindet sich daher schon eine (vereinfachende) Darstellung (hexadezimal). Assembler XOR XOR MOV MOV XCHG MOV ADD MOV ADD MOV ADD MOV XCHG ADD MOV ADD MOV ADD EAX,EAX ECX,ECX EBX,ip_pointer AX,WORD PTR[EBX] AL,AH CX,WORD PTR[EBX+2] EAX,ECX CX,WORD PTR[EBX+4] EAX,ECX CX,WORD PTR[EBX+6] EAX,ECX CX,WORD PTR[EBX+8] CL,CH EAX,ECX CX,WORD PTR[EBX+10] EAX,ECX CX,WORD PTR[EBX+12] EAX,ECX Immerhin lesbarer Text. Für jede CPU-Familie anders. Sehr kleinteilig. Jede Zeile ist ein einzelner Befehl. Für nahezu jede sinnvolle kleine Aufgabe sind mehrere Befehle nötig. Aber sehr detailliert. Auch Sonderfunktionen einer CPU sind zugänglich. 7 Haase, Grundlagen der Software-Entwicklung 8 Hochsprachen Hochsprachen sind unabhängig von der verwendeten Rechnerarchitektur – oder sollten es zumindest sein. Sie haben aber zumeist Einschränkungen hinsichtlich der angebotenen Funktionalität (kleinster gemeinsamer Nenner). Für allgemeine Aufgaben sind sie bestens geeignet. Hardwarenahe Programmierung ist nicht möglich oder erfordert zusätzliche, spezielle Maßnahmen. Für die Programmierung moderner grafischer Benutzeroberflächen sind spezielle Bibliotheken erforderlich, die jedoch meist plattformspezifisch sind. Compiler für verschiedene Rechnerarchitekturen Spezielle Zielsysteme: Embedded Controller, Smartphones Compiler vs Interpreter Compiler: C/C++, Pascal, Fortran schnell jeweils für ein bestimmtes Zielsystem Interpreter Basic, Python langsam plattformübergreifend Haase, Grundlagen der Software-Entwicklung 9 Zwischenformen z.B. Java Varianten z.B. C-Interpreter, Basic-Compiler Programmiersprachen müssen nicht unbedingt in textueller Form vorliegen. Es gibt auch grafische Programmiermethode, ähnlich den Schaltbildern von elektronischen Schaltungen (IEC 61131, Function Block Diagramm, Ladder Diagramm). Meist benutzt man integrierte Entwicklungsumgebungen (Integrated Development Environment, IDE), die neben dem Compiler auch Programmier-Editore und Projektverwaltung enthalten. Für die Vorlesung C/C++ IDE und Compiler C/C++ ist die meistverwendete Programmiersprache - ca. 50% von allem. Java auf dem 2. Platz - eher wegen Web- und Smartphone-Anwendungen. Für technische und naturwissenschaftliche Anwendungen weniger geeignet. C ist eine sehr leistungsfähige Programmiersprache. Aber sie „erleichtert“ es auch Fehler zu machen. Dies hat sehr zu ihrem Ruf als „gefährliche“ Programmiersprache beigetragen. 1.3.1. Windows Microsoft C++ IDEs Mehrere Versionen von C++ Arbeitsumgebungen sind bei Microsoft kostenlos erhältlich https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs Ab Windows 7 ist auch Microsoft Visual Studio 2013 geeignet. Vorhandene Projekte der 2010-Version (z.B. Praktikumsaufgaben) werden automatisch konvertiert. Microsoft Visual Studio Community, ebenfalls ab Windows 7, ist für Studenten kostenlos erhältlich. Ein Installationsstarter ist im CIP-Pool auf L:/Haase/GSE vorhanden. Alle Beispiele arbeiten als Konsolen-Anwendungen. Um zu verhindern, dass sich das Ein-/Ausgabefenster am Programmende sofort schließt, ist in den Beispielen für Visual C++ eine Eingabeanforderung _getch() am Programmende angefügt. Hierzu ist auch die Präprozessordirektive #include <conio.h> eingebunden. Die Programme warten am Ende also auf eine Eingabe, die jedoch nicht mehr weiterverarbeitet wird. 1.3.2. Linux Code::Blocks Auf einem Linux-Rechner oder in einer virtuellen Umgebung mit einem Linux Betriebssystem. Code::Blocks benutzt den gcc-Compiler und den gdb-Debugger. Statt auf einem Linux-Rechner kann Linux auch in einer virtuellen Umgebung (virtual machine) laufen. VirtualBox findet man bei https://www.virtualbox.org/ Haase, Grundlagen der Software-Entwicklung 10 1.3.3. Arduino Netter, preiswerter Embedded Controller. Sehr verbreitet, viel anschließbares Zubehör. Arduino UNO ca. 25 €, teilweise ab 15 €, weitere kleinere oder größere Versionen, NANO, MEGA etc. IDE mit Compiler ist kostenlos. http://arduino.cc Ein aktuelle Version ist im CIP-Pool auf L:/Haase/GSE vorhanden. Ein- und Ausgaben müssen beim Arduino über die serielle Schnittstelle (USB-Anschluß) in ein Fenster der Arduino-IDE geleitet werden (oder zu einem seriellen Monitorprogramm). 1.3.4. Android CppDroid Einfache, kleine Programme in C/C++ auf dem Android-Smartphone oder Tablet programmieren. Auf Google Play Store oder im CIP-Pool auf L:/Haase/GSE. Kostet ein paar Euro um die Reklame loszuwerden (in-app Kauf). Eine ähnliche App C4droid findet man im Google Play Store. 1.3.5. Diverse Programmierplattformen Für industrielle Anwendungen von Embedded Controllern: Keil IAR Raisonance AVR-Studio teilweise als kostenlose Test- oder Demoversion erhältlich. Überwiegend für Windows konzipiert. Für Apple OS X ist gcc oder Xcode bedingt geeignet. 1.3.6. Programmbeispiele Parallel zu diesem Skript gibt es Programmbeispiele für erwähnten Programmierplattformen. Diese Beispiele sind weitgehend ähnlich oder gleich gehalten. Soweit möglich verwenden diese Beispiele die Ein- und Ausgabe cin und cout von C++. Hierzu ist auch die Präprozessordirektive #include <iostream> eingebunden. Bei Arduinos wird jedoch die für diese Plattform normale serielle Ein- und Ausgabe verwendet. Einige Beispiele insbesondere für Arduinos sind als zyklische Programme ausgelegt. Dies entspricht der gängigen Anwendung von Embedded Controller. Haase, Grundlagen der Software-Entwicklung 11 1.4. Zahl- und Zeichendarstellung Computer benutzen „umschaltbare“ Elemente – genannt Bits. Jeweils nur einer von 2 Zuständen möglich – Null oder Eins, Strom oder kein Strom, wahr oder falsch. Da man mit nur einem Bit kaum etwas anfangen kann, kombiniert man mehrere Bits zu größeren Einheiten. So sind 8 Bits üblicherweise 1 Byte. 16, 32 oder 64 Bits werden ein Wort genannt, abhängig von der verwendeten Rechner-Architektur. 1.4.1. Zahlen Zur Darstellung von Zahlen gängig Zehnerystem es gibt nur die Ziffern 0 .. 9, also 10 Ziffern In der Datenverarbeitung viel verwendet Dualsystem (Zweiersystem, Binärsystem) es gibt nur die Ziffern 0 und 1, also 2 Ziffern Hexadezimalsystem es gibt 16 Ziffern 0 ..9 und zusätzlich (ersatzweise) A .. F als Ziffern 10 bis 15 Stellenschreibweise vgl. römische Zahlenschreibweise Zehnersystem (10er-System) Stellen für Einer, Zehner, Hunderter, ... Binärsystem (Zweiersystem) eine Ziffer steht für 1 Bit Stellen für Einer, Zweier, Vierer, … Hexadezimalsystem eine Ziffer steht für 4 Bit Stellen für Einer, 16-er, 256-er, … Schreibweise in C/C++ mit vorangestelltem 0x, z.B. 17 hexadezimal 0x11 Die Rechenmethoden sind immer gleich. ► Beispiel Dec2Hex ► Beispiel Hex2Dec Ganzzahlen Man unterscheidet in der Rechnertechnik bei ganzzahligen Werten nach der Anzahl der verwendeten Haase, Grundlagen der Software-Entwicklung 12 Bytes und ob auch negative Zahlenwerte darstellbar sein sollen. Größe mit Vorzeichen ohne Vorzeichen 1 Byte char unsigned char 2 Byte short unsigned short 4 Byte long unsigned long 8 Byte long long unsigned long long ► Beispiel Sizeof Die 1 Byte großen char dienen auch der Speicherung von Buchstaben und Ziffern – daher der Name. Bei den meisten Compilern speichern sie Zahlen mit Vorzeichen. Gelegentlich einstellbar. Auch mit vorangestelltem signed oder unsigned. Sehr häufig findet man auch die ursprünglichen Ganzzahltypen (integer) int und unsigned int. Je nach Architektur können diese jedoch 2, 4 oder sogar 8 Bytes haben. Für die Varianten mit Vorzeichen darf auch signed vorangestellt werden. Allen außer char darf auch int nachgestellt werden – z.B. unsigned short int. Neuere Compiler kennen auch ganzzahlige Datentypen Namen, die die Bitgröße unmittelbar im Namen tragen – z.B. int8_t, int16_t, uint32_t etc. Negative Zahlen Einerkomplement – 1 Bit als Vorzeichen. Ungebräuchlich. Zweierkomplement Beispiel für Zweierkomplement mit 4 Binärstellen (Zahlenbereich -8 bis +7) binär 0 1 1 0 1 0 0 1 1 1 0 1 0 dezimal + 6 Komplement von 6 addiere 1 - 6 Probe 0 1 1 0 + 6 + 1 0 1 0 - 6 ---------------------1 0 0 0 0 0 Der Übertrag in die (nicht existierende) 5. Stelle entfällt / wird ignoriert. Überlauf Wird der darstellbare Bereich bei einer Operation über- oder unterschritten, so ergeben sich unsinnige bzw. völlig falsche Werte. Wird beispielsweise zu der Zahl 65535 mit dem Datentyp unsigned short (2 Bytes) eine 1 addiert so ergibt sich nicht 65536 sondern der Wert 0. Im Binärsystem besteht die Zahl 65535 aus 16 mal 1er-Bits. Eine Addition von 1 führt zu 16 mal 0er-Bits und einem Übertrag in das nicht mehr vorhandene 17. Bit. Haase, Grundlagen der Software-Entwicklung 13 Fließkommazahlen Festpunktzahlen werden langsam unüblich (2014). Stattdessen exponentielle Darstellung mit Mantisse und Exponenten - allerdings binär und nicht dezimal. Früher gab es diverse Standards, heute überwiegend IEEE-754 / IEC 60559. Größe Datentyp 4 Bytes float 8 Bytes double 10 Bytes Extended (selten) 16 Bytes long double (sehr selten) ► Beispiel Sizeof Fließkommazahlen haben nur eine begrenzte Genauigkeit – gewissermaßen ein „Anzahl gültiger Stellen“. Außerdem werden sie rechnerintern in binärer Form dargestellt. Daraus resultieren gelegentlich „Überraschungen“. Beispielweise hat der Wert von 1/10 in dezimaler Darstellung nur eine einzige Nachkommastelle – ist also leicht exakt darstellbar. Als binäre Zahl hingegen ist 1/10 ein unendlicher, periodischer Bruch und daher mit endlich vielen Bits nicht exakt darzustellen. Die Rechenmethoden sind gleich. Beispiel: Division von 1 durch 10 Die interne Darstellung nach IEEE 754 verwendet allerdings ein anderes Format - eine binäre Exponential-Darstellung. Die Genauigkeit der Fließkommazahlen liegt bei etwa 7 Stellen für die 4-Bytes float und bei etwa 16 Stellen für die 8-Bytes double. 1.4.2. Zeichen Um Buchstaben, Ziffern, Sonderzeichen zu behandeln, ist eine eindeutige Zuordnung von Bitmustern zu den einzelnen Zeichen erforderlich. „Kleines Alphabet“ für die wichtigsten, englischen Zeichen (1 Byte). Haase, Grundlagen der Software-Entwicklung 14 128 Zeichen (ASCII) einigermaßen einheitlich standardisiert. Die übrigen 128 Zeichen (hexadezimal 80 bis FF) wurden mehrfach unterschiedlich definiert. Einzelne Zeichen kann man in einem Programm benutzen, indem man diese zwischen einfache Hochkommata setzt. char aChar = 'L'; oder if ( ch == '4' ) Für einige Sonderzeichen gibt es spezielle Formen, die einen vorangestellten rückwärtigen Schrägstrich nutzen. \n Zeilenwechsel \r Wagenrücklauf \f nächste Seite \t Tabulator \' einfaches Anführungszeichen \" doppeltes Anführungszeichen \\ rückwärtiger Schrägstrich In besonderen Fällen kann man auch die hexadezimalen oder dezimalen Äquivalente der Zeichen angeben. char letterL = 0x4C; oder if ( aDigit == 0x37 ) // check for digit '7' Fremdländische Zeichen wie beispielsweise Ä, Ö, Ü, ß sind in den ersten 128 Zeichen nicht vorhanden, wurden aber länderabhängig und auf unterschiedliche Weise in den zweiten 128 Zeichen untergebracht. Für kyrillisch, griechisch, arabisch oder gar chinesisch ist diese Methode jedoch ungeeignet. „Großes Alphabet“ genannt Unicode (ISO/IEC 10646) für alle existierenden Zeichen (4 Bytes) (lebend, tot, graphisch, …) z.B. Japanisch (hier Katakana) Haase, Grundlagen der Software-Entwicklung 15 Für die chinesische Schrift ca. 90000 Zeichen. Da reichen inzwischen auch 2 Bytes zur Codierung nicht mehr aus (Windows). In der nächst größeren Stufe von 4 Bytes werden z.Z. 23 Bits von Unicode genutzt (http://site.icuproject.org/). Es gibt mehrere Formen der Kodierung hierfür. Eine weit verbreitete Codierung genannt UTF-8 nutzt je nach Zeichen (bzw. code point) 1 bis 6 Bytes (Email, Web, etc.). 1.4.3. C++ Datentypen In moderneren C Implementierungen und in C++ gibt es einige zusätzliche Datentypen. Teilweise sind diese unmittelbar in die Sprache eingebaut, teilweise gehören sie zu Standardbibliotheken. bool (in C++) Ein (ganzzahliger) Datentyp, der nur die Werte 0 und 1 annehmen kann. Als Synonyme sind auch die Schlüsselwörter true und false erlaubt. bool bFertig = true; bFertig = 0; if ( bFertig ) ... // mit oder ohne Initialisiernng // oder bFertig = false // als Bedingung complex (ab C99 und in C++) Eine vergleichsweise neuere Ergänzung für komplexe Zahlen. string (in C++) Als Teil einer sehr bequeme anzuwendenden Standard-Bibliothek. Üblicherweise sind string-Variablen nicht auf ein Feld aus 1-Byte char beschränkt sondern erlauben einenTeil oder den ganzen UnicodeZeichensatz. string singerName = "Amy Winehouse"; string singerName("Amy Winehouse"); Für den Datentyp string sind eine Vielzahl von Funktionen definiert. ► Beispiel String Haase, Grundlagen der Software-Entwicklung 16 1.5. Algorithmen Zu den ersten, ursprünglichen Aufgaben von Computern gehörten Berechnungen, oftmals militärischer Art. Heute sind sie aus kaum einem Lebensbereich wegzudenken. Nur sehr einfache Berechnungen können durch eine einzelne Formel ausgedrückt werden. Meist sind längere Rechenwege erforderlich. Solche Rechenwege nennt man Algorithmen. Sie bestehen aus einer mehr oder weniger festgelegten Reihenfolge einzelner Rechenoperationen und können auch Fallunterscheidungen enthalten. Algorithmen werden gelegentlich durch sogenannte Flussdiagramme dargestellt. Das nachfolgende Diagramm zeigt die iterative Berechnung einer Quadratwurzel. ► Beispiel SquareRoot Solche Diagramme (oder Struktogramme) sind intuitiv sehr leicht verständlich. Sie werden daher auch für andere Zwecke als Programmierung verwendet. Es gibt einige alternative Diagramme – Nassi Shneiderman, UML etc. Haase, Grundlagen der Software-Entwicklung 17 2. C / C++ Programmbeispiel #include <iostream> using namespace std; int main() { cout << "Hello World!" << endl; return 0; } Ein Programm besteht aus einer Reihe von Deklarationen und Anweisungen. Alle Anweisungen werden mit einem Semikolon abgeschlossen. ► Beispiel HelloWorld Die Methoden für die Ein- und Ausgabe sind in den Programmiersprachen sehr unterschiedlich. Cout für Ausgaben und cin für Eingaben gehören zu C++. In reinem C verwendet man u.a. printf- und scanf-Funktionen. In grafischen Benutzeroberflächen müssen (üblicherweise) darzustellende Werte an Objekte übergeben werden. Das Beispiel ist ein sogenanntes Konsolen-Programm. Die Darstellung erfolgt in einem Textfenster oder unmittelbar auf dem Bildschirm, im Gegensatz zu einem graphischen (GUI-) Programm. Im Beispiel wird der Text Hello World! auf dem Bildschirm ausgegeben. Die Ausgabe von endl bewirkt einen Übergang in die nächste Zeile. Aller ausführbarer Programm-Code befindet sich in C/C++ in Funktionen. Im Beispiel gibt es nur die Funktion main, die in diesem Fall keine Übergabeparameter in () benötigt und einen int-Wert als Resultat zurückliefert. Die Bestandteile, der Body (Körper) einer Funktion steht innerhalb geschweifter Klammern. Die Funktion main ist gewissermaßen der Hauptteil eines Programms und wird als erstes aufgerufen. Der Rückgabewert kann als Fehlerkennzeichnung verwendet werden. Der Wert 0 gilt als fehlerfrei ausgeführt. Am Anfang eines Programm-Codes findet man meist spezielle Compiler-Direktiven. Mit diesen wird dem Compiler mitgeteilt, wie er arbeiten soll. Im Beispiel oben wird die iostream-Bibliothek eingebunden – wg cout - und der Namensraum std ohne explizite Angabe genutzt – ebenfalls wg cout. Es ist üblich Programm-Code gut lesbar zu schreiben. Zwingend nötig ist dies in C bzw. C++ nicht. Das Programm oben hätte man auch in eine einzige Zeile und weitgehend ohne Leerzeichen schreiben dürfen (ausgenommen die Prozessor-Direktive in der ersten Zeile). #include <iostream> using namespace std;int main(){cout<<"Hello World!"<<endl;return 0;} Es gibt aber keine festen Regeln für die Schreibweise. Solange der Compiler Schlüsselwörter, Namen u.a. identifizieren kann, sind Leerzeichen, Tabulatoren oder Zeilenwechsel funktional bedeutungslos. Sie können aber die Lesbarkeit verbessern. Leerzeichen etc. werden in C/C++ als white space bezeichnet. Haase, Grundlagen der Software-Entwicklung 18 Normalerweise sollten Programme in textueller Form kommentiert werden. Außer bei trivialen Programmen kann man aus dem Programm-Code allein unmöglich oder allenfalls mühsam den Zweck einzelner Anweisungen verstehen. In /C/C++ werden Kommentare in den Programm-Code eingestreut und für den Compiler in besonderer Weise als Kommentar gekennzeichnet. Die älteste Form, der C-Kommentar, verwendet die Zeichenfolge /* als Anfang eines Kommentars und die Zeichenfolge */ als Ende des Kommentars. Der komplette Text dazwischen hat für den Compiler keine Bedeutung. Er darf sich auch über mehrere Zeilen erstrecken. In C++ trat ein weiterer Kommentar hinzu. Er beginnt mit der Zeichenfolge // und geht bis zu Ende der Zeile. /* dies ist ein einzeiliger Kommentar */ /* dies ist ein mehrzeiliger Kommentar */ // und hier ein Kommentar bis zum Ende der Zeile Haase, Grundlagen der Software-Entwicklung 19 2.1. Variablen Sinnvolles Programmbeispiel – Umrechnung von Celsius-Temperaturen in Fahrenheit-Temperaturen. #include <iostream> using namespace std; int main() { double tC, tF; cout << "Celsius 2 Fahrenheit\n"; cout << "Enter C: "; cin >> tC; tF = 1.8 * tC + 32.0; cout << tC << "C = " << tF << "F" << endl; return 0; } Variablendeklarationen bestehen aus der Angabe des gewünschten Datentyps – z.B. double – und einem Namen für die Variable – z.B. tC. Mehrere Variablen können gleichzeitig - durch Komma getrennt – deklariert werden – z.B. double tC, tF. Jede Deklaration muss mit einem Semikolon abgeschlossen werden. Die Namen von Variablen müssen mit einem Buchstaben (oder einem Unterstrich) beginnen, gefolgt von beliebig vielen weiteren Buchstaben, Ziffern und Unterstrichen. Groß- und Kleinbuchstaben sind in C/C+ + verschiedene Zeichen (anders als beispielsweise in Pascal oder Strukturiertem Text). VA und Va sind in C/C++ also die Namen von 2 verschiedenen Variablen. Einige Namen sind für die Programmiersprache reserviert – z.B. using, return oder die Bezeichner für die Standard-Datentypen int, char, double etc. Variablennamen sollten aussagekräftig gewählt werden. Also eher TempC und TempF statt tC und tF oder sogar temperaturCelsius und temperaturFahrenheit. Häufig wird jedes Teilwort (evtl. außer dem ersten) in einem Variablennamen mit einem Großbuchstaben geschrieben – z.B. temperaturMessungPt100InCelsius – genannt Camel Notation. Eine andere häufig verwendete Methode stellt jeder Variablen einen typabhängigen Präfix voran. IntegerVariablen erhalten ein vorangestelltes n, Variablen vom Typ double wird ein d vorangestellt. int nAnzahl; double dTemperatur; Hier spricht man von der Ungarischen Notation (Hungarian Notation), benannt nach dem Microsoft Programmierer Charles Simonyi. ► Beispiel C2F ► Beispiel F2C ► Beispiel SquareRoot 2.1.1. Initialisierung Häufig sollen Variablen schon von Anbeginn festgelegte Werte besitzen, initialisiert werden. Für das Haase, Grundlagen der Software-Entwicklung Programmbeispiel oben – Umrechnung von Celsius-Temperaturen in Fahrenheit-Temperaturen – sind folgende initialisierte Variablen denkbar. double scaleC2F = 1.8; double offsetC2F = 32.0; ... tF = scaleC2F * tC + offsetC2F; Erforderlichenfalls können solche Variablen auch noch als Konstante – also unveränderlich – definiert werden. const double scaleC2F = 1.8; const double offsetC2F = 32.0; Solche initialisierten und unveränderlichen Variablen können durch gut gewählte Namensgebung zum Verständnis eines Programms beitragen. Werden in einem Programm häufig Umrechnungen von Bogenmaß zu Winkelmaß und umgekehrt benötigt, so sind folgende Variablen praktisch. const double Pi = 3.141592653589793; const double Radiant2Angle = 180.0/Pi; const double Angle2Radiant = Pi/180.0; 20 Haase, Grundlagen der Software-Entwicklung 21 2.2. Einfache Anweisungen Die typischen Aufgaben von Programmen sind Berechnungen irgendwelcher Art. Programm-Code hat daher eine gewisse Ähnlichkeit mit mathematischen Gleichungen. KreisUmfang = 2 * Pi * Radius; KreisUmfang, Radius und wohl auch Pi sind die Namen von Speicherplätzen oder Variablen. Die Gleichung verwendet die bereits vorliegenden Werte für Pi und Radius zur Berechnung des Kreisumfangs. Das Ergebnis wird in der Variablen KreisUmfang abgelegt. Betrachtet man solche einfachen Anweisungen an der Stelle des Gleichheitszeichens geteilt, so findet man auf der rechten Seite ein Rechenvorschrift bestehend aus Konstanten und Werten aus Variablen verknüpft durch mathematische Operationen. Die linke Seite vom Gleichheitszeichen besteht lediglich aus dem Namen einer einzelnen Variablen in der das Resultat abgelegt werden soll. Das Gleichheitszeichen entspricht also nicht dem Gleichheitszeichen der Mathematik sondern bedeutet eine Wertzuweisung. Deshalb ist X = X + 1 eine sinnvolle Anweisung. Mathematisch betrachtet wäre es sinnlos. Jedoch als programmierte Anweisung bedeutet es ein Inkrementieren (um 1 erhöhen) der Variablen X. In C/C++, aber auch in vielen anderen modernen Programmiersprachen, muss jede Anweisung mit einem Semikolon abgeschlossen werden. Dies hilft dem Compiler bei der Arbeit. Der rechtsseitige Ausdruck muss alle mathematischen Operationen explizit angeben. Die in der Mathematik übliche und unmissverständliche Schreibweise KreisUmfang = 2 Pi Radius; ist nicht zulässig und ergibt eine Fehlermeldung durch den Compilers. 2.2.1. Operatoren Neben den gängigen Operatoren Plus (+), Minus (-), Mal (*) und Geteilt (/) gibt es eine Vielzahl weiterer Operatoren. Da es nicht genügend Tasten auf der Tastatur für alle mathematischen Operatoren gibt, werden einige Operatoren durch 2 ohne Leerzeichen aufeinanderfolgende Zeichen dargestellt – z.B. die Vergleichsoperation auf größer oder gleich ≥ wird >= geschrieben. In der Mathematik haben Operatoren eine Rangfolge – Punktrechnung geht vor Strichrechnung. Schon im Beispiel der Konvertierung von Celsius nach Fahrenheit wurde diese Vorrangregel ohne Erwähnung benutzt. Bei C/C++ (und bei fast allen anderen Programmiersprachen) gelten die normalen mathematischen Vorrangregeln. Die Operatoren legen auch die Ergebnisdatentypen fest. Meist sind es die gleichen Datentypen wie die Operanden. In 1 + 2 sind 1 und 2 Ganzzahlen. Daher wird auch das Ergebnis ganzzahlig sein. Eine Überraschung gibt es bei der ganzzahligen Division. 5 / 2 ist nicht etwa 2.5 sondern 2. Auch hier ist das Resultat ganzzahlig Haase, Grundlagen der Software-Entwicklung und es wird nach unten abgerundet. C/C++ kennt eine Vielzahl von speziellen Operatoren, die in anderen Programmiersprachen nicht zu finden sind. ?? reichlich Klammern verwenden 2.2.2. Ausführungspriorität Aus http://de.cppreference.com 22 Haase, Grundlagen der Software-Entwicklung 2.2.3. Typkonvertierung Für gelegentlich notwendige Umwandlungen von einem Datentyp in einen anderen gibt es in C/C++ sogenannte Type Casts. Die möglichen Umwandlungen sind allerdings beschränkt. Man kann eine Ganzzahl in eine Fließkommazahl wandeln da hierbei kein Stellenverlust eintritt. Umgekehrt kann man aber eine Fließkommazahl nicht in eine Ganzzahl konvertieren, weil hierbei der Nachkommateil abgeschnitten wird. Für einen Type Cast verwendet man in C den Datentyp in Klammern vorangestellt, z.B. aDouble = (double)anInt; anUnsignedShort = (unsigned short)aSignedShort; In C++ kann ein Type Cast auch wie eine Funktion geschrieben werden. Allerdings geht dies nur mit Datentypen die aus einem einzelnen Schlüsselwort bestehen, also nicht als unsigned int (). aDouble = double(anInt); aSignedShort = short(anUnsignedShort); ► Beispiel TypeCast ► Beispiel PtrCast Eine andere wichtige Anwendung werden Type Casts im weiteren Verlauf bei Zeigern finden. ?? C11 named cast: static_cast, dynamic_cast, const_cast, reinterpret_cast 23 Haase, Grundlagen der Software-Entwicklung 24 2.3. Felder Häufig müssen mehrere gleichartige Werte (vom gleichen Datentyp) zusammengehalten werden. Vektoren oder Matrizen sind gängige Beispiele. Aber auch zeitlich aufeinanderfolgende Messwerte können etwa für eine Mittelwertbildung zusammengehalten werden. Für derartige Zwecke verwendet man ein- oder mehrdimensionale Arrays / Felder. 2.3.1. Deklaration Die Deklaration von Feldern erfolgt in C/C++ mit angehängten eckigen Klammern und der Dimensionsangabe darin. double double Vector[3]; Matrix[3][4]; // Vektor der Dimension 3 // 3x4-Matrix ► Beispiel Vector Falls die Felder global sind, werden sie automatisch mit 0.0 initialisiert. Lokale Felder in Funktionen hingegen werden nicht initialisiert. Die Elemente enthalten mehr oder weniger zufällige Werte. C11 bietet ergänzende (Template-)Klassen für Container. 2.3.2. Initialisierung Felder können auch initialisiert werden. double double NullPunktVektor[3] = { 0.0, 0.0, 0.0 }; Rotation2D90Grad[2][2] = { 0.0, -1.0, 1.0, 0.0 }; alternativ double Rotation2D90Grad[2][2] = { { 0.0, -1.0 }, { 1.0, 0.0 } }; Die Initialisierungswerte werden durch Kommas getrennt in geschweiften Klammern geschrieben. Bei mehrdimensionalen Feldern kann man die Zeilen jeweils nacheinander in geschweiften Klammern schreiben. Hier wird berücksichtigt, dass die Elemente einer Matrix in C/C++ (und den meisten anderen Programmiersprachen, jedoch nicht in FORTRAN) zeilenweise gespeichert werden. Es wächst also der letzte Index zuerst. ► Beispiel Array Ein Feld kann auch durch Angabe seiner Elemente in der Initialisierung dimensioniert werden. double Messung[] = { 1, 2, 3 }; // Feld mit 3 Werte initialisert Die Dimensionsangabe in den eckigen Klammern darf hier entfallen. Andererseits kann man ein Feld auch nur teilweise initialisieren. double Messung[5] = { 1, 2 }; // Feld mit 5 Elementen Haase, Grundlagen der Software-Entwicklung 25 Hier werden nur die ersten beiden Elemente explizit initialisiert mit 1 und 2. Die übrigen 3 Elemente werden vom Compiler automatisch mit 0 initialisiert. Die Dimensionsangabe ist hier natürlich nötig, da sonst die Dimension nicht festgelegt wäre. Wenn in der Initialisierung zu viele Werte angegeben werden, erfolgt eine Fehlermeldung beim Übersetzungsvorgang durch den Compiler. double Messung[3] = { 1, 2, 3, 4, 5 }; // falsch 2.3.3. Verwendung Die Indizierung der Feldelemente erfolgt in C/C++ immer mit 0 beginnend. Auch hier dienen die eckigen Klammern zur Kennzeichnung. double Messung[3]; // Feld für 3 Messwerte In diesem Feld gibt es die Feldelemente Messung[0], Messung[1] und Messung[2]. Insbesondere beachte man, es gibt kein Feldelement Messung[3]. Ein Zugriff auf ein solches nicht existierendes Element ist einer der „beliebtesten“ Fehler, oftmals mit katastrophalen Folgen. Messung[0] Messung[1] Messung[2] Mittelwert = = = = 1.1; 2.2; 3.3; ( Messung[0] + Messung[1] + Messung[2] ) / 3; Statt einen Index unmittelbar als Zahl anzugeben, kann er auch berechnet werden. Es sei N = 1 der Wert einer ganzzahligen Variablen. Dann wird mit Messung[N+1] wird auf das letzte Element Messung[2] zugegriffen. Auch in diesen Fällen achte man darauf, nicht versehentlich auf nicht existierende Feldelemente zuzugreifen. Unsinnige Resultate oder sogar Software-Abstürze sind die Folge. ?? mehrdimensionale Felder ?? C11 begin() und end() bei Feldern ► Beispiel Vector 2.3.4. Zeichenfelder Sehr häufig werden Felder gebraucht, die Zeichenfolgen enthalten - character arrays. In ihnen können dann Text, Namen, Bezeichnungen etc. in lesbarer Form gespeichert werden. Man sagt statt Zeichenfolge auch String. Aber diese sollten nicht mit dem C++ Datentyp string verwechselt werden. C besitzt für Zeichenfolgen eine spezielle Regel. Die verwendeten Felder sind normalerweise größer als nötig, oftmals erheblich größer. Der enthaltene Text beginnt auf dem Element mit dem Index 0 und hinter dem letzten Element des Textes wird ein Zeichen mit dem binären Wert 0 eingetragen. char VorName[50] = "Tobias"; Hier wird ein Zeichenfeld mit dem Variablennamen VorName definiert. Es besteht aus 50 Speicherplätzen vom Typ char und es wird mit der Zeichenfolge "Tobias" initialisiert. Zeichenfolgen-Konstanten, auch Haase, Grundlagen der Software-Entwicklung 26 String-Konstanten genannt, werden in doppelten Anführungszeichen eingeschlossen. Die Anführungszeichen gehören nicht zu der Zeichenfolge, sondern sind lediglich ein Hinweis an den Compiler. Die Zeichenfolge "Tobias" besteht aus 6 Zeichen, die in den Feldelementen VorName[0] bis VorName[5] eingetragen sind. In dem Element VorName[6] wird der Wert 0 eingetragen als Kennzeichen für das Ende der Zeichenfolge. Da ein solches 0-Zeichen in jeder Zeichenfolge als Endekennzeichen notwendig ist, kann in einem Zeichenfeld von 50 Elementen wie VorName[50] maximal ein Text/Name bestehend aus 49 Zeichen vorliegen. Auch bei der Definition von Zeichenfolgen kann man dem Compiler die Ermittlung der Feldgröße überlassen. char VorName[] = "Tobias"; In diesem Fall wird der Compiler das Feld VorName auf 7 char festlegen, 6 für den Namen Tobias und ein weiteres für das abschließende 0-Zeichen. ► Beispiel Char Für die Behandlung von Zeichenfolgen besitzt C/C++ Bibliotheksfunktionen, die berücksichtigen, dass Zeichenfolgen immer mit dem 0-Zeichen abgeschlossen sind. Da das 0-Zeichen bereits als Endekennzeichen einer Zeichenfolge benutzt wird, enthält eine Zeichenfolge normalerweise kein 0-Zeichen. Für Unicode-Zeichen (ISO/IEC 10646) gibt es eine Darstellungsform UTF-8, die für jedes Zeichen 1 bis 6 char verwendet. Die Zeichen des grundlegenden ASCII-Zeichensatzes bleiben unverändert, sind also 1 char lang. Alle anderen Zeichen werden über mehrere char kodiert dargestellt. Beispielsweise besteht die japanische Zeichenfolge aus den hexadezimalen Werten 0xE3, 0x83, 0x8E, 0xE3, 0x82, 0xA6, 0xE3, 0x82, 0xB5, 0xE3, 0x82, 0xAE und einem abschließenden 0-Zeichen. Für jedes japanische Zeichen (in der Norm code point genannt) werden hier 3 Bytes (char) benötigt. Haase, Grundlagen der Software-Entwicklung 27 2.4. Steueranweisungen Eine lineare Folge von Einzelanweisungen ist in den seltensten Fällen ausreichend. Häufig sind Fallunterscheidungen und Wiederholungen nötig. Hierfür gibt es spezielle Steueranweisungen. Ein zweite wichtige Eigenart von C/C++ ist die Bildung von sogenannten Blöcken. Ein oder mehrere Anweisungen können durch geschweifte Klammern zu Blöcken zusammengefasst werden. Sie nehmen dann die gleiche Stelle ein, die sonst eine einzelne Anweisung inne hätte. tF = 1.8 * tC + 32.0; Diese Anweisung kann funktional gleichwertig ersetzt werden durch folgende zwei aufeinanderfolgende Anweisungen. { tF = 1.8 * tC; tF = tF + 32.0; } Ohne die beiden geschweiften Klammern könnten diese beiden Anweisungen jedoch nicht an einer Stelle stehen, an der nur eine einzelne Anweisung erlaubt wäre. Mit den geschweiften Klammern entsteht ein Block, der dann die Stelle einer einzelnen Anweisung einnimmt. Viele Steueranweisungen erlauben jeweils nur eine einzelne Anweisung oder aber erfordern einen Block, der dann beliebig viele Anweisungen enthalten darf. Die Steueranweisungen selbst gelten ebenfalls als Anweisungen. Ein Block hat an seinem Ende, hinter der schließenden geschweiften Klammer, kein Semikolon. 2.4.1. If-Anweisungen Ein sehr häufig verwendete Steueranweisung, die if-Anweisung, führt eine Fallunterscheidung durch. if ( <Bedingung> ) <Anweisung oder Block> else <Anweisung oder Block> // Bedingung ist erfüllt, true // Bedingung ist nicht erfüllt, false Die if-Anweisung besteht aus dem Schlüsselwort if, einer Bedingung in runden Klammern und der Anweisung bzw. dem Block, welcher auszuführen ist, wenn die Bedingung erfüllt ist. Erforderlichenfalls, aber nicht zwingend, kann nach dem Schlüsselwort else die Anweisung bzw. der Block folgen, welcher auszuführen ist, wenn die Bedingung nicht erfüllt ist. ► Beispiel IfElse If-Anweisungen können auch gekettet werden. if ( <Bedingung 1> ) <Anweisung oder Block> else if ( <Bedingung 2> ) <Anweisung oder Block> else <Anweisung oder Block> Haase, Grundlagen der Software-Entwicklung 28 ► Beispiel IfElseIf If-Anweisungen können auch geschachtelt werden. if ( <Bedingung 1> ) if ( <Bedingung 2> ) <Anweisung oder Block> else <Anweisung oder Block, gehört zum zweiten if> Hier ist zunächst nicht klar ersichtlich, ob der else-Pfad zu dem ersten oder dem zweiten if gehört. Die Regeln von C/C++ besagen, daß das else zu dem letzten vorausgegangenen if ohne zugehöriges else gehört. Einrücken durch Leerzeichen oder Tabulatoren wie im Beispiel kann die Absicht allenfalls verdeutlichen. Besser wäre es, das zweite if mit dem zugehörigen else als Block in geschweifte Klammern zu setzen. Wenn das else zu dem ersten if gehören soll, ist es sogar notwendig das zweite if als Block zu formulieren. if ( <Bedingung 1> ) { if ( <Bedingung 2> ) <Anweisung oder Block> } else <Anweisung oder Block, gehört zum ersten if> Als Bedingung ist jeder beliebige Ausdruck geeignet. Wenn der Wert des Ausdrucks ungleich 0 ist, dann gilt die Bedingung als erfüllt. Nur der Wert 0 bedeutet nicht erfüllt. Dies erleichtert die Formulierung von Bedingungen in vielen Fällen, führt aber auch zu merkwürdig zu lesende Bedingungen. if ( nAnzahl ) ... else ... // Wert in Variable nAnzahl ungleich 0 // Wert in Variable nAnzahl gleich 0 Man beachte, daß eine if-Anweisung nur jeweils eine Anweisung im if- und eine im else-Teil erlaubt. Werden mehrere Anweisungen benötigt. So sind diese in einem Block einzuschließen. Ein Spezialfall einer if-Anweisung ist das arithmetische if. Es wird aus Fragezeichen und Doppelpunkt gebildet Vor dem Fragezeichen steht die Bedingung und der Doppelpunkt trennt die Teile für die erfüllte und nicht erfüllte Bedingung. <Bedingung> ? <Ausdruck für erfüllt> : <Ausdruck für nicht erfüllt> Variable c soll das Maximum von a und b erhalten. Statt if ( a > b ) c = a; else c = b; kann man kürzer schreiben c = ( a > b ) ? a : b; Entsprechend den Vorrangregeln sind auch die Klammern nicht erforderlich. Sie dienen allenfalls der Haase, Grundlagen der Software-Entwicklung 29 besseren Lesbarkeit. Zwar spricht man hier häufig von einem arithmetischen if, für die Bedingung vor dem Fragezeichen ist aber jeder beliebige Ausdruck erlaubt (erforderlichenfalls in Klammern). Besondere Vorsicht ist bei Vergleichen von Fließkommawerten nötig. Wegen der begrenzten Genauigkeit liefern Vergleiche auf Gleichheit oder Ungleichheit oftmals unerwartete Resultate. 2.4.2. Switch-Anweisungen Häufig erfordert eine Fallunterscheidung die Prüfung einer ganzen Reihe von Varianten. Verkettete ifAnweisungen sind hierfür zwar geeignet, werden aber schnell unübersichtlich. C/C++ bietet hierfür die switch-Anweisung an. Sie besteht aus dem Schlüsselwort switch gefolgt von der bedingenden Variablen in runden Klammern und einem Block mit den Fallunterscheidungen. Für die einzelnen Fälle wird der Wert mit vorangehendem Schlüsselwort case und nachfolgendem Doppelpunkt aufgelistet. Die fallabhängige Bearbeitung beginnt mit der nächsten Anweisung nach der Fallselektion und endet bei der nächsten break-Anweisung oder mit dem Ende des Blocks. Für alle nicht ausdrücklich selektierten Fälle gibt es optional eine eigene Alternative, die mit dem Schlüsselwort default eingeleitet wird. switch ( N ) { case 0 : . . . // auszuführen bei N gleich 0 break; case 1 : . . . // auszuführen bei N gleich 1 // hier kein break, Fall N gleich 1 geht weiter case 2 : . . . // auszuführen bei N gleich 1 oder 2 break; case 3 : case 4 : . . . // auszuführen bei N gleich 3 oder 4 break; default : . . . // auszuführen falls N weder 0, noch 1, noch 2, noch 3, noch 4 ist break; // break hier nicht erforderlich, da ohnehin am Ende des switch } . . . // weitere Anweisungen immer ausgeführt ► Beispiel Switch Das Beispiel auch zeigt wie ein Teil der Anweisungen für zwei Fälle (N gleich 1 oder 2) ausgeführt werden können, indem man das break an geeigneter Stelle weglässt. Andererseits gehört ein fehlendes break zu den „beliebtesten“ Fehlern. Leider muss die Steuervariable einer switch-Anweisung immer ganzzahlig sein. Hierzu gehören auch einzelne Zeichen. Fließkommawerte oder Zeichenfolgen sind nicht erlaubt. 2.4.3. Schleifen Viele Aufgaben sind nur durch wiederholte Ausführung von sehr gleichen oder ähnlichen Operationen mit unterschiedlichen Daten lösbar. Die Suche nach einem Namen in einer Liste beispielsweise erfordert den Haase, Grundlagen der Software-Entwicklung 30 wiederholten Vergleich eines Namens aus der Liste mit dem gesuchten Namen. Zur Bildung solcher Schleifen bietet C/C++ mehrere Möglichkeiten. Nachfolgend wird der bereits beschriebene Algorithmus zur Berechnung einer Quadratwurzel W einer Zahl X unter Verwendung der Schleifen-Alternativen gezeigt. Für den Vergleich, ob die nötige Genauigkeit erreicht ist, wird die jeweils zuvor berechnete Näherung als Walt gespeichert. Nota: es gibt zur Berechnung einer Quadratwurzel eine schnellere Bibliotheksfunktion sqrt(). 2.4.4. Schleife mittels goto Die einfachste (und zugleich schlechteste) Variante ist die Bildung einer Schleife mit einer SprungAnweisung (goto) zu einem Sprungziel (label). Der Algorithmus zur Berechnung einer Quadratwurzel sähe dann etwa so aus. W = X / 2.0; loop: Walt = W; W = ( W + X / W ) / 2.0; if ( W != W alt ) goto loop; Das Sprungziel - hier loop - darf einen beliebigen, eindeutigen Namen haben. Sprünge sind vorwärts und rückwärts erlaubt. Start und Ziel müssen aber im gleichen Block liegen. ► Beispiel Goto ► Beispiel SquareRoot Das Hauptproblem bei der Verwendung von goto ist die erschwerte Lesbarkeit. Ab einer gewissen Menge von Sprüngen spricht man von Spagetti-Code. Im Fehlerfall kann man solche Programme nicht mehr „gezielt“ reparieren sondern nur noch „gezielt“ wegwerfen. 2.4.5. while-Schleife Die einfachste, strukturierte Schleife ist die while-Schleife. Sie besteht aus dem Schlüsselwort while und einer Fortsetzungsbedingung in runden Klammern gefolgt von einer einzelnen Anweisung oder einem Block. while ( <Fortsetzungsbedingung> ) <Anweisung oder Block> Hinter die schließende runde Klammer gehört kein Semikolon. ► Beispiel WhileLoop Die Fortsetzungsbedingung wird vor jeder Ausführung der Anweisung oder des Blocks ermittelt. Jeder Ausdruck mit ganzzahligem Wert ist als Bedingung geeignet und auch hier gilt nur der Wert 0 als nicht erfüllt (false), alle anderen Werte ungleich 0 als erfüllt (true). Haase, Grundlagen der Software-Entwicklung 31 Manchmal wird die Fortsetzungsbedingung nicht genutzt, indem sie auf immer erfüllt gesetzt wird, und die Schleife wird durch ein passendes break innerhalb des kontrollierten Blocks beendet. Der Algorithmus zur Berechnung einer Quadratwurzel sähe dann etwa so aus. while ( true ) { <Anweisungen> if ( <Endebedingung> ) break; } Der Algorithmus für die Berechnung der Quadratwurzel in einer while-Schleife lautet dann W = X / 2.0; Walt = 0.0; while ( Walt != W ) { Walt = W; W = ( W + X / W ) / 2.0; } Ein sehr beliebter Fehler bei while-Schleifen ist ein überflüssiges Semikolon hinter der Bedingung. while ( <Bedingung> ) <Anweisung> ; // falsch, nicht in der Schleife Hier ist gemeint, dass die <Anweisung> solange ausgeführt wird, wie die <Bedingung> erfüllt ist. Doch die von while kontrollierte Anweisung ist die "leere Anweisung" bestehend aus dem einzelnen Semikolon. Die <Anweisung> befindet sich nicht in der while-Schleife sondern hinter ihr. 2.4.6. for-Schleife Die for-Anweisung besteht aus dem Schlüsselwort for gefolgt von drei steuernden Anteilen getrennt voneinander durch Semikolon und in runden Klammern, gefolgt von einer einzelnen Anweisung oder einem Block. for ( <Anfangsoperation>; <Fortsetzungsbedingung>; <Endeoperation> ) <Anweisung oder Block> Hinter die schließende runde Klammer gehört kein Semikolon. Zum besseren Verständnis sei die for-Schleife mit ihren jeweiligen Teilen nochmals unter Zuhilfenahme der while-Schleife dargestellt. <Anfangsoperation> while ( <Fortsetzungsbedingung> ) { <Anweisung oder Block> <Endeoperation> } Haase, Grundlagen der Software-Entwicklung 32 Die Anfangsoperation wird also nur einmalig ausgeführt. Sie enthält typischerweise die Schleifeninitialisierung. Die Anfangsoperation kann auch leer sein, weggelassen werden. Das nachfolgende Semikolon ist jedoch erforderlich. Die Fortsetzungsbedingung wird vor jeder Ausführung der Anweisung oder des Blocks ermittelt. Jeder ganzzahlige Ausdruck ist als Bedingung geeignet und auch hier gilt nur der Wert 0 als nicht erfüllt (false), alle Werte ungleich 0 als erfüllt (true). Die Fortsetzungsbedingung wird also am Ende der Rechnung ein weiteres letztes mal ermittelt, liefert den Wert 0 und die Schleife endet. Wenn die Fortsetzungsbedingung schon bei der ersten Ermittlung 0 (false) ist, wird der Schleifeninhalt übersprungen. Auch kann die Fortsetzungsbedingung weggelassen werden, wenn die Schleife durch eine andere Maßnahme, etwa ein break, beendet wird. Das nachfolgende Semikolon ist jedoch auch hier erforderlich. In der Endeoperation wird üblicherweise die Zählvariable oder der Laufindex hochgezählt. Prinzipiell ist hier jede beliebige Anweisung möglich. Die Endeoperation kann sogar entfallen, wenn dies sinnvoll sein sollte. ► Beispiel ForLoop ► Beispiel NestedLoops Der Algorithmus für die Berechnung der Quadratwurzel in einer for-Schleife lautet dann W = X / 2.0; for ( int i=0; i<100000; i++ ) { Walt = W; W = ( W + X / W ) / 2.0; if ( W == Walt ) break; } ► Beispiel ForLoopBreak Hier wird die Zählvariable i für die Schleife in der for-Anweisung definiert. Dies ist nur in C++ möglich. In C muß i vorab deklariert werden. In modernen C++-Compilern existiert die so deklarierte Zählvariable nur innerhalb der for-Schleife. Bei dem hier gewählten Algorithmus ist die nötige Anzahl von Schleifendurchläufen a priori unbekannt. Der Algorithmus konvergiert bekanntermaßen sehr gut. Die hier angegebene Höchstzahl 100000 von Schleifendurchläufen ist um viele Größenordnungen zu hoch. Das Ende der Schleife wird durch das break in der if- Anweisung erreicht, wenn die maximal mögliche Genauigkeit erreicht ist. Die geschweiften Klammern sind in diesem Fall notwendig, weil die for-Anweisung mehr als eine einzelne Anweisung kontrollieren soll – hier sind es drei Anweisungen in einem Block. Prinzipiell muss eine for-Schleife keine Zählvariable enthalten, obwohl hierin die typische Anwendung liegt. Die Wurzelberechnung kann auch wie folgt abgekürzt werden. W = X / 2.0; for ( Walt = 0.0 ; W != Walt; ) { Walt = W; W = ( W + X / W ) / 2.0; } Haase, Grundlagen der Software-Entwicklung 33 Da die Variable Walt außer in der Schleife selbst nicht gebraucht wird, könnte sie auch lokal als Schleifenvariable deklariert werden – for ( double Walt = X ; ... Der Fortsetzungsbedingung sollte man besondere Aufmerksamkeit widmen, weil hier ein weiterer „beliebter“ Fehler lauert. Die Schleife for ( int i = 0; i . . . < 10; i++ ) wird genau 10-mal durchlaufen, ein letztes mal mit i gleich 9. Hingegen wird die Schleifen for ( int i = 0; i . . . <= 10; i++ ) 11 mal durchlaufen, ein letztes mal mit i gleich 10. Ein sehr beliebter Fehler bei for-Schleifen ist ein überflüssiges Semikolon am Ende. for ( <Anfangsoperation>; <Fortsetzungsbedingung>; <Endeoperation> ) <Anweisung> // falsch, nicht in der Schleife ; Hier ist gemeint, dass die <Anweisung> solange ausgeführt wird, wie die <Fortsetzungsbedingung> erfüllt ist. Doch die von for kontrollierte Anweisung ist die "leere Anweisung" bestehend aus dem einzelnen Semikolon. Die <Anweisung> befindet sich nicht in der for-Schleife sondern hinter ihr. ?? range for of C11 2.4.7. do-while-Schleife Die do-while-Schleife ist eine Variante der while-Schleife bei der die Fortsetzungsbedingung nicht zu Anfang sondern am Ende geprüft wird. Sie besteht aus dem Schlüsselwort do, einer einzelnen Anweisung oder einem Block gefolgt von dem Schlüsselwort while und der Forsetzungsbedingung in runden Klammern. do <Anweisung oder Block> while ( <Fortsetzungsbedingung> ) ; Hinter der schließenden runden Klammer steht ein Semikolon, weil die do-while-Anweisung hier komplett ist. ► Beispiel DoWhileLoop Die Anweisung oder der Block werden also mindestens einmal ausgeführt. In der while-Schleife und in der for-Schleife kann bei passender Bedingung der kontrollierte Teil unausgeführt bleiben. Dies ist der Grund, warum die do-while-Schleife seltener gebraucht wird. Der Algorithmus für die Berechnung der Quadratwurzel in einer do-while-Schleife lautet dann Haase, Grundlagen der Software-Entwicklung W = X / 2.0; do { Walt = W; W = ( W + X / W ) / 2.0; } while ( Walt != W ); Auch eine do-while-Schleife kann mit break vorzeitig verlassen werden. 2.4.8. continue Manchmal muss der Code in einer Schleife nicht gänzlich durchlaufen werden, sondern der nächste Schleifendurchlauf kann vorzeitig gestartet werden. Dies ermöglicht die continue-Anweisung, typischerweise als Teil einer if-Anweisung. while ( <Bedingung> ) { // evtl. erste Berechnung, immer durchzuführen if ( <Bedingung für vorzeitige Fortsetzung> ) continue; // Berechnung, nicht immer nötig } ► Beispiel ForLoopContinue 34 Haase, Grundlagen der Software-Entwicklung 35 2.5. Funktionen Im Grunde bestehen Programme in C/C++ nur aus einer Vielzahl von Funktionen. Funktionen sind das wichtigste Ordnungsmittel eines Programms. Alle Funktionen haben einen Namen und einen Körper (Body). Dem Funktionsnamen vorangestellt ist der Datentyp des Rückgabewertes. Wenn eine Funktion keinen Wert zurückliefert, wird stattdessen als Rückgabe-Datentyp void (Abfall, Müll) verwendet. Hinter dem Funktionsnamen und vor dem Body werden die Übergabeparameter in runden Klammern angegeben - die sogenannten Formalparameter. Falls die Funktion keine Parameter benötigt, so sind dennoch die Klammern erforderlich. Die Parameter werden durch Komma getrennt und der jeweilige Datentyp wird vorangestellt. Der Body einer Funktion steht - als Block - immer in geschweiften Klammern, auch wenn er aus nur einer Zeile besteht. z.B. int Multiply(int x, int y) { return x * y; } Hier sind x und y formale Parameter. Mit ihnen sind die beiden übergebenen Werte innerhalb des Bodies der Funktion Multiply über einen Variablennamen zugänglich. Diese Namen implizieren aber keinen Namen für die beim Aufruf verwendeten Variablen. Aufgerufen wird eine Funktion mit ihrem Namen und aktuellen Werten für die erforderlichen Parameter die sogenannten Aktualparameter. z.B. int a, b=3, c=5; a = Multiply( b, c ); Die Namen der aktuellen Parameter b und c müssen nicht mit den Namen der formalen Parameter übereinstimmen. Häufig werden Funktionen an einer anderen Stelle definiert, als vor der Stelle ihrer Verwendung, z.B. Standardfunktionen in Bibliotheken. Eine Funktion, die an anderer Stelle definiert ist, kann man durch Angabe ihres Prototypen typsicher verwenden. Als Prototyp dient die Funktionsdeklaration ohne den Body, aber mit angehängtem Semikolon. int Multiply(int x, int y); Derartige Prototypen werden häufig in sogenannten Header-Dateien untergebracht und dieser bei der Anwendung inkludiert. Dies erlaubt eine zuverlässige Bildung von Bibliotheken und modulare Programmierung. Prototypen sind auch dann erforderlich, wenn eine Funktion in einer Quelldatei erst nach ihrer Verwendung deklariert wird. Bei der Deklaration von Prototypen dürfen die Namen der formalen Parameter weggelassen werden. int Multiply(int, int); Hier kommt es, soweit es den Compiler betrifft, nur auf die Datentypen der Parameter an. Erst in der Definition der Funktion sind die Namen erforderlich. Allerdings sind Namen in den Prototypen sehr oft Haase, Grundlagen der Software-Entwicklung 36 für ein Verständnis der Arbeitsweise einer Funktion hilfreich. Eine recht bequeme Ergänzung sind vorbelegte Parameter. Bei der Deklaration einer Funktionen können der letzte, die letzten oder sogar alle Parameter einen Vorbelegungswert erhalten. void EineFunktion( int Parameter1, int Parameter2 = 17, int Parameter3 = 0 ); Bei Aufruf der Funktion, also bei deren Verwendung, dürfen dann ein oder mehrere der letzten vorbelegten Parameter weggelassen werden. EineFunktion( EineFunktion( EineFunktion( EineFunktion( Variable1 ); // Variable1, Variable2 ); // Variable1, Variable2, Variable3 ); // Variable1, Variable3 ); // erlaubt erlaubt erlaubt falsch Wenn, wie oben, der Vorbelegungswert bereits in einem Funktions-Prototypen angegeben wurde, darf diese Vorbelegung bei der Funktionsdefinition nicht wiederholt werden. void EineFunktion( int Parameter1, int Parameter2, int Parameter3 ) { ... } Oftmals wird stattdessen der Vorbelegungswert als Kommentar eingefügt - z.B. Parameter3 /*=0*/. Als eine weitere Besonderheit von Funktionen in C gibt es die Möglichkeit beliebig viele Parameter zu handhaben. Einige Bibliotheksfunktionen nutzen dies. Aber auch eigene Funktionen kann man entsprechend programmieren. 2.5.1. Standardfunktionen Viele regelmäßig verwendete Funktionen sind in Bibliotheken untergebracht und müssen daher nicht neu programmiert werden. Um sie zu benutzen – aufzurufen – bindet man in den Quellcode sogenannte KopfDateien oder Header-Files ein. In diesen sind die Prototypen der Funktionen aufgezeichnet, damit der Compiler erkennen kann, wie sie aufzurufen sind. Um beispielsweise die Sinus-Funktion benutzen zu können, bindet man das Header-File math.h für mathematische Funktionen ein und linked das Programm mit der zugehörigen Bibliothek. #include <math.h> Für eingebundene Standard-Bibliotheken verwendet man üblicherweise die Spitzen Klammern. Ein vollständiger Pfad ist normalerweise nicht nötig, weil der Grundpfad zu allen Bibliotheken in der IDE bzw. im Compiler eingestellt ist. math.h ist eine Textdatei, gehört zum Compiler und enthält unter anderem den Prototyp der SinusFunktion. double sin(double x); Die Sinus-Funktion erfordert also einen Parameter oder Argument vom Typ double und liefert einen double Wert als Resultat zurück. Haase, Grundlagen der Software-Entwicklung 37 Die #include Prozessordirektiven für Standardfunktionen werden üblicherweise in spitzen Klammern angegeben - z.B. <math.h>. Die Standard-Bibliotheken bzw. die zugehörigen Header-Dateien sind bei den verschiedenen Betriebssystemen etwas unterschiedlich. Zur Orientierung hier einige der vielleicht wichtigsten Funktionen in häufig gebrauchten Bibliotheken. conio.h _kbhit Auf Eingabe eines Zeichens testen _getch einzelnes Zeichen lesen _getche einzelnes Zeichen lesen mit Anzeige des Zeichens putchar einzelnes Zeichen ausgeben stdio.h puts Zeichenfolge ausgeben printf formatierte Ausgabe getchar einzelnes Zeichen lesen gets Zeichenfolge lesen scanf formatierte Zeichenfolge lesen Die Funktion printf erlaubt eine sehr flexible Formatierung von Ausgaben, ist mit seinen kryptischen Formatanweisungen aber auch sehr kompliziert. Ähnliche Formatierungen werden in der Funktion scanf für Eingaben verwendet. stdlib.h atoi Zahlzeichenfolge in Wert wandeln itoa Wert in Zahlzeichenfolge wandeln rand, srand NULL Zufallszahlen-Generator NULL-Zeiger math.h M_PI, ... sin, cos, tan, asin, ... log, log10 Konstante π (und andere Konstanten) trigonometrische Funktionen Logarithmen exp Exponentialfunktion pow Potenzen sqrt Quadratwurzeln ceil, floor fabs Runden zu Ganzzahlen Absolutbetrag Für die Behandlung von Zeichenfolgen in C (nicht C++ strings) gibt es eine Reihe von Funktionen in string.h Haase, Grundlagen der Software-Entwicklung strlen Länge einer Zeichenfolge strcpy Zeichenfolge kopieren strchr Zeichen in Zeichenfolge suchen strcat Zeichenfolge an andere Zeichenfolge anhängen strcmp Zeichenfolgen vergleichen 38 Man beachte, dass viele dieser Funktionen keine Längenprüfung beinhalten. Sie gelten daher als unsicher, weil es bei falscher Verwendung zu buffer overruns kommen kann. 2.5.2. Vom Anwender definierte Funktionen Natürlich kann auch der Anwender eigene Funktionen definieren. Im einfachsten Fall geschieht dies in der gleichen C/C++ Quelldatei in der auch die Funktion main() vorliegt. Allerdings kann die Funktion erst genutzt werden, nachdem sie deklariert oder definiert ist. In der ersten Form steht die Funktion einschließlich ihres Bodies (Definition) vor der ersten Verwendung. Dann ist ein Prototyp nicht erforderlich. int Multiply( int x, int y ) { return x * y; } int main() { int a, b = 3, c = 5; a = Multiply( b, c ); } // Definition der Funktion Multiply // erste Verwendung von Multiply ► Beispiel Function Steht die Definition der Funktion hinter der ersten Verwendung, dann muss vor der ersten Verwendung eine Deklaration durch einen Prototypen der Funktion erfolgen. int Multiply( int x, int y ); int main() { int a, b = 3, c = 5; a = Multiply( b, c ); } int Multiply( int x, int y ) { return x * y; } ► Beispiel FunctionForward 2.5.3. Rekursionen // Deklaration von Multiply, beachte Semikolon // erste Verwendung von Multiply // Definition der Funktion Multiply Haase, Grundlagen der Software-Entwicklung 39 Es ist in C/C++ erlaubt, dass eine Funktion sich selbst aufruft. Man nennt dies rekursiv. In manchen Fällen kann man durch rekursives Programmieren sehr elegante Problemlösungen erzielen. Folgende Funktion berechnet die Fakultät einer Zahl. unsigned int Factorial(unsigned int x) { if ( x == 0 ) return 1; return x * Factorial( x – 1 ); } ► Beispiel Factorial Rekursive Programme sind weniger effektiv (schnell) als iterative Varianten. Und alle rekursiven Programme können auch durch Iteration programmiert werden. Manchmal leidet die Überschaubarkeit darunter. 2.5.4. Überladene Funktionen In C++, jedoch nicht in C, gibt es die Möglichkeit Funktionen mit mehreren Varianten für unterschiedliche Datentypen oder Übergabeparametern zu überladen. Diese überladenen Funktionen haben den gleichen Namen. int Quadrat(int x) { return x*x; } double Quadrat(double x) { return x*x; } Hier liegen zwei Funktionen zur Berechnung des Quadrates einer Zahl vor - einmal für den Datentyp int und einmal für double. ► Beispiel FunctionOverloaded Statt unterschiedlichen Datentypen ist ein Überladen auch mit unterschiedlicher Anzahl von Übergabeparametern möglich. int aFunction(int x); int aFunction(int x, int y); Diese beiden Funktionen (hier nur ihre Prototypen) unterscheiden sich in der Anzahl der Parameter. Je nach Verwendung - mit 1 oder mit 2 Parametern - selektiert der Compiler die jeweils richtige Variante. Haase, Grundlagen der Software-Entwicklung 40 2.6. Strukturen Häufig behandelt man in Programmen nicht einzelne Werte sondern Datensätze, die aus mehreren, miteinander in Zusammenhang stehenden Werten bestehen. Einträge etwa in einem Kontakt bestehend aus Name, Anschrift, Rufnummern etc. wäre ein verbreitetes Beispiel. Um derartige Daten softwaretechnisch zusammenfassen zu können, gibt es in C/C++ Strukturen. Anders als Felder, die immer eine Anzahl Daten vom gleichen Typ enthalten. besteht eine Struktur aus mehreren Daten, die auch verschiedene Datentypen enthalten. Jedem Element einer Struktur ist ein Elementname zugeordnet, über den der Zugang abgewickelt wird. Strukturen werden wie ganz normale Datentypen verwendet. 2.6.1. Deklaration Eine Struktur wird definiert durch das Schlüsselwort struct gefolgt von einem Namen für den Datentyp, den diese Struktur bildet und einer Zusammenstellung der enthaltenen Daten zwischen geschweiften Klammern. Abgeschlossen wird die Strukturdefinition durch ein Semikolon. Die enthaltenen Daten werden wie normale Variablen deklariert. struct Kontakt { char Vorname[50]; char Nachname[50]; short Geburtsjahr; }; struct Position { double x, y, z; }; ► Beispiel Struct 2.6.2. Initialisierung Strukturen werden elementweise in geschweiften Klammern und durch Komma getrennt wie einzelne Variablen initialisiert. Kontakt John = { "John", "Deere", 1804 }; Position pos = { 1, 2, 3 }; // C++ struct Kontakt John = { "John", "Deere", 1804 }; struct Position pos = { 1, 2, 3 }; // C Das Schlüsselwort struct ist bei der Definition in C++ nicht erforderlich, wohl aber in C. 2.6.3. Verwendung Haase, Grundlagen der Software-Entwicklung Für den Zugriff auf einzelne Elemente einer Struktur wird dem Namen der Strukturvariablen ein Punkt und der Elementname angehangen. cout << John.Vorname; pos.x = 2.5; Eine Struktur darf natürlich auch Felder oder andere Strukturen als Elemente enthalten und auch Felder von Strukturen sind möglich. Auf diese Weise sind sehr komplex verschachtelte Variablen definierbar. 41 Haase, Grundlagen der Software-Entwicklung 42 2.7. Zeiger Zeiger sind eine der leistungsfähigsten, aber auch gefährlichsten Werkzeuge in C. Durch den falschen Einsatz von Zeigern können sehr schwer zu findende Fehler in einem Programm entstehen. Zeiger sind einer der Gründe für den schlechten Ruf von C. 2.7.1. Zeiger und Adressen Jede Variable eines Programms liegt auf einem wohldefinierten Platz im Speicher des Rechners. Da die Speicherplätze von Null beginnen durchnummeriert sind, kann man für jede Variable den Speicherplatz (bzw. den ersten von mehreren) in Form eines Zahlenwertes angeben. Man spricht hier einer Adresse ähnlich den Hausnummern einer Straße. Da die Adresse einer Variablen nichts anderes als eine Ganzzahl ist, kann man auch eine Variable haben, die eben diese Adresse enthält. Als Ganzzahl kann man allerdings nicht viel damit anfangen. Erst wenn man der Variablen mit der Adresse auch noch den Datentyp der eigentlichen Variablen mitgibt, entsteht ein nutzbringendes Gebilde. Die Variable mit der Adresse einer anderen Variablen nennt man einen Zeiger (pointer). Man sagt, ein Zeiger zeigt (mit seinem Wert) auf eine Variable. Zur Deklaration eines Zeigers wird dem Namen des Zeigers ein Multiplikationszeichen (*) vorangestellt. double Value; double *pValue; // eine Variable mit dem Namen Value vom Datentyp double // ein Zeiger mit dem Namen pValue auf eine Variable vom Typ double Dem Namen eines Zeigers den Buchstaben p voranzustellen, ist eine gängige Praxis, aber nicht notwendig. Man beachte, dass die Variable (der Zeiger) pValue im Beispiel oben noch nicht initialisiert ist. Insbesondere zeigt die nicht auf die Variable Value sondern an eine noch völlig beliebige Stelle im Speicher. Um einem Zeiger einen Wert zu geben muss man einen Adress-Operator verwenden. Hierzu dient das Zeichen & vor dem Namen einer Variablen. pValue = & Value; // Zeiger pValue bekommt die Adresse der Variablen Value Analog kann der Zeiger auch initialisiert werden. double *pValue = &Value; // Zeiger pValue wird mit Adresse von Value initialisiert Man sieht dies oftmals auch in der Form double* pValue = &Value; Vor und hinter dem *- und dem &-Zeichen dürfen Leerzeichen oder Tabulatoren stehen. In seltenen Fällen ist ein Zeiger notwendig, der auf Variablen von beliebigem Datentyp zeigen kann. void *pVoid; // Zeiger mit dem Namen pVoid kann auf alles zeigen Um erkennbar zu machen, dass einem Zeiger noch kein Wert zugewiesen wurde, kann man ihn mit 0 bzw. NULL initialisieren oder mit einer entsprechenden Wertzuweisung “ungültig“ machen. Auf der Adresse 0 Haase, Grundlagen der Software-Entwicklung 43 kann normalerweise bei keine Variable liegen. Die meisten Prozessoren verbieten sogar den Zugriff auf diese Adresse. double *pValue; pValue = NULL; // noch nicht initialisiert // initialisiert aber zeigt jetzt nicht auf eine Variable 2.7.2. Verwendung von Zeigern Ein Zeiger kann dazu verwendet werden, um auf den Wert der Variablen zuzugreifen, auf die der Zeiger zeigt. Hierzu wird ebenfalls das Multiplikationszeichen (*) verwendet. double X; X = *pValue; // definiert eine Variable vom Datentyp double // X erhält den Wert der Variablen auf die der Zeiger pValue zeigt Man spricht *pValue als “Inhalt von pValue“. Mit dem “Inhalt von“-Operator (*) kann man also auf den Wert einer Variablen an einem anderen Ort zugreifen. Da zu der Definition eines Zeigers der Datentyp gehört, kann der Compiler überprüfen, ob die Datentypen zusammenpassen. Ein Zeiger auf eine Variable vom Typ double kann nur auf eine Variable vom Typ double zeigen. ► Beispiel Pointer In einer Bedingung kann auf NULL oder auf einen bestimmten Wert abgefragt werden. if ( pValue == NULL ) ... if ( pValue == &Value ) ... // Abfrage auf ungültigen Wert // Abfrage ob pValue auf die Variable Value zeigt Ein Zeiger kann auch auf eine Struktur zeigen. struct Position { double x, y, z; }; Position pos = { 1, 2, 3 }; Position *pPos = &pos; Hier ist pPos ein Zeiger auf eine Position Struktur und mit der Adresse der Variablen pos initialisiert. Man kann ein Element der Position auch über den Zeiger pPos erreichen. Aufgrund der Vorrangregeln werden hier Klammern erforderlich, die unbequem und schwer lesbar sind. (*pPos).x = 1.5; Als einfachere Schreibweise bedient man sich der beiden Zeichen -> zur Selektion eines Elementes. pPos->x = 1.5; Zeiger dürfen auch als Elemente von Strukturen verwendet werden. Eine Besonderheit stellen hierbei rekursive Strukturen dar, also Strukturen, die ein Element enthalten, das ein Zeiger auf eine solche Struktur ist. Haase, Grundlagen der Software-Entwicklung struct PositionsListe { PositionsListe *pNachfolger; Position Pos; }; 44 // in C: struct PositionsListe *pNachfolger; Hier ist die Struktur Positionsliste als ein Element einer verzeigerten Liste gedacht. Jedes Element der Liste enthält einen Zeiger auf seinen Nachfolger. Das letzte Element einer solchen Liste enthält dann oftmals ein Zeiger pNachfolger mit dem Wert NULL als Endekennzeichen. Eine ähnliche Struktur mit 2 (oder mehr) Zeigern wird für den Aufbau baumartiger Strukturen verwendet. Bäume sind oftmals besser zur Datenspeicherung geeignet als Felder. 2.7.3. Übergabe von Parameter an eine Funktion Für die Übergabe von Argumenten an Funktionen kennt C zwei prinzipielle Methoden – die Parameterübergabe als Wert (pass by value) und die Parameterübergabe durch einen Zeiger (pass by address). Bei C++ kommt eine dritte Methode der Parameterübergabe durch eine Referenz hinzu (pass by reference). ► Beispiel ParameterPassage Zeiger und Referenzen werden häufig benutzt, um einer Funktion die Rückgabe von mehreren oder komplexen Daten zu ermöglichen. Parameterübergabe als Wert Wird ein Parameter als Wert übergeben, so wird der formale Parameter durch den aktuellen Wert ersetzt. int aFunction(int aValue) { return ++aValue; } int aResult, aNumber = 3; aResult = aFunction( aNumber ); ► Beispiel PassByValue In der Funktion aFunction ist aValue eine Variable, die unabhängig von der Variablen aNumber ist. aValue ist eine Kopie von aNumber. Die Funktion verändert (inkrementiert) die Variable aValue, doch hat das keinen Einfluss auf die Variable aNumber. Parameterübergabe durch Zeiger In dieser Variante wird nicht der Wert einer Variablen, sondern ihre Adresse übergeben. int aFunction(int *aValue) { return ++*aValue; } int aResult, aNumber = 3; Haase, Grundlagen der Software-Entwicklung 45 aResult = aFunction( &aNumber ); ► Beispiel PassByAddress Da hier aValue ein Zeiger auf eine (fremde) Variable ist, kann die Funktion den Wert bzw. Inhalt der Variablen verändern. aValue ist keine Kopie. Im Beispiel hier wird der Zeiger auf die Variable aNumber übergeben. Die Funktion verändert (inkrementiert) diese Variable aber auch. Parameterübergabe durch Referenz (nur C++) Referenzen gibt es nur in C++. Zur Deklaration einer Referenz wird das Zeichen & verwendet. Rein äußerlich werden referenzierte Variablen so wie normale (lokale, kopierte) Variablen benutzt. Sie sind jedoch ebenfalls Zeiger. int aFunction(int &aValue) { return ++aValue; } int aResult, aNumber = 3; aResult = aFunction( aNumber ); ► Beispiel PassByReference Obwohl hier aValue ein Zeiger ist, muss kein "Inhalt von"-Operator * verwendet werden. Die Funktion hat vollen Zugang zu der originalen Variablen aNumber und verändert (inkrementiert) diese auch. Weil Funktionen in C/C++ nur einen einzelnen Wert zurückliefern können, werden Zeiger (oder Referenzen) häufig dazu genutzt, weitere Werte für eine Funktion veränderbar zu machen. 2.7.4. Zeiger und Felder Eine wichtige Anwendung finden Zeiger bei der Bearbeitung von Feldern. Beispielsweise wird an eine Bibliotheks-Funktion strlen zur Berechnung der Länge einer Zeichenfolge ein Zeiger auf den Anfang der Zeichenfolge übergeben. Die Funktion könnte beispielsweise folgendermaßen implementiert sein. unsigned int strlen(char *s) // Länge einer Zeichenfolge { unsigned int n = 0; // Länge zunächst 0 while ( *s++ ) // Inhalt von s auf 0 prüfen, danach s Zeiger weitersetzen ++n; // wenn nicht 0 dann Länge erhöhen return n; // Länge zurückgeben } Hier wird in der Bedingung der while-Schleife einerseits der Wert des Zeichens geprüft (*s), auf den der Zeiger zeigt und andererseits der Zeiger um 1 erhöht (s++), damit er bei der nächsten Überprüfung auf das nächste Zeichen zeigt. Die Verwendung eines Zeigers auf eine Zeichenfolge stellt hier eine besonders knappe Methode der Parameterübergabe dar, spart sie doch eine Kopie der Zeichenfolge zu erstellen. Im Beispiel strlen wird der Zeiger s mit s++ jeweils um 1 Zeichen, also um 1 Byte, weitergeschoben. Der Speicher wird bei fast allen Rechnern byteweise durchnummeriert, unabhängig von der verarbeiteten Haase, Grundlagen der Software-Entwicklung 46 Wortbreite (32-Bit-, 64-Bit-CPU). C/C++ besitzt eine sehr bequeme Adressarithmetik für Zeiger. Einen Zeiger um 1 zu erhöhen/verringern setzt den Zeiger nicht um 1 Byte weiter/zurück, sondern um die Größe eines Element-Datentyps. Dies ist möglich, weil die Zeiger jeweils auf einen festgelegten Datentyp zeigen, die Zeiger sind typisiert. Für den C-Compiler ist erkennbar, wie groß ein Element ist, auf das der Zeiger zeigt. struct Position { double x, y, z; }; // eine Raumposition Position Position // eine Feld mit 100 Raumpositionen // ein Zeiger auf eine Raumposition PositionsFeld[100]; *PositionsZeiger; PositionsZeiger = PositionsFeld; PositionsZeiger = &PositionsFeld[0]; ++PositionsZeiger; PositionsZeiger++; PositionsZeiger = PositionsZeiger + 1; PositionsZeiger += 1; PositionsZeiger += 2; --PositionsZeiger; // // // // // // // // Zeiger Zeiger Zeiger Zeiger Zeiger Zeiger Zeiger Zeiger zeigt zeigt zeigt zeigt zeigt zeigt zeigt zeigt auf Element [0] auf Element [0] jetzt auf Element [1] jetzt auf Element [2] nunmehr auf Element [3] auf Element [4] danach auf Element [6] jetzt auf Element [5] In diesem Beispiel ist Position eine Struktur mit 3 double Werten - x, y und z. Mit einer typischen Größe von 8 Bytes pro double ist eine Variable vom Typ Position 24 Bytes groß. PositionsFeld ist ein Array von 100 Positionen, also 100 * 24 = 2400 Bytes. PositionsZeiger ist ein Zeiger auf eine beliebige Position. In den letzten Zeilen des Beispiels wird der PositionsZeiger zunächst auf zwei verschiedene Weise auf das Anfangselement des PositionsFeldes eingestellt. Anschließend wird der Zeiger mit unterschiedlichen Befehlen auf die nachfolgenden Elemente des Feldes verschoben. Die nötige Adressberechnung muss also nicht der Anwender machen, sondern wird vom Compiler erledigt. 2.7.5 Dynamische allozierter Speicherplatz Oftmals wird in einem Programm Speicherplatz in vorab unbekannter Menge benötigt. Wird vom Programm zum Beispiel eine Textdatei eingelesen, so ist zum Zeitpunkt der Programmerstellung nicht bekannt, wie groß diese Datei sein wird. Für solche Aufgaben gibt es dynamisch allozierten Speicherplatz. Die klassischen Funktionen Speicherplatz vom Betriebssystem anzufordern und an es zurückzugeben sind malloc, calloc und free. malloc und calloc fordern beide Speicherplatz an, calloc vorbelegt/löscht den erhaltenen Speicherplatz zusätzlich mit 0. Aus Gründen der Datensicherheit wird oftmals auch der von malloc gelieferte Speicher mit 0 oder mit Datenschrott initialisiert. Die Funktion free wird allozierter Speicherplatz an das Betriebssystem zurückgegeben. Speicher, der mit malloc oder calloc angefordert wurde, sollte an geeigneter Stelle im Programm auch wieder mit free zurückgegeben werden. Da die Funktionen malloc und calloc nur eine Mengeninformation in Bytes aber keine Information über die spätere Verwendung des Speichers haben, liefern sie den Speicher mit einem Zeiger auf void zurück, einen void*. Es muss daher immer in den erforderlichen Datentyp gecastet werden. double *pFeldDouble = (double*)malloc(20*sizeof(double)); // Feld für 20 double Haase, Grundlagen der Software-Entwicklung 47 char *pFeldChar = (char*)calloc(100, sizeof(char)); . . . free(pFeldDouble); free(pFeldChar); // Feld für 100 char // Felder benutzen // Speicher zurückgeben malloc besitzt nur einen Übergabeparameter, die Größe des angeforderten Speichers in Bytes, wohingegen calloc zwei Übergabeparameter getrennt für die Anzahl der Objekte und die Einzelgröße erwartet. Falls die Speicheranforderung nicht erfüllt werden kann, liefern beide Funktionen den Wert NULL zurück. Gelegentlich setzt man einen Zeiger auf zurückgegebenen Speicher auf NULL, um anzudeuten, dass der Speicher nicht mehr zugänglich ist. free(pFeldChar); pFeldChar = NULL; // Speicher zurückgeben // optional Zeiger auf NULL setzen Außerdem liefern viele Betriebssysteme bei einem Zugriff über einen NULL-Zeiger eine Unterbrechung, um auf den Fehler hinzuweisen. ► Beispiel MallocFree ► Beispiel TypeCast In C++ werden Objekte, zu denen dann auch die Grunddatentypen zählen, durch new alloziert und mit delete zurückgegeben. double *pFeld = new double[100]; . . . delete [] pFeld; // Feld für 100 double // Feld verwenden // Speicher zurückgeben Wenn lediglich Einzelelemente alloziert werden, entfallen die eckigen Klammern. double *pEinzelElement = new double; . . . delete pEinzelElement; // nur 1 double // pEinzelElement verwenden // Speicher zurückgeben ► Beispiel NewDelete Ein beliebter Fehler besteht darin, Speicherplatz zu allozieren, aber nicht wieder zurückzugeben. Im Laufe der Zeit kann dabei sehr viel Speicherplatz “verbraucht“ werden, bis kein weiterer mehr vorhanden ist. Ein anderer häufiger Fehler ist es, Speicherplatz zurückzugeben, aber weiterhin darauf zuzugreifen (dangling pointer). Zurückgegebener Speicher kann für andere Aufgaben genutzt werden und es entstehen dabei zwei konkurrierende Verwendungen. 2.7.6 Speicherplatzverwaltung In aller Regel überlässt man die Zuordnung von Speicherplatz für die Teile eines Programms dem Betriebssystem (z.B. Windows, Linux, etc). Allgemein üblich sind (mindestens) 3 Bereiche für den Programm-Code, den Stack und den Heap. Haase, Grundlagen der Software-Entwicklung Im Bereich für den Programm-Code wird der ausführbare Anteil untergebracht, also ohne die Daten. Die lokalen Daten und die Rückkehradressen von Unterprogrammaufrufen werden im Stack hinterlegt. Abhängig von der Unterprogrammstruktur eines Programms "atmet" der Stack - üblicherweise nach unten. In C wird dieser Bereich auch automatic genannt. Der restliche, dritte Bereich ist der freie Speicher oder auch Heap. Dynamisch mit malloc oder new erzeugte Daten oder Objekte werden hier hinterlegt und evtl. auch wieder freigegeben. Hierdurch kann der Heap in viele kleine Abschnitte geteilt werden und so zu Speicherplatzproblemen führen (Fragmentierung). Daneben gibt es meist weitere Speicherplatzbereiche etwa Speicher für initialisierte Daten, für automatisch auf Null gesetzte Daten und anderes. 48 Haase, Grundlagen der Software-Entwicklung 49 2.8. Typedef Eine weitere Methode neue Datentypen zu erzeugen besteht aus typedef-Anweisungen. Mittels typedef wird ein neuer Name (Alias) für einen (bereits bestehenden) Datentyp festgelegt. typedef <Datentyp> <AliasName> ; Beispiele typedef typedef typedef typedef typedef int char short char double KlausurNote; *PtrName; SmallNumber; VerySmallNumber; *PtrToDouble; // // // // // neuer Typ KlausurNote alias int neuer Typ Zeiger auf einen String/Namen 16-Bit Zahlenwert 8-Bit Zahlenwert Zeiger auf ein double Die Standardfunktion time() (vgl. time.h) liefert eine Zeitangabe in Form einer Ganzzahl aus vergangenen Sekunden seit 00:00 am 1. Januar 1970 UTC (UNIX birthday :-) Für diese Zeitangabe wurde ein neuer Name time_t definiert. typedef long time_t; Nota: Neuere Systeme definieren einen 32 Bit und einen 64 Bit Typ für time_t, da der 32 Bit Typ nur bis ins Jahr 2038 reicht und dann überläuft - vgl. mit dem Jahr 2000 Problem (Y2K) bei Microsoft Windows. Durch die Verwendung von solchen Alias-Datentypen kann etwas mehr Klarheit in die vorhandenen Variablen gebracht werden. #include <time.h> time_t t_now; time(&t_now); // Alias-Datentypen und Funktionen // Speicherplatz für einen Zeitpunkt // aktuellen Zeitpunkt ermitteln ► Beispiel Cyclic Auch kompliziertere Datentypen etwa Felder, Strukturen oder Funktionen können so definiert werden. So kann eine 3x3-Matrix als neuer Datentyp definiert werden durch typedef double Matrix_3x3[3][3]; ► Beispiel Typedef Eine Funktion, die den Namen eines Studenten als Parameter erhält und die Klausurnote zurückliefert, hätte z.B. folgende typedef-Definition typedef KlausurNote (*KlausurResultat)(PtrName Student); Haase, Grundlagen der Software-Entwicklung 50 2.9. Ein-/Ausgabe Nahezu jedes Programm erfordert die Eingabe und Ausgabe von Daten. Der klassische Ein-/Ausgabekanal war die Konsole, ein Gerät ähnlich einer Schreibmaschine. Heute dient ein Fenster für Textzeilen als Ersatz. Doch auch diese Standard-Ein-Ausgabe wird zunehmend von grafischen Bedienoberflächen verdrängt. Wenn die Daten über eine längere Zeit gespeichert werden müssen, verwendet man Dateien als Eingabe und/oder Ausgabe. Diese Dateien müssen nicht unbedingt auf dem gleichen Rechner vorliegen, sondern können auch in entfernten Rechner zugänglich sein. 2.9.1. Standard-Ein-Ausgabe Die sogenannte Standard-Ein-Ausgabe benutzt eine zeilenweise Aufzeichnung von Eingaben durch den Anwender und Ausgaben/Antworten durch einen Computer. In der zu C/C++ gehörenden Laufzeitumgebung gehören auch einige Funktionen, die die Ein- und Ausgabe über die Konsole ermöglichen. Im einfachsten Fall werden Zeichen oder Zeichenfolgen auf die Konsole geschrieben oder auf der Tastatur eingegeben, auf der Konsole lesbar gemacht und an das Programm geliefert. Für die anderen Datentypen gibt es Funktionen, mit denen die Werte von Variablen gezielt verarbeitet werden können. Die erforderlichen Prototypen gibt es in stdio.h. Einige dieser Funktionen können bei falscher Verwendung sehr leicht zu katastrophalen Fehler oder gar Rechnerabstürzen führen. In C++ hat man daher versucht eine zuverlässigere und besser prüfbare Umgebung zu schaffen, die iostream Bibliothek. ► Beispiel NumberInOut ► Beispiel StringInOut Die Möglichkeit Ausgaben gezielt zu formatieren (positionieren) und Eingaben zielgerichtet zu untersuchen und zu übernehmen gibt es sowohl mit den klassischen Funktionen als auch mit den stream Funktionen von C++. ?? stdin, stdout, stderr 2.9.2. Formatierte Ausgabe Die bekannteste formatierte Ausgabe von C erfolgt durch die printf-Funktion. Diese Funktion erwartet als ersten Parameter eine Zeichenfolge, welche die gewünschte Formatierung aller weiteren übergebenen Parameter beschreibt. Der Rückgabewert ist die Anzahl ausgegebener Zeichen. Die Funktion kann mit beliebig vielen Parametern aufgerufen werden. Hierin liegt auch zugleich die Gefahr. Die übergebenen Parameter passen nicht mit den Formatierungsanweisungen im ersten Parameter zusammen oder es sind zu wenige oder zu viele. Hier einige einfache Beispiele int aInt = 42; double aDouble = 42.0; Haase, Grundlagen der Software-Entwicklung 51 printf( “%6d\n“, aInt ); printf( “%8.3lf\n“, aDouble ); printf( “%6d, %8.3lf\n“, aInt, aDouble ); Die Anweisungen zur Formatierung im ersten Parameter sind typischerweise sehr kryptisch, aber auch äußerst flexibel. Die Formatierungsangaben beginnen alle mit dem Prozentzeichen gefolgt von ein oder mehr weiteren Zeichen. Die übrigen, nicht zu einer Formatangabe gehörenden Zeichen werden ausgegeben wie im ersten Parameter geschrieben. Ausgenommen sind nur ein paar sogenannte EscapeSequenzen, die spezielle Bedeutungen haben – z.B. \n im Beispiel für den Übergang zum Anfang der nächsten Zeile. Eine Liste der Formatanweisungen findet man beispielsweise bei http://www.cplusplus.com/reference/cstdio/printf/ oder (besser) in den Dokumentationen der jeweiligen Entwicklungsumgebungen und Compilern. ?? eigene Tabelle hier 2.9.3. Formatierte Eingabe Eine viel benutzte Eingabe auf der Tastatur mit Anzeige in der Konsole realisiert die scanf-Funktion. Für die Eingabe werden die gleichen Formatierungsanweisungen wie für die Ausgabe verwendet. Statt der Werte sind jedoch hier Zeiger auf passende Variablen als Parameter anzufügen. int aInt = 42; scanf( “%d“, &aInt ); Eine Liste der Formatanweisungen findet man beispielsweise bei http://www.cplusplus.com/reference/cstdio/scanf/ oder (besser) in den Dokumentationen der jeweiligen Entwicklungsumgebungen und Compilern. ?? eigene Tabelle hier 2.9.4. Dateizugriff Informationen oder Resultate werden häufig in Dateien gespeichert. Dateien wiederum sind in C über Funktionen zum Zugriff auf Dateisysteme zugänglich. In PCs werden als Dateisysteme meist Festplatten verwendet. In jüngerer Zeit sind zunehmend auch Flash-Speicher in Gebrauch. In Embedded Controllern kommen aber auch Dateisysteme auf anderen Medien vor, etwa als ROM-Speicher. Dateien haben zumeist einen Namen und werden zumeist hierarchisch in Verzeichnissen angelegt. Vor Verwendung einer Datei muss diese geöffnet und am Ende der Verwendung muss sie geschlossen werden. Dazwischen kann eine Datei gelesen und/oder geschrieben werden. In der Mehrzahl der Fälle ist auch ein Positionieren an eine lineare Stelle möglich. Die Zugriffsfunktionen benutzen entweder einen eindeutigen Integer-Index (file descriptor) zur Kennzeichnung einer zur Bearbeitung geöffneten Datei (low-level IO) oder einen Zeiger auf eine FILEStruktur (stream IO). Abhängig vom jeweiligen Betriebssystem können weitere Varianten hinzukommen bei Microsoft z.B. sogenannte handle. Low-level IO (file descriptor) Haase, Grundlagen der Software-Entwicklung _open Datei öffnen _create Datei erzeugen _close Datei schließen _write in Datei schreiben _read aus Datei lesen _tell aktuelle Schreib-/Lese-Position ermitteln _lseek aktuelle Schreib-/Lese-Position einstellen fopen Datei öffnen oder erzeugen fclose Datei schließen fwrite in Datei schreiben fread aus Datei lesen ftell aktuelle Schreib-/Lese-Position ermitteln fseek aktuelle Schreib-/Lese-Position einstellen Stream IO (FILE*) Für Stream IO gibt es eine Reihe von zusätzlichen Funktionen für die Verarbeitung von Zeichenfolgen/Text und für die Standard-Ein-Ausgabe. In C++ stehen für die Ein-/Ausgabe zur Konsole weitere Funktionen in der iostream Bibliothek zur Verfügung (cin, cout) und in der iomanip Bibliothek gibt es Funtionen etwa zur Einstellung der Ausgabegenauigkeit. 52 Haase, Grundlagen der Software-Entwicklung 53 2.10. Gültigkeitsbereiche von Variablen Variablen in einem Programm haben abhängig von der Lage ihrer Definition unterschiedliche Bereiche, in denen auf sie zugegriffen werden kann. int A = 100; void aFunction() { int X = 42; . . . { int X = 5; int A = 1; . . . } . . . } int main() { int X = 3; . . . aFunction(); . . . } // global // lokal in aFunction() // lokal im Block, verdeckt X in aFunction // lokal im Block, verdeckt globales A // lokal in main() In diesem Beispiel ist die Variable A überall zugänglich, wird aber in einem Block in der Funktion aFunction von einem lokalen A verdeckt. Sowohl aFunction als auch main besitzen eine lokale Variable X, die jeweils getrennt voneinander nur innerhalb dieser Funktionen zugänglich sind. Der Block in aFunction besitzt auch ein eigenes, lokales X, welches das X in aFunction innerhalb des Blocks verdeckt. Globale Variablen sollten sparsam verwendet werden. Ihr Vorteil, von überall aus zugänglich zu sein, ist zugleich auch ihr Nachteil. Nur mit einem Überblick über das gesamte Programm (evtl. abertausende Zeilen verteilt über viele Dateien) wird deutlich, wo und wie diese eingesetzt werden und welchen Zweck sie haben. Bei lokalen Variablen ist der Gültigkeitsbereich meist schnell überschaubar. Fehlerhafte Verwendung kann daher leichter erkannt bzw. ausgeschlossen werden. Auch innerhalb eines Blocks können neuerlich Variablen definiert werden, die gegebenenfalls andere lokale oder globale Variablen verdecken. Ganz typisch sind hierfür die Zählvariablen i von for-Schleifen in C++, die eventuell sogar innerhalb einer Funktion mehrfach definiert werden. Einige Compiler liefern Warnungen, wenn eine Variablendefinition eine andere Variable gleichen Namens verdeckt. ► Beispiel Scope 2.10.1. Lokale Daten einer Funktion Lokale Daten einer Funktion existieren nur während des Aufrufs der Funktion. Beim Verlassen der Funktion werden auch diese Daten zerstört. Speichern ist so nicht möglich. Haase, Grundlagen der Software-Entwicklung void aFunction() { int X = 42; static int Y = 22; . . . } 54 // lokal, jedesmal neu mit 42 initialisiert // im globalen Speicher, nur hier zugänglich // X wird zerstört, Y bleibt erhalten Mit dem Schlüsselwort static wird der Compiler angewiesen, eine Variable im globalen Speicher anzulegen. Die Variable überdauert daher die einzelne Ausführung der Funktion und kann Daten speichern. Sie behält also ihren Wert zwischen den Aufrufen. Allerdings ist die Variable nur innerhalb der Funktion zugänglich, in der sie deklariert ist, ► Beispiel Static 2.10.2. Lokale Daten und Funktionen in einer Datei Da Programme aus mehreren Dateien bestehen können, kann es vorkommen, das man den Zugang zu einzelnen globalen Variablen und Funktionen auf eine einzige Quellcode-Datei beschränken möchte. Auch hierzu dient das Schlüsselwort static. static int X; // nur in dieser Quelldatei zugänglich, mit 0 initialisiert static void aFunction() { . . . } // Funktion nur in dieser Quelldatei zugänglich 2.10.3. Weitere Speicherklassen Es gibt einige weitere Speicherklassen in C. Diese sind teils historisch bedingt oder für spezielle Anwendungsbereiche vorgesehen. Auch wird diese Methodik in manchen Entwicklungsumgebungen für spezielle Aufgaben eingesetzt. register Mit dem Schlüsselwort register vor dem Datentyp einer (lokalen) Variablendefinition wird dem Compiler mitgeteilt, dass diese Variable sehr oft gebraucht wird und daher möglichst in einem (schnellen) Register der CPU aufzuheben ist. Moderne Compiler brauchen solche Hinweise des Programmierers nicht mehr. Meist erzeugen sie sehr gut optimierten Code. volatile Mit dem Schlüsselwort volatile vor dem Datentyp einer Variablendefinition wird dem Compiler mitgeteilt, dass diese Variable sich von außen her gesteuert verändern kann und daher nicht zum schnelleren Zugriff in einem Register der CPU zwischengelagert werden darf. const Mit dem Schlüsselwort const in einer Variablendefinition wird dem Compiler mitgeteilt, dass diese Variable nicht verändert werden darf. Haase, Grundlagen der Software-Entwicklung const double PI = 3.14159; // unveränderlicher Wert char * const s1 = "s1"; // unveränderlicher Zeiger // auf veränderliche Zeichenfolge char const *s2 = "s2"; // veränderlicher Zeiger auf // unveränderliche Zeichenfolge char const * const s3 = "s3"; // unveränderlicher Zeiger auf // unveränderliche Zeichenfolge 55 Weil das Schlüsselwort const rechtsseitig bindet sind die Zusammenhänge oftmals schwer überschaubar (irgendwie nicht logisch, zumindest schlecht designed). ► Beispiel Const In einigen Entwicklungsumgebungen bzw. Bibliotheken wird diese Schwäche durch passende Typ- oder Klassendefinitionen umgangen. Für Embedded Controller wurden die Compiler oftmals um weitere Schlüsselwörter erweitert, um spezielle Speicherbereiche zugänglich zu machen. Der Speicherbereich für physikalische Ein- und Ausgänge oder ein eventuell vorhandener ROM-Speicherbereich (nur lesbar) sind typische Beispiele. Bei den Arduino Boards wird beispielsweise PROGMEM und F(“..“) bei Zeichenketten verwendet, um die betreffenden Variablen oder Strings ins Flash-Memory zu legen. Haase, Grundlagen der Software-Entwicklung 56 2.11. Modulare Programmierung Ab einer gewissen Größe eines Programms (einige hundert Zeilen und mehr) wird der Quellcode in mehrere Dateien aufgeteilt. Meist nennt man die Zusammenfassung dann ein Projekt.. Ein Programm bzw. Projekt wird dadurch übersichtlicher und die Entwicklung wird einfacher. Außerdem ist es dann leichter möglich, dass mehrere Programmierer an einem Projekt arbeiten. Ein Projekt in C++ besteht u.a. aus mehreren Quellcode-Dateien und dazu passenden Header-Dateien. Typischerweise gibt es die Dateien pärchenweise mit gleichem Namen und den Endungen .h für die Header-Dateien und .cpp für die Quellcode-Dateien. In den Header-Dateien befinden sich idealerweise nur die Deklarationen von Variablen und Funktionen. Die Definitionen werden in den Quellcode-Dateien vorgenommen. Header-Datei xyz.h extern double Messwerte[100]; double MesswerteMittelwert(); // Deklaration // Prototyp Quellcode-Datei xyz.cpp #include "xyz.h" double Messwerte[100]; double MesswerteMittelwert() { . . . } // Definitionen In beliebiger anderer Quellcode-Datei abc.cpp #include "xyz.h" // nutzt Variablen oder Funktionen aus xyz.cpp Die in Header-Dateien untergebrachten Deklarationen und Prototypen werden bei jeder Anwendung in die Quellcode-Dateien inkludiert. Dies erlaubt eine zuverlässige Bildung von Bibliotheken und modulare Programmierung. Manchmal verwendet man auch einzelne Header-Dateien für eine Vielzahl von Quellcode-Dateien. Bei den Standard-Bibliotheken - etwas strdio.h - ist dies typisch. In seltenen Fällen wird auch die Implementierung statt in einer eigenen cpp-Quellcode-Datei unmittelbar in der Header-Datei vorgenommen (gilt jedoch als schlechter Programmierstil). Bei der Deklaration von Prototypen dürfen die Namen der formalen Parameter weggelassen werden. int Multiply(int, int); Hier kommt es, soweit es den Compiler betrifft, nur auf die Datentypen der Parameter an. Erst in der Definition der Funktion sind die Namen erforderlich. Allerdings sind Namen in den Prototypen sehr oft für ein Verständnis der Arbeitsweise einer Funktion hilfreich. ► Beispiel HeaderFileUsage ► Beispiel InOutFunction Die #include Direktive wird sowohl mit spitzen Klammern als auch mit doppelten Hochkomma verwendet. Mit den spitzen Klammern sucht der Compiler die angegebene Datei bzw. den angegebenen relativen Pfad zu der Datei ausschließlich in den ihm bekannten, vordefinierten Verzeichnissen. Diese sind typischerweise bereits durch die Installation des Compilers bzw. der IDE festgelegt. Mit doppelten Haase, Grundlagen der Software-Entwicklung Hochkomma sucht der Compiler zunächst im aktuellen Arbeitsverzeichnis und erst danach in den Standardverzeichnissen. Das bedeutet insbesondere, dass lokale Header-Dateien immer in doppelten Hochkomma angegeben werden müssen. 57 Haase, Grundlagen der Software-Entwicklung 58 2.12. Multitasking und Echtzeit Von Computern erwartet man heute die gleichzeitige Bearbeitung mehrerer Aufgaben zur gleichen Zeit Multitasking genannt. Bei PCs ist es längst üblich mehrere Programme geöffnet auf dem Bildschirm zu haben und im Wechsel zu benutzen, während andere Programme im Hintergrund nach Emails schauen, Downloads durchführen, auf Updates prüfen, nach Viren und Trojanern fahnden, Backups erstellen,... Selbst Telefone sind, zu Smartphones mutiert, inzwischen vollwertige Rechner und mit einem Multitasking ausgestattet. Vor Jahren war dies Prozessrechnern und SPSen in industriellen Automationen vorbehalten. Inzwischen sind Steuergeräte in Benzinmotoren zu Motor-Management-System ausgewachsen, die nicht nur zum richtigen Zeitpunkt Kraftstoff einspritzen und den Zündfunken liefern sondern viele weitere Aufgaben erfüllen. In sehr einfachen Fällen reichen programmierte Verzögerungen - delay- oder sleep-Funktionen - zwischen einzelnen Aufgaben aus. Die normale Betriebssoftware eines ruft unablässig die Funktion loop auf. In dieser kann beispielsweise eine LED geschaltet werden um ein Blinken zu erzeugen. void loop() { digitalWrite(pinLED, HIGH); delay(1000); digitalWrite(pinLED, LOW); delay(1000); } ► Beispiel Blink Im Einzelfall kann eine Aufgabe auch maximal schnell, also ohne festgelegtes Timing bearbeitet werden. ► Beispiel Button Wenn jedoch zeitgleich weitere Aufgaben anstehen, ist aktives Warten in einer Funktion delay() nicht möglich. In einem Multitasking-Betrieb werden die einzelnen Aufgaben in jeweils kurzen Zeitabschnitten ganz oder teilweise bearbeitet - time slicing. Makroskopisch entsteht dadurch der Eindruck einer Gleichzeitigkeit. Bei komplexeren Aufgaben muss sichergestellt werden, dass die Bearbeitung einer Aufgabe auch dann gewährleistet ist, wenn gerade eine andere, weniger wichtige Aufgabe bearbeitet wird. Die wichtigere Aufgabe muss eine weniger wichtige, weniger zeitkritische verdrängen können. Ein solches System besitzt ein sogenanntes Echtzeit-Multitasking. Auch spricht man von einem preemptiven Multitasking, weil ein höher priorer Task/Thread einen niedriger prioren unterbrechen kann. Werden die konkurrierenden Aufgaben von unabhängigen Programmen durchgeführt, so spricht man von einzelnen Tasks. Sind die Aufgaben jedoch Teil eines einzelnen Programms, so nennt man diese Threads statt Tasks. Die jeweilige Wichtigkeit wird über Prioritäten gesteuert. Jeder Task bzw. jeder Thread besitzt eine Priorität in Form einer Zahl. Häufig bedeutet die kleinere Zahl eine höhere Priorität. Haase, Grundlagen der Software-Entwicklung 59 ► Beispiel BedsideLamp ► Beispiel Cyclic Automatisierungssysteme besitzen typischerweise ein Echtzeit-Multitasking. Windows besitzt zwar ein Multitasking und Prioritäten, ist aber kein Echtzeit-Multitasker. Die (wirksamen) Prioritäten werden dynamisch angepasst, sind also nicht fest. Dadurch kommen auch Tasks mit schlechter Priorität gelegentlich zum Zuge (faires Scheduling). Bei einem Echtzeit-Multitasking kann ein Task/Thread mit hoher Priorität alle niederprioren verdrängen. Neuere Linux-Kernels besitzen (zusätzlich) ein EchtzeitMultitasking oder können entsprechend konfiguriert werden. Oftmals wird unterstellt, dass Echtzeitsysteme besonders kurze Reaktionszeiten haben. Dies ist zwar häufig der Fall, jedoch kein zwingendes Kriterium. Im Computerhandel von Börsen sind extrem schnelle Echtzeitsysteme notwendig. Flugbuchungssysteme jedoch kommen mit moderaten Geschwindigkeiten aus. Haase, Grundlagen der Software-Entwicklung 60 3. Technische Systeme Computer werden nicht nur für allgemeine mathematische Aufgaben, Bürodienste und Kommunikation eingesetzt, sondern auch in technischen Anlagen oder Geräten verwendet. Dort erfüllen sie aber oft völlig andere Aufgaben und sind häufig nicht einmal als Computer erkennbar (Embedded Controller). So sind die Bedienelemente von Waschmaschinen, Radios, Mikrowellen etc. zunehmend an kleinen Rechnern (single chip processors) angeschlossen. Moderne Fernsehgeräte (Smart TV) sind (zumindest intern) vollwertige Computer. Ebenso sind Mobilfunk-Telefone überwiegend kleine Computer (Smart Phone). Jeder USB-Speicherstick und jede SD-Speicherkarte enthält einen Computer ohne den der Speicher nicht funktionieren könnte. 3.1. Digitale Ein- und Ausgänge Für sehr viele technische Aufgaben sind lediglich ein Ein- und Ausschaltvorgänge erforderlich. Hierfür verwendet man oftmals spezielle kleine Prozessoren, insbesondere, wenn keine hohe Rechenleistung erforderlich oder wenn geringer Stromverbrauch wichtig ist. Derartige Prozessoren verfügen über zusätzliche Anschlüsse, die digitale/binäre Ein- und Ausgänge realisieren. Der Spannungsbereich ist meist 5 Volt, seltener ca. 3 Volt. Digitale Ausgänge liefern entweder ca. 0 Volt wenn "ausgeschaltet" oder ca. 5 Volt wenn eingeschaltet. Der maximal mögliche Strom ist aber auf wenige Milliampere begrenzt und daher allenfalls für LEDs und andere kleine Stromverbraucher geeignet. Sind höhere Spannungen und Ströme und/oder Wechselspannungen erforderlich, werden Leistungstransistoren oder Relais nachgeschaltet. Über Relais, aber auch durch Optokoppler kann eine galvanische Trennung erreicht werden. Die digitalen Eingänge erkennen zwei Spannungsbereiche, nominell 0 Volt und 5 bzw. ca. 3 Volt als binäre 0 oder 1. Allerdings werden auch Spannungen bis zu etwa 0.8 Volt als binär 0 und Spannungen über etwa 2 Volt als binär 1 erkannt. Die Grenzen sind aber von der jeweiligen Technologie abhängig (TTL, MOS, ...). Die 5-Volt-Technik wird häufig als TTL-Technik bezeichnet. In Steuerungen (SPSen) werden für digitale Ein- und Ausgänge meist 24 Volt Gleichspannung verwendet. Im KFZ-Bereich sind vor allem 12 und 24 Volt verbreitet. Es ist allgemein üblich mehrere, meist 8, Ein- und Ausgänge zu Registern zusammenzufassen. Jedes einzelne Bit repräsentiert einen Ein- oder Ausgang. Registerweise kann oftmals noch festgelegt werden, ob die Anschlüsse Ein- oder Ausgänge sein sollen. Nach dem Einschaltvorgang des Bausteins sind die Anschlüsse üblicherweise aus Sicherheitsgründen als Eingang geschaltet oder in einem dritten hochohmigen Zustand. Derartige Ein- und Ausgänge sind in den Prozessoren meist nicht als normale Speicherplätze sondern als speziell adressierbare E/A realisiert. Da C und C++ hierfür keine Sprachmittel bereitstellen muss ein Zugriff auf die Ein- und Ausgänge anders durchgeführt werden. Man kann für den Zugang Funktionen in Assembler erstellen und diese dann von C/C++ aus aufrufen - etwa inp() und outp(). Einige Compiler erlauben es Assemblerbefehle für die jeweiligen Prozessoren unmittelbar einzubinden - etwas als "asm(...)" Anweisung. Und als dritte Möglichkeit kann die Programmiersprache selbst erweitert werden Keil beispielsweise verwendet sogenannte sbit. Für den physikalischen Anschluss von Ein- und Ausgängen gibt es zwei grundlegende Schaltungsarten - Haase, Grundlagen der Software-Entwicklung 61 NPN und PNP. NPN wird überwiegend in Asien, PNP überwiegend in Europa eingesetzt. 3.1.1. PNP- und NPN-Eingänge Schaltungen von Sensoren an SPS- oder Controller-Eingänge. Die Signalbildung ist hier als einfacher Schalter dargestellt. Nicht alle Sensoren benötigen eine (eigene) Spannungsversorgung, z.B. die Schalter im Bild. Auch kann die Spannungsversorgung der Sensoren getrennt ausgeführt sein. Eine gemeinsame Leitung (neben der Signalleitung) ist aber erforderlich. Bei Leitungsbruch unterscheiden sich die beiden Schaltungsarten, bei einem PNP-Sensor wir V-, bei einem NPN-Sensor wird V+ erkannt. Haase, Grundlagen der Software-Entwicklung 62 3.1.2. PNP- und NPN-Ausgänge Schaltungen von Aktuatoren an SPS- oder Controller-Ausgänge. Die angeschlossenen Aktuatoren sind hier als einfache Widerstände dargestellt. Nicht alle Aktuatoren benötigen eine Spannungsversorgung, z.B. wenn die im Bild gezeigten Widerstände als LEDs ausgeführt sind. Auch kann die Spannungsversorgung der Aktuatoren getrennt ausgeführt sein. Eine gemeinsame Leitung (neben der Signalleitung) ist aber erforderlich. 3.2. Analoge Ein- und Ausgänge Analoge Ein- und Ausgänge sind nötig, wenn diese eine Vielzahl von Werten unterscheiden sollen. Bei analogen Eingängen wird eine Spannung oder ein Strom in einen Zahlenwert konvertiert - AnalogDigital-Wandler ADC. Bei den analogen Ausgängen wird ein Zahlenwert in eine Spannung oder einen Strom umgesetzt - Digital-Analog-Wandler DAC. All diese Wandler haben eine definierte Auflösung und die verwendeten Zahlenwerte haben entsprechend viele Bits. Mit 8 Bit erreicht man eine Auflösung von 1/256 des Gesamtbereiches. Für 1024 unterscheidbare Werte benötigt man 10 Bit, für 4096 unterscheidbare Werte 12 Bit. Die Zahlenwerte werden softwareseitig in Ganzzahlen, char oder int, untergebracht. Die Kodierung ist aber sehr herstellerspezifisch. Üblich sind Ganzzahlen von 0 bis zum Maximalwert (z.B. 255 bei 8 Bit), erforderlichenfalls mit einem Vorzeichenbit oder als 2er-Komplement-Zahl. Aber selbst BCD-kodierte Zahlenwerte (jeweils 4 Bit für jede Stelle des 10er-Sytems) werden verwendet. Recht kompliziert gestaltet sich oft auch die Vorgehensweise bei Analog-Digital-Wandlern. Haase, Grundlagen der Software-Entwicklung 1. Kanal selektieren ADC haben oftmals mehrere Kanäle über Register wählbar 2. weitere Einstellungen Bereich, Einfach- oder Differenz-Messung, etc. 3. Messung starten Typischerweise durch eine Schreibbefehl in ein Steuerregister 4. auf Messung fertig warten häufig aktives Warten auf ein Bit in einem Steuerregister 5. Wert auslesen steht in einem Register bereit 6. evtl. Wert formatieren Controlbits maskieren, Vorzeichenbits auffüllen, etc. 63 Digital-Analog-Wandlern sind meist einfacher zu bedienen. Embedded Controller verwenden meist als Spannung die 5 Volt Versorgungsspannung, können also nur zwischen 0 und 5 Volt Eingangsspannung messen bzw. zwischen 0 und 5 Volt Ausgangsspannung erzeugen. Speziell für diesen Zweck konzipierte ADC- oder DAC-Bausteine unterstützen meist einstellbar eine ganze Reihe von Bereichen, etwa 0..5 Volt, 0..10 Volt, -10..+10 Volt, 0..20 mA, 4..20 mA. Die Strombereiche sind sinnvoll bei längeren Leitungswegen, weil dann der Spannungsabfall auf der Leitung das Ergebnis nicht verändert. Der Bereich 4..20 mA kann zur Kabelbrucherkennung eingesetzt werden und erforderlichenfalls auch als Stromversorgung dienen. Früher wurden die schwer verständlichen, kodierten Zahlenwerte unmittelbar in der Software verwendet. Heute arbeitet man überwiegend mit Werten, die auf physikalische Größen konvertiert wurden. Nur in Ausnahmefällen bei Billigprodukten mit sehr hohen Stückzahlen sind "Primitiv-Prozessoren" noch anzutreffen. Als Beispiel für eine Skalierung eines 12-Bit Analog-Digital-Wandlers im Arbeitsbereich 0..10 Volt mit unsigned short kodierten Werten von 0 bis 4095 könnte folgendermaßen aussehen. Damit ergibt sich folgende Rechenvorschrift Value = Value 12 Bit ∗ ( MaxValue−MinValue) + MinValue 4095.0 Bei bipolarem Arbeitsbereich wird die Umrechnung etwas komplizierter. Außerdem muss beachtet werden, dass positiver und negativer Teilbereich asymmetrisch sein können, z.B. Zahlenwerte von -2048 bis +2047. ?? Bild, Methoden Haase, Grundlagen der Software-Entwicklung 64 3.3. Spezielle Interface Nicht alle Mess- und Stellaufgaben lassen sich über digitale oder analoge Schnittstellen abwickeln. Für Pulsbreitenmodulationen (PWM) gibt es spezielle Elemente. Schnelle Impulszähler, die evtl. auch noch bei erreichen bestimmter Werte Ausgänge einstellen, sind auch mit leistungsfähigen CPU nicht realisierbar. Auch hierfür gibt es Sonderhardware. Für technische Aufgaben sind manchmal die Standardlösungen der Datenverarbeitung nicht einsetzbar oder nicht wünschenswert. So vermeidet man für manche Aufgaben alle beweglichen Teile aus Gründen der Zuverlässigkeit. Festplattenlaufwerke sind dann nicht einsetzbar. Flash-Speicher haben nur eine sehr begrenzte Lebensdauer und sind ebenfalls manchmal nicht verwendbar. Ebenfalls unerwünscht können Batterien und Akkumulatoren sein. Solche Forderungen bedingen oftmals ungewöhnliche Speicher. Als unveränderliche Speicher kommen beispielsweise ROMs in Betracht, die in "normalen" Computern nur für den Erstanlauf (boot) verwendet werden. 3.4. Bus-Systeme Die klassischen Peripheriegeräte eines Computers sind Bildschirm, Tastatur und Drucker. In jüngerer Vergangenheit kamen auch noch Touchscreens hinzu. Mit dem Erscheinen von Smartphones verbreiten sich auch zuvor sehr ungewöhnliche Sensoren in der Consumer-Elektronik. Lagesensoren, Beschleunigungssensoren, GPS etc. gehörten früher noch zu Sonderanwendungen. Dennoch brauchen viele Anwendungsprogramme sehr spezielle Hardware, die von einem Computer wegen ihre Vielfalt nicht unmittelbar unterstützt werden kann. Stattdessen wird sie meist über standardisierte Schnittstellen angeschlossen - drahtlos über Funk oder kabelgebunden. Die Wichtigsten drahtlosen Verbindungen benutzen WLAN oder Bluetooth. Kabelgebunden stehen sich Ethernet-Netzwerke und Feldbusse gegenüber. Alle diese Verbindungen benötigen "Spielregeln", wie ihre Kommunikation aufgebaut, abgewickelt und abgebrochen wird. Die entsprechenden Standards oder Normen beginnen mit den verwendeten Pegeln, Frequenzen, Parallelität etc. und reichen bis in Kommunikationsdetails des Aufbaus von übertragenen Daten. Feldbusse werden überwiegend in Automationen verwendet. Bestimmte Bereiche der Automation haben sich für bestimmte Feldbusse entschieden oder benutzen diese bevorzugt. Im Automobilbereich sind dies CAN-Bus und seit einiger Zeit LIN. In der Prozessautomation sind Modbus und Profibus neben vielen anderen sehr verbreitet. Haase, Grundlagen der Software-Entwicklung 65 4. C++ In C, und in allen klassischen Programmiersprachen, sind Daten und die sinnvoll möglichen Operationen mit ihnen, also Funktionen, voneinander getrennt. Man kann den Daten bzw. deren Deklaration nicht ansehen, welche Funktionen auf sie wirken können und man kann den Funktionen nicht ansehen, mit welchen Daten sie arbeiten können. In den objektorientierten Programmiersprachen werden Daten und Funktionen zusammengefasst. In C++ spricht man von Klassen, welche einerseits Strukturen sind mit den nötigen Daten und welche andererseits die zugehörigen Funktionen festlegen. Die Daten werden häufig auch Attribute genannt und die Funktionen nennt man meist Methoden - method oder member functions. Mit der objektorientierten Programmierung (OOP) entstehen neue, zusätzliche Eigenschaften Abstraktion, Kapselung, Vererbung und Polymorphismus. Variablen vom Typ einer Klasse nennt man Exemplare, Objekte oder auch Instanzen. Eine Exemplar einer Klasse wird durch instanziieren erzeugt. 4.1. Klassen Strukturen fassen zwar Daten von unterschiedlichen Datentypen zusammen, aber sie organisieren keinerlei zugeordnete Funktionen. Klassen erweitern die Strukturen um solche Funktionen. Hier eine Klassen für Positionen im Raum. class Position3D // { public: Position3D(); // Position3D(double x, double y, double z); ~Position3D(); // public: void Clear(); // double GetX() { return m_x; } // double GetY() { return m_y; } double GetZ() { return m_z; } void Set(double x, double y, double z); // void Show(); void Read(); private: double m_x, m_y, m_z; // }; Klasse für 3D-Positionen 2 Konstruktoren Destruktor Methoden getter setter private Daten, x,y,z-Koordinaten Eine Klasse stellt eine (umfangreiche) Erweiterung einer Struktur dar. Statt des Schlüsselwortes struct wird das Schlüsselwort class verwendet. Dem folgt der Klassenname, dann in geschweiften Klammern die Deklaration der Klasseneigenschaften und ein abschließendes Semikolon. ► Beispiel ClassPosition3D Zunächst enthält ein Exemplar der Klasse Position ähnlich einer Struktur die 3 double Werte m_x, m_y und m_z für die Koordinaten. Der hier gewählte Präfix m_ oder auch nur m ist gebräuchlich. Allerdings sind die Namen beliebig. Vor diesen Daten/Attributen steht das Schlüsselwort private mit angehangenem Doppelpunkt. Das Zugriffsspezifikation private legt fest, dass auf die nachfolgend deklarierten Variablen Haase, Grundlagen der Software-Entwicklung 66 oder Methoden nur von innerhalb der Klasse zugegriffen werden darf. Von außen sind die Variablen oder Methoden unzugänglich. Man nennt dies Kapselung oder auch information hiding. Die übrigen Deklarationen in der Klasse sind als public (ebenfalls mit angehangenem Doppelpunkt) gekennzeichnete Methoden. Diese sind überall zugänglich und können auf Exemplare der Klasse und nur auf diese angewendet werden. Die Zugriffsspezifikation public ist dringend notwendig, weil alle Variablen und Methoden ohne eine Zugriffsspezifikation private sind. Als eine dritte Variante neben public und private gibt es protected. Protected Variablen oder Methoden sind nur innerhalb der Klasse und von abgeleiteten Klassen zugänglich (siehe unten). 4.1.1. Konstruktoren und Destruktor Die ersten drei Methoden sind zwei Konstruktoren und der Destruktor. Konstruktoren und Destruktoren haben immer den Namen der Klasse und haben keinen Rückgabewert, nicht einmal void. Ohne einen Konstruktor enthält ein Exemplar zunächst nur Datenmüll. Und ein Destruktor darf entfallen, wenn es keine Notwendigkeit für spezielle Aufräumarbeiten gibt, wenn es also reicht nur den Speicherplatz des Objektes freizugeben.. Der Destruktor wird bei der Zerstörung eines Exemplars aufgerufen. Es gibt in einer Klasse höchstens einen Destruktor. Er hat den Namen der Klasse aber ihm wird eine Tilde ~ vorangestellt und er hat niemals Übergabeparameter. Konstruktoren zur Erzeugung eines Exemplars kann es viele geben. Meist unterscheiden sie sich in ihren Übergabeparametern. Im Beispiel gibt es zwei Konstruktoren, die ein Exemplar entweder mit 0-Werten oder mit drei übergebenen Werten initialisieren. Ein Konstruktor wird bei Erzeugung eines Exemplars aufgerufen. Bei automatisch erzeugten Exemplaren wird auch der Destruktor automatisch aufgerufen. // innerhalb eines Blocks { Position3D p1; // erster Konstruktor, mit 0 initialisiert Position3D p2(1,2,3); // zweiter Konstruktor, mit 1,2,3 initialisiert . . . } // hier wird der Destruktor für beide Exemplare aufgerufen ?? default initializer 4.1.2. Methoden In übrigen Methoden der Beispielsklasse Position3D sind ebenfalls public und können von überall aufgerufen werden. Für die 3 Methoden GetX(), GetY() und GetZ() ist die (triviale) Implementierung unmittelbar hinter der Deklaration angegeben. In diesem Fall entfällt das abschließende Semikolon, es ist nicht nötig. Einige Compiler akzeptieren es trotzdem, andere geben eine Warnung aus. Methoden wie GetX() und Set() werden auch Zugriffsfunktionen oder getter und setter genannt. Hier ist die Klasse so aufgebaut, dass die Speicher für die (kartesischen) Koordinaten nicht unmittelbar, Haase, Grundlagen der Software-Entwicklung 67 sondern nur über die Zugriffsfunktionen möglich sind. Die Klasse könnte daher so abgeändert werden, dass sie z.B. Kugelkoordinaten benutzt und in den Zugriffsfunktionen die nötigen Transformationen durchführt. Nirgendwo sonst ergäbe sich eine Änderung in Programmen, die die Position3D-Klasse benutzen. Ein bedeutender Vorteil den Datenkapselung bzw. information hiding mit sich bringen. Für die verbleibenden Methoden muss die Definition noch erfolgen. Hierfür muss jeweils eine Funktion geschrieben werden, die zu einer bestimmten Klasse gehört. Damit der Compiler die Funktion zuordnen kann wird der Klassenname mit angehangenen doppelten Doppelpunkten vor den Funktions-/Methodennamen gesetzt. Position3D::Position3D() { Clear(); } // mit 0 initialisiert Position3D::Position3D(double x, double y, double z) { Set(x, y, z); // mit übergebenen Werten initialisiert } Position3D::~Position3D() { } void Position3D::Clear() { m_x = m_y = m_z = 0.0; } // alle Koordinaten auf 0 setzen void Position3D::Set(double x, double y, double z) { m_x = x; // Koordinaten einstellen m_y = y; m_z = z; } void Position3D::Show() { cout << m_x << "," << m_y << "," << m_z; } void Position3D::Read() { cin >> m_x >> m_y >> m_z; } Abgesehen von Klassennamen und Doppelpunkten stellen die Methoden fast normale Funktionen dar. Sie arbeiten immer auf das Exemplar, für das sie aufgerufen wurden. Realisiert wird dies durch einen versteckten this-Zeiger in jeder Methode. Für den Aufruf wird dem Namen der Methode der Name des Exemplars und ein Punkt vorangestellt. Mit den beiden Position3D-Objekten p1 und p2 oben also p1.Clear(); p2.Set(1,2,3); Der this-Zeiger ist gelegentlich sogar nützlich. Falls eine Klasse eine Variable data enthält und eine Funktion einen Parameter data hierfür übergibt, dann ist der this-Zeiger nötig, die beiden zu unterscheiden. Haase, Grundlagen der Software-Entwicklung 68 void SetData(int data) { this->data = data; } ► Beispiel ClassPosition3D 4.1.3. Klassen und Header-Dateien Objektorientierung bedeutet auch, dass Klassen möglichst alle Aspekte der Objekte behandeln. Für jede Funktionalität sollte es passende Methoden innerhalb der Klasse geben. Dadurch sind Klassen oftmals ohne Änderung wiederverwendbar oder sollten es zumindest sein. Klassendefinition und Header-Dateien liegen daher typischerweise in Dateien mit gleichem Namen vor. Header-Dateien haben zumeist die Erweiterung/Endung .h, seltener .hpp oder .hxx. Die Dateien mit der Klassendefinition haben meist die Erweiterung/Endung .cpp oder auch .cxx. Um den Übersetzungsvorgang zu beschleunigen (aber auch um fehlerhafte Duplizierung von Code zu vermeiden) enthalten die mehrmals eingebundenen Header-Dateien spezielle Direktiven, damit der Compiler die Deklaration nur einmalig bearbeitet. #ifndef _POSITION3D_ #define _POSITION3D_ . . . // Klassendeklaration #endif Bei der ersten Bearbeitung ist _POSITION3D_ noch nicht definiert, wird in nachfolgender Zeile definiert und die Deklaration der Klassen wird vom Compiler übersetzt. Bei jeder weiteren Bearbeitung ist _POSITION3D_ bereits definiert und die Zeilen bis #endif werden vom Compiler übersprungen. ► Beispiel ClassPosition3D ► Beispiel Inheritance Bei den meisten Compilern wird inzwischen auch die bequemere Methode mit einem #pragma unterstützt. #pragma once Der dem #pragma once folgende Teil wird nur einmalig verarbeitet. 4.2. Vererbung Exemplare von Klassen können zwar unterschiedliche Daten enthalten, aber es sind immer die gleichen Funktionen, welche die Daten handhaben. Durch Vererbung können Klassen mit ähnlichen Eigenschaften und spezifischen Daten und Funktionen programmiert werden. 4.2.1. Basisklasse und abgeleitete Klassen Nachfolgend (beispielhaft und unvollständig) eine Klasse für 2-dimensionale Objekte und 2 abgeleitete Klassen für Kreise und Rechtecke. Normalerweise würde man hier getrennte Dateien für Deklaration und Haase, Grundlagen der Software-Entwicklung 69 Definition benutzen und wohl auch die Klassen voneinander trennen. Hier sind die Member-Funktionen unmittelbar mit den Deklarationen zusammengefasst. class Object2D { public: Object2D() { m_PosX = m_PosY = 0.0; } Object2D(double PosX, double PosY) { m_PosX = PosX; m_PosY = PosY; } virtual ~Object2D() {} double PosX() { return m_PosX; } double PosY() { return m_PosY; } void Move(double DeltaX, double DeltaY) { m_PosX += DeltaX; m_PosY += DeltaY; } virtual double Area() { return 0.0; } private: double m_PosX, m_PosY; }; class Circle : public Object2D { public: Circle() { m_Radius = 0.0; } Circle(double PosX, double PosY, double Radius) : Object2D(PosX, PosY) { m_Radius = Radius; } virtual ~Circle() {} virtual double Area() { return M_PI * m_Radius * m_Radius; } private: double m_Radius; }; class Rectangle : public Object2D { public: Rectangle() { m_EdgeX = 0.0; m_EdgeY = 0.0; } Rectangle(double PosX, double PosY, double EdgeX, double EdgeY) : Object2D(PosX, PosY) { m_EdgeX = EdgeX; m_EdgeY = EdgeY; } virtual ~Rectangle() {} virtual double Area() { return m_EdgeX * m_EdgeY; } private: double m_EdgeX, m_EdgeY; }; In diesen Klassen sind die Daten jeweils private und von der Basisklasse Object2D wird public geerbt. Object2D Object2D Object2D O(1,2); *C = new Circle(1,2,3); *R = new Rectangle(1,2,3,4); O.Move(1,2); C->Move(1,2); R->Move(1,2); delete R; delete C; ► Beispiel Inheritance Nur das Exemplar O wird automatisch erzeugt. die Exemplare von Circle und Rectangle werden dynamisch über new erzeugt und initialisiert. Da Circle und Rectangle von Object2D abgeleitete Klassen sind, können die Adressen auf Exemplare dieser Klassen auch in Zeiger der Basisklassen gespeichert Haase, Grundlagen der Software-Entwicklung 70 werden. So ist C zwar als Zeiger auf ein Object2D deklariert, aber enthält einen Zeiger auf einen Circle. Die Umkehrung ist nicht erlaubt. Ein Zeiger auf ein Objekt kann niemals einen Zeiger auf ein Exemplar einer seiner Basisklassen enthalten. Circle *X = new Object2D(1,2); // unzulässig Da hier die Objekte/Exemplare C und R dynamisch mittels new erzeugt werden, müssen sie nach Gebrauch mit delete zerstört werden, d.h. der Destruktor wird aufgerufen und der Speicherplatz wird zurückgegeben. Für das (automatisch erzeugte) Objekt O übernimmt der Compiler den Aufruf des Destruktors. Im Zusammenspiel mit der Vererbung kommt ein weiteres Zugriffsattribut protected hinzu. Auf public Variablen und Funktionen darf von überall her zugegriffen werden, wohingegen private Variablen nur von den Funktionen der Klasse selbst gelesen und verändert und private Funktionen nur von den Funktionen der Klasse selbst aufgerufen werden dürfen. Sind Variablen und/oder Funktionen als protected gekennzeichnet, dürfen sie nicht nur von der Klasse selbst, sondern auch von den abgeleiteten Klassen benutzt werden. Normalerweise wird die Basisklasse an die abgeleitete Klasse als public vererbt, im Beispiel mittels : public Object2D. Hier sind auch private und protected möglich. Ohne Angabe geht der Compiler von private als default aus. Außerdem hat die Einstellung hier Vorrang vor den Angaben bei den Variablen und Funktionen. 4.2.2 Initialisierung der Anteile der Basisklassen und Elemente Für Konstruktoren von abgeleiteten Klassen können die der Basisklassen explizit angegeben werden. Hierzu wird in den jeweiligen Konstruktoren nach einem Doppelpunkt der Konstruktor der Basisklasse aufgeführt. Circle(double PosX, double PosY, double Radius) : Object2D(PosX, PosY) { m_Radius = Radius; } Aber auch die trivialen Elemente können auf diese Weise initialisiert werden. Circle(double PosX, double PosY, double Radius) : Object2D(PosX, PosY), m_Radius(Radius) { } 4.2.3 Virtuelle Funktionen Im Beispiel oben sind viele (fast alle) Member-Funktionen als virtual gekennzeichnet. Solche virtuelle Funktionen werden zusätzlich in einer Art Tabelle aufgeführt/referenziert (virtual method table). Diese Tabelle wird gemeinsam mit dem Objekt verknüpft. Ein Aufruf erfolgt dann nicht direkt sondern immer über den Eintrag in dieser Tabelle. Hierdurch wird gewährleistet, dass die richtige Funktion aufgerufen wird, auch wenn sie über einen Zeiger auf eine der Basisklassen erfolgt - wie im Beispiel oben. Im dem Beispiel sind C und R als Zeiger auf ein Object2D, also der Basisklasse, deklariert. Tatsächlich sind sie aber ein Circle bzw. ein Rectangle. Wird nun mittels der Member-Funktion Area() die Fläche berechnet Haase, Grundlagen der Software-Entwicklung 71 cout << "O.Area() = " << O.Area() << endl; cout << "C->Area() = " << C->Area() << endl; cout << "R->Area() = " << R->Area() << endl; so wird bei C und R, deklariert als Zeiger auf Object2D, trotzdem nicht die Funktion Area() in der Basisklasse Object2D aufgerufen, sondern die jeweils richtige Area()-Funktion von Circle bzw. Rectangle. Area() wurde als virtual definiert. Daher wird Area() nicht unmittelbar aufgerufen, welches zur Area()-Funktion von Object2D führte, sondern es wird über die virtual function table die zum wirklichen Objekttyp passende Area()-Funktion benutzt. Ohne den Zusatz virtual wäre hier die Fläche falsch berechnet worden. ► Beispiel Inheritance ► Beispiel VirtualvsNonVirtual Falls eine Klasse virtuelle Funktionen enthält oder wenn es von ihr abgeleitete Klassen gibt, sollten die Destruktoren ebenfalls virtuell sein. 4.3. Statische Variablen und Methoden Auch bei Klassen gibt es statische Variablen und Methoden. Diese sind, sofern sie als public deklariert sind, ebenfalls global und ohne Bezug auf ein Objekt der Klassen zugänglich - daher static. Für den Zugriff kann zwar auch ein Objekt der Klasse dienen, besser aber benutzt man den Namen der Klasse vor einem doppelten Doppelpunkt. class ClassWithStatic { public: static int GetStaticVar() { return m_nStaticVar; } public: static int m_nStaticVar; // Deklaration der statischen Variablen }; int ClassWithStatic::m_nStaticVar = 42; int n; ClassWithStatic ClassWithStaticInstance; n = ClassWithStaticInstance.GetStaticVar(); n = ClassWithStatic::GetStaticVar(); n = ClassWithStatic::m_nStaticVar; // Definition der statischen Variablen // // // // Exemplar der Klasse ClassWithStatic ok, aber nicht gut besser so, mit Klassenname und :: geht auch da static und public Die statische Variable m_nStaticVar existiert nur genau einmal. Beim Aufruf nicht statischer Member-Funktionen wird durch den Compiler ein zusätzlicher (unsichtbarer) Zeiger auf das jeweilige Objekt/Exemplar als erster Parameter übergeben. Bei statischen Member-Funktionen ist dies nicht erforderlich. Naturgemäß kann eine statische Funktion einer Klasse nicht auf die Variablen in einem Objekt/Exemplar zugreifen, wohl aber auf die statischen Variablen der Klasse. ► Beispiel ClassWithStatic Statische Variablen in einer Klasse benutzt man um Daten, die für alle Exemplare der Klasse gelten, zugänglich zu machen. Entsprechend liefern oder bearbeiten statische Funktionen allgemeine Eigenschaften der Klasse. Gelegentlich nennt man diese auch Klassendaten und Klassenmethode. Haase, Grundlagen der Software-Entwicklung 72 4.4. Automatisch erzeugte Methoden Durch den C++ Compiler werden einige Funktionen automatisch generiert wenn sie (noch) nicht definiert wurden. default constructor myClass(); copy constructor myClass(const myClass& other); move constructor myClass(myClass&& other) noexcept; copy assignment operator MyClass& operator=(const myClass& other); move assignment operator MyClass& operator=(myClass&& other) noexcept; C++11 destructor ~myClass(); C++11 ► Beispiel CopyConstructor Bei den automatisch erzeugten Kopier-Konstruktoren werden alle Daten nur einfach kopiert. Wenn unter diesen jedoch Zeiger auf allozierten Speicher sind, wird das in den meisten Fällen falsch sein. Die Konstruktoren müssen in solchen Fällen zweckmäßig durch den Anwender definiert werden. 4.5. Überladene Operatoren C++ erlaubt es sogar die Operatoren mit neuen Funktionen zu definieren. Eine Klasse kann beispielsweise festlegen wie die Addition mit dem +-Zeichen auf zwei Elementen arbeiten soll. Hierbei muss es sich nicht unbedingt um eine Addition im mathematischen Sinn handeln. Ebenso könnte eine Addition ein Hinzufügen eines Datensatzes zu einer Datenbank bewirken. Überladen werden können folgende Operatoren + * / = < <<= >>= == != <= >= ~ &= ^= |= && || new delete new[] delete[] > ++ %= += -[] -= % () Ein einfaches Beispiel wäre eine Klasse für 3*3-Matrizen. class Matrix3x3 { public: Matrix3x3(); Matrix3x3(double M11, double M12, double M13, double M21, double M22, double M23, double M31, double M32, double M33); ~Matrix3x3(); public: void Print(); Matrix3x3& operator=(Matrix3x3& Matrix); Matrix3x3& operator+(Matrix3x3& Mright); Matrix3x3& operator-(Matrix3x3& Mright); Matrix3x3& operator*(Matrix3x3& Mright); private: *= & , /= ^ ->* << ! → >> | Haase, Grundlagen der Software-Entwicklung 73 double m_M[3][3]; }; Mit einer solchen Klasse kann man 3*3-Matrizen als neue Datentypen verwenden und mit ihnen rechnen. Matrix3x3 A, B, C; C = A + B; ► Beispiel OperatorOverloading 4.6. Standard C++ Klassen string und Containerklassen C++ besitzt neben den in C enthaltenen Zeichenfelder (C-strings) eine eigene Klasse string. Sie bedient Zeichenfolgen, teilweise auch in UTF-8 oder UTF-16, als eigenständige Datentypen. Ihr besonderer Vorteil liegt in der automatischen Speicherverwaltung. Es ist also nicht mehr erforderlich die jeweils erforderlichen Größen selbst zu verwalten. Zusätzlich gibt es eine Vielzahl von Methoden zur Manipulation von Zeichenfolgen. Folgender Code ist damit möglich: #include <string> string S; S = “ABC“; for ( size_t n=0, n<100; ++n ) S += “ 123“; Hier wird ein string S mit “ABC“ initialisiert und danach wird 100 mal ein string mit 4 Zeichen “ 123“ angefügt. Die Laufzeitumgebung sorgt für die automatische Allozierung des erforderlichen Speicherplatzes. Weitere Klassen für Container vereinfachen die Arbeit mit Feldern und Listen. Sie sind allerdings etwas komplizierter weil sie Templates benutzen (hier nicht behandelt). Haase, Grundlagen der Software-Entwicklung 74 5. Beliebte Fehler Es gibt natürlich Fehler, die besonders oft vorkommen. Einige von ihnen sind sogar schwer zu erkennen und haben den längst nicht immer guten Ruf von C/C++ verursacht. Der Compiler kann nicht jeden Fehler finden. Manche Fehler bemerkt der Compiler erst ein paar Zeilen später (z.B. fehlende oder überflüssige geschweifte Klammern). Reihenfolge ohne Wertung 5.1 Mangelhafte oder fehlende Kommentierung Schreiben Sie Kommentare, sonst verstehen weder Sie noch andere ihre Programme. 5.2 Schlecht Wahl von Variablennamen A und B sind keine gute Wahl für die Namen von Variablen. Wählen Sie “sprechende“ Variablennamen wie z.B. TemperaturWasser oder auch TempH2O. 5.3 Warnungen des Compilers beachten Häufig “wundert“ sich ein Compiler. Er gibt eine Warnung aus. Manchmal verbirgt sich hinter diesen Warnungen ein Fehler. Also Warnungen beachten und möglichst so programmieren, dass keine Warnungen vorkommen. 5.4 RYFM J 5.5 Schlechte Strukturierung und Formatierung von Quellcode Schlecht strukturierter und formatierter Code ist sehr anfällig für Fehler und schwer zu überschauen. Auch wenn Ordnung keineswegs “das halbe Leben“ ist, es hilft. 5.6 Mangelnde Fehlerüberprüfung Viele Funktionen (auch ihre eigenen) liefern Hinweise auf Fehler. Beispielsweise kann die Funktion zum Öffnen einer Datei melden, das die Datei nicht oder nicht an der angegeben Stelle existiert und daher auch nicht geöffnet werden konnte. Mögliche Fehlerquellen sollten immer überprüft und behandelt werden. 5.7 Nicht deklarierte Variablen Haase, Grundlagen der Software-Entwicklung int main() { cin >> x; cout << x; } x ist nicht definiert. 5.8 Nicht definierte Funktionen int main() { menu(); } void menu() { //... } Entweder es fehlt zu Anfang ein Prototyp für menu oder die Funktion main muss nach menu folgen. 5.9 Semikolon vergessen int main() { int x; cin >> x cout << x; } Zeile mit cin erfordert am Zeilenende ein Semikolon. 5.10 Überflüssiges Semikolon void menu(); { //... } Zwischen Anweisungen dürfen überflüssige Semikolon stehen. Sie sind dann leere Anweisungen. Aber eine Funktion wird ohne Semikolon definiert – anders als die Deklaration des Prototyps einer Funktion. 5.11. Nicht initialisierte Variablendefinition int count; while (count < 100) { cout << count; count++; } Hier ist Variable count nicht initialisiert und kann einen völlig beliebigen und zufälligen Wert enthalten. 75 Haase, Grundlagen der Software-Entwicklung 76 5.12 Einfaches Gleich-Zeichen für Vergleich verwendete char done = 'Y'; while ( done = 'Y' ) { //... cout << "Continue? (Y/N)"; cin >> done; } Ein Vergleich auf Gleichheit erfordert doppelte Gleich-Zeichen “==“. Ein einfaches Gleich-Zeichen bedeutet eine Anweisung. Im Beispiel wird done ständig auf 'Y' eingestellt. 5.13 Semikolon an falscher Stellen int x; for ( x = 0; x < 100; x++ ) ; cout << x; Hier kontrolliert die For-Schleife nur die leere Anweisung bestehen aus dem Semikolon am Ende der Zeile. Offensichtlich ist dieses Semikolon unerwünscht. Die For-Schleife sollte die Ausgabe mit cout kontrollieren. Ein ähnlicher Fehler ist auch mit while möglich. 5.14 break in einer switch-Anweisung vergessen int x = 2; switch(x) { case 2: cout << "two" << endl; case 3: cout << "three" << endl; } In fast allen Fällen erfordert jeder case am Ende ein break. 5.15 Feldgrenzen nicht beachten int array[10]; //... for (int x = 1; x <= 10; x++) array[x] = x; Hier wird array[0] nicht behandelt und auf den Speicherplatz des nicht existierenden Elementes array[10] (also im Speicher hinter dem Feld array) wird geschrieben. Haase, Grundlagen der Software-Entwicklung 77 5.16 Verwechseln der && und || Operatoren int value; do { //... value = 10; } while ( !(value == 10) || !(value == 20) ); Hier bricht die Schleife nicht ab obwohl value den Wert 10 hat. Im Sprachgebrauch unterscheidet sich die Verwendung von UND und ODER. Vgl. auch Ein Programmierer wird von seiner Frau um Folgendes gebeten: "Geh bitte zum Laden und kaufe einen Laib Brot. Falls die Eier haben, bring ein Dutzend mit." Der Programmierer kommt zurück mit 12 Broten. Oder auch Nach der Geburt eines Kindes. Ist es ein Junge oder ein Mädchen. Antwort: JA. 5.17 Ganzzahlige Division double d = 5 / 2; Das Resultat ist 2 und nicht 2.5. Zuerst kommt die ganzzahlige Division und erst danach wird in eine Fließkommazahl gewandelt. 5.18 Nicht initialisierte Zeiger char * st; /* defines a pointer to a char or char array */ strcpy(st, "abc");/* what char array does st point to? */ Zeiger st hat einen zufälligen Wert, d.h. er zeigt irgendwohin im Speicher. 5.19 Zeichenfelder auf Gleichheit testen char st1[] = "abc"; char st2[] = "abc"; if ( st1 == st2 ) printf("Yes"); else printf("No"); Der Vergleich prüft zwei Zeiger st1 und st2 die sicher unterschiedlich sind. Es wird nicht der Inhalt verglichen. Hier wäre die Funktion strcmp angebracht. Haase, Grundlagen der Software-Entwicklung Missing chapters: ?? enum ► Beispiel Enum ?? #ifdef und andere Direktiven ?? const parameter, const methods ?? friend class and functions ?? templates ?? order of evaluation ?? typische Strukturen der Programmierung, Linked Lists, Bäume, ... ?? auto, try-catch ?? L“..“ etc, ?? decltype ?? mutable ?? static_cast etc ?? extern 78 Haase, Grundlagen der Software-Entwicklung 79 Anhang Stichwortverzeichnis abgeleitet............................................................................................................................................66, 68ff. Abstraktion...................................................................................................................................................65 ADC...........................................................................................................................................................62f. Adresse...............................................................................................................................................42ff., 69 Aktualparameter...........................................................................................................................................35 Algorithmen.................................................................................................................................................16 allozieren......................................................................................................................................................47 alloziert......................................................................................................................................................46f. Anweisungen.........................................................................................................17f., 21, 27ff., 31f., 49, 51 Arduino........................................................................................................................................................10 Assembler.................................................................................................................................................7, 60 Attribut.........................................................................................................................................................65 Ausführungspriorität....................................................................................................................................22 Automation............................................................................................................................................58, 64 Basisklasse................................................................................................................................................68ff. Bedingung..............................................................................................................................15, 27ff., 43, 45 Bibliothek.............................................................................................8, 15, 17, 26, 30, 35ff., 45, 50, 52, 56 Binärsystem...............................................................................................................................................11f. Bit.............................................................................................................................11ff., 15, 46, 49, 60, 62f. Block........................................................................................................................................9, 27ff., 35, 53 Body.......................................................................................................................................................17, 35 bool..............................................................................................................................................................15 boot..............................................................................................................................................................64 break..............................................................................................................................................29, 31f., 34 Buchstaben.....................................................................................................................................12f., 19, 42 Bus...............................................................................................................................................................64 calloc..........................................................................................................................................................46f. case...............................................................................................................................................................29 char....................................................................................................12, 14f., 19, 25f., 40, 45, 47, 49, 55, 62 cin.........................................................................................................................................10, 17, 19, 52, 67 class..................................................................................................................................................65, 71, 78 Code::Blocks..................................................................................................................................................9 Compiler..................................................................8ff., 17f., 21, 25f., 35f., 39, 43, 46, 53ff., 60, 66ff., 70f. const.....................................................................................................................................................20, 54f. constructor....................................................................................................................................................72 Container......................................................................................................................................................73 continue........................................................................................................................................................34 cout.................................................................................................................................10, 17, 19, 41, 52, 67 CppDroid......................................................................................................................................................10 CPU...........................................................................................................................................6f., 46, 54, 64 DAC...........................................................................................................................................................62f. dangling........................................................................................................................................................47 Datentyp................................................................12f., 15, 19, 21, 23ff., 35, 39f., 42f., 46f., 49f., 54, 56, 65 Debugger........................................................................................................................................................9 default.....................................................................................................................................................29, 70 Deklaration..........................................................................17, 19, 24, 35f., 38, 40, 42, 45, 56, 65f., 68f., 71 delete....................................................................................................................................................47, 69f. Haase, Grundlagen der Software-Entwicklung 80 Destruktor..........................................................................................................................................65f., 70f. Direktive..............................................................................................................................9f., 17, 37, 56, 68 do-while.....................................................................................................................................................33f. double...............................................................13, 19f., 23ff., 33, 36, 39f., 42f., 46f., 49f., 55f., 65, 67, 69f. double NullPunktVektor[3] = { 0.0, 0.0, 0.0 };............................................................................................24 Dualsystem...................................................................................................................................................11 dynamisch................................................................................................................................46, 48, 59, 69f. Echtzeit......................................................................................................................................................58f. else.............................................................................................................................................................27f. Embedded Controller...................................................................................................5, 8, 10, 51, 55, 60, 63 endl.........................................................................................................................................................17, 19 false............................................................................................................................................15, 27, 30, 32 Fehler...........................................................................................................................................................74 Felder.....................................................................................................................................24f., 40f., 45, 47 Fließkommazahlen.......................................................................................................................................13 float..............................................................................................................................................................13 Flussdiagramme...........................................................................................................................................16 for.................................................................................................................................................14, 31ff., 53 Formalparameter..........................................................................................................................................35 Formatanweisung...................................................................................................................................37, 51 Formatierung.......................................................................................................................................37, 50f. Fortsetzungsbedingung.............................................................................................................................30ff. Fragmentierung............................................................................................................................................48 free.............................................................................................................................................................46f. Funktion..................................................................8, 15, 17, 23, 35ff., 44ff., 49ff., 56, 58, 60, 65, 67f., 70f. Ganzzahl...................................................................................................................11, 21, 23, 37, 42, 49, 62 getter..........................................................................................................................................................65f. global...................................................................................................................................................53f., 71 goto..............................................................................................................................................................30 Gültigkeitsbereiche......................................................................................................................................53 Header-File.................................................................................................................................35ff., 56f., 68 Heap...........................................................................................................................................................47f. Hexadezimalsystem......................................................................................................................................11 Hochsprachen.................................................................................................................................................8 IDE...................................................................................................................................................9f., 36, 56 IEC 61131......................................................................................................................................................9 if.....................................................................................................................................14f., 27ff., 34, 39, 43 Indizierung...................................................................................................................................................25 Initialisierung...............................................................................................................................................70 Instanz..........................................................................................................................................................65 int....................................................................12, 17, 19, 23, 32f., 35f., 38f., 44f., 49ff., 53f., 56, 62, 68, 71 Integrated Development Environment...........................................................................................................9 Interface.......................................................................................................................................................64 Interpreter....................................................................................................................................................8f. Kapselung..................................................................................................................................................65f. Klasse.......................................................................................................................................................65ff. Kommentar.............................................................................................................................................18, 36 Kommunikation.....................................................................................................................................60, 64 Konstruktor................................................................................................................................................65f. label..............................................................................................................................................................30 lokal.........................................................................................................................................33, 45, 53f., 57 long......................................................................................................................................................12f., 49 Haase, Grundlagen der Software-Entwicklung 81 main............................................................................................................................................17, 19, 38, 53 malloc........................................................................................................................................................46f. member.........................................................................................................................................................65 Member-Funktion.....................................................................................................................................69ff. method..........................................................................................................................................................65 Methode.......................................................................................................7, 14, 17, 19, 44f., 49, 65ff., 71f. Microsoft C++................................................................................................................................................9 modular..................................................................................................................................................35, 56 Multitasking...............................................................................................................................................58f. new.......................................................................................................................................................47, 69f. NULL...........................................................................................................................................37, 42ff., 47 Objekt..............................................................................................................................17, 47, 65, 67f., 70f. objektorientiert.............................................................................................................................................65 Operator.......................................................................................................................................................72 Operatoren.................................................................................................................................................21f. Parameter...........................................................................................17, 35f., 39, 44f., 47, 49ff., 56, 66f., 71 pointer..............................................................................................................................................7, 42f., 47 Polymorphismus..........................................................................................................................................65 preemptive....................................................................................................................................................58 printf..............................................................................................................................................17, 37, 50f. Priorität......................................................................................................................................................58f. Programm............................................................................................14, 17f., 20f., 36, 42, 46ff., 50, 53, 56 protected.................................................................................................................................................66, 70 Prototyp.................................................................................................................................35f., 38f., 50, 56 public................................................................................................................................................65f., 69ff. Quadratwurzel....................................................................................................................................16, 30ff. Quellcode.........................................................................................................................................36, 54, 56 RAM..............................................................................................................................................................6 Referenz.....................................................................................................................................................44f. Rekursion.....................................................................................................................................................38 rekursive.................................................................................................................................................39, 43 ROM..................................................................................................................................................6, 51, 55 Schleifen.......................................................................................................................................29ff., 33, 53 Schlüsselwort.........................................................................................15, 17, 23, 27, 29ff., 33, 40, 54f., 65 Schnittstelle............................................................................................................................................10, 64 setter...........................................................................................................................................................65f. short......................................................................................................................................12, 23, 40, 49, 63 sizeof..........................................................................................................................................................46f. Sizeof.........................................................................................................................................................12f. Sonderzeichen............................................................................................................................................13f. Spagetti-Code...............................................................................................................................................30 Speicher............................................................................................................6, 42, 45ff., 51, 54, 60, 64, 66 Speicherplatz............................................................................................................................42, 46f., 49, 70 Sprungziel....................................................................................................................................................30 Stack..........................................................................................................................................................47f. Standardfunktion..........................................................................................................................................49 Statisch.........................................................................................................................................................71 Steueranweisungen.......................................................................................................................................27 string..................................................................................................................................15, 25f., 37, 49, 73 struct........................................................................................................................................40, 43f., 46, 65 Struktur......................................................................................................................19, 40f., 43f., 46, 51, 65 switch...........................................................................................................................................................29 Haase, Grundlagen der Software-Entwicklung 82 Switch..........................................................................................................................................................29 Task............................................................................................................................................................58f. Thread........................................................................................................................................................58f. true................................................................................................................................................15, 27, 30ff. Type Cast......................................................................................................................................................23 typedef..........................................................................................................................................................49 Typkonvertierung.........................................................................................................................................23 überladen..............................................................................................................................................39, 72f. Überladen.....................................................................................................................................................72 Unicode................................................................................................................................................14f., 26 UTF-8.....................................................................................................................................................15, 26 Variablen........................................................................15, 19ff., 25, 29, 35, 40ff., 49ff., 53f., 56, 65f., 70f. Vererbung.........................................................................................................................................65, 68, 70 virtual....................................................................................................................................................9, 69ff. virtuell..........................................................................................................................................................70 volatile..........................................................................................................................................................54 Vorbelegungswert.........................................................................................................................................36 Vorrangregeln...................................................................................................................................21, 28, 43 while............................................................................................................................................30f., 33f., 45 Y2K..............................................................................................................................................................49 Zehnersystem...............................................................................................................................................11 Zeichenfelder...............................................................................................................................................25 Zeichenfolge..................................................................................................18, 25f., 29, 37f., 45, 50, 52, 55 Zeiger..........................................................................................................23, 37, 42ff., 49, 51, 55, 67, 69ff. Zweierkomplement......................................................................................................................................12 Zweiersystem...............................................................................................................................................11