Grundlagen der Software-Entwicklung GSE

Werbung
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
Herunterladen