Informatik Und C Skript Release 2 5 6 IOE 2021
Informatik Und C Skript Release 2 5 6 IOE 2021
Dieses Skript dient als Grundlage für die Informatikvorlesung im ersten und zweiten
Semester, es kann und will keine Bücher ersetzen, es ist aber durchaus auch zum Selbst-
studium einsetzbar.
Ivo Oesch
April 2001
bis
September 2021
Eine Einführung in die Informatik und die Programmiersprache C
Inhaltsverzeichnis
1 Einführung............................................................................................................................................8
2 Grundlagen.........................................................................................................................................11
3 Datentypen..........................................................................................................................................19
4 Konstanten..........................................................................................................................................22
5 Variablen............................................................................................................................................24
6 Ein-/Ausgabe einfacher Datentypen...................................................................................................27
7 Operatoren..........................................................................................................................................30
8 Anweisungen/Statements...................................................................................................................36
9 Kontrollstrukturen..............................................................................................................................38
10 Funktionen........................................................................................................................................46
11 Felder (Arrays).................................................................................................................................50
12 Strings...............................................................................................................................................54
13 Strukturen.........................................................................................................................................56
14 Unions...............................................................................................................................................60
15 Bitfelder............................................................................................................................................61
16 Enums...............................................................................................................................................62
17 Zeiger (Pointer)................................................................................................................................63
18 Preprocessor.....................................................................................................................................72
19 Bibliotheksfunktionen......................................................................................................................74
20 Modulares Programmieren...............................................................................................................89
21 Datei I/O...........................................................................................................................................93
22 Standardargumente...........................................................................................................................96
23 Sortieren...........................................................................................................................................97
24 Suchen............................................................................................................................................105
25 Rekursion........................................................................................................................................107
26 Dynamische Speicherverwaltung...................................................................................................110
27 Listen..............................................................................................................................................114
28 (Binäre) Bäume..............................................................................................................................122
29 Hashtabellen...................................................................................................................................126
30 Vergleich einiger Datenstrukturen.................................................................................................128
31 Ringbuffer (Circular Array)...........................................................................................................129
32 Stack, LIFO, Queue und FIFO.......................................................................................................131
33 Software Engineering.....................................................................................................................133
34 Analyse/Design...............................................................................................................................142
35 Designmethoden.............................................................................................................................146
36 Systematisches Testen von Software..............................................................................................159
37 Projektorganisation und Projektleitung..........................................................................................166
38 Anhang A, weiterführende Literatur..............................................................................................169
39 Anhang B, Debugging Tips und Tricks..........................................................................................170
40 Anhang C, Versionsgeschichte von C............................................................................................171
41 Anhang D, Details zu C99 / C11....................................................................................................172
42 Index...............................................................................................................................................174
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 2/168
Eine Einführung in die Informatik und die Programmiersprache C
1 Einführung
1.1 Vorwort
In diesem Skript und in der Vorlesung können nur die theoretischen Grundlagen der Programmier-
sprache C und der strukturierten Programmentwicklung vermittelt werden, das Programmieren selbst
kann nur durch selbständiges Anwenden und Üben wirklich erlernt werden. Deshalb werden viele
Übungsaufgaben zum Stoff abgegeben und es ist Zeit für praktisches Arbeiten vorgesehen. Nutzen sie
diese Zeit, selbständiges Üben wird für ein erfolgreiches Abschliessen in diesem Fach vorausgesetzt!
Für viele Übungen sind auf dem Netzwerk oder beim Dozenten Musterlösungen erhältlich. Denken
Sie daran, im Gegensatz zu anderen Fächern gibt es in der Informatik nicht nur eine richtige Lösung
für ein Problem, sondern beliebig viele. (Natürlich gibt es aber noch mehr falsche Lösungen...). Die
Musterlösungen sind deshalb nur als eine von vielen möglichen Lösungen zu betrachten und sollen
Hinweise auf mögliche Lösungsstrategien geben. Vergleichen Sie Ihre Lösungen auch mit Ihren Kol-
legen, sie werden dabei viele unterschiedliche Lösungsansätze kennenlernen.
1.2 Hinweise
Um 1999, 2011 und 2018 sind neue Standards für die Programmiersprache C verabschiedet worden,
bekannt unter den Namen ANSI C99, C11 und C17/C18. In C99 und C11 wurde die Sprache um eini-
ge Elemente erweitert, in C17 nur Unstimmigkeiten im Standard C11 behoben.
Einige Erweiterungen werden in diesem Dokument ebenfalls aufgeführt, aber mit dem Vorsatz [C99/
C11] kenntlich gemacht. Diese Erweiterungen sind noch nicht auf allen Compilern implementiert, und
speziell bei älteren oder Mikrocontroller-Compilern ist damit zu rechnen, dass diese Features nicht un-
terstützt werden. Wenn auf höchste Kompatibilität Wert gelegt wird, sollte auf sie verzichtet werden.
(Die neuen Schlüsselwörter hingegen sollten nicht mehr als Bezeichner verwendet werden)
Das Kapitel über die Bibliotheksfunktionen ist ein Auszug der WEB-Seite von Axel Stutz und Peter
Klingebiel, FH Fulda, DVZ, unter 'http://www.fh-fulda.de/~klingebiel/c-stdlib/index.html' erreichbar.
Versionen
2.0 02.2003 Erste veröffentlichte Version.
2.1 10.2003 Leicht überarbeitet und Fehler korrigiert, mit Kapitel zu Extreme Programming ergänzt.
2.2 09.2005 Leicht überarbeitet und Fehler korrigiert, mit Kapitel zu systematischem Testen ergänzt.
2.3 09.2007 Leicht überarbeitet und Fehler korrigiert, mit Kapitel zu Mergesort, TDD und Testprotokoll ergänzt.
2.4/2.4.1 09.2008/09.2009 Leicht überarbeitet und Fehler korrigiert.
2.4.2 09.2010 Leicht überarbeitet, Ringbuffer, Stack und Queue hinzugefügt, auf Open Office portiert.
2.4.3 09. 2011 Detailkorrekturen, 11.: Combsort und Optimales Sortieren eingefügt.
2.4.4 09. 2012 Detailkorrekturen, Einführung und SWE leicht überarbeitet, Sequenzdiagramm eingefügt.
2.4.5 09. 2013 Detailkorrekturen, Hashtabelle mit Abschnitt zu beschleunigtem Vergleich ganzer Datensätze ergänzt.
2.4.6 09. 2014 Detailkorrekturen, Kapitel modulares Programmieren erweitert, Fehlerhandling und Refactoring in SWE ergänzt , C11, History
2.4.7/8 09. 2015 Detailkorrekturen (Bufferoverflow in Beispielen vermieden, %s in scanf(), gets() ), Um Architektur und Schnittstellenbsp. ergänzt
2.5 10. 2015 Anhang Details zu C99/C11 hinzugefügt (Makros für neue Typen).
2.5.1/2 02/09. 2016 State-event und Modulediadramm an UML-Notation angepass, / C11 History ergänzt um Makros für freiwillige Featurest, complex.h in Anhang
2.5.3/4/5 09.2018/19/20 Aufgabe Struktogramm ersetzt, Detailkorrekturen / V Modell Detailkorrekturen / Detailkorrekturen, added Python to History, new C++ Keywords
2.5.6 09.2021 Kapitel Vergleich Datenstrukturen hinzugefügt, kleinere Detailkorrekturen
Copyright
Dieses Skript darf in unveränderter Form für Unterrichtszwecke weitergegeben werden. Der Autor
freut sich aber über Rückmeldungen zum Einsatz des Skriptes.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 3/168
Eine Einführung in die Informatik und die Programmiersprache C
Eigentlicher Rechner
Prozessor
Steuerung Verarbeitung
Arbeitsspeicher (RAM)
Peripherie
Ein Computer besteht grundsätzlich aus einem Prozessor, Arbeitsspeicher (RAM) und Peripherie.
Im Arbeitsspeicher werden auszuführende Programme und gerade benötigte Daten abgelegt. Der In-
halt dieses Arbeitsspeichers hat die unangenehme Eigenschaft flüchtig zu sein, d.h. Beim Ausschalten
des Rechners gehen alle Daten verloren, welche nicht vorher auf ein anderes Medium kopiert wurden.
Unter Peripherie versteht man Ein-/Ausgabegeräte wie zum Beispiel:
die Tastatur (Eingabe von Daten)
den Monitor (Ausgabe/Anzeige von Daten)
Speichermedien wie Harddisk und USB-Stick (Austauschen oder Aufbewahren von Daten).
Weitere Peripherie wie Drucker, Netzwerkanschluss, CD/DVD-Laufwerke...
Der Prozessor ist für das Ausführen von Berechnungen und Programmen zuständig. Ein Programm
besteht aus einer Folge von einfachen Prozessor Anweisungen und Datenstrukturen, die im Arbeits-
speicher abgelegt sind. Der Prozessor holt sich der Reihe nach die Anweisungen aus dem Arbeitsspei-
cher, verarbeitet diese und legt die berechneten Ergebnisse wieder im Arbeitsspeicher ab. Der Arbeits-
speicher ist so organisiert, dass die einzelnen Speicherzellen von 0 an durchnummeriert werden. Der
Zugriff auf die Speicherzellen und die darin abgelegten Daten und Befehle erfolgt über diese Nummer
(Adresse). Die Darstellung des Arbeitsspeichers soll darauf hinweisen, dass die Daten sequentiell (li-
near, d.h. ein Datum nach dem anderen) im Speicher abgelegt sind.
1.5 Hochsprachen
Da es für Menschen sehr aufwendig und fehleranfällig ist, Programme direkt in Prozessoranweisungen
(Assembler) zu Schreiben, verwendet man im allgemeinen eine Hochsprache wie zum Beispiel C zum
Programmieren. Diese Hochsprachen werden aber vom Prozessor nicht direkt verstanden und müssen
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 4/168
Eine Einführung in die Informatik und die Programmiersprache C
deshalb mit weiteren Programmen wie Compiler oder Interpreter in Prozessorbefehle übersetzt wer-
den. Aus einem Befehl der Hochsprache wird dabei eine Sequenz von mehreren Prozessorbefehlen
(Maschinencode) erzeugt, und Variablennamen werden wieder in Speicheradressen übersetzt. Erst
diese Prozessorbefehle werden, sobald in den Arbeitsspeicher geladen, vom Prozessor verstanden und
ausgeführt. Interpreter wandeln die Hochsprachenbefehle zur Laufzeit des Programms jedes mal wie-
der einzeln um, Compiler wandeln das ganze Programm auf einmal in eine ausführbare Datei um, wel-
che anschliessend selbständig lauffähig ist. Compilierte Programme laufen üblicherweise deutlich
schneller (Faktoren) als interpretierte. Bei einigen Sprachen wird die Hochsprache in einen Zwischen-
code übersetzt, welcher dann seinerseits interpretiert wird, die dabei erreichte Laufgeschwindigkeit ist
besser als bei einem reinem Interpreter.
Nachfolgend einige typische Vertreter von compilierten und interpretierten Sprachen:
Python
C
B Objective C
C#
BCPL
C++
CPL
JAVA
ALGOL
OAK
FORTRAN SIMULA
Smalltalk
Eiffel
ADA
LISP
ObjektPascal
Pascal
COBOL
Borland Pascal Object
Modula
BASIC
Delphi
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 5/168
Eine Einführung in die Informatik und die Programmiersprache C
2 Grundlagen
2.1 Algorithmen und Struktogramme
Die allgemeine Vorgehensweise beim Programmieren lautet:
Problemanalyse (Woraus besteht das Problem, was genau ist zu tun)
Entwurf eines Algorithmus (einer Strategie) zur Lösung des Problems
Erstellung des Programms (inklusive alle Testphasen)
Das mag einem etwas banal vorkommen, aber beobachten Sie sich selbst einmal beim Arbeiten: Wie
viel Zeit geht allein dadurch verloren, dass man sich nicht richtig klar gemacht hat, wie das Problem
eigentlich aussieht?
Ein Algorithmus ist ein genau festgelegtes Ablaufschema für wiederkehrende Vorgänge, das nach ei-
ner endlichen Anzahl von Arbeitsschritten zu einem eindeutigen Ergebnis führt. Jeder Algorithmus
zeichnet sich dadurch aus, dass er absolut reproduzierbare Ergebnisse liefert. Das bedeutet, unter im-
mer gleichen Voraussetzungen bzw. Anfangsbedingungen muss ein bestimmter Algorithmus stets das-
selbe Endergebnis liefern.
Jeder Algorithmus kann in eine Abfolge von drei Typen von Handlungsvorschriften zerlegt werden:
Sequenz, Alternation und Iteration. Für die hier zu behandelnden Algorithmen gibt es ein Verfahren,
das die Arbeit im Umgang mit Algorithmen sehr erleichtern kann: Struktogramme. Ein Struktogramm
ist eine grafische Veranschaulichung eines Algorithmus. Für die drei Typen von Handlungsvorschrif-
ten gibt es jeweils eine bestimmte graphische Darstellung:
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 6/168
Eine Einführung in die Informatik und die Programmiersprache C
Struktogramme bieten die Möglichkeit, ein Problem systematisch anzugehen, indem man es zuerst nur
grob strukturiert, und anschliessend durch allmähliche Verfeinerung bis zum fertig ausgearbeiteten
Programmcode gelangt.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 7/168
Eine Einführung in die Informatik und die Programmiersprache C
Editor Editor
Editor
C-Source (.c)
C-Source (.c)
C-Source (.c)
Header
Header (.h)
(.h) Preprocessor
Header
Header(.h)
(.h) Preprocessor
Preprocessor
Preprocessed Source
Preprocessed
Preprocessed Source Source
Compiler Compilerfehler
Compiler Compiler
Objektdatei (.o)
ObjektdateiObjektdatei
(.o) (.o)
Bibliotheken
Linker Linkerfehler
Das obige Diagramm zeigt die wichtigsten Phasen, welche bei der Programmerstellung durchlaufen
werden. Mit dem Editor wird das C-Programm geschrieben und verändert. Der Quellcode des C-Pro-
gramms wird vor der Compilierung durch den Präprozessor bearbeitet. Dazu gehört:
das Entfernen von Kommentaren
das Ersetzen von Makros (#define)
das Einfügen gewisser Dateien (#include)
Der Compiler erzeugt aus der vom Präprozessor erzeugten Datei eine Objektcode-Datei, die aus Ma-
schinensprache und Linkeranweisungen besteht. Der Linker wiederum verknüpft die vom Compiler
erzeugte(n) Datei(en) mit den Funktionen aus Standardbibliotheken oder anderen, vom Programmierer
generierten Objekt-Code-Dateien. Wenn an einer Stelle dieses Prozesses ein Fehler auftritt, muss die
fehlerverursachende Sourcedatei korrigiert werden, und der Prozess beginnt von vorne. (Compiliert
werden müssen aber nur die veränderten Dateien). Die Verwaltung von mehreren Dateien in einem
Projekt, und die richtige Steuerung von Linker und Compiler wird bei allen modernen Compilersyste-
men von einer IDE (Integrated Developpment Environment) übernommen, der Programmierer muss
nur definieren, welche Dateien zum Projekt gehören, den Rest erledigt die IDE.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 8/168
Eine Einführung in die Informatik und die Programmiersprache C
Für die meisten Programmierumgebungen gibt es sogenannte Debugger-Programme, die einem bei
der Fehlersuche unterstützen. Man kann damit unter anderem das Programm schrittweise durchlaufen
und sich die Inhalte von Variablen und Speicher ansehen oder auch modifizieren. In den Übungen zu
diesem Kurs werden Sie eine solche Umgebung kennen lernen.
Programmierfehler können auch mit der printf()-Anweisung lokalisiert werden, indem man in-
nerhalb des Programms ständig den Inhalt von interessanten Variablen und den aktuellen Ort im Code
ausgibt, so kann der Programmablauf auch verfolgt, resp. rekonstruiert werden. Die Funktion
printf() wird später noch vorgestellt. Den Kreislauf - Editieren - Kompilieren - Linken - Ausfüh-
ren - Fehlersuche müssen Sie solange durchlaufen, bis das Programm fehlerfrei die gestellte Aufgabe
erfüllt. Mit einer guten Analyse und einem guten Entwurf kann der Kreislauf beträchtlich verkürzt
werden, da viele Denkfehler so bereits früh eliminiert werden.
int main(int argc, char *argv[]) /* Die Funktion main (das Hauptprogramm) */
/* beginnt hier. */
{
/* main ruft die Bibliotheksfunktion printf auf, um die
Zeichenfolge zu drucken */
printf("Hello, world\n");
return 0; /* Fehlerfreie Ausfuehrung anzeigen */
} /* Hier endet das Programm (die Funktion main() ) */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 9/168
Eine Einführung in die Informatik und die Programmiersprache C
/* ...und ausgeben */
printf("Die Fläche ist %d\n", Flaeche);
return 0;
}
2.3.2.1 Kommentare
Kommentare sollen das Verstehen und Nachvollziehen eines Programmes erleichtern, sie sind für den/
die Programmierer gedacht, und werden vom Compiler ignoriert. Unkommentierte Programme sind
schlecht wartbar, und nach kurzer Zeit selbst vom Autor meist nicht mehr einfach nachvollziehbar.
In C werden Kommentare zwischen /* und */ eingeschlossen, in C99 wurde neu auch der Zeilenkom-
mentar // (Ein Kommentar, der vom // bis zum Ende der Zeile geht) eingeführt.
2.3.2.2 Präprozessorbefehle
Die Zeilen mit dem Doppelkreuz # am Anfang bilden die Präprozessor-Direktiven. Das sind die Be-
fehle, die der Präprozessor vor dem Kompilieren ausführen soll. In unserem Beispiel:
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 10/168
Eine Einführung in die Informatik und die Programmiersprache C
#include<stdio.h>
bewirkt, dass der Compiler Information über die Standard-Ein/Ausgabe-Bibliothek an dieser Stelle
einfügt. Die Standard-Ein/Ausgabe-Bibliothek wird im Kapitel Bibliotheksfunktionen beschrieben.
2.3.2.3 main
int main(int argc, char* argv[])
{
/* Code von main */
return 0;
}
Jedes C-Programm muss mindestens die Funktion main() enthalten. Diese ist das eigentliche
Hauptprogramm und wird automatisch vom System aufgerufen wenn das Programm gestartet wird.
In unserem Beispiel ist main() die einzige selbstgeschriebene Funktion. Die Form von main()
(Rückgabewert und Argumente) ist vorgeschrieben. In den Argumenten argc und argv werden dem
Programm Informationen vom Betriebssystem übergeben (die Befehlszeilenargumente). Diese Argu-
mente können vom Programm ignoriert werden. main() muss dem Betriebssystem am Ende einen
Fehlercode zurückliefern, normalerweise 0 bei fehlerfreiem Programmablauf. Wenn das Programm
weitere Funktionen enthält, werden diese direkt oder indirekt von main() aufgerufen.
2.3.2.4 Anweisungen
Die geschweiften Klammern {} umgeben die Anweisungen, aus denen die Funktion besteht. Die
Funktion main() von HelloWorld enthält nur eine Anweisung:
printf("Hello, world");
Die Funktion printf wird mit dem Argument "Hello, world\n" aufgerufen. printf() ist
eine Bibliotheks-Funktion, die Ausgaben erzeugt; in diesem Fall wird die Zeichenkette (engl. string)
zwischen den doppelten Anführungszeichen ('Gänsefüsschen') ausgegeben.
Die Zeile
Flaeche = Laenge * Breite;
ist ebenfalls eine Anweisung, es soll nämlich das Produkt der Variablen Laenge und Breite gebildet
und in der Variablen Flaeche abgespeichert werden.
2.3.2.5 Zeichensatz
Jede Sprache besteht aus Wörtern, die selbst wieder aus Zeichen aufgebaut sind. Die Wörter der Spra-
che C können nur aus folgenden Zeichen bestehen:
den 26 Grossbuchstaben:
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
den 26 Kleinbuchstaben:
a b c d e f g h i j k l m n o p q r s t u v w x y z
den 10 Ziffern:
1 2 3 4 5 6 7 8 9 0
den 29 Sonderzeichen:
! „ # : . { } ^ % & ‘ ; [ ] ~ < = > ? | \ _ ( ) * + , - /
den Zwischenraumzeichen:
Leerzeichen, horizontaler und vertikaler Tabulator, neue Zeile und neue Seite
Der Ausführungs-Zeichensatz (Wird zur Laufzeit für die Ein- und Ausgaben des Programms verwen-
det) enthält darüber hinaus noch folgende Zeichen:
Das Null-Zeichen ’\0’, um das Ende von Strings zu markieren.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 11/168
Eine Einführung in die Informatik und die Programmiersprache C
In der nachfolgenden Tabelle sind alle Schlüsselwörter von C und C++ aufgelistet, ANSI-C 89 Schlüs-
selworte sind Fett gedruckt, ANSI-C 99 / C11 Schlüsselworte sind Fett mit dem Vorsatz [C99/
C11] gedruckt, zusätzliche C++ Schlüsselworte sind schwach kursiv gedruckt
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 12/168
Eine Einführung in die Informatik und die Programmiersprache C
2.3.2.7 Bezeichner
Bezeichner (engl. identifier) sind Namen von Variablen, Funktionen, Makros, Datentypen usw. Für die
Bildung von Bezeichnern gelten die folgenden Regeln:
· Ein Bezeichner besteht aus einer Folge von Buchstaben (A bis Z, a bis z), Ziffern (0 bis 9)
und Unterstrich (_).
· Das erste Zeichen darf keine Ziffer sein.
· Gross- und Kleinbuchstaben werden unterschieden.
· Ein Bezeichner kann beliebig lang sein. Signifikant sind in der Regel nur die ersten 31 Zei-
chen. (in C99 die ersten 63)
· Der Linker (Globale Bezeichner, extern) muss nur 6 Zeichen beachten und darf Gross/Klein
ignorieren, (in C99 31 Zeichen und muss Gross/Klein berücksichtigen).
Bezeichner werden vom Programmierer vergeben, oder sind von Bibliotheken (Z. B. der Standardbi-
bliothek) vorgegeben. Schlüsselwörter sind reserviert und dürfen nicht als Bezeichner verwendet wer-
den.
Achtung, Bezeichner die mit einem Unterstrich beginnen sind für den Compilerhersteller reserviert,
und sollten nicht verwendet werden.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 13/168
Eine Einführung in die Informatik und die Programmiersprache C
3 Datentypen
3.1 Basisdatentypen
C ist eine stark typisierte Programmiersprache, das heisst dass stark zwischen verschiedenen Arten
von Daten unterschieden wird. Man unterscheidet grundsätzlich zwischen einfachen Datentypen und
zusammengesetzten Datentypen. In diesem Kapitel werden die einfachen Datentypen vorgestellt. Alle
einfachen Datentypen in C sind für skalare Werte definiert.
Bei den einfachen Datentypen wird zunächst zwischen Integer (Ganze Zahlen) und Fliesskomma Da-
tentypen unterschieden. Zeichenketten (Strings) sind ein Spezialfall von Feldern (Arrays), welche in
einem späteren Kapitel behandelt werden. Es gibt zwar Stringliterale (Konstanten), aber keinen eigent-
lichen String-Datentypen.
Ganzzahlige Datentypen sind int und char, Fliesskommatypen sind float und double. Der Da-
tentyp char ist eigentlich zur Aufnahme von einzelnen Buchstaben (ASCII-Codes) vorgesehen, kann
aber auch für kleine Zahlen (8-Bit-Werte) benutzt werden. Die ganzzahligen Datentypen können zu-
sätzlich mit den Qualifizierern signed und unsigned explizit als vorzeichenbehaftet (positiv und
negativ), oder vorzeichenlos (nur positiv) festgelegt werden. Die Grösse des Datentyps int kann zu-
dem mit den qualifizierern short und long modifiziert werden. Alle int Typen sind per Default
vorzeichenbehaftet. Beim Datentyp char ist nicht festgelegt (!), ob er mit oder ohne Vorzeichen im-
plementiert ist. (Siehe auch Kapitel 41.1 für exakt definierte Datentypen seit C99).
Datentyp (Siehe auch Kap 41.1) Bits *2) Wertebereich Dez Stellen Literal (Konstante)
[C99]_Bool, bool >=8 0 und 1 - true, false, 0, 1
char 8 -128 ... +127 2 'a' '\n' 'B'
*1)
oder 0 ... 255
unsigned char 8 0 ... 255 2 'a' '\n' 'B'
signed char 8 -128 ... 127 2 'a' '\n' 'B'
short int 16 -32768 ... 32767 4 ---
unsigned short int 16 0 ... 65535 4 ---
signed short int 16 -32768 ... 32767 4 ---
31 31
int 16-32 -2 ...2 -1 4-9 123 0x123 077 -44
32
unsigned int 16-32 0 ... 2 -1 4-9 123u 0x123U 077u
31 31
signed int 16-32 -2 ...2 -1 4-9 123 0x123 077
31 31
long int 32 -2 ...2 -1 9 123L 0x123L 077 -44L
32
long unsigned int 32 0 ... 2 -1 9 123uL 0x123uL 077UL
31 31
long signed int 32 -2 ...2 -1 9 123L 0x123L 077L
63 63
[C99] long long int >= 64 -2 ...2 -1 >18 123LL 0x123LL 077LL
64
[C99] unsigned long long int >= 64 0 ... 2 -1 >18 12uLL 0x123ULL 07ULL
63 63
[C99] signed long long int >= 64 -2 ...2 -1 >18 123LL 0x123LL 077LL
38 38
float 32 -3.4*10 ...3.4*10 6 1.23f 3.14f 1e-10f 0.0f
308 308
double 64 -1.7*10 ..1.7*10 15 1.23 3.14 1e-10 0.0
4932
long double 64...80 +/-1.18*10 15-19 1.23L 3.14L 1e-10L 0.0L
char * (String) n*8 - - "Hallo" "Welt \n"
31 32 63 18 64 18
2 = 2'147'483'648, 2 = 4'294'967'296, 2 = 9.22*10 , 2 = 18.4*10
*1)
Der C-Standard lässt es dem Compilerhersteller frei, ob char signed oder unsigned imple-
mentiert wird (Im Zweifelsfall explizit angeben oder Nachschlagen) !!!
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 14/168
Eine Einführung in die Informatik und die Programmiersprache C
*2)
Der Wertebereich der Datentypen ist vom Standard nicht festgelegt, es ist einzig vorgeschrieben
das char <= short int <= int <= long [[C99] <= long long ]] und float <= dou-
ble <= long double. Angegeben wurden heute übliche Werte. (Siehe auch Kapitel 41.1)
Bei unsigned, signed, short, long und [[C99]long long] kann das Schlüsselwort int weg-
gelassen werden
[C99] In stdint.h werden, sofern vom Zielsystem unterstützt, folgende Typedefs definiert:
intN_t, uintN_t (Genau N Bits), int_leastN_t, uint_leastN_t (Mindes-
tens N Bits), int_fastN_t, uint_fastN_t (Schnellster mit mindestens N Bits),
intptr_t, uintptr_t (Gross genug für Pointer), intmax_t, uintmax_t
(Grösster int). Zudem werden entsprechende Makros für Suffixes von Konstanten sowie die
Formatstrings in printf() und scanf() definiert. (Siehe Kapitel 41.1 Makros für Formatanwei-
sungen und Literale der Integerdatentypen Seite 164).
[C99] Seit C99 werden auch komplexe Zahlen unterstützt (complex.h), ab C11 freiwillig.(Siehe Ka-
pitel 41.2, Seiten 165)
3.2 Qualifizierer
Alle Datentypen (Einfache und Zusammengesetzte) können zusätzlich noch mit den Qualifizierern
const, volatile, [C99] restrict (nur für Pointer) versehen werden, auch dabei ist int je-
weils redundant.
Mit const bezeichnet man Variablen, welche nur gelesen werden sollen (Also Konstanten), wobei
Schreibzugriffe darauf trotzdem möglich sind, aber das Ergebnis ist undefiniert. (const erlaubt dem
Compiler, gewisse Optimierungen vorzunehmen)
const float Pi = 3.1416f; /* Konstante definieren */
int Flaeche;
Flaeche = r * r * Pi; /* OK So ists gedacht */
Pi = 17; /* Nicht verboten, aber Ergebnis undefiniert */
Mit volatile bezeichnet man Variablen, welche 'flüchtig' sind. Das sind Variablen die ändern kön-
nen ohne dass das dem Compiler ersichtlich ist. Mit volatile zwingt man den Compiler, den Wert
dieser Variablen bei jeder Benutzung erneut aus dem Speicher zu lesen, Schreibzugriffe immer und
sofort durchzuführen, und mehrfaches aufeinanderfolgendes Lesen oder Schreiben nicht wegzuopti-
mieren. Das ist wichtig bei Speicheradressen und Variablen, die Zustände von Hardwarekomponenten
anzeigen, oder von der Hardware oder Interruptroutinen verändert werden.
volatile int Tastenzustand; /* Wird von Interrupt gesetzt */
Tastenzustand = 0;
while (Tastenzustand == 0) { /* Ohne volatile koennte der Compiler */
/* Warten auf Taste */ /* daraus eine Endlosschlaufe erzeugen */
} /* da er nicht wissen kann das der */
/* Zustand Tastenzustand waehrend der */
/* Schleife aendern kann */
[C99] Mit restrict können Zeiger qualifiziert werden, bei denen der Programmierer garantiert,
dass innerhalb ihrer Lebensdauer nie andere Zeiger auf die selben Werte zeigen. Dies eröffnet
dem Compiler weitere Optimierungsmöglichkeiten. Ein Compiler muss das Schlüsselwort ak-
zeptieren, darf es aber ignorieren.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 15/168
Eine Einführung in die Informatik und die Programmiersprache C
typedef short int I16; /* Definiert einen neuen Typ I16, der */
/* einem short int entspricht */
typedef unsigned short int U16; /* Definiert einen neuen Typ U16, der */
/* einem unsigned short int entspricht */
Mit typedef können einerseits häufig verwendete, komplizierte Datentypen mit einem einfacheren
Namen benannt werden, die Benutzung von typedef erlaubt aber auch eine einfachere Portierung
von Programmen. Wenn z. B. wichtig ist, dass bestimmte Variablen immer 32Bit gross sind, definiert
man einen Typ I32 (Oder ähnlicher Name), benutzt diesen Typ für all diese Variablen, und beim
typedef setzt man den Typ I32 auf int bei Sytemen in welchen int 32 Bit ist, und auf long int
bei Systemen in welchen long 32 und int 16 Bit ist.
typedef int I32; /* Bei Systemen mit sizeof(int) == 4 */
Typedefs können auch zur Erzeugung von komplizierten Typen eingesetzt werden:
Frage: Wie deklariere ich ein Array a von N Zeigern auf Funktionen ohne Argumente, welche Zei-
ger auf argumentlose Funktionen welche ihrerseits Zeiger auf char zurückliefern, zurückliefern?
Antwort 2:
char *(*(*a[N])(void))(void); /* Alles klar ??? */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 16/168
Eine Einführung in die Informatik und die Programmiersprache C
4 Konstanten
Bei Konstanten (Literalen) wird der Datentyp durch die Schreibweise festgelegt. Unter C99 wurden
neu auch zusammengesetzte Literale für Arrays und Strukturen eingeführt, diese werden in den ent-
sprechenden Kapiteln näher beschrieben.
Achtung, auch eine Buchstabenkonstante ist nur ein numerischer Wert, nämlich einfach der ASCII-
Code des Zeichens. Mit Buchstabenkonstanten kann deshalb gerechnet werden wie mit jeder anderen
Zahl auch:
'A' + 2 /* Ergibt 67 oder 'C' */
'x' / 2 /* Macht weniger Sinn, aber ergibt 60 oder '<' */
'Z' - 'A' /* Ergibt die Anzahl Buchstaben zwischen A und Z */
Für wchar_t, char16_t und char32_t Literale werden die Präfixes L, u und U benutzt: L'A' ist ein A
vom Type wchar_t, u'A' ein A vom Typ char16_t und U'A' ein A vom Typ char32_t. (Unicode).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 17/168
Eine Einführung in die Informatik und die Programmiersprache C
Für erweiterte Zeichensätze (Z.B. Unicode) stehen die Präfixe u8 (UTF-8 String), L u und U zur Ver-
fügung. Z. B. u8"Hallo Welt" für einen UTF8 codierten String.
4.3 Integerkonstanten:
Der Typ der Konstanten wird durch ein angehängtes Suffix kenntlich gemacht, es spielt keine Rolle, ob
der Suffix mit Gross- oder Kleinbuchstaben geschrieben wird, bei L/l empfiehlt sich aber aus Ver-
wechslungsgründen dringend das grosse 'L':
Ganzzahlige Konstanten können auch in hexadezimaler oder oktaler Schreibweise angegeben werden.
Hexadezimale Konstanten werden durch ein führendes 0x gekennzeichnet, oktale mit einer führenden
0. Achtung 033 ist somit nicht 33 Dezimal, sondern 33 Oktal, also 27 Dezimal.
4.4 Fliesskommakonstanten:
Fliesskommakonstanten werden durch einen Dezimalpunkt und/oder die Exponentialschreibweise als
solche kenntlich gemacht. Float und long double werden wiederum durch Suffixes gekennzeich-
net, Fliesskommakonstanten ohne Suffix sind vom Typ double:
Konstante Typ
0.0 double
3.14
1e5
1.0f float
1e-20L long double
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 18/168
Eine Einführung in die Informatik und die Programmiersprache C
5 Variablen
Um einen Computer sinnvoll nutzen zu können, müssen die anfallenden Werte irgendwo gespeichert
werden können. Der Speicher eines Computers ist eigentlich nichts anderes als eine grosse Ansamm-
lung von durchnumerierten Speicherplätzen. Ein Computer mit 128Mbyte Speicher hat dementspre-
chend 134217728 Speicherplätze, die von 0 bis 134217727 durchnumeriert sind. Es wäre aber sehr
umständlich, die Speicherplätze selbst zu verwalten, und jedesmal über ihre Nummer (Adresse) anzu-
sprechen. Deshalb werden für die Speicherplätze Namen vergeben, diese benannten Speicherplätze
werden als Variablen bezeichnet. Der Compiler vergibt automatisch den nächsten freien Platz, wenn
eine neue Variable definiert wird, um die Adresse (Position im Speicher) der Variable braucht sich der
Programmierer meist nicht zu kümmern (Ausnahme: Pointer).
Es ist auch möglich, mehrere Variablen desselben Typs auf einmal zu definieren, hinter dem Datentyp
folgen die einzelnen Namen durch Kommas getrennt:
int Laenge, Breite, Hoehe;
Anhand des Datentyps weiss der Compiler, wieviel Speicherplatz eine Variable benötigt, und kann
auch die passenden arithmetischen Operationen bei Berechnungen auswählen.
Ein Variablenname (Bezeichner) kann aus Buchstaben (Keine Umlaute und Sonderzeichen, nur a-z
und A-Z), Ziffern und Unterstrich (_) bestehen, wobei an erster Stelle keine Ziffer stehen darf. Die
Länge des Namens darf bis zu 31 ([C99] 63) Buchstaben bestehen, vom Linker werden 6 ([C99] 31)
berücksichtigt. Bezeichner dürfen auch länger sein, aber der Compiler betrachtet nur die ersten n Zei-
chen als signifikant, Variablen, die sich in den ersten n Zeichen nicht unterscheiden gelten als gleich.
Der Compiler unterscheidet zwischen Gross- und Kleinschreibung, der Linker muss nach Standard
nicht zwischen Gross und Kleinschreibung unterscheiden (Die meisten Linker achten aber darauf, und
in C99 muss ein Linker auch die Gross/Kleinschreibung beachten).
Achtung! Variablen dürfen nur ausserhalb von Funktionen, oder direkt nach der öffnenden geschweif-
ten Klammer eines Code-Blockes definiert werden.
int Laenge = 15; /* OK, globale Variable, ausserhalb jeder Fkt */
int Test(void)
{
double Example = 3.14; /* OK, Definition zu Beginn des Blockes */
Example *= 3; /* Ausfuehrbarer Code */
int k = 7; /* Fehler, nur zu Beginn eines Blockes moeglich */
return k;
}
[C99] In C99 dürfen Varablen wie in C++ auch mitten im Code definiert werden, sowie im Initi-
alisierungsteil der for()-Schlaufe. Sie dürfen aber erst nach der Deklaration benutzt wer-
den.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 19/168
Eine Einführung in die Informatik und die Programmiersprache C
Wenn mehrere Variablen auf einer Zeile definiert und initialisiert werden, müssen sie einzeln initiali-
siert werden:
int x=17, y=17, z=25;
Bei der Initialisierung wird der Variable gleich bei der Erzeugung ein bestimmter Wert zugewiesen.
Nicht initialisierte globale Variablen werden automatisch auf den Wert 0 gesetzt, lokale Variablen hin-
gegen haben zufälligen Inhalt, wenn sie nicht initialisiert werden!!!!
Auf den Unterschied zwischen globalen und lokalen Variablen wird im Kapitel Funktionen noch näher
eingegangen.
5.4 Speicherklassen:
Alle Variablen in einem Programm werden verschiedenen Speicherklassen zugeordnet. Es gibt die
Klasse Static, die Klasse Automatic und die Klasse Register.
Die Zuordnung zu den Speicherklassen erfolgt mit den Schlüsselworten register, auto,
static und extern sowie dem Ort der Definition (Innerhalb oder ausserhalb von Blöcken).
Globale Variablen (Ausserhalb von Blöcken definierte) und static Variablen gehören zur Klasse
Static. Sie werden zu Beginn des Programmlaufes einmal initialisiert und sind bis zum Ende des Pro-
grammlaufes ständig verfügbar, sie existieren während des gesamten Programmlaufs.
Nur Variablen innerhalb von Blöcken (Mit {} eingeschlossene Anweisungen) können zu den Klassen
Automatic oder Register gehören. Variablen der Speicherklasse Automatic werden automatisch er-
zeugt, wenn der Programmlauf in ihren Block eintritt, und wieder gelöscht, wenn der Block verlassen
wird. Alle Variablen, die innerhalb von Blöcken definiert werden, sind per Default automatische Vari-
ablen, deshalb wird das Schlüsselwort auto eigentlich kaum verwendet.
Variablen der Speicherklasse Register werden in Prozessorregister abgelegt, wenn dies möglich ist.
Das ist sinnvoll für Variablen die besonders oft verwendet werden. Auf Variablen dieser Klasse kann
jedoch der Adressoperator nicht angewendet werden, da Register keine Speicheradresse besitzen. Bei
modernen Compiler ist das Schlüsselwort register eigentlich auch überflüssig, da sie sehr gute
Optimierungsalgorithmen besitzen und Codeanalysen durchführen, um selbst oft gebrauchte Variablen
und Werte zu identifizieren, und diese selbständig in Register ablegen. Mit dem Schlüsselwort re-
gister kann man diese Optimierungen sogar stören oder zunichte machen.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 20/168
Eine Einführung in die Informatik und die Programmiersprache C
5.5 Lebensdauer
Die Lebensdauer einer Variable hängt vom Ort ihrer Definition und ihrer Speicherklasse ab. Globale
Variablen und static-Variablen Leben solange wie das Programm läuft. Automatische und Registerva-
riablen beginnen zu existieren, sobald der Programmlauf bei ihrer Definition ankommt, und hören auf
zu existieren, sobald der Programmlauf den Block verlässt, in dem die Variable definiert wurde.
int g; /* Globale Variable, lebt waehrend ganzem Programmlauf */
static int i; /* Lokale Variable, lebt waehrend ganzem Programmlauf */
5.6 Sichtbarkeit
Unter Sichtbarkeit versteht man, von wo aus auf eine Variable zugegriffen werden kann. Auf Varia-
blen, die innerhalb von Blöcken definiert werden, kann nur ab Definition innerhalb dieses Blockes
(und darin eingebettete Blöcke) zugegriffen werden. Wenn ausserhalb des Blockes eine Variable glei-
chen Namens existiert, wird sie durch die Blockvariable verdeckt und ist nicht sichtbar. Auf globale
Variablen (ohne static ausserhalb jedes Blockes definiert) kann von überall her zugegriffen wer-
den, auch von anderen, zum Projekt gehörenden Modulen (In den anderen Modulen muss diese Varia-
ble aber als extern deklariert werden). Auf Modullokale (Ausserhalb jedes Blockes static defi-
niert) kann nur von Funktionen innerhalb dieses Moduls (Datei) zugegriffen werden.
int g; /* Von ueberall her sichtbar (Auch anderen Dateien) */
static int i; /* Nur in dieser Datei sichtbar */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 21/168
Eine Einführung in die Informatik und die Programmiersprache C
Wenn die Ausgabe in hexadezimaler Notation erfolgen soll, muss x anstelle von d verwendet werden.
(Die Zahl wird als unsigned betrachtet)
%x für Daten vom Typ int und unsigned int, (geht auch für unsigned char)
%hx für Daten vom Typ short und unsigned short
%lx für Daten vom Typ long und unsigned long
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 22/168
Eine Einführung in die Informatik und die Programmiersprache C
Wenn auf Formatierungen verzichtet wird, ergeben sich die folgenden Anweisungen:
%c für Daten vom Typ char, liest einen Buchstaben ein, speichert dessen ASCII-Code ab
%d für Daten vom Typ int, liest eine ganze Zahl ein und speichert diese ab
%hd für Daten vom Typ short, liest eine ganze Zahl ein und speichert diese ab
%ld für Daten vom Typ long, liest eine ganze Zahl ein und speichert diese ab
Wenn die Eingabe in hexadezimaler Notation erfolgen soll, muss x anstelle von d verwendet werden.
(Die Zahl wird als unsigned betrachtet)
%x für Daten vom Typ int und unsigned int
%hx für Daten vom Typ short und unsigned short
%lx für Daten vom Typ long und unsigned long
Zum Einlesen von Strings muss %s benutzt werden, Achtung, bei Stringvariablen darf der &-Operator
nicht benutzt werden. Strings werden zudem nur bis zum ersten Whitespace (Leerzeichen, Tabulator,
Zeilenvorschub...) gelesen. Mit %s können somit nur einzelne Worte eingelesen werden. Bufferover-
flows vermeidet man mit einer Längenbegrenzung bei %s: z. B %20s liest maximal 20 Zeichen
Da scanf() wie erwähnt beim Einlesen unbenutzte Zeichen im Tastaturpuffer zurücklässt, sollte der
Puffer vor dem nächsten Einlesen geleert werden (Oder die ganze Zeile mit gets_s() auf einmal
eingelesen werden und erst anschliessend mit sscanf() konvertiert werden):
scanf("%d", &i); /* Einlesen */
while (getchar() != '\n') {}; /* Puffer leeren (Lesen bis Zeilenvorschub)*/
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 23/168
Eine Einführung in die Informatik und die Programmiersprache C
[C99] C99 erlaubt die Bezeichner %hhu und %hhd um Zahlen in (unsigned) char einzulesen oder
auszugeben.
Beispiel:
double l, b; /* Variablen definieren */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 24/168
Eine Einführung in die Informatik und die Programmiersprache C
7 Operatoren
Mit Operatoren können ein oder zwei (beim ?-Operator sogar drei) Werte miteinander verknüpft wer-
den. Jeder Operator liefert ein der Verknüpfung entsprechendes Ergebnis zurück. Der Typ des Ergeb-
nisses hängt vom Operator und den beteiligten Operanden ab. Bei den arithmetischen Operatoren ist
der Ergebnistyp derselbe wie jener der Operanden, wenn alle beteiligten Operanden von selben Typ
sind. Ansonsten werden die Operanden in den Typ des genauesten (Grössten) beteiligten Operanden
umgewandelt, und erst anschliessend die Berechnung durchgeführt. Das Ergebnis ist ebenfalls von
diesem Typ.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 25/168
Eine Einführung in die Informatik und die Programmiersprache C
Eine Zuweisung kann mit den meisten Operatoren zu einer kombinierten Zuweisung zusammengefasst
werden:
Anstelle von a = a + b kann a += b geschrieben werden.
Operator Beschreibung Beispiel
= Einfache Zuweisung, weist einer Variablen einen Wert zu a = b
c = 17
+= Kombinierte Zuweisung, addiert den Wert b zu der Variablen a. a a += b
+= b ist die Kurzform von a = a + b
-= Weitere kombinierte Zuweisungen, siehe Beschreibung bei +=. a %=b
a &= b
/= *= %= a /= b entspricht a = a / b, a *= b
&= |= ^= a <<= b entspricht a = a << b
<<= >>=
Beispiele:
int a = 3, b = 4, c = 1;
a += b; /* Gleich wie a = a + b */
c <<= 3; /* Gleich wie c = c << 3 */
b = a + b;
a %= c; /* Gleich wie a = a % c */
b &= c; /* Gleich wie b = b & c */
a = b == c; /* Weist a 0 zu wenn b ≠ c und 1 wenn b gleich wie c ist */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 26/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 27/168
Eine Einführung in die Informatik und die Programmiersprache C
7.7 Typumwandlungen
Eine Typumwandlung bewirkt, dass der Wert eines Ausdrucks einen neuen Typ erhält. Dies ist nur für
skalare Typen möglich, also für arithmetische Typen und Zeiger. Die Typumwandlung wird stets so
durchgeführt, dass der Wert erhalten bleibt, sofern der Wert mit dem neuen Typ darstellbar ist.
Eine Typumwandlung kann implizit sein, d.h. sie wird vom Compiler automatisch vorgenommen,
oder explizit, d.h., sie wird durch die Anwendung des Cast-Operators erzwungen.
(Beim ternären Operator (?:) wird nach dem gleichen Prinzip ein gemeinsamer Typ aus den beiden möglichen Operanden gebildet (Bei
a?b:c wird nach diesen Regeln aus b und c der Rückgabetyp bestimmt, wenn b und c Unions, Strukturen oder Pointer sind, gelten spezi -
elle Regeln: Bei Union und Strukturen müssen b und c vom selben Typ sein, bei Pointer müssen b und c kompatibel sein, void * domi-
niert, Qualifizierer (const, volatile) dominieren, d.h. wenn b oder c void* sind ist das Resultat auch vom Typ void*, wenn b oder c ein
const oder volatile Pointer ist, ist der Resultattyp auch ein const, volatile oder const volatile Pointer))
Beispiel:
int a = 5; long l = 4L; float f = 3.14f; double d = 2.0;
a = a * l + a * f * d;
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 28/168
Eine Einführung in die Informatik und die Programmiersprache C
Beispiele:
int a = 5, b = 2;
float f = 0.0;
int c = 257;
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 29/168
Eine Einführung in die Informatik und die Programmiersprache C
7.8 Präzedenzen
Wie in der gewöhnlichen Mathematik gibt es auch in C eine Operatorrangfolge, nach deren Regeln
die Rechenreihenfolge der Berechnungen definiert ist (Analog zu der Regel 'Punkt vor Strich'). Diese
Reihenfolge kann durch das setzten von Klammern ausser Kraft gesetzt werden, Klammern haben die
höchste Priorität. Es empfiehlt sich, auch Klammern zu setzen wo es nicht nötig ist, Klammern schaf-
fen Klarheit.
In der nachfolgenden Tabelle stehen die Operatoren nach Priorität geordnet, die mit der höchsten Prio-
rität stehen zuoberst. Die Operatoren innerhalb eines Feldes haben dieselbe Priorität.
Die Assoziativität definiert, ob bei gleichrangigen Operatoren von rechts nach links, oder von links
nach rechts gerechnet wird.
Beispiel:
Setzen Sie Klammern nach den Präzedenzregeln, und bestimmen Sie den Wert des Ausdrucks:
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 30/168
Eine Einführung in die Informatik und die Programmiersprache C
8 Anweisungen/Statements
8.1 Ausdrücke/Expressions
Ausdrücke (Expressions) sind im wesentlichen nichts anderes als Berechnungen. Ein Ausdruck ist eine
erweiterte Rechenvorschrift, welche ein Ergebnis/Resultat liefert.
Beispiele für Ausdrücke:
(12+Durchschnitt)/15
Radius*2.0*3.1416
sin(2.0*3.1416*f)+0.5*sin(4.0+3.1416*f)
Ausdrücke können eine beliebige Anzahl von Operatoren, Operanden und Kombinationen derselben
enthalten, es können auch Klammern gesetzt werden und Funktionsaufrufe benutzt werden. Die Funk-
tionen müssen aber einen Wert zurückliefern.
Je nach Datentyp von Expression wird einer der Ausdrücke Expressionx ausgewählt, wenn
keiner passt wird der Ausdruck bei default genommen. Wenn default fehlt ergibt sich
ein Compilerfehler wenn kein passender Datentyp angegeben ist. Die Auswahlliste mit den
Datentypen kann beliebig lang sein, sie muss aus einer durch Kommas getrennten Folge von
Elementen in der Form Datentyp:Ausdruck sein.
Die generische Auswahl wird meist zusammen mit Makros verwendet, da nur hier eine
Typabhängigkeit auftreten kann. Im normalen Programmiereralltag wird dieses Konstrukt
wohl eher selten eingesetzt, es wird vorallem beim Programmieren von Bibliotheken einge-
setzt. Ein Beispiel dafür ist die Headerdatei tgmath.h, in dieser Datei werden die mathe-
matischen Funktionen aus math.h und complex.h mit _Generic definiert, so dass beim Aufruf
einer mathematischen Funktion vom Compiler die passende Variante ausgewählt wird, bei-
spielsweise wird bei sin(x) je nach Datentyp von x sin(), sinf(), sinl(), csin(), csinf() oder
csinl() ausgewählt.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 31/168
Eine Einführung in die Informatik und die Programmiersprache C
int f = 3.14f;
long l = 123456789;
int i = 17;
MagicPrint(f);
MagicPrint(l);
MagicPrint(i);
8.4 Blöcke
Mehrere Anweisungen können mit geschweiften Klammern zu einem Block und so zu einer einzigen
Anweisung (compound statement) zusammengefasst werden:
{
int Radius = 3; /* Deklarationen */
static long Flaeche;
++Radius; /* Anweisungen */
Flaeche = Radius*Radius*3.1416;
printf("Hallo");
if(Flaeche >= 34)
{ ... } /* noch ein Block */
}
Das Einrücken des Blockinhaltes ist nicht erforderlich, erhöht aber die Übersicht im Code und erleich-
tert das Verstehen des Codes. Jeder gute Programmierer wird seinen Code so übersichtlich wie mög-
lich gestalten.
Am Anfang eines neuen Blocks dürfen Variablen definiert werden. Variablen, die innerhalb eines
Blocks definiert werden, gelten nur innerhalb des Blocks, und verdecken Variablen des gleichen Na-
mens von ausserhalb des Blockes. Sobald der Block verlassen wird, sind innerhalb dieses Blocks defi-
nierte Variablen nicht mehr gültig und verlieren ihren Inhalt (Ausser statische Variablen).
[C99] Ab C99 dürfen Variablen an beliebigen Stellen innerhalb eines Blockes definiert werden,
können aber erst nach ihrer Definition benutzt werden.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 32/168
Eine Einführung in die Informatik und die Programmiersprache C
9 Kontrollstrukturen
Da eine einfache Abfolge von Befehlen für viele Programme nicht ausreicht, gibt es Anweisungen zur
Steuerung des Programmflusses. Dazu stehen dem Programmierer folgende Anweisungen zur Verfü-
gung:
Verzweigungen mit if else oder switch,
Schleifen mit while, do while oder for,
Unbedingte Sprünge mit goto, continue, break oder return.
9.1 Verzweigungen
Mit Verzweigungen können abhängig von Bedingungen bestimmte Codeteile ausgeführt oder ignoriert
werden. Damit kann auf verschiedene Daten unterschiedlich reagiert werden.
Anweisung A
if(Ausdruck)
Anweisung B
else
Anweisung C
Anweisung D
Ausdruck muss einen skalaren Typ haben. Zuerst wird der if-Ausdruck ausgewertet. Ist das Ergebnis
ungleich 0, d.h. wahr, wird die Anweisung B ausgeführt. Andernfalls wird bei vorhandenem else-
Zweig die Anweisung C ausgeführt.
Es gibt häufig auch den Fall, dass der else-Zweig mit der Anweisung C entfällt:
Anweisung A
if(Ausdruck)
Anweisung B
Anweisung D
Wenn der Ausdruck falsch ist, wird nach A also sofort D abgearbeitet.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 33/168
Eine Einführung in die Informatik und die Programmiersprache C
Beispiel:
/* Eine zahl einlesen und auf Null testen */
#include <stdio.h>
Wenn die der if-Abfrage folgende Anweisung aus mehreren Anweisungen besteht, müssen diese in
einem Block zusammengefasst werden (In geschweiften Klammern eingeschlossen werden). Entspre-
chendes gilt für die auf else folgenden Anweisung(en). Grundsätzlich empfiehlt es sich, auch ein-
zelne Anweisungen in Klammern zu setzen.
Fehlermöglichkeiten:
Das Zusammenfassen mehrerer Instruktionen zu einem Block wird vergessen, oder direkt hinter dem
if() ein Semikolon gesetzt (= leere Anweisung), und statt "if(i == 0)" wird vor allem von An-
fängern oft "if(i = 0)" geschrieben (Dies ist eine gültige Syntax und wird vom Compiler höchs-
tens mit einer Warnung geahndet, es wird der Variable i der Wert 0 zugewiesen, und der Wert des
Ausdrucks ist 0, und somit falsch).
Beispiel:
Bei der Eingabe von Werten durch den Benutzer muss mit Fehleingaben gerechnet werden. Zum Bei-
spiel mit ungültigen Zeichen bei der Eingabe von Zahlen. Solche Fehler können (und sollten) bereits
bei der Eingabe abgefangen werden. Die Funktion scanf() gibt einen Integerwert zurück, der die
Anzahl der erfolgreichen Umwandlungen angibt: Wenn das Einlesen eines oder mehrerer Werte nicht
erfolgreich war, ist die Anzahl der erfolgreichen Umwandlungen kleiner als die Anzahl der einzule-
senden Argumente. Programmseitig kann man diese Eigenschaft wie folgt nutzen:
int i;
int n; /* Variable zum speichern der Anzahl gelesenen Eingaben */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 34/168
Eine Einführung in die Informatik und die Programmiersprache C
9.1.2 switch
Mit der switch-Anweisung kann eine Mehr-
Anweisung A fachverzweigung realisiert werden. Der Wert des
Ausdruck switch-Ausdrucks wird mit den Konstanten
bei den case-Marken verglichen, anschliessend
case1: case -3: case 9: default: wird zur entsprechenden Marke verzweigt und
die entsprechende Anweisung ausgeführt. Wenn
Anw. 1 Anw. 2 Anw. 3 Anw. d
keine passende Marke existiert, wird bei der
Marke default weitergefahren, falls vorhan-
Anweisung B den, und sonst nach der switch-Anweisung
Anweisung A
switch ( Ausdruck ) { /* Ausdruck wird ausgewertet */
case 1: Anweisung 1 /* Falls Ausdruck 1 ergibt, wird hier weitergefahren */
break; /* Ohne break wird naechste Anweisung (2) ausgefuehrt */
case -3: Anweisung 2 /* Falls Ausdruck -3 ergibt, wird hier weitergefahren */
break;
case 9: Anweisung 3 /* Falls Ausdruck 9 ergibt, wird hier weitergefahren */
break;
default: Anweisung d /* In allen anderen Faellen wird hier weitergefahren */
break;
}
Anweisung B
Ausdruck muss ein ganzzahliges Ergebnis haben. Alle case-Konstanten müssen verschieden und
ganzzahlig sein und es ist nicht möglich, bei einer Marke einen Bereich anzugeben. Um einen Bereich
abzudecken müssen alle enthaltenen Werte explizit mit Marken angegeben werden.
Achtung: Ohne break wird mit der nächsten Anweisung (Meist nächster case) weitergefahren.
Einsatz:
Eine mögliche Anwendung für die switch-Anweisung ist das Überprüfen einer Eingabe, die in ver-
schiedene Fälle verzweigt. Zum Beispiel bei der Behandlung einer Menuabfrage. Dabei wird zuerst
eine Bildschirmmaske mit den verschiedenen Eingabemöglichkeiten angezeigt, die Auswahl des Be-
nutzers eingelesen und anschliessend in eine der vier Möglichkeiten verzweigt.
printf("Bitte waehlen Sie eine der folgenden Aufgaben \n");
printf("K - Kontostand abfragen \n"); Ausgabe des Programms:
printf("D - Dauerauftrag einrichten \n"); Bitte waehlen Sie eine der folgenden Aufgaben
printf("U - Ueberweisung taetigen \n"); K - Kontostand abfragen
printf("B - Beenden \n"); D - Dauerauftrag einrichten
scanf("%c", &eingabe); U - Ueberweisung taetigen
switch(eingabe) { A - Aufhoeren
case 'k': /* Kein break, mit naechaster Anweisung */
/* weiterfahren (kontostand ausgeben) */
case 'K': kontostand(); /* Kontostand abfragen */
break;
case 'D': dauerauftrag(); /* Dauerauftrag einrichten */
break;
case 'U': ueberweisung(); /* Ueberweisung taetigen */
break;
case 'B': beenden(); /* Beenden (Kehrt nicht zurueck!) */
break;
default : printf("Bitte vertippen Sie sich nicht andauernd \n");
break;
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 35/168
Eine Einführung in die Informatik und die Programmiersprache C
Nach der Verzweigung zu einer case-Marke wird das Programm sequentiell fortgesetzt, wobei die
weiteren Marken keine Bedeutung mehr haben. Mit der break-Anweisung kann ein switch jeder-
zeit beendet werden. Ein break ist also notwendig, wenn die Anweisungen der nächsten case-Kon-
stanten nicht ausgeführt werden sollen. Es ist keine Reihenfolge für case und default vorgeschrie-
ben.
9.2 Schleifen
In einer Schleife wird eine Gruppe von Anweisungen, der sogenannte Schleifen-Rumpf, mehrfach aus-
geführt. Zur Bildung einer Schleife stehen in C drei Anweisungen zur Verfügung: while, do-whi-
le und for.
9.2.1 While
Bei der while-Anweisung wird die zugehörige Anwei-
Anweisung A sung solange wiederholt, wie der Ausdruck wahr ist. So-
while(Ausdruck) bald der Ausdruck falsch ist, wird mit der auf die whi-
le-Schleife folgenden Anweisung B weitergefahren.
Anweisung
Wenn der while Ausdruck von Anfang an falsch ist,
Anweisung B wird die Schleifen-Anweisung gar nie Ausgeführt.
Anweisung A
while (Ausdruck)
Anweisung
Anweisung B
Nehmen wir an, Sie wollen herausfinden, wie lange Sie sparen müssen um Millionär zu werden, wenn
Sie jeden Monat hundert Taler sparen und zur Bank bringen, wobei das Kapital noch verzinst wird. In
C könnte das Problem folgendermassen gelöst werden:
mein_guthaben = 0.0;
monat = 0;
while(mein_guthaben < 1000000.0) { /* Wiederholen bis Millionaer */
mein_guthaben = mein_guthaben * 1.003; /* monatl. Zinssatz 0,3% */
mein_guthaben += 100.0; /* Monatsrate */
monat++; /* Monatszaehler inkrementieren */
}
printf("Nach %d Monaten bin ich endlich Millionaer \n", monat);
9.2.1.1 Fehlerquelle
Wenn am Ende der Whileschlaufe ein Semikolon gesetzt wird, gilt dieses als leere Anweisung, welche
solange wiederholt wird, wie die while-Bedingung wahr ist.
/* Fehler, Block wird genau einmal durchlaufen wenn Guthaben > 1000000 ist */
/* while wird nie beendet wenn Guthaben zu beginn < 1000000 ist */
Diesen Fehler kann man vermeiden, wenn die öffnende geschweifte Klammer gleich an while ange-
fügt wird (Zumindest wird so der Fehler offensichtlicher, wenn ein Semikolon vor { steht):
while(mein_guthaben < 1000000.0) { ; /* Semikolon hinter { stoert nicht */
mein_guthaben = mein_guthaben * 1.003 + 100.0; /* monatl. Zins u. Rate */
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 36/168
Eine Einführung in die Informatik und die Programmiersprache C
9.2.2 do while
Auch mit der do while-Anweisung wird ein Pro-
Anweisung A grammstück solange wiederholt, wie ein Ausdruck
do wahr ist. Diese Iteration unterscheidet sich von der vo-
Anweisung
rigen dadurch, dass die Iterations-Bedingung am Ende
while(Ausdruck) abgefragt wird; diese Schleife wird deshalb in jedem
Fall mindestens einmal durchlaufen.
Anweisung B
Anweisung A
do
Anweisung
while (Ausdruck); /* Nach dem while muss ein Semikolon stehen !!!*/
Anweisung B
do {
epsilon = epsilon / 2.0f;
} while((1.0f + epsilon) != 1.0f);
9.2.3 for
Die for-Schleife wird verwendet, wenn die
Anzahl der Iterationen (Schleifendurchläufe)
Anweisung A bereits im Voraus bekannt ist. Im Strukto-
gramm sieht die for-Schlaufe gleich wie die
for(Ausdruck1; Ausdruck2; Ausdruck3)
while-Schlaufe aus.
Anweisung Die Schleife wird solange wiederholt wie
Anweisung B Ausdruck2 wahr ist. Ausdruck1 wird nur ein-
mal vor dem ersten Schlaufendurchlauf aus-
gewertet, Ausdruck3 am Ende jeder Iteration.
Anweisung A
for(ausdruckl; ausdruck2; ausdruck3)
Anweisung
Anweisung B
Der obige allgemeine Ausdruck für die for-Schlaufe wird im folgenden Struktogramm illustriert.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 37/168
Eine Einführung in die Informatik und die Programmiersprache C
Der oben beschriebene Sachverhalt kann auch mit einer while-Schlaufe erzielt werden. Er ist nämlich
äquivalent zu:
ausdruckl;
while(Ausdruck2) {
Anweisung
ausdruck3;
}
Anweisung B
Da beide Formen äquivalent sind, kann man sich aussuchen, für welche man sich entscheidet. Allge-
mein gilt die Regel: for(ausdruck1; ausdruck2; ausdruck3) sollte in eine Zeile passen,
ansonsten sollte man while(ausdruck2) wählen, was sicher kürzer ist als der entsprechende for-
Ausdruck, aber theoretisch natürlich ebenfalls länger als eine Zeile werden kann.
Hier ein typisches Beispiel für den Einsatz einer for-Schlaufe:
float x = 2.0f; /* was passiert hier wohl? */
for(i=0; i<10; i++) {
printf("%d\n", i);
x = x*x;
}
Die obige Iteration beginnt bei i=0 und endet mit i=9. Andere Initialisierungen für den ersten Wert
von i und andere Schrittweiten (z.B. i += 13 anstelle von i++) sind möglich. Die for-Schlaufe ist
aber noch viel allgemeiner und flexibler einsetzbar.
Mit der leeren Anweisung for(;;) kann eine Endlosschlaufe erzeugt werden:
for(;;) {
/* Diese Schlaufe hoert nie auf, wenn kein break enthalten ist */
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 38/168
Eine Einführung in die Informatik und die Programmiersprache C
Hier ein Programm, welches das Kapitalwachstum einer Einheit (1000) über einen bestimmten Zeit-
raum bei einem bestimmten Zinssatz tabellarisch darstellt:
#include <stdio.h>
[C99] In C99 können innerhalb des for-Statements im Initialisierungsteil auch gleich Variablen
definiert werden. Diese gelten für den ganzen Schleifenkörper.
/* [C99], Definition von Variablen in Initialisierung */
for (int r = 17; r > 0; r -= 2) {
a = a * r;
}
/* ist äquvalent zu */
{
int r; /* Variablendefinition, nur innerhalb des Blocks gueltig */
for (r = 17; r > 0; r -= 2) {
a = a * r;
}
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 39/168
Eine Einführung in die Informatik und die Programmiersprache C
Die break-Anweisung wird innerhalb von while-, do-, for- und switch-Anweisungen einge-
setzt. Mit break wird die aktuelle Schleife abgebrochen und die weitere Abarbeitung des Programms
mit der nächsten auf diese Schleife folgenden Anweisung fortgesetzt. Die häufigste Anwendung von
break finden Sie in der weiter oben schon beschriebenen switch-Anweisung. Wie continue
sollte auch break (Ausser in switch-Anweisungen) nur mit grosser Vorsicht benutzt werden.
Die goto-Anweisung ist in guten Programmiererkreisen so verpönt, dass schon ihre blosse Erwäh-
nung anstössig wirkt. Ein Programm mit goto-Anweisungen ist meist so unverständlich und verwi-
ckelt ("Spaghetti-Code"), dass nur der Entwickler selbst (wenn überhaupt!) noch einen Überblick dar-
über besitzt, was in seinem Programm vorgeht. goto wird deshalb hier nur der Vollständigkeit halber
erwähnt. Die goto-Anweisung hat folgende allgemeine Form:
goto bezeichnung;
...
bezeichnung: Anweisung;
...
goto bezeichnung;
Mit einem goto kann nur innerhalb einer Funktion herumgesprungen werden, das Springen in andere
Funktionen ist nicht möglich.
[C99] In C99 dürfen goto's nicht in den Gültigkeitsbereich eines VLAs springen, das Verlassen des
Gültigkeitsbereichs hingegen ist gestattet.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 40/168
Eine Einführung in die Informatik und die Programmiersprache C
10 Funktionen
Im Gegensatz zu anderen Programmiersprachen wird in C nicht zwischen Funktionen und Prozeduren
unterscheiden. In C gibt es nur Funktionen, wobei eine Funktion aber nicht zwingend einen Rückga-
bewert haben muss.
Eine Funktion ist ein Unterprogramm, eigentlich ein Stück Code, welches einen Namen hat, dem
Werte übergeben werden können und das ein Ergebnis zurückliefern kann. Jede Funktion kann von
anderen Funktionen aus über ihren Namen aufgerufen werden.
Ein C-Programm besteht grundsätzlich aus einer Menge von Funktionen, und mindestens die Funktion
main() muss vorhanden sein, denn diese wird beim Start eines Programmes automatisch aufgerufen,
und wenn main() an sein Ende gelangt wird auch das Programm beendet.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 41/168
Eine Einführung in die Informatik und die Programmiersprache C
Eine Funktion, die keine Argumente hat, besitzt entsprechend eine leere Parameterliste:
double CalculatePi(void) /* Berechnet Pi */
{
return 22.0/7.0; /* Algorithmus kann noch verbessert werden */
}
Analog sind auch Funktionen möglich, die weder Argumente, noch Rückgabewerte besitzen.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 42/168
Eine Einführung in die Informatik und die Programmiersprache C
Funktionen sind in C reentrant, d.h. sie können rekursiv verwendet werden, also sich selbst aufrufen
(Rekursionen werden später noch behandelt). Nachfolgend die Berechnung der Fakultät als Beispiel
für eine rekursive Funktion:
int Fakultaet(int n)
{ Definition der Fakultät:
if (n > 0) { n! = n * (n-1)! und 0! = 1
return n * Fakultaet(n-1); oder weniger formal:
} else {
n! = n * (n-1) * (n-2) * ... * 3 * 2 * 1
return 1;
}
}
(Die Fakultät kann mit Schleifen natürlich viel effizienter berechnet werden)
Prototypen stehen für globale Funktionen normalerweise in Headerdateien (.h -Files), für lokale (sta-
tic) Funktionen zu Beginn der entsprechenden Datei.
Count++;
printf("Die Funktion wurde %d mal aufgerufen\n", Count);
return Count;
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 43/168
Eine Einführung in die Informatik und die Programmiersprache C
[C11] Seit C11 ist der Support für VLA freiwillig, wenn der Compiler VLA's nicht unterstützt muss
er das Makro __STDC_NO_VLA__ definieren.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 44/168
Eine Einführung in die Informatik und die Programmiersprache C
11 Felder (Arrays)
Um mit grösseren Mengen von Daten effizient zu arbeiten, können Felder (Arrays) eingesetzt werden.
Bei einem Feld wird eine Gruppe von Werten unter einem Namen zusammengefasst, die einzelnen
Werte haben keinen eigenen Namen mehr, sondern nur noch eine Nummer (Den Index). Der Index be-
ginnt bei 0, und endet bei Arraygrösse - 1.
Die Grösse eines Arrays muss zur Compile-Zeit bekannt sein, und muss deshalb ein konstanter Aus-
druck sein. (Ausnahme: VLAs unter [C99], siehe Kap. 11.5)
Wenn weniger Werte angegeben werden, als das Array gross ist, werden die restlichen Arrayelemente
auf 0 gesetzt. Wenn ein Array initialisiert wird, muss seine Grösse nicht angegeben werden. Ohne
Grössenangabe wird die Grösse automatisch auf die Anzahl der Initialwerte gesetzt:
long int Zustand[] = {1, 1, 1, 1, 1, 1}; /* Array hat die Grösse 6 */
Achtung, es findet keine Überprüfung auf Bereichsüberschreitung statt. Werder die Arraygrenzen ver-
lassen, wird irgendwo auf den Speicher zugegriffen. Dabei können eigene Variablen oder Daten unbe-
merkt verändert werden, oder das Programm stürzt ab oder legt merkwürdiges Verhalten an den Tag.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 45/168
Eine Einführung in die Informatik und die Programmiersprache C
[C11] Seit C11 ist der Support für VLA freiwillig, wenn der Compiler VLA's nicht unterstützt muss
er das Makro __STDC_NO_VLA__ definieren.
Nach einer expliziten Zuweisung wird mit den nachfolgenden Initialisiererwerten an dieser
Stelle weitergefahren. Es können auch bereits initialisierte Werte wieder überschrieben wer-
den, auch wenn das nicht besonders sinnvoll und verständlich ist:
int Vektor[10] = {1, 2, 3, 4, 5, 6, 7, 8, [4] = 9, 8, 7, 6, 5, 4};
/* Initialisiert den Vektor auf folgenden Inhalt: 1 2 3 4 9 8 7 6 5 4 */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 46/168
Eine Einführung in die Informatik und die Programmiersprache C
11.7 Arrayzuweisungen
Zuweisungen von ganzen Arrays sind nicht möglich, sie müssen elementweise kopiert werden:
int a1[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int b3[10];
Die Initialisiererliste folgt dabei den Konventionen der Initialisiererliste gewöhnlicher Ar-
rays. Arrayliterale können überall verwendet werden, wo auch ein gewöhnliches Array be-
nutzt werden kann, sie sind eigentlich namenlose Variablen, welche denselben Gültigkeitsbe-
reich wie eine an dieser Stelle definierte Variable haben.
Arrayliterale können insbesondere an Funktionen übergeben und an Pointer zugewiesen wer-
den.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 47/168
Eine Einführung in die Informatik und die Programmiersprache C
Die Grössenangabe ist für die erste Dimension optional, und wird auch nicht überprüft, d.h. die Grösse
des übergebenen Arrays muss nicht mit der Grösse in der Funktionsdefinition übereinstimmen.
int b1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int b2[3] = {1, 2, 3};
int b3[15] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
int c;
In diesem Fall übergibt man aber besser auch gleich noch die effektive Grösse des Arrays als weiteren
Parameter, und benutzt ihn innerhalb der Funktion, z.b. als Schlaufenendwert.
void Modify(int a1[], int Groesse);
Bei mehrdimensionalen Arrays ist nur die erste Dimensionsangabe optional, die restlichen Dimensio-
nen müssen angegeben werden, und müssen auch beim Aufruf übereinstimmen. (Man kann sich mehr-
dimensionale Arrays einfach als ein Array von Arrays mit einer Dimension weniger vorstellen, also
ein Array welches aus Arrays besteht)
void DoSomething(int a1[][10][5][3], int Groesse); /* Erste Dimension optional*/
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 48/168
Eine Einführung in die Informatik und die Programmiersprache C
12 Strings
12.1 Grundsätzliches
Strings oder Zeichenketten werden in C nicht direkt unterstützt. Zur Verarbeitung von Strings müssen
Bibliotheksfunktionen aus der Standardlibrary (string.h) oder selbstgeschriebener Code herangezogen
werden.
Zeichenketten werden in C als Buchstabenfelder (char-Arrays) repräsentiert. Das Ende einer Zei-
chenkette wird durch den ASCII-Wert 0 ('\0') kenntlich gemacht.
Die einzige direkte Unterstützung für Strings in C sind die Stringliterale (Beispiel: "Hans") sowie die
Arrayinitialisierung mit Strings:
char Text[] = "Mueller";
Bei Stringliteralen und Arrayinitialisierungen wird automatisch das Endekennzeichen '\0' am Ende der
Zeichenkette angehängt. Die Variable Text würde im Speicher also so aussehen:
Der Typ char * welcher oft für Strings benutzt wird, ist eigentlich ein Zeiger auf einen Buchstaben,
er enthält also nur die Adresse (Den Platz im Speicher) an welcher des erste Zeichen des Strings steht.
Deshalb ist auch das Zeichen '\0' am Ende erforderlich, weil die Funktionen sonst das Ende des Textes
nicht erkennen könnten.
Achtung: gets() und scanf("%s") ohne Längenbegrenzung können zu Bufferoverflows führen. scanf()
immer mir Längenbegrenzung und gets_s() [C11, optional] oder fgets() anstelle von gets() benutzen.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 49/168
Eine Einführung in die Informatik und die Programmiersprache C
[C11] Viele Stringfunktionen sind potentiell gefährlich, da nirgends auf Überschreitung von Array-
grenzen geprüft wird. C11 stellt deshalb sichere Varianten dieser Stringfunktionen zur Verfü-
gung, wie strcpy_s(), strcat_s() usw. Diese Funktionen erhalten als zusätzliches Argument die
maximale Bufferkapazität, und führen ihren Auftrag nur aus, wenn die Kapazität ausreicht.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 50/168
Eine Einführung in die Informatik und die Programmiersprache C
13 Strukturen
Mit Strukturen können mehrere verschiedene Variablen zu einer Einheit zusammengefasst werden. So
können zusammengehörende Variablen auch zusammen behandelt werden. Unterschiedliche Struktu-
ren gelten jeweils als neuer, eigenständiger Datentyp.
Eine Strukturdeklaration wird mit dem Schlüsselwort struct eingeleitet, gefolgt von einem fakulta-
tiven Strukturnamen und einem anschliessenden Block mit einer Liste von Variablendeklarationen.
Der Strukturname kann bei späterer Benutzung anstelle der gesamten Deklaration verwendet werden.
[C99] In C99 darf des letzte Element einer Struktur ein Array mit einer nicht bekannten Grösse sein.
Dies macht aber nur im Zusammenhang mit dynamischer Speicherverwaltung Sinn, wenn der
Benutzer selbst für die Zuordnung von Speicher verantwortlich ist.
struct Telegram { /* Struktur fuer Datenkommunikation */
int Empfaenger;
int Absender;
int Laenge;
int Typ;
int Command;
char Data[]; /* Flexible Datenlaenge */
};
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 51/168
Eine Einführung in die Informatik und die Programmiersprache C
Auch diese Initialisierung darf unvollständig sein, nicht explizit initialisierte Werte werden
auf 0 gesetzt.
Die direkte Initialisierung kann mit der gewöhnlichen Initialisierung beliebig gemischt wer-
den. (Allerdings kann die Übersicht darunter leiden). Nach einer expliziten Zuweisung wird
mit den nachfolgenden Initialisiererwerten nach der soeben initialisierten Variablen weiterge-
fahren. Es können auch bereits initialisierte Werte wieder überschrieben werden, auch wenn
das nicht besonders sinnvoll und verständlich ist:
/* Etwas Komplizierter (Kombination v. Array u. Strukturinitialisieren */
struct Eintrag {
char Name[30];
struct Datum Termine[10];
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 52/168
Eine Einführung in die Informatik und die Programmiersprache C
DatumType GetDate(void)
{
DatumType Result = {1, 1, 1911};
return Result; /* Struktur als Rückgabewert */
}
13.10 Verschachtelung
Strukturen und Arrays können beliebig kombiniert und verschachtelt werden
struct Person {
char Name[20];
int Gewicht;
struct Datum Geburtsdatum; /* Variante [C11]: struct Datum; */
} Student = {"Hans", 70, {1, 2, 1975}};
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 53/168
Eine Einführung in die Informatik und die Programmiersprache C
Klasse[12].Gewicht = 78;
Klasse[12].Geburtsdatum.Tag = 1;
Klasse[12].Geburtsdatum.Monat = 12;
Klasse[12].Geburtsdatum.Jahr = 1999;
Klasse[12].Name[0] = 'U';
Klasse[12].Name[1] = 'R';
Klasse[12].Name[2] = 'S';
Klasse[12].Name[3] = '\0';
Die Initialisiererliste folgt dabei den Konventionen der Initialisiererliste gewöhnlicher Struk-
turen.
Strukturliterale können überall verwendet werden, wo auch eine gewöhnliche Struktur be-
nutzt werden kann, sie sind eigentlich namenlose Variablen, welche denselben Gültigkeitsbe-
reich wie eine an dieser Stelle definierte Variable haben.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 54/168
Eine Einführung in die Informatik und die Programmiersprache C
14 Unions
Eine Union ist fast dasselbe wie eine Struktur, mit dem Unterschied dass alle Datenfelder denselben
Platz im Speicher belegen (Sich also am selben Ort im Speicher aufhalten). In einer Union kann somit
immer nur eine der enthaltenen Variablen verwendet werden. Die Union braucht immer nur soviel
Platz wie die grösste der in ihr enthaltenen Variablen. Eine Union kann verwendet werden, wenn zur
selben Zeit immer nur eine der möglichen Variablen benutzt wird, damit kann Speicherplatz gespart
werden.
union Convert {
char Bytes[4]; /* Ein Array von 4 Bytes */
long Value; /* und ein long teilen sich denselben Speicher */
};
union Convert x;
long l = 0x12345678;
x.Value = l; /* Den Longwert in den gemeinsamen Speicher schreiben */
Dieses Beispiel zerlegt einen Long-Wert in seine einzelnen Bytes. Achtung, dieser Code ist nicht por-
tabel, da jeder Prozessor/Compiler die Bytes von Variablen in unterschiedlicher Reihenfolge im Spei-
cher ablegt und ein long nicht bei jedem System 4 Bytes (32 Bit) belegt.
Der Unterschied zwischen Struktur und Union ist nachfolgend zu sehen:
Struktur Union
L C
L
S
S
Die Union legt die Elemente übereinander im selben Speicherbereich ab, und benötigt deshalb weni-
ger Speicherplatz, dafür kann immer nur eines der Elemente unbeeinflusst verwendet werden.
Unions können wie Strukturen bei der Definition gleich initialisiert werden, wobei aber immer nur ein
Initialisierer angegeben werden darf, es wird immer das erste Element der Union initialisiert.
[C99] In C99 kann analog zur Struktur mit einem direkten Initialisierer ein beliebiges Element der
Union initialisiert werden.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 55/168
Eine Einführung in die Informatik und die Programmiersprache C
15 Bitfelder
Bei Strukturen kann für int Member angegeben werden, wieviele Bit's sie belegen sollen. (Aber pro
Feld nicht mehr als es Bits in einem int hat). Die Bits werden fortlaufend von einem int vergeben,
und sobald es für ein neues Feld keinen Platz im angebrochenen int mehr hat, wird das ganze Feld in
einem neuen int abgelegt und von diesem int mit der Bitzuteilung weitergefahren. Bitfelder können
nicht über int-Grenzen hinweggehen.
Mit Bitfeldern kann Speicherplatz gespart werden, oder es können Register von Peripheriebausteinen
abgebildet werden. Achtung, bei Bitfeldern ist fast alles herstellerabhängig, Programme die von der
tatsächlichen Bitposition im Speicher abhängen, sind mit Sicherheit nicht portabel!
struct Date {
int Day : 5; /* Fuer den Tag brauchen wir nur 5 Bit (0...31) */
int Month : 4; /* Fuer den Monat reichen 4 Bit (0...15) */
int Year : 7; /* Fuer 2 stellige Jahrzahlen genuegen 7 Bit (0...128) */
} Heute;
Heute.Day = 24;
Heute.Month = 12;
Heute.Year = 99;
Diese Struktur Date für Daten braucht somit nur 16 Bit oder 2 Bytes Speicherplatz.
Auch Flags (Einzelne Bits) können so Platzsparend abgelegt werden:
struct FileInfo {
int Readonly : 1;
int Hidden : 1;
int System : 1;
int Archived : 1;
int Link : 1;
};
Diese Flags benötigen nur 5 Bits, können also problemlos in einem Byte abgelegt werden.
Man muss sich aber im klaren sein, dass man so zwar Speicherplatz spart, aber der Rechenaufwand
grösser wird. Schliesslich muss der Compiler auf die einzelnen Bits mit Schieben, And- und Or- Ver-
knüpfungen zugreifen.
Beispiel Datestruktur:
/* Die Anweisung */
Heute.Month = a;
/* wird vom Compiler übersetzt zu */
Heute = (Heute & 0xFE1F) | ((a & 0x000F) << 5); /* 1111111000011111 = 0xFE1F */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 56/168
Eine Einführung in die Informatik und die Programmiersprache C
16 Enums
Mit dem Aufzählungstyp (Enum) können Konstanten definiert werden. Ein enum ist ein int, der vor-
definierte Konstanten aufnehmen kann. Obwohl ein enum eigentlich ein eigener Datentyp ist, können
Enums und int gemischt werden.
Wenn Enumkonstanten bei der Definiton kein Wert zugewiesen wird, werden sie fortlaufend nume-
riert, beginnend bei 0 oder dem Wert des Vorgängers plus 1.
Die Namen der Enumkonstanten gelten über alle Enums. Wenn ein Name bereits in einem Enum ver-
geben wurde, kann er in einem anderen Enum nicht nochmals verwendet werden.
/* Fehler, Do, Mi und So bereits in Enum Tag verwendet!!! */
enum Tonleiter {Do, Re, Mi, Fa, So, La, Ti};
Enumkonstanten können die Lesbarkeit eines Programmes stark erhöhen und auch helfen, Fehler zu
vermeiden.
Bei der Ausgabe von Enums mit printf() wird jedoch der numerische Wert, und nicht der Name ausge-
geben. Wenn man den Namen ausgeben möchte, muss man eine eigene Ausgabefunktion schreiben.
(Mit einem Array oder einem switch).
Beispiel:
enum Tag {Mo, Di, Mi, Do, Fr, Sa, So};
char *Translate[] = {"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"};
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 57/168
Eine Einführung in die Informatik und die Programmiersprache C
17 Zeiger (Pointer)
Zeiger sind in C ein sehr wichtiges Sprachmittel, es gibt kaum ein Programm das ohne Zeiger aus-
kommt. Um richtig C Programmieren zu können, muss man Zeiger verstehen.
Im Speicher eines jeden Computers sind die Speicherplätze (Bytes) durchnumeriert. Jede Variable hat
somit eine eindeutige Adresse (Platz im Speicher). Als Adresse von mehrbytigen Variablen wird übli-
cherweise die Adresse des ersten von ihr belegten Speicherplatzes verwendet.
Zeiger sind nun spezielle Variablen, welche die Adresse (Position) einer anderen Variablen aufneh-
men können. Über die Zeigervariable kann nun ebenfalls auf die Originalvariable zugegriffen werden,
sogar wenn deren Name an dieser Stelle unbekannt oder sie nicht sichtbar ist. Bei der Deklaration ei-
ner Zeigervariablen muss angegeben werden, auf welchen Variablentyp sie zeigt. (Es sind auch Zeiger
auf Zeiger usw. möglich).
Mit dem * Operator wird einerseits eine Zeigervariable deklariert und definiert, andererseits kann da-
mit auf die von einem Zeiger adressierte Variable zugegriffen werden (Man sagt dazu: 'der Zeiger
wird dereferenziert').
Mit dem & Operator kann die Adresse einer beliebigen Variable ermittelt werden.
int a = 3; a 3 a 7
int b = 4;
int c = 5; b 4 b 4
int * Ptr1;
int * Ptr2 = NULL; c 5 c 5
*Ptr1= 7;
a 7 a 7
Ptr1= &b;
b 4 b 8
*Ptr1= 8;
c 5 c 5
Ptr1 Ptr1
a = 7 + *Ptr1; a 15 a 15
c = 7 * *Ptr1;
b 8 b 8
c 56 c 56
Ptr2 = Ptr1;
Ptr1 Ptr1
Ptr2 Ptr2
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 58/168
Eine Einführung in die Informatik und die Programmiersprache C
Achtung, wenn nicht initialisierte Zeiger verwendet werden, verhält sich ein Programm unvorherseh-
bar, im günstigsten Fall stürzt es ab, sonst werden irgendwo zufällig Variablen verändert. Bevor ein
Zeiger benutzt wird, muss ihm immer eine gültige Adresse zugewiesen werden. Zeiger die nirgendwo-
hin zeigen, sollte man den Wert NULL zuweisen (NULL ist eine in stdio.h vordefinierte Konstante).
Vor der Benutzung eines Zeigers sollte man ihn entsprechend sicherheitshalber auf den Wert NULL
testen, und im Falle von NULL nicht verwenden und ev. eine Fehlermeldung ausgeben.
Achtung, auf einfache Literale kann der Adressoperator nicht angewendet werden.
int* p1 = &5; /* Nicht erlaubt, 5 hat keine Adresse */
Achtung, bei der Deklaration/Definiton von Pointer gehört der * zum Pointer, nicht zum Datentyp!
int* p1, p2, p3; /* Definiert nicht 3 Pointer, sondern 1 Pointer p1, sowie
2 int Variablen p2 und p3 */
int *p1, *p2, *p3; /* Definiert 3 Pointer */
Achtung, Pointervariablen können einander nur zugewiesen werden, wenn sie vom selben Typ sind,
also auf denselben Typ zeigen. Ausgenommen davon sind void-Pointer, diese können an alle ande-
ren Zeiger zugewiesen, und von allen Zeigern zugewiesen werden).
int i = 3;
float f = 3.14;
int *ip1, *ip2 = &i;
float *fp1, *fp2 = &f;
void *Universalpointer;
ip1 = ip2; /* OK */
fp1 = fp2; /* OK */
fp1 = ip2; /* Error, benoetigt cast : fp1 = (float *)ip2; */
Analogie Bibliothek Call by Reference: Dem Kunden zeigen wo das Büchergestell ist
Call by Value: Eine Kopie des Büchergestells zum Kunden tragen
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 59/168
Eine Einführung in die Informatik und die Programmiersprache C
17.2 Pointerarithmetik
Mit Zeiger kann auch gerechnet werden. Zeiger können inkrementiert und dekrementiert werden, da-
bei werden sie jeweils um die Grösse des Datentyps auf den sie zeigen, verändert. Damit gelangt man
zur nächsten Variablen desselben Typs, vorausgesetzt dass die Variablen genau hintereinander im
Speicher liegen. Dies ist zum Beispiel bei einem Array der Fall. Pointerarithmetik macht deshalb ei-
gentlich nur bei Arrays Sinn.
Beispiel:
char Text[] = "Hallo Welt";
Zu Pointer können Integer-Werte addiert oder subtrahiert werden. Dabei wird der Zeiger um die ent-
sprechende Anzahl von Variablen vor- oder zurückgestellt.
Was wird in diesem Beispiel ausgegeben?
char Text[] = "0H1a2l3l4o5";
char *Ptr = &(Text[1]);
int Offset = 2;
putchar(*Ptr);
Ptr += 2;
putchar(*Ptr);
Ptr += Offset;
putchar(*Ptr);
Ptr += 2;
putchar(*Ptr);
Ptr += 2;
putchar(*Ptr);
putchar( *(Ptr + 2) ); /* gibt 'W' aus, Zeiger zeigt weiterhin auf 'o' */
putchar( *(Ptr - 3) ); /* gibt 'a' aus, Zeiger zeigt weiterhin auf 'o' */
putchar( *(Ptr + Offset ) ); /* gibt 'l' aus, Zeiger zeigt weiterhin auf 'o' */
putchar( *Ptr ); /* gibt 'o' aus, Zeiger zeigt weiterhin auf 'o' */
Die Differenz zwischen zwei Pointer gleichen Typs, die auf Elemente innerhalb des gleichen Arrays
zeigen ergibt die Anzahl von Elementen zwischen den beiden Zeigern. (Dass beide Zeiger dabei in
dasselbe Array zeigen ist dazu Voraussetzung, ansonsten macht das Ergebnis keinen grossen Sinn). Es
ist nicht möglich, Pointer verschiedenen Typs voneinander zu subtrahieren.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 60/168
Eine Einführung in die Informatik und die Programmiersprache C
/* Gibt ebenfalls den Inhalt des gesamten Arrays aus (jeweils 2 mal) */
Ptr = Tabelle;
for (i = 0; i < 12; i++) {
printf("%f\n", Tabelle[i]);
printf("%f\n", Ptr[i]);
}
p1 = a; /* Nicht Korrekt */
p2 = a; /* Korrekt */
p3 = a; /* Nicht Korrekt, Array hat nur 3 Spalten */
p4 = a; /* Nicht Korrekt */
/* Auf alle oben definierten Pointer kann aber mit einer 2-Dimensionalen */
/* Indizierung zugegriffen werden, aber die darunterliegenden Datenstrukturen */
/* sind Grundverschieden */
p1[3][2] = 7; /* Korrekter Zugriff, aber nicht auf Array */
p2[3][2] = 7; /* Korrekter Zugriff auf n*3 Array, passt zu a */
p3[3][2] = 7; /* Korrekter Zugriff auf n*7 Array */
p4[2][3] = 7; /* Korrekter Zugriff, aber nicht auf Array */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 61/168
Eine Einführung in die Informatik und die Programmiersprache C
P1
P2
P3
P4
Ptr = &Demo;
Der Zugriff auf ein Strukturelement via Pointer sieht eigentlich so aus:
(*Ptr).Wert = 77; /* Zuerst Pointer dereferenzieren und dann Element waehlen */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 62/168
Eine Einführung in die Informatik und die Programmiersprache C
EintragType Feld[4] = {
{11, "Venus", NULL},
{22, "Erde", NULL},
{33, "Mars", NULL},
{44, "Jupiter", NULL}
};
EintragType *Sonne;
Sonne = &(Feld[1]);
Feld[0].Next = &(Feld[1]);
Feld[1].Next = &(Feld[3]);
Feld[2].Next = &(Feld[0]);
Feld[3].Next = &(Feld[2]);
Zeichnen Sie die sich daraus ergebenden Daten und Zeiger in folgendem Diagramm ein:
Sonne
Feld 0 1 2 3
Wert Wert Wert Wert
Text Text Text Text
Next Next Next Next
Tragen Sie jetzt mit einer anderen Farbe ein, wie das Diagramm nach der Ausführung der folgenden
Zeilen aussieht:
for (i = 0; i < 2; i++) {
Sonne->Wert = i;
Sonne = Sonne->Next;
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 63/168
Eine Einführung in die Informatik und die Programmiersprache C
Achtung, die Klammern um * und den Funktionsnamen sind nötig, sonst Deklariert man schlicht eine
Funktion die einen Zeiger auf int zurückliefert:
int *f (int x); /* Funktion, die einen Zeiger auf int zurückliefert */
int (*f) (int x); /* Ein Zeiger auf eine Funktion, die int zurückliefert */
int *(*f) (int x); /* Ein Zeiger auf eine Funktion, die einen Zeiger
auf int zurückliefert */
int f1(int y)
{
return y - 2;
}
int f2(int y)
{
return y + 2;
}
Achtung, die Funktion muss zum Zeigertyp passen, einem Funktionszeiger kann nur die Adresse einer
Funktion übergeben werden, die eine passende Argumentenliste und einen passenden Rückgabetyp
besitzt.
int (*FPtr)(int a);
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 64/168
Eine Einführung in die Informatik und die Programmiersprache C
Damit kann die Funktion Anwenden() sehr universell eingesetzt werden, sie kann auch Funktionen
verwenden, die beim Programmieren der Funktion Anwenden noch gar nicht bekannt waren.
Durch den Einsatz der Funktion Anwenden() ist es auch sehr einfach, die Datenstruktur zu ändern.
Wenn die Daten nicht mehr in einem Feld, sondern in einer Liste abgelegt werden sollen, muss nur die
Funktion Anwenden() angepasst werden. Wenn anstelle der Funktion Anwenden() überall eine eigene
Schlaufe verwendet worden wäre, müssten bei Änderungen alle Schlaufen angepasst werden.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 65/168
Eine Einführung in die Informatik und die Programmiersprache C
Ein void-Pointer kann nur zum Aufbewahren einer Adresse und sonst für nichts verwendet werden. Er
kann weder dereferenziert werden (*Pointer), noch ist Pointerarithmetik (++, --, Pointer + 4, ...) mög-
lich. Einem void Pointer kann jeder beliebige andere Zeiger oder jede beliebige Adresse zugewiesen
werden, und ein void-Pointer kann jedem anderen Pointertyp zugewiesen werden.
int a;
int *p = &a;
float *fp;
struct Datum* sptr;
void *Pointer = p;
void *Pointer2;
/* aber */
fp = sptr; /* Error, nicht erlaubt */
sptr = fp; /* Error, nicht erlaubt */
int *p2;
p2 = (int *) 0x1020; /* Pointer direkt auf Adresse 1020 Hex setzen */
/* Mit dem Cast wird der Wert in einen Zeiger umgewandelt */
*P2 = 77; /* Den Wert 77 an diese Adresse schreiben */
Achtung, solche direkten Speicherzugriffe sollten nur durchgeführt werden, wenn man genau weiss
was man tut, ansonsten ist der Programmabsturz oder Fehlverhalten des Systems garantiert.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 66/168
Eine Einführung in die Informatik und die Programmiersprache C
18 Preprocessor
Der Preprocessor ist theoretisch ein eigenständiges Programm, das vor dem eigentlichen Compiler ge-
startet wird. Heutzutage ist der Präprozessor oft in den Compiler integriert, aber das Prinzip ist immer
noch dasselbe, zuerst behandelt der Präprozessor das Programm, und erst anschliessend der Compiler.
Der Präprozessor hat zwei Hauptaufgaben, nämlich alle Kommentare aus dem Code zu entfernen und
alle Preprozessordirektiven (#-Befehle) ausführen. Er ist eigentlich nichts anderes als ein vielseitiges
'Suche und Ersetze Programm', er wird dabei mit den #-Befehlen gesteuert. Der Präprozessor führt alle
Ersetzungen rein Textmässig aus, er versteht nichts von C.
q = Square(r+w);
/* wird zu: q = r + w * r + w,
was als q = r + (w * r) + w gerechnet wird */
Makros können auch mehr als ein Argument besitzen.
#x Stringizer, nur innerhalb Makros, Macht aus Makroargument einen String
#define S(x) #x S(100) wird zu "100"
prefix##x Tokenizer, nur innerhalb Makros, Macht aus Makroargument und Text neuen
#define T(x) p_##x Token. T(DoIt) wird zu p_DoIt
#if Bedingte Compilierung. Wenn die Bedingung nach #if wahr ist, wird der Code
#else zwischen #if und #else normal behandelt, alles zwischen #else und #en-
#endif dif wie Kommentar. Wenn die Bedingung falsch ist, entsprechend umgekehrt.
Das #else kann auch weggelassen werden. Die Bedingung nach #if muss aus
konstanten Integerausdrücken bestehen (und defined()), und kann beliebige
logische und arithmetische Verknüpfungen enthalten.
defined(Name) Kann nach #if verwendet werden, liefert 1 wenn das genannte Makro exisiert,
sonst 0.
#ifdef Name Kurzform für #if defined(Name)
#ifndef Name Kurzform für #if ! defined(Name)
#pragma Mit dieser Zeile können herstellerabhängige Direktiven implementiert werden.
Der Compiler ignoriert #pragma-Zeilen, die er nicht versteht.
#error irgendwas Gibt eine Fehlermeldung aus
#define V(...) X( __VA_ARGS__) [C99] Makro mit variabler Argumentenliste
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 67/168
Eine Einführung in die Informatik und die Programmiersprache C
18.3 Beispiele
/* Definition einiger Makros */
#define DEBUG
#define PI 3.1416
#define ERROR_MESSAGE(x) \
printf("Internal error in file %s at line %d: %s", __FILE__, __LINE__, x)
return 0;
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 68/168
Eine Einführung in die Informatik und die Programmiersprache C
19 Bibliotheksfunktionen
19.1 Mathematische Funktionen <math.h>
Die Definitionsdatei <math.h> vereinbart mathematische Funktionen und Makros.
Winkel werden bei trigonometrischen Funktionen im Bogenmass (Radiant) angegeben.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 69/168
Eine Einführung in die Informatik und die Programmiersprache C
double fdim(double x, double y) Liefert die positive Differenz von x und y oder 0, (x-y) wenn (x>y), sonst 0
double fma(double x, double y, double z) Multiplizieren und Addieren, berechnet (x*y) + z mit hoher Präzission
double fmax(double x, double y) Liefert des Maximum von x und y
double fmin(double x, double y) Liefert des Minimum von x und y
double nextafter(double x, double y) Liefert den nächsten möglichen Wert von x aus in Richtung y.
double nexttoward(double x, long double y) Wie nextafter, aber y ist vom Typ long double
long long llrint(double x) Rundet zur nächsten ganzen Zahl, liefert long long zurück) (Mode*)
long lrint(double x) Rundet zur nächsten ganzen Zahl, liefert long zurück (Mode*)
long long llround(double x) Rundet zur nächsten ganzen Zahl, liefert long long zurück)
long lround(double x) Rundet zur nächsten ganzen Zahl, liefert long zurück
double nearbyint(double x) Rundet zur nächsten ganzen Zahl, ohne allfällige inexact Exception. (Mode*)
double round(double x) Rundet auf die nächste ganze Zahl (Grenze x.5)
double rint(double x) Rundet zur nächsten ganzen Zahl, mit allfälliger inexact Exception. (Mode*)
double trunc(double x) Rundet zur nächsten betragsmässig kleineren oder gleichen ganzen Zahl.
double copysign(double x, double y) Liefert den Wert von x mit dem Vorzeichen von y
double scalbln(double x, long n) Berechnet x*FLT_RADIXn (n ist long), erzeugt effizient Fliesskommazahlen
double scalbn(double x, int n) Berechnet x*FLT_RADIXn (n ist int), erzeugt effizient Fliesskommazahlen
int ilogb(double x) Liefert den Exponenten der Fliesskommazahl X als Integerwert.
double nan(const char *s) Liefert eine NaN unter Berücksichtigung des Strings s.
[C99] Zudem sind unter C99 alle mathematischen Funktionen auch für float und long double verfüg-
bar, jeweils mit dem Suffix f oder l (Beispiel: sin(), sinf(), sinl()).
[C99] In C99 wurde mit complex.h zudem Unterstützung für komplexe Zahlen hinzugefügt, die meis-
ten Funktionen aus math.h sind mit dem Prefix 'c' auch für komplexe Zahlen verfügbar. Zum
Beispiel csin() oder cabs(). Der Umgang mit komplexen Zahlen wird in Kapitel 41.2 (Seite
165) kurz behandelt
[C99] Zum einfacheren Umgang mit dieser Vielfalt an Funktionen existiert die Headerdatei
tgmath.h. Wenn anstelle von math.h oder complex.h die Datei tgmath.h eingebunden
wird, wählt der Compiler automatisch die zum Datentyp passende Funktion aus. Beispielsweise
wird beim Ausdruck sin(x) vom Compiler je nach Datentyp von x sin(), sinf(), sinl(), csin(), cs-
inf() oder csinl() ausgewählt. Dies wird durch Makros und Generics (Siehe Kap. 8.3, Seite 31)
bewerkstelligt.
[C11] Seit C11 ist der Support für komplexe Zahlen freiwillig, wenn der Compiler komplexe Zahlen
nicht unterstützt muss er das Makro __STDC_NO_COMPLEX__ definieren.
Einige Implementationen definieren in math.h Konstanten wie M_E, M_PI, M_SQRT2, das ist jedoch
nicht Standardkonform und nicht portabel.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 70/168
Eine Einführung in die Informatik und die Programmiersprache C
char *strcpy(char * s, const char * ct) Zeichenkette ct in Array s kopieren, inklusive '\0'; liefert s.
char *strncpy(char * s, const char * ct, size_t n)
Höchstens n Zeichen aus String ct in String s kopieren; liefert s. Mit '\0' auf-
füllen, wenn String ct weniger als n Zeichen hat.
char *strcat(char * s, const char * ct) Zeichenkette ct hinten an die Zeichenkette s anfügen; liefert s.
char *strncat(char * s, const char * ct, size_t n)
Höchstens n Zeichen von ct hinten an die Zeichenkette s anfügen und s mit '\0'
abschliessen; liefert s.
int strcmp(const char * cs, const char * ct)
Strings cs und ct vergleichen; liefert <0 wenn cs<ct, 0 wenn cs==ct, oder >0,
wenn cs>ct. (Reiner ASCII-Vergleich, nicht Lexikalisch)
int strncmp(const char * cs, const char * ct, size_t n)
Höchstens n Zeichen von cs mit der Zeichenkette ct vergleichen; liefert <0
wenn cs<ct, 0 wenn cs==ct, oder >0 wenn cs>ct.
char *strchr(const char * cs, char c) Liefert Zeiger auf das erste c in cs oder NULL, falls nicht vorhanden.
char *strrchr(const char * cs, char c) Liefert Zeiger auf das letzte c in cs, oder NULL, falls nicht vorhanden,
size_t strspn(const char * cs, const char * ct)
Liefert Anzahl der Zeichen am Anfang vom String cs, die sämtliche im String
ct auch vorkommen.
size_t strcspn(const char * cs, const char * ct)
Liefert Anzahl der Zeichen am Anfang vom String cs, die sämtliche im String
ct nicht vorkommen.
char *strpbrk(const char * cs, const char * ct)
Liefert Zeiger auf die Position in String cs, an der irgendein Zeichen aus ct
erstmals vorkommt, oder NULL, falls keines vorkommt.
char *strstr(const char * cs, const char * ct)
Liefert Zeiger auf erstes Auftreten von ct in cs oder NULL, falls nicht vorhan-
den. (Suchen von String in anderem String)
size_t strlen(const char * cs) Liefert Länge von cs (ohne '\0').
char *strerror(size_t n) Liefert Zeiger auf Zeichenkette, die in der Implementierung für den Fehler mit
der Nummer n definiert ist.
char *strtok(char * s, const char * ct) strtok durchsucht s nach Zeichenfolgen, die durch Zeichen aus ct begrenzt
sind. (Zerlegt einen String in Teilstrings)
[C11] C11 bietet für alle potentiell gefährliche Stringfunktionen sichere Varianten mit dem Suffix _s
an, dabei muss als zusätzliches Argument der zur Verfügung stehende Platz im Ziel angegeben
werden, wie zum Beispiel:
errno_t strcpy_s(char * restrict s1, rsize_t s1max, const char * restrict s2)
errno_t strcat_s(char * restrict s1, rsize_t s1max, const char * restrict s2)
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 71/168
Eine Einführung in die Informatik und die Programmiersprache C
Die mem... Funktionen sind zur Manipulation von Speicherbereichen gedacht; sie behandeln den Wert
\0 wie jeden anderen Wert, deshalb muss immer eine Bereichslänge angegeben werden.
void *memcpy(void * s, const void * ct, size_t n)
Kopiert n Zeichen/Bytes von ct nach s; liefert s.
void *memmove(void * s, const void * ct, size_t n)
Wie memcpy, funktioniert aber auch, wenn sich die Bereiche überlappen.
int memcmp(const void * cs, const void * ct, size_t n)
Vergleicht die ersten n Zeichen vom Bereich cs mit dem Bereich ct; Resultat
analog zu strcmp.
void *memchr(const void * cs, char c, size_t n)
Liefert Zeiger auf das erste Byte mit dem Wert c in cs oder NULL, wenn das
Byte in den ersten n Zeichen nicht vorkommt.
void *memset(void * s, char c, size_t n) Setzt die ersten n Bytes von s auf den Wert c, liefert s. (Speicher füllen)
#include <wchar.h>
fputws(String, stdout);
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 72/168
Eine Einführung in die Informatik und die Programmiersprache C
double atof(const char *s) atof wandelt den String s in double um. Beendet die Umwandlung beim ersten
unbrauchbaren Zeichen.
int atoi(const char *s) atoi wandelt den String s in int um. Beendet die Umwandlung beim ersten un-
brauchbaren Zeichen.
long atol(const char *s) atol wandelt den String s in long um. Beendet die Umwandlung beim ersten
unbrauchbaren Zeichen.
double strtod(const char *s, char **endp) strtod wandelt den Anfang der Zeichenkette s in double um, dabei wird Zwi-
schenraum am Anfang ignoriert. Die Umwandlung wird beim ersten unbrauch-
baren Zeichen beendet. Die Funktion speichert einen Zeiger auf den ev. nicht
umgewandelten Rest der Zeichenkette bei *endp, falls endp nicht NULL ist.
Falls das Ergebnis zu gross ist, (also bei overflow), wird als Resultat
HUGE_VAL mit dem korrekten Vorzeichen geliefert; liegt das Ergebnis zu
dicht bei Null (also bei underflow), wird Null geliefert. In beiden Fällen erhält
errno den Wert ERANGE.
long strtol(const char *s, char **endp, int base)
strtol wandelt den Anfang der Zeichenkette s in long um, dabei wird Zwi-
schenraum am Anfang ignoriert. Die Umwandlung wird beim ersten unbrauch-
baren Zeichen beendet. Die Funktion speichert einen Zeiger auf den ev. nicht
umgewandelten Rest der Zeichenkette bei *endp, falls endp nicht NULL ist.
Hat base einen Wert zwischen 2 und 36, erfolgt die Umwandlung unter der
Annahme, dass die Eingabe in dieser Basis repräsentiert ist. Hat base den Wert
Null, wird als Basis 8, 10 oder 16 verwendet, je nach s; eine führende Null be-
deutet dabei oktal und 0x oder 0X zeigen eine hexadezimale Zahl an. In jedem
Fall stehen Buchstaben für die Ziffern von 10 bis base-l; bei Basis 16 darf 0x
oder 0X am Anfang stehen. Wenn das Resultat zu gross werden würde, wird je
nach Vorzeichen LONG_MAX oder LONG_MIN geliefert und errno erhält
den Wert ERANGE.
unsigned long strtoul(const char *s, char **endp, int base)
strtoul funktioniert wie strtol, nur ist der Resultattyp unsigned long und der
Fehlerwert ist ULONG_MAX.
int rand(void) rand liefert eine ganzzahlige Pseudo-Zufallszahl im Bereich von 0 bis
RAND_MAX; dieser Wert ist mindestens 32767.
void srand(unsigned int seed) srand benutzt seed als Ausgangswert für eine neue Folge von Pseudo-Zufalls-
zahlen. Der erste Ausgangswert ist 1. Vor erstmaliger Benutzung von rand()
sollte in jedem Programm einmal (Z.B. zu Beginn) srand() aufgerufen werden,
z. B: srand(time(NULL)) (Die aktuelle Zeit reicht meistens als zufälliger Start-
wert).
void *calloc(size_t nobj, size_t size) calloc liefert einen Zeiger auf einen Speicherbereich für einen Vektor von
nobj Objekten, jedes mit der Grösse size, oder NULL, wenn die Anforderung
nicht erfüllt werden kann. Der Bereich wird mit Null-Bytes initialisiert.
void *malloc(size_t size) malloc liefert einen Zeiger auf einen Speicherbereich für ein Objekt der Grösse
size oder NULL, wenn die Anforderung nicht erfüllt werden kann. Der Be-
reich ist nicht initialisiert.
void *realloc(void *p, size_t size) realloc ändert die Grösse des Objekts, auf das p zeigt, in size ab. Bis zur klei-
neren der alten und neuen Grösse bleibt der Inhalt unverändert. Wird der Be-
reich für das Objekt grösser, so ist der zusätzliche Bereich uninitialisiert. real-
loc liefert einen Zeiger auf den neuen Bereich oder NULL, wenn die Anforde-
rung nicht erfüllt werden kann; in diesem Fall wird der Inhalt nicht verändert.
void free(void *p) free gibt den Bereich frei, auf den p zeigt; die Funktion hat keinen Effekt,
wenn p den Wert NULL hat. p muss auf einen Bereich zeigen, der zuvor mit
calloc, malloc oder realloc angelegt wurde.
void abort(void) abort sorgt für eine anormale, sofortige Beendigung des Programms.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 73/168
Eine Einführung in die Informatik und die Programmiersprache C
void exit(int status) exit beendet das Programm normal. atexit-Funktionen werden in umgekehrter
Reihenfolge ihrer Hinterlegung aufgerufen, die Puffer offener Dateien werden
geschrieben, offene Ströme werden abgeschlossen, und die Kontrolle geht an
die Umgebung des Programms zurück. Wie status an die Umgebung des Pro-
gramms geliefert wird, hängt von der Implementierung ab, aber Null gilt als
erfolgreiches Ende. Die Werte EXIT_SUCCESS und EXIT_FAILURE kön-
nen ebenfalls angegeben werden.
int atexit(void (*fcn)(void)) atexit hinterlegt die Funktion fcn, damit sie aufgerufen wird, wenn das Pro-
gramm normal endet, und liefert einen von Null verschiedenen Wert, wenn die
Funktion nicht hinterlegt werden kann.
int system(const char *s) system liefert die Zeichenkette s an die Umgebung zur Ausführung. Hat s den
Wert NULL, so liefert system einen von Null verschiedenen Wert, wenn es ei-
nen Kommandoprozessor gibt. Wenn s von NULL verschieden ist, dann ist der
Resultatwert implementierungsabhängig.
char *getenv(const char *name) getenv liefert die zu name gehörende Zeichenkette aus der Umgebung oder
NULL, wenn keine Zeichenkette existiert. Die Details hängen von der Imple-
mentierung ab.
void *bsearch(const void *key, const void *base, size_t n, size_t size,
int (*cmp)(const void *keyval, const void *datum))
bsearch durchsucht base[0] ... base[n-l] nach einem Eintrag, der gleich *key
ist. Die Funktion cmp muss einen negativen Wert liefern, wenn ihr erstes Ar-
gument (der Suchschlüssel) kleiner als ihr zweites Argument (ein Tabellenein-
trag) ist, Null, wenn beide gleich sind, und sonst einen positiven Wert. Die
Elemente des Arrays base müssen aufsteigend sortiert sein. In size muss die
Grösse eines einzelnen Elements übergeben werden. bsearch liefert einen Zei-
ger auf das gefundene Element oder NULL, wenn keines existiert.
void qsort(void *base, size_t n, size_t size, int (*cmp)(const void *, const void *))
qsort sortiert ein Array base[0] ... base[n-1] von Objekten der Grösse size in
aufsteigender Reihenfolge. Für die Vergleichsfunktion cmp gilt das gleiche
wie bei bsearch.
int abs(int n) abs liefert den absoluten Wert seines int Arguments n.
long labs(long n) labs liefert den absoluten Wert seines long Arguments n.
div_t div(int num, int denom) div berechnet Quotient und Rest von num/denom. Die Resultate werden in
den int Komponenten quot und rem einer Struktur vom Typ div_t abgelegt.
ldiv_t ldiv(long num, long denom) div berechnet Quotient und Rest von num/denom. Die Resultate werden in
den long Komponenten quot und rem einer Struktur vom Typ ldiv_t abgelegt.
Anwendung von Qsort:
typedef struct Entry {
char Name[];
int Age;
} Entry;
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 74/168
Eine Einführung in die Informatik und die Programmiersprache C
struct tm {
int tm_sec; Sekunden nach der vollen Minute (0, 61)*
int tm_min; Minuten nach der vollen Stunde (0, 59)
int tm_hour; Stunden seit Mitternacht (0, 23)
int tm_mday; Tage im Monat (1, 31)
int tm_mon; Monate seit Januar (0, 11)
int tm_year; Jahre seit 1900
int tm_wday; Tage seit Sonntag (0, 6)
int tm_yday; Tage seit dem 1. Januar (0, 365)
int tm_isdst; Kennzeichen für Sommerzeit
}
(*Die zusätzlich möglichen Sekunden sind Schaltsekunden)
tm_isdst ist positiv, wenn Sommerzeit gilt, Null, wenn Sommerzeit nicht gilt, und negativ, wenn
die Information nicht zur Verfügung steht.
clock_t clock(void) clock liefert die Rechnerkern-Zeit, die das Programm seit Beginn seiner Aus-
führung verbraucht hat, oder -1, wenn diese Information nicht zur Verfügung
steht. clock()/CLOCKS_PER_SEC ist eine Zeit in Sekunden.
time_t time(time_t *tp) time liefert die aktuelle Kalenderzeit oder -1, wenn diese nicht zur Verfügung
steht. Wenn tp von NULL verschieden ist, wird der Resultatwert auch bei *tp
abgelegt.
double difftime(time_t time2, time_t timel)
difftime liefert time2 - timel ausgedrückt in Sekunden.
time_t mktime(struct tm *tp) mktime wandelt die Ortszeit in der Struktur *tp in Kalenderzeit um, die so
dargestellt wird wie bei time. Die Komponenten erhalten Werte in den angege-
benen Bereichen. mktime liefert die Kalenderzeit oder -1, wenn sie nicht dar-
gestellt werden kann.
Die folgenden vier Funktionen liefern Zeiger auf statische Objekte, die von anderen Aufrufen über-
schrieben werden können.
char *asctime(const struct tm *tp) asctime konstruiert aus der Zeit in der Struktur *tp eine Zeichenkette der Form
Sun Jan 3 15:14:13 1988\n\0
char *ctime(const time_t *tp) ctime verwandelt die Kalenderzeit *tp in Ortszeit; dies ist äquivalent zu ascti-
me(localtime(tp))
struct tm *gmtime(const time_t *tp) gmtime verwandelt die Kalenderzeit *tp in Coordinated Universal Time
(UTC). Die Funktion liefert NULL, wenn UTC nicht zur Verfügung steht. Der
Name gmtime hat historische Bedeutung.
struct tm *localtime(const time_t *tp) localtime verwandelt die Kalenderzeit *tp in Ortszeit.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 75/168
Eine Einführung in die Informatik und die Programmiersprache C
size_t strftime(char *s, size_t smax, const char *fmt, const struct tm *tp)
strftime formatiert Datum und Zeit aus *tp in s unter Kontrolle von fmt, ana-
log zu einem printf-Format. Gewöhnliche Zeichen (insbesondere auch das ab-
schliessende '\0') werden nach s kopiert. Jedes %... wird so wie unten beschrie-
ben ersetzt, wobei Werte verwendet werden, die der lokalen Umgebung ent-
sprechen. Höchstens smax Zeichen werden in s abgelegt. strftime liefert die
Anzahl der resultierenden Zeichen, mit Ausnahme von '\0'. Wenn mehr als
smax Zeichen erzeugt wurden, liefert strftime den Wert Null.
Beispiel:
#include <time.h>
#include <stdio.h>
return 0;
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 76/168
Eine Einführung in die Informatik und die Programmiersprache C
19.6.1 Dateioperationen
Die folgenden Funktionen beschäftigen sich mit Dateioperationen. Der Typ size_t ist der vorzei-
chenlose, ganzzahlige Resultattyp des sizeof-Operators. EOF ist eine vordefinierte Konstante, welche
das Dateiende (End Of File) anzeigt.
FILE *fopen(const char *filename, const char *mode) fopen eröffnet die angegebene Datei und liefert einen Da-
tenstrom oder NULL bei Misserfolg. Zu den erlaubten Werten von mode ge-
hören
"r" Textdatei zum Lesen öffnen
"w" Textdatei zum Schreiben erzeugen; gegebenenfalls alten Inhalt weg-
werfen
"a" anfügen; Textdatei zum Schreiben am Dateiende öffnen oder erzeu-
gen
"r+" Textdatei zum Ändern öffnen (Lesen und Schreiben)
"w+" Textdatei zum Ändern erzeugen; gegebenenfalls alten Inhalt weg-
werfen
"a+" anfügen; Textdatei zum Ändern öffnen oder erzeugen, Schreiben
am Ende
Ändern bedeutet, dass die gleiche Datei gelesen und geschrieben werden darf;
fflush oder eine Funktion zum Positionieren in Dateien muss zwischen einer
Lese- und einer Schreiboperation oder umgekehrt aufgerufen werden. Enthält
mode nach dem ersten Zeichen noch b, also etwa "rb" oder "w+b", dann wird
auf eine binäre Datei zugegriffen. Dateinamen sind auf FILENAME_MAX
Zeichen begrenzt. Höchstens FOPEN_MAX Dateien können gleichzeitig offen
sein.
[C11] C11 bitet zusätzlic x für exklusiven Zugriff an, eine Datei die mit "...x"
(Z.B. "wx") erzeugt wurde, kann von keiner anderen Applikation zur selben
Zeit benutzt werden bis sie geschlossen wird. 'x' ist nur zusammen mit 'w' er-
laubt, das Erzeugen schlägt fehl wenn die Datei schon existiert. (Dient z. B.
zur Verwaltung von Lock-Files) .
FILE *freopen(const char *filename, const char *mode, FILE *stream)
freopen öffnet die Datei für den angegebenen Zugriff mode und verknüpft
stream damit. Das Resultat ist stream oder Null bei einem Fehler. Mit freo-
pen ändert man normalerweise die Dateien, die mit stdin, stdout oder stderr
verknüpft sind. (Neue Datei mit bestehendem Stream verknüpfen).
int fflush(FILE *stream) Bei einem Ausgabestrom sorgt fflush dafür, dass gepufferte, aber noch nicht
geschriebene Daten geschrieben werden; bei einem Eingabestrom ist der Ef-
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 77/168
Eine Einführung in die Informatik und die Programmiersprache C
fekt undefiniert. Die Funktion liefert EOF bei einem Schreibfehler und sonst
Null. fflush(NULL) bezieht sich auf alle offenen Dateien.
int fclose(FILE *stream) fclose schreibt noch nicht geschriebene Daten für stream, wirft noch nicht ge-
lesene, gepufferte Eingaben weg, gibt automatisch angelegte Puffer frei und
schliesst den Datenstrom. Die Funktion liefert EOF bei Fehlern und sonst Null.
int remove(const char *filename) remove löscht die angegebene Datei, so
dass ein anschliessender Versuch, sie zu öffnen, fehlschlagen wird. Die Funk-
tion liefert bei Fehlern einen von Null verschiedenen Wert.
int rename(const char *oldname, const char *newname)
rename ändert den Namen einer Datei und liefert nicht Null, wenn der Versuch
fehlschlägt.
FILE *tmpfile(void) tmpfile erzeugt eine temporäre Datei mit Zugriff "wb+", die automatisch ge-
löscht wird, wenn der Zugriff abgeschlossen wird, oder wenn das Programm
normal zu Ende geht. tmpfile liefert einen Datenstrom, oder NULL, wenn die
Datei nicht erzeugt werden konnte.
char *tmpnam(char s[L_tmpnam]) tmpnam(NULL) erzeugt eine Zeichenkette, die nicht der Name einer existie-
renden Datei ist, und liefert einen Zeiger auf einen internen Vektor im stati-
schen Speicherbereich. tmpnam(s) speichert die Zeichenkette in s und liefert
auch s als Resultat; in s müssen wenigstens L_tmpnam Zeichen abgelegt wer-
den können. tmpnam erzeugt bei jedem Aufruf einen anderen Namen; man
kann höchstens von TMP_MAX verschiedenen Namen während der Ausfüh-
rung des Programms ausgehen. Zu beachten ist, dass tmpnam einen Namen
und keine Datei erzeugt.
int setvbuf(FILE *stream, char *buf, int mode, size_t size)
setvbuf kontrolliert die Pufferung bei einem Datenstrom; die Funktion muss
aufgerufen werden, bevor gelesen oder geschrieben wird, und vor allen ande-
ren Operationen. Hat mode den Wert _IOFBF, so wird vollständig gepuffert,
_IOLBF sorgt für zeilenweise Pufferung bei Textdateien und _IONBF verhin-
dert Puffern. Wenn buf nicht NULL ist, wird buf als Puffer verwendet; an-
dernfalls wird ein Puffer angelegt. size legt die Puffergrösse fest. Bei einem
Fehler liefert setvbuf nicht Null. (Der Einsatz von Puffern kann Dateizugriffe
zum Teil massiv beschleunigen).
void setbuf(FILE *stream, char *buf) Wenn buf den Wert NULL hat, wird der Datenstrom nicht gepuffert. Andern-
falls ist setbuf äquivalent zu (void) setvbuf(stream, buf, _IOFBF, BUFSIZ).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 78/168
Eine Einführung in die Informatik und die Programmiersprache C
Die Format-Zeichenkette enthält zwei Arten von Objekten: gewöhnliche Zeichen, die in die Ausgabe kopiert werden, und
Umwandlungsangaben, die jeweils die Umwandlung und Ausgabe des nächstfolgenden Arguments von fprintf veranlas-
sen. Jede Umwandlungsangabe beginnt mit dem Zeichen % und endet mit einem Umwandlungszeichen. Zwischen % und
dem Umwandlungszeichen kann der Reihenfolge nach folgendes angegeben werden:
Steuerzeichen (flags) (in beliebiger Reihenfolge), welche die Umwandlung modifizieren:
- veranlasst die Ausrichtung des umgewandelten Arguments in seinem Feld nach links.
+ bestimmt, dass die Zahl immer mit Vorzeichen ausgegeben wird.
Leerzeichen wenn das erste Zeichen kein Vorzeichen ist, wird ein Leerzeichen vorangestellt.
0 legt bei numerischen Umwandlungen fest, dass bis zur Feldbreite mit führenden Nullen aufgefüllt wird.
# verlangt eine alternative Form der Ausgabe. Bei o ist die erste Ziffer eine Null. Bei x oder X werden 0x
oder 0X einem von Null verschiedenen Resultat vorangestellt. Bei e, E, f, g und G enthält die Ausgabe
immer einen Dezimalpunkt; bei g und G werden Nullen am Schluss nicht unterdrückt.
eine Zahl, die eine minimale Feldbreite festlegt. Das umgewandelte Argument wird in einem Feld ausgegeben, das min-
destens so breit ist und bei Bedarf auch breiter. Hat das umgewandelte Argument weniger Zeichen als die Feldbreite
verlangt, wird links (oder rechts, wenn Ausrichtung nach links verlangt wurde) auf die Feldbreite aufgefüllt. Nor-
malerweise wird mit Leerzeichen aufgefüllt, aber auch mit Nullen, wenn das entsprechende Steuerzeichen angege-
ben wurde.
Einen Punkt,gefolgt von einer Zahl, (die Genauigkeit), welche die maximale Anzahl von Zeichen festlegt, die von einer
Zeichenkette ausgegeben werden, oder die Anzahl Ziffern, die nach dem Dezimalpunkt bei e, E, oder f Umwand-
lungen ausgegeben werden, oder die Anzahl signifikanter Ziffern bei g oder G Umwandlung oder die minimale
Anzahl von Ziffern, die bei einem ganzzahligen Wert ausgegeben werden sollen (führende Nullen werden dann bis
zur gewünschten Breite hinzugefügt).
Ein Buchstabe als Längenangabe: h, l oder L.
"h" bedeutet, dass das zugehörige Argument als short oder unsigned short ausgegeben wird;
"l" bedeutet, dass das Argument long oder unsigned long ist:
"L" bedeutet, dass das Argument long double ist.
[c99] "ll" für long long, "hh" für char, "j", "z" "t" für vordefinierte Typen wie intmax_t oder size_t
Als Feldbreite oder Genauigkeit kann jeweils * angegeben werden; dann wird der Wert durch Umwandlung von dem
nächsten oder den zwei nächsten Argumenten festgelegt, die den Typ int besitzen müssen.
Aufbau (Ohne Leerzeichen dazwischen, alles ausser dem Umwandlungszeichen kann weggelassen werden):
% Flag Zahl . Zahl Längenangabe Umwandlungszeichen
Beispiel: printf("%+08.4d", 17); gibt > +0017< aus
Die Umwandlungszeichen und ihre Bedeutung:
Zeichen Argument Ausgabe
d, i int dezimal mit Vorzeichen.
o int oktal ohne Vorzeichen (ohne führende Null).
x, X int hexadezimal ohne Vorzeichen (ohne führendes 0x oder 0X) mit abcdef bei 0x oder
ABCDEF bei 0X.
u int dezimal ohne Vorzeichen. (unsigned)
c int einzelnes Zeichen (Buchstabe), nach Umwandlung in unsigned char.
s char* aus einer Zeichenkette werden Zeichen ausgegeben bis vor '\0', aber maximal so viele
Zeichen, wie die Genauigkeit (im Formatstring) erlaubt.
f double dezimal als [-]mmm.ddd, wobei die Genauigkeit die Anzahl der d festlegt. Voreinstel-
lung ist 6; bei 0 entfällt der Dezimalpunkt.
a, A [c99] double [c99] hexadezimal als [-]0xh.hhhhhhp±d
e, E double dezimal als [-]m.dddddde±xx oder [-]m.ddddddE±xx, wobei die Genauigkeit die An-
zahl der d festlegt. Voreinstellung ist 6; bei 0 entfällt der Dezimalpunkt.
g, G double %e oder %E wird verwendet, wenn der Exponent kleiner als -4 oder nicht kleiner als
die Genauigkeit ist; sonst wird %f benutzt. Null und Dezimalpunkt am Schluss wer-
den nicht ausgegeben.
p void* als Zeiger (Speicheradresse, Darstellung hängt von Implementierung ab).
n int* die Anzahl der bisher von diesem Aufruf von printf ausgegebenen Zeichen wird im
Argument abgelegt. Es findet an dieser Stelle keine Ausgabe statt.
% - ein % wird ausgegeben.
Wenn das Zeichen nach % kein gültiges Umwandlungszeichen ist, ist das Ergebnis undefiniert.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 79/168
Eine Einführung in die Informatik und die Programmiersprache C
Die Format-Zeichenkette enthält normalerweise Umwandlungsangaben, die zur Interpretation der Eingabe verwendet
werden. Die Format-Zeichenkette kann folgendes enthalten:
Leerzeichen oder Tabulatorzeichen, die ignoriert werden.
Gewöhnliche Zeichen (nicht aber %), die dem nächsten Zeichen nach Zwischenraum im Eingabestrom entsprechen
müssen. Sie definieren so eine Eingabemaske. (scanf("(%d)", &i) z. B. akzeptiert nur Eingabewerte, die in
Klammern stehen, der Benutzer muss also auch die Klammern eintippen!!)
Umwandlungsangaben, bestehend aus einem %; einem optionalen Zeichen *, das die Zuweisung an ein Argument
verhindert; einer optionalen Zahl, welche die maximale Feldbreite festlegt; einem optionalen Buchstaben h, l, oder L,
([c99] auch hh, ll, j, z, t), der die Länge des Ziels beschreibt; und einem Umwandlungszeichen.
Eine Umwandlungsangabe bestimmt die Umwandlung des nächsten Eingabefelds. Normalerweise wird das Resultat in der
Variablen abgelegt, auf die das zugehörige Argument zeigt. Wenn jedoch * die Zuweisung verhindern soll, wie bei %*s,
dann wird das Eingabefeld einfach übergangen und eine Zuweisung findet nicht statt. Ein Eingabefeld ist als Folge von
Zeichen definiert, die keine Zwischenraumzeichen sind; es reicht entweder bis zum nächsten Zwischenraumzeichen, oder
bis eine explizit angegebene Feldbreite erreicht ist. Daraus folgt, dass scanf über Zeilengrenzen hinweg liest, um seine Ein-
gabe zu finden, denn Zeilentrenner sind Zwischenraumzeichen. (Zwischenraumzeichen sind Leerzeichen, Tabulatorzei-
chen \t, Zeilentrenner \n, Wagenrücklauf \r, Vertikaltabulator \v und Seitenvorschub \f).
Das Umwandlungszeichen gibt die Interpretation des Eingabefelds an. Das zugehörige Argument muss ein Zeiger sein.
Den Umwandlungszeichen d, i, n, o, u und x kann h vorausgehen, wenn das Argument ein Zeiger auf short statt int ist,
oder der Buchstabe l, wenn das Argument ein Zeiger auf long ist ([c99] ll für long long, hh für char, j, z, t für vorde-
finierte Typen wie intmax_t, size_t, ptrdiff_t). Vor den Umwandlungszeichen e, f und g kann der Buchstabe
l stehen, wenn ein Zeiger auf double und nicht auf float in der Argumentenliste steht, und L, wenn es sich um einen
Zeiger auf long double handelt.
Aufbau (Ohne Leerzeichen dazwischen, alles ausser dem Umwandlungszeichen kann weggelassen werden):
% Stern(*) Feldbreite(Zahl) Modifier(l, L oder h) Umwandlungszeichen
Achtung, scanf(...) lässt alle nicht verwertbaren Zeichen im Eingabepuffer stehen, der Eingabepuffer sollte deshalb vor
dem nächsten Aufruf von scanf() geleert werden, sonst wird scanf() zuerst die verbliebenen Zeichen umzuwandeln versu-
chen. Wenn die Zeichen erneut für die geforderte Eingabe nicht verwendbar sind, verbleiben sie weiterhin im Eingabepuf -
fer. So kann ein ungültiges Zeichen im Eingabepuffer jeden weiteren Einlesevorgang blockieren, solange es nicht aus dem
Puffer entfernt wird (Z. B. mit getchar() oder gets_s()).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 80/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 81/168
Eine Einführung in die Informatik und die Programmiersprache C
int fgetc(FILE *stream) fgetc liefert das nächste Zeichen aus stream als unsigned char (umgewandelt
in int) oder EOF bei Dateiende oder bei einem Fehler.
char *fgets(char *s, int n, FILE *stream) fgets liest höchstens die nächsten n-1 Zeichen in s ein und hört vorher auf,
wenn ein Zeilentrenner gefunden wird. Der Zeilentrenner wird im String s ab-
gelegt. Der String s wird mit '\0' abgeschlossen. fgets liefert s oder NULL bei
Dateiende oder bei einem Fehler.
int fputc(int c, FILE *stream) fputc schreibt das Zeichen c (umgewandelt in unsigned char) in stream. Die
Funktion liefert das ausgegebene Zeichen oder EOF bei Fehler.
int fputs(const char *s, FILE *stream) fputs schreibt die Zeichenkette s (die '\n' nicht zu enthalten braucht) in stream.
Die Funktion liefert einen nicht-negativen Wert oder EOF bei einem Fehler.
Es wird nicht automatisch ein Zeilentrenner ausgegeben (Im Gegensatz zu
puts()).
int getc(FILE *stream) getc ist äquivalent zu fgetc, kann aber ein Makro sein und dann das Argument
für stream mehr als einmal bewerten.
int getchar(void) getchar ist äquivalent zu getc(stdin).
char *gets(char *s) [Ab C11 nicht mehr unterstützt!] gets liest die nächste Zeile von stdin in das
Array s und ersetzt dabei den abschliessenden Zeilentrenner durch '\0'. Die
Funktion liefert s oder NULL bei Dateiende oder bei einem Fehler.
Grundsätzlich unsicher, da Bufferoverflows nicht abgefangen werden können!
[C11 opt.] char *gets_s(char *s, int size) gets_s liest die nächste Zeile von stdin in das Array s und ersetzt dabei den ab-
schliessenden Zeilentrenner durch '\0'. Die Funktion liefert s oder NULL bei
Dateiende oder bei einem Fehler. Dabei werden nicht mehr als size-1 Zeichen
im Buffer abgelegt, damit ist diese Funktion sicherer als gets() und ersetzt ab
C11 gets(). Enthält eine Zeile mehr als size-1 Zeichen wird die ganze Zeile
verworfen und s enthält einen Leerstring (s[0] wird auf 0 gesetzt). Achtung,
gets_s() ist in [c11] optional und muss nicht angeboten werden.
fgets(s, size, stdin) ist besser geeignet um mit langen Zeilen umzugehen.
int putc(int c, FILE *stream) putc ist äquivalent zu fputc, kann aber ein Makro sein und dann das Argument
für stream mehr als einmal bewerten.
int putchar(int c) putchar(c) ist äquivalent zu putc(c, stdout).
int puts(const char *s) puts schreibt die Zeichenkette s und einen Zeilentrenner in stdout. Die Funkti-
on liefert EOF, wenn ein Fehler passiert, andernfalls einen nicht-negativen
Wert.
int ungetc(int c, FILE *stream) ungetc stellt c (umgewandelt in unsigned char) in stream zurück, von wo das
Zeichen beim nächsten Lesevorgang wieder geholt wird. Man kann sich nur
darauf verlassen, dass pro Datenstrom ein Zeichen zurückgestellt werden kann.
EOF darf nicht zurückgestellt werden. ungetc liefert das zurückgestellte Zei-
chen oder EOF bei einem Fehler.
EOF EOF ist eine vordefinierte Konstante, welche das Dateiende anzeigt. Der Wert
der Konstante ist System- und Compilerabhängig, ist aber meist ein negativer
Wert.
Achtung, fgets() und fputs() behandeln Zeilentrenner ('\r\n') anders als gets()/gets_s() und puts(). Die f... Funktionen trans-
portieren den Text unverändert von der Quelle zum Ziel, hingegen entfernt gets_s() den Zeilentrenner am Ende der Zeile,
und puts() fügt einen Zeilentrenner am Ende der Ausgabe hinzu.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 82/168
Eine Einführung in die Informatik und die Programmiersprache C
19.6.7 Fehlerbehandlung
Viele der Bibliotheksfunktionen notieren Zustandsangaben, z. B. wenn ein Dateiende oder ein Fehler
gefunden wird. Diese Angaben können explizit gesetzt und getestet werden. Ausserdem kann der Inte-
ger-Ausdruck errno (der in <errno.h> deklariert ist) eine Fehlernummer enthalten, die mehr In-
formation über den zuletzt aufgetretenen Fehler liefert.
void clearerr(FILE *stream) clearerr löscht die Dateiende- und Fehlernotizen für stream.
int feof(FILE *stream) feof liefert einen von Null verschiedenen Wert, wenn für stream ein Dateien-
de notiert ist. (Also das Dateiende erreicht wurde).
int ferror(FILE *stream) ferror liefert einen von Null verschiedenen Wert, wenn für stream ein Fehler
notiert ist.
void perror(const char *s) perror(s) gibt s und eine von der Implementierung definierte Fehlermeldung
aus, die sich auf die Fehlernummer in errno bezieht. Die Ausgabe erfolgt im
Stil von fprintf(stderr, "%s: %s\n", s, "Fehlermeldung")
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 83/168
Eine Einführung in die Informatik und die Programmiersprache C
20 Modulares Programmieren
Jedes grössere Programm wird normalerweise in mehrere Module (Dateien) aufgeteilt. Ein Modul ist
eine Sammlung von Funktionen oder Prozeduren, die zusammen eine logische Einheit bilden und eine
bestimmte (Teil-)Aufgabe erledigen. Jedes Modul hat eine Schnittstelle. Die Schnittstelle definiert die
Funktionen und Datentypen, welche von dem Modul gegen aussen zur Verfügung gestellt werden und
wird mit der entsprechenden .h Datei zur Verfügung gestellt.
Das Beispielprogramm besteht aus den Modulen A, B, C, D, E, F und G. Jedes dieser Module besteht
aus einem Implementationsteil (.c Datei) und einer Schnittstelle (.h-Datei). Zudem existiert noch eine
Modulübergreifende T.h-Datei mit allgemeingültigen Definitionen (Datentypen, Konfigurationsein-
stellungen und ähnliches).
Jedes Modul, welches Dienste eines anderen Moduls in Anspruch nimmt, muss dessen Schnittstelle
(.h-Datei) mittels #include “....h“ einbinden (Hier durch Pfeile angedeutet). Um Abhängigkeiten zu
minimieren und die Wartbarkeit zu erhöhen, sollten die Includes in den Implementationsdateien (.c-
Dateien) erfolgen. Schnittstellendateien (.h-Dateien) sollten nur in Ausnahmefällen andere Schnittstel-
lendateien einbinden. (Wenn zum Beispiel die Regel, dass jede .h-Datei für sich alleine Compilierbar
sein soll, verletzt wird.). Zudem sollte jedes Modul nur diejenigen Schnittstellendateien einbinden, de-
ren Dienste auch effektiv benutzt werden.
20.2 Modulverwaltung
Die Verwaltung der Module innerhalb eines Projektes wird üblicherweise durch die IDE übernommen,
kann aber auch durch ein Makefile erfolgen. In beiden Fällen wird vom Entwickler eine Liste mit allen
zum Programm gehörenden Modulen (C-Dateien) erstellt, bei Makefiles in einem Texteditor, bei einer
IDE mittels File-Dialogen. In beiden Fällen wird beim Erzeugen des Programmes jede C-Datei einzeln
für sich compiliert, und die compilierten Module werden erst anschliessend durch den Linker zum aus-
führbaren Programm zusammengefügt. Wichtig: Das Compilieren und Übersetzen erfolgt gemäss der
Modulliste in der IDE (Oder dem Makefile), und nicht aufgrund von #include-Anweisungen!
Im Normalfall werden dabei nur diejenigen Dateien neu compiliert, die seit dem letzten Compiliervor-
gang geändert haben. (Dabei gilt eine Änderung an einer direkt oder indirekt eingebundenen .h-Datei
ebenfalls als Änderung. Auch aus diesem Grund empfiehlt sich eine Minimierung der Abhängigkeiten,
da sonst bei jeder Änderung in einer .h Datei unnötig viele c-Dateien erneut compiliert werden müs-
sen).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 84/168
Eine Einführung in die Informatik und die Programmiersprache C
20.3 Modulstruktur
Ein Modul enthält üblicherweise lokale Variablen und Funktionen/Prozeduren, welche nur innerhalb
des Moduls verwendet werden und von aussen nicht zugänglich sind, sowie globale Funktionen und
Variablen, auf welche von anderen Modulen aus zugegriffen werden kann.
(Globale Variablen sollten allerdings vermieden werden, da sie die Wartbarkeit eines Programmes enorm erschweren).
Ein Beispiel für ein Modul wäre stdio.h. Die Funktion printf() gehört zur Schnittstelle dieses Mo-
duls, aber innerhalb dieses Moduls gibt es noch lokale Funktionen, die z. B. die Umwandlung von
Zahlen in Text vornehmen und die von aussen nicht zugänglich sind.
Ein Modul sollte möglichst wenig Elemente global definieren. Das hat den Vorteil, dass Änderungen
an lokalen Funktionen keine direkten Auswirkungen auf andere Module haben können. (Das Ändern
von globalen Funktionen, insbesondere wenn Namen, Typ oder Anzahl von Argumenten oder Typ des
Rückgabewertes ändern, betrifft allen Code, der die entsprechenden Funktionen benutzt.)
20.3.1 Modulschnittstelle
Die Schnittstelle eines Moduls(.c) wird normalerweise in seiner Headerdatei (.h) definiert. Zur
Schnittstelle gehören die Prototypen der zur Verfügung gestellten Funktionen, sowie die benötigten
Datentypen (Typedefs, Enum- und Strukturdefinitionen) und Konstanten (#defines oder enums).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 85/168
Eine Einführung in die Informatik und die Programmiersprache C
sayhello.h
Dieses Projekt besteht aus den beiden Modulen #ifndef SAYHELLO_H
(Dateien) main.c und sayhello.c. Main.c benutzt #define SAYHELLO_H
die Funktion SayHelloWorld() und die globale
Variable SayAllCount vom Modul sayhello.c /* exported (global) function */
und muss deshalb die Headerdatei sayhello.h extern void SayHelloWorld(void);
mit '#include' einbinden. In dieser Headerdatei
sind die Schnittstellen des gleichnamigen c-mo-
/* exported (global) variable */
duls deklariert. Lokale Funktionen und Varia-
extern int SayAllCount;
blen des Moduls sayhello.c sind als static dekla-
riert und von Ausserhalb nicht zugreifbar. (Das #endif
#ifndef #endif Konstrukt verhindert des
mehrfache einbinden in dieselbe .c Datei)
main.c sayhello.c
#include "sayhello.h" #include <stdio.h>
#include "sayhello.h"
int main(int argc, char *argv[])
{ /* Prototypes (local functions) */
while(SayAllCount < 5) { static void SayHello(void);
SayHelloWorld(); static void SayWorld(void);
} /* Global Variables */
return 0; int SayAllCount = 0;
}
/* Local Variables */
static int Count = 0;
/* Die Variable Count sowie die */
/* Funktionen SayHello() und */ /* Function Definitions */
/* SayWorld() sind von hier */ void SayHelloWorld(void)
/* aus nicht zugreifbar */ {
SayHello();
printf(" ");
Ein Beispiel zum mehrfachem Einbinden einer SayWorld();
Headerdatei:
printf("\n");
A.h
SayAllCount++;
}
B.h static void SayHello(void)
#include "A.h" {
printf("Hello");
Count++;
M.c
}
#include "A.h" static void SayWorld(void)
#include "B.h" {
printf("World");
Die Datei A.h wird doppelt in das Modul M.c Count++;
eingebunden, dies kann zu Problemen führen. }
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 86/168
Eine Einführung in die Informatik und die Programmiersprache C
/* exported constants */
#define MAX_NUMBER_OF_POINTS 999
/* exported types */
typedef double CoordinateType;
/* exported structures */
struct Point {
CoordinateType x;
CoordinateType y;
CoordinateType z;
};
#endif
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 87/168
Eine Einführung in die Informatik und die Programmiersprache C
21 Datei I/O
Das Arbeiten mit Dateien läuft grundsätzlich über FILE-Objekte. Sämtliche Dateifunktionen benöti-
gen einen Zeiger auf ein solches Objekt. Eine vollständige Beschreibung aller Dateifunktionen befin-
det sich im Kapitel 19.6.1. Eine Datei wird in C grundsätzlich als eine Folge von Bytes betrachtet.
Bevor mit einer Datei gearbeitet werden kann, muss mit fopen() eine neues, mit dieser Datei ver-
knüpftes FILE-Objekt erzeugt werden. fopen() liefert einen Zeiger auf das FILE-Objekt zurück,
wenn das Öffnen der Datei erfolgreich war, und NULL wenn ein Fehler aufgetreten ist. Nach dem
Öffnen einer Datei muss immer auf Fehler geprüft werden bevor mit ihr gearbeitet wird.
#include <stdio.h>
FILE *DieDatei;
if (DieDatei == NULL) {
printf("Fehler, konnte die Datei nicht oeffnen!\n");
}
Das erste Argument der Funktion fopen() ist der Name der zu öffnenden Datei, das zweite die Zu-
griffsart auf die Datei:
"r" Textdatei zum lesen öffnen
"w" Textdatei zum Schreiben erzeugen; gegebenenfalls alten Inhalt wegwerfen
"a" anfügen; Textdatei zum Schreiben am Dateiende öffnen oder erzeugen
"r+" Textdatei zum Ändern öffnen (Lesen und Schreiben)
"w+" Textdatei zum Ändern erzeugen; gegebenenfalls alten Inhalt wegwerfen
"a+" anfügen; Textdatei zum Ändern öffnen oder erzeugen, Schreiben am Ende
Dateien können im Binär oder Textformat geöffnet werden, bei UNIX Betriebssystemen hat das For-
mat keine Bedeutung, bei Dos und Windows wird im Textmodus der Zeilenvorschub ('\n') beim
Schreiben in die Sequenz von Zeilenvorschub und Wagenrücklauf ('\n' '\r') umgewandelt, und beim
Lesen umgekehrt die Sequenz '\n' '\r' wieder in ein einfaches '\n'. Beim Lesen im Textmodus wird un-
ter Dos/Windows zudem das Zeichen mit dem ASCII-Code 26 (EOF, CTRL-Z) als Dateiende angese-
hen, der Rest der Datei nach diesem Zeichen kann somit nicht gelesen werden.
Um im Binärmodus zu arbeiten, muss den oben angegebenen Zugriffsarten noch ein 'b' angehängt
werden, also z. B. "rb" oder "w+b".
[C11] C11 unterstützt mit 'x' zusammen mit 'w' auch die exklusive Dateierzeugung, solche Dateien
dürfen bis zum Schliessen von keiner anderen Applikation benutzt werden. (Z.B. Lockfiles).
Wenn die Datei nicht mehr benötigt wird, muss sie mit fclose() wieder geschlossen werden.
fclose (DieDatei);
Zur Arbeit mit Textdateien können ähnliche Funktionen wie zur Ein/Ausgabe über Bildschirm und
Tastatur verwendet werden, den Funktionsnamen ist einfach ein f vorangestellt und sie haben einen
File-Pointer als zusätzliches Argument:
Tastatur/Bildschirm Funktion Datei
printf("Wert ist %d", i); Formatierte Ausgabe fprintf(DieDatei, "Wert ist %d",i);
scanf("%f", &f); Formatierte Eingabe fscanf(DieDatei, "%f", &f);
c = getchar(); Zeichen einlesen c = fgetc(DieDatei);
putchar(c); Zeichen ausgeben fputc(c, DieDatei);
gets(Buffer); Zeile einlesen *) fgets(Buffer, MaxLength, DieDatei);
puts(Text); Zeile ausgeben *) fputs(Text, DieDatei);
*)
Achtung, Zeilenwechsel werden von den f... Funktionen anders behandelt (Nicht entfernt und hinzugefügt).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 88/168
Eine Einführung in die Informatik und die Programmiersprache C
21.2 Beispiele
Beispiel für die Arbeit mit Textdateien
Das folgende Programm öffnet die Datei Test.txt, liest deren Inhalt zeilenweise und schreibt die gele-
senen Zeilen mit vorangestellter Zeilennummer in die Datei Test.out.
#include <stdio.h>
#include <stdlib.h>
/* Zeile einlesen... */
if (fgets(Buffer, 200, InputFile) != NULL) {
/* ...und mit vorangestellter Zeilennummer wieder ausgeben */
fprintf(OutputFile, "%04d: %s", ZeilenNummer, Buffer);
}
ZeilenNummer++;
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 89/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 90/168
Eine Einführung in die Informatik und die Programmiersprache C
22 Standardargumente
Wenn ein Programm gestartet wird, erhält es vom Betriebssystem Argumente. Diese Argumente wer-
den der Funktion main(int argc, char *argv[]) in den Variablen argc und argv überge-
ben. Die Namen sind nicht zwingend vorgeschrieben, haben sich aber eingebürgert.
In der Variablen argc (argument count) ist die Anzahl der Argumente abgelegt, und die Variable
argv (argument vector) ist ein entsprechend grosses Array aus Strings, welche die Argumente enthal-
ten. Es können also nur Strings übergeben werden. Das erste Argument ist immer der Programmname,
und zwar der effektive Namen der beim Aufruf gültig ist, inklusive dem Pfad zum Programm. (Wenn
die Datei umbenannt wurde, wird der neue Name geliefert, und nicht der zur Compilezeit gültige).
Die Argumente müssen dem Programm beim Aufruf via Commandline durch Leerzeichen separiert
hinter dem Programmnamen angegeben werden. Ob und wie die Argumente bei nicht Commandline-
Basierten Programmen übergeben werden, hängt vom Betriebssystem ab.
Das nachfolgende Programm gibt einfach alle seine Argumente auf dem Bildschirm aus:
#include <stdio.h>
#include <stdlib.h>
So können Programmen bereits beim Start zum Beispiel Namen von Dateien, oder Optionen überge-
ben werden. Sehr viele Programme lassen sich über Comandlineargumente steuern, wie zum Beispiel
der Befehl 'dir' in der Dos Shell.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 91/168
Eine Einführung in die Informatik und die Programmiersprache C
23 Sortieren
Einer der am häufigsten gebrauchten Algorithmen ist das Sortieren. Überlegen Sie sich als einleiten-
des Beispiel kurz wie Sie das folgende Array mit 5 Zahlen in aufsteigender Reihenfolge sortieren wür-
den (Als Anweisungsfolge an eine Person):
int Werte[5] = {9, 17, 3, 21, 5};
Es gibt sehr viele verschiedene bekannte Sortierverfahren, die sich durch Effizienz und Komplexität
voneinander unterscheiden. Die am einfachsten zu verstehenden Verfahren sind im allgemeinen auch
die am wenigsten effizienten. Einige bekannte Verfahren sind in der folgenden Tabelle aufgeführt
(Siehe 'The Art of Computerprogramming', Donald E.. Knuth, Addsion Wesley):
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 92/168
Eine Einführung in die Informatik und die Programmiersprache C
Bubblesort:
Bubblesort ist einer der einfachsten Sortieralgorithmen, er ist aber auch nicht gerade sehr effizient.
Vorgehen: Wir durchwandern das zu sortierende Feld vom Anfang zum Ende und vergleichen jeweils
das aktuelle Feld mit seinem nächsten Nachbarn, und wenn die Reihenfolge der beiden Elemente nicht
stimmt werden sie vertauscht. Wenn wir das Ende des Feldes erreicht haben, beginnen wir wieder von
vorne. Das wird solange wiederholt bis das Feld sortiert ist. Der Algorithmus heisst Bubblesort, weil
in jedem Durchgang die noch verbliebene grösste Zahl wie eine Luftblase an ihren korrekten Platz
aufsteigt.
Da nach jedem Durchgang ein Element mehr bereits am korrekten Platz ist, kann der Vorgang so opti-
miert werden, das die bereits korrekt platzierten Elemente nicht mehr weiter sortiert werden. Es wird
nicht jedesmal bis zum Ende des Feldes getauscht, sondern nur bis zum Beginn des bereits sortierten
Bereichs.
Der Algorithmus braucht im optimierten Fall (N-1)*(N / 2) Vergleiche sowie im Durchschnitt halb so
viele Vertauschungen (Im Schnitt muss nur jedes 2. Mal vertauscht werden). Eine Vertauschung kostet
allerdings meistens 2-3mal mehr Rechenzeit als ein Vergleich.
Das Feld ist sortiert, sobald in einem Durchlauf keine Vertauschung mehr durchgeführt werden muss-
te. Dies ist garantiert nach N Durchläufen der Fall.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 93/168
Eine Einführung in die Informatik und die Programmiersprache C
Combsort:
Combsort ist ein modifizierter Bubblesort, bei dem nicht direkte Nachbarn, sondern Elemente mit der
einer bestimmten Distanz 'Gap' verglichen und getauscht werden. Dieser Gap wird in jedem Durch-
gang verkleinert, bis er zu 1 wird. Mit einem Gap von 1 wird das Verfahren zu Bubblesort. Mit dem
Gap 1 werden soviele Durchgänge absolviert, bis keine Vertauschungen mehr stattfinden (Bubblesort).
Wenn mit dem Gap 1 keine Vertauschungen mehr erfolgen, ist das Feld sortiert. Gap wird bei jedem
Durchlauf um den Faktor 1.3 (Empirisch ermittelter Wert) verkleinert, und vor dem ersten Durchlauf
auf N/1.3 gesetzt. Gap wird dabei auf die nächste ganze Zahl abgerundet und darf nie kleiner als 1
werden.
Pass Gap 0 1 2 3 4 5 6 7 8 9 10
0 8 (= 11/1.3) 5 9 1 7 8 2 10 4 0 3 6
1.1 8 5 9 1 7 8 2 10 4 0 3 6 Aktueller
1.2 8 0 9 1 7 8 2 10 4 5 3 6 Vergleich
1.3 8 0 3 1 7 8 2 10 4 5 9 6
2.1 6 (= 8/1.3) 0 3 1 7 8 2 10 4 5 9 6
2.2 6 0 3 1 7 8 2 10 4 5 9 6
2.3 6 0 3 1 7 8 2 10 4 5 9 6
2.4 6 0 3 1 7 8 2 10 4 5 9 6
2.5 6 0 3 1 7 8 2 10 4 5 9 6
3.1 4 (= 6/1.3) 0 3 1 7 6 2 10 4 5 9 8
3.2 4 0 3 1 7 6 2 10 4 5 9 8
3.3 4 0 2 1 7 6 3 10 4 5 9 8
3.4 4 0 2 1 7 6 3 10 4 5 9 8
3.5 4 0 2 1 4 6 3 10 7 5 9 8
3.6 4 0 2 1 4 5 3 10 7 6 9 8
3.7 4 0 2 1 4 5 3 10 7 6 9 8
4.1 3 (= 4/1.3) 0 2 1 4 5 3 8 7 6 9 10
4.2 3 0 2 1 4 5 3 8 7 6 9 10
4.3 3 0 2 1 4 5 3 8 7 6 9 10
4.4 3 0 2 1 4 5 3 8 7 6 9 10
4.5 3 0 2 1 4 5 3 8 7 6 9 10
4.6 3 0 2 1 4 5 3 8 7 6 9 10
4.7 3 0 2 1 4 5 3 8 7 6 9 10
4.8 3 0 2 1 4 5 3 8 7 6 9 10
5.1 2 (= 3/1.3) 0 2 1 4 5 3 8 7 6 9 10
5.2 2 0 2 1 4 5 3 8 7 6 9 10
5.3 2 0 2 1 4 5 3 8 7 6 9 10
5.4 2 0 2 1 4 5 3 8 7 6 9 10
5.5 2 0 2 1 3 5 4 8 7 6 9 10
5.6 2 0 2 1 3 5 4 8 7 6 9 10
5.7 2 0 2 1 3 5 4 8 7 6 9 10
5.8 2 0 2 1 3 5 4 6 7 8 9 10
5.9 2 0 2 1 3 5 4 6 7 8 9 10
6.1 1(= 1/1.3) 0 2 1 3 5 4 6 7 8 9 10
6.2 1 0 2 1 3 5 4 6 7 8 9 10
6.3 1 0 1 2 3 5 4 6 7 8 9 10
6.4 1 0 1 2 3 5 4 6 7 8 9 10
6.5 1 0 1 2 3 5 4 6 7 8 9 10
6.6 1 0 1 2 3 4 5 6 7 8 9 10
6.7 1 0 1 2 3 4 5 6 7 8 9 10
6.8 1 0 1 2 3 4 5 6 7 8 9 10
6.9 1 0 1 2 3 4 5 6 7 8 9 10
6.10 1 0 1 2 3 4 5 6 7 8 9 10
7.1 1 0 1 2 3 4 5 6 7 8 9 10
... ... ... ... ... ... ... ... ... ... ... ... ...
7.11 1 0 1 2 3 4 5 6 7 8 9 10
Der Algorithmus ist wesentlich effizienter als Bubblesort, er liegt im Durchschnitt etwa bei ln(N)*N
Vergleichen, kann aber im Worstcase N*N Vergleiche erreichen. Die Effizienzsteigerung wird er-
reicht, indem Elemente zuerst über grosse Distanzen ausgetauscht werden, somit sind im Vergleich zu
Bubblesort weniger Vertauschungen nötig um grosse Distanzen zu überwinden. Durch den Gap > 1
wird Combsort jedoch instabil.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 94/168
Eine Einführung in die Informatik und die Programmiersprache C
Pass 0 1 2 3 4 5
0 5 9 1 7 6 4
1.1 5 9 1 7 6 4 Bereich zum Schieben
2.1 5 9 1 7 6 4 Aktueller Vergleich
Bereits Sortiert
2.2 5 9 1 7 6 4
aktuelles Element
2.3 5 9 1 7 6 4
3.1 1 5 9 7 6 4
3.2 1 5 9 7 6 4
3.3 1 5 9 7 6 4
3.4 1 5 9 7 6 4
3.5 1 5 9 7 6 4
4.1 1 5 7 9 6 4
4.2 1 5 7 9 6 4
4.3 1 5 7 9 6 4
4.4 1 5 7 9 6 4
4.5 1 5 7 9 6 4
5.1 1 5 6 7 9 4
5.2 1 5 6 7 9 4
5.3 1 5 6 7 9 4
5.4 1 5 6 7 9 4
1 4 5 6 7 9
1 4 5 6 7 9
Der Algorithmus braucht durchschnittlich (N-1)*(N / 2) Vergleiche und muss im Durchschnitt etwa
ebensoviele Elemente verschieben. (Das Suchen der Einfügeposition könnte mit binary search erfol-
gen, damit wären nur noch etwa log2(N!) *) Vergleiche nötig, aber die Anzahl der Verschiebungen
bleibt nach wie vor bei (N-1) * (N / 2) ).
*)
log2(N!) = log2(1) + log2(2) + log2(3) + ... + log2(N)
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 95/168
Eine Einführung in die Informatik und die Programmiersprache C
Pass 0 1 2 3 4 5
0 5 9 7 1 6 4 Aktueller Tausch
1.1 5 9 7 1 6 4 Bereits Sortiert
2.1 1 9 7 5 6 4 aktuell kleinstes Element
2.2 1 9 7 5 6 4
2.3 1 4 7 5 6 9
3.1 1 4 7 5 6 9
3.2 1 4 5 7 6 9
3.3 1 4 5 7 6 9
3.4 1 4 5 6 7 9
3.5 1 4 5 6 7 9
4.1 1 4 5 6 7 9
4.2 1 4 5 6 7 9
1 4 5 6 7 9
Der Algorithmus braucht durchschnittlich (N-1)*(N / 2) Vergleiche zum Suchen des kleinsten Ele-
mentes und muss etwa (N-1) Elemente Vertauschen.
23.3 Hinweis
Die bisher vorgestellten Algorithmen haben (Mit Ausnahme von Combsort) alle ein gemeinsames Pro-
blem, nämlich dass die benötigte Rechenzeit quadratisch zur Anzahl der zu sortierenden Elemente zu-
nimmt. Es gibt bessere Algorithmen, deren Rechenaufwand nur mit N*log(N) zunimmt. (Z. B. Quick-
sort oder Heapsort).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 96/168
Eine Einführung in die Informatik und die Programmiersprache C
23.4 Quicksort:
Quicksort ist einer der effizientesten und wahrscheinlich auch am häufigsten eingesetzten Sortieralgo-
rithmen. Die Funktionsweise dieses Sortierverfahrens ist nicht allzukompliziert, aber es ist dennoch
nicht so einfach, dieses auch fehlerfrei zu implementieren. Deshalb greift man meist auf entsprechende
Bibliotheksfunktionen zurück (z. B. qsort() in stdlib.h).
Die Idee des Algorithmus ist sehr einfach:
Man sucht sich zufällig ein Element aus dem Datenfeld aus (Oft als 'Pivot' bezeichnet), und zerteilt
das Feld anschliessend in zwei Teile, und zwar so, dass im einen Teil nur Zahlen grösser als der Pivot,
und im anderen nur Zahlen kleiner oder gleich dem Pivot enthalten sind. Anschliessend werden die
beiden Teile mit dem genau gleichen Verfahren weiterbehandelt. Dies wiederholt man so oft bis die
Grösse der Teilfelder 1 wird. Somit lässt sich Quicksort am einfachsten rekursiv implementieren.
Im Optimalfall würde das Feld in jedem Schritt exakt halbiert werden, was log2(N) Schritte ergeben
würde, und jedem Schritt müssen etwa N-1 Vergleiche zwischen Zahlen und Pivot durchgeführt wer-
den. Im optimalen Fall sind somit nur N*log2(N) Vergleiche nötig. Da das Feld jedoch im Allgemei-
nen nicht in der Mitte halbiert wird, sondern an einer zufälligen Stelle, ergeben sich im Durchschnitt
2*ln(N) * N oder 1.38*N*log2(N) Vergleiche, also etwa 38% mehr Aufwand (Siehe Robert Sedge-
wick, Algorithmen in C++).
Im schlimmsten Fall wird immer das kleinste oder grösste Element als Pivot gewählt, somit wird das
Feld nicht geteilt, sondern nur um 1 vermindert. In diesem Fall benötigt der Algorithmus N*N Ver-
gleiche. Es gibt einige Verbesserungen, um diese Fälle zu vermeiden (Sedgewick, Knuth).
Der Algorithmus heisst Quicksort, weil es eines der schnellsten Sortierverfahren ist.
Nachfolgend sind die Arbeitsschritte von Quicksort an einem Beispiel dargestellt. Es wird hier immer
das mittlerste Element des Feldes als Pivot verwendet.
Pass 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
1.0 10 13 2 17 12 1 21 8 20 5 6 11 19 18 15 4 9 3 7 14 16
1.1 3 4 2 5 1 6 12 21 8 20 17 11 19 18 15 13 9 10 7 14 16
2.0 3 4 2 5 1 6 12 21 8 20 17 11 19 18 15 13 9 10 7 14 16
2.1 1 2 4 5 3 6 12 16 8 14 17 11 7 15 13 9 10 18 19 20 21
3.0 1 2 4 5 3 6 12 16 8 14 17 11 7 15 13 9 10 18 19 20 21
3.1 1 2 4 5 3 6 10 9 8 7 11 17 14 15 13 16 12 18 19 20 21
4.0 1 2 4 3 5 6 10 9 8 7 11 17 14 15 13 16 12 18 19 20 21
4.1 1 2 3 4 5 6 7 8 9 10 11 12 14 13 15 16 17 18 19 20 21
5.0 1 2 3 4 5 6 7 8 9 10 11 12 14 13 15 16 17 18 19 20 21
5.1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
6.0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
6.1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
7.0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
7.1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
Ein Problem von Quicksort ist, dass es nicht stabil ist, d.h. eine ursprüngliche Ordnung bleibt nicht er -
halten, man kann also nicht zuerst nach Vornamen, und dann nach Nachnamen sortieren um Leute mit
gleichem Nachnamen nach Vornamen sortiert zu erhalten.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 97/168
Eine Einführung in die Informatik und die Programmiersprache C
Originalfeld: 8 1 12 13 2 16 15 7 9 13 6 3 14 10 4 5
In 16 Listen
a 1 Element 8 1 12 13 2 16 15 7 9 13 6 3 …
aufgeteilt:
Zu 8 Listen
a 2 Elemente
zusammen-
1 8 12 13 2 16 7 15 9 13 3 6 10 14 …
gefügt
Zu 4 Listen
a 4 Elemente 1 8 12 13 2 7 15 16 3 6 9 13 4 5 10 14
zusammen-
gefügt
Zu 2 Listen
a 8 Elemente 1 2 7 8 12 13 15 16 3 4 5 6 9 10 13 14
zusammen-
gefügt
Zu 1 Liste
a 16 Elemente 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
zusammen-
gefügt
In jedem Teilschritt wird so die Anzahl der Listen halbiert und die Länge der Teillisten verdoppelt.
Somit endet der Algorithmus nach log2(N) Teilschritten. In jedem Schritt müssen N/k Listen mit k
Elementen zu N/(2*k) Listen mit 2*k Elementen kombiniert werden. Dazu müssen Pro neue Liste
2*k-1 Vergleiche durchgeführt werden, also pro Teilschritt N/(2*k)*(2*k-1) (Also ungefähr N) Ver-
gleiche. Somit werden für den ganzen Vorgang ungefähr log2(N)*N Vergleiche benötigt.
Listmerge lässt sich sehr einfach mittels Rekursion implementieren. Es lässt sich problemlos stabil
(Beibehaltung einer ursprünglichen Ordnung) implementieren.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 98/168
Eine Einführung in die Informatik und die Programmiersprache C
Hier der optimierte Algorithmus für 5 Werte (Donald E. Knuth, The Art of Computer Programming, Vol 3) :
Es sollen die Elemente A, B, C, D, E miteinander verglichen werden. Die untere Grenze an Verglei -
chen ist ceil(log2(5!)) = ceil(6.9) = 7.
1. Schritt: Die Paare A,B und C,D jeweils Ordnen, so dass in B, resp. D das grössere Element liegt, so-
mit ist A < B und C < D (2 Vergleiche)
B D
A C E
2. Schritt: Die grösseren Elemente der beiden Paare ordnen (Paare tauschen, so dass D > B ist), somit
ist A < B < D und C < D (1 Vergleich)
B D
A C E
3. Schritt: Das Element E durch binäres Suchen (Zuerst mit B vergleichen, dann mit A oder D -> 2
Vergleiche) an der richtigen Stelle einfügen, dies gibt 4 Möglichkeiten: C < D und
(E<A<B<D oder A<E<B<D oder A<B<E<D oder A<B<D<E)
B D E B D B E D B D E
E A C A C A C A C
4. Schritt: Das Element C durch binäres Suchen am der richtigen Stelle einfügen (Es ist bekannt, dass
C vor D liegen muss) (2 Vergleiche)
B D E B D B E D B D E
E A C A C A C A C
Zuerst mit A vergleichen, Zuerst mit E vergleichen, Zuerst mit B vergleichen, Zuerst mit B vergleichen,
dann mit B oder E dann mit B oder A dann mit A oder E dann mit A.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 99/168
Eine Einführung in die Informatik und die Programmiersprache C
24 Suchen
Suchen von Werten innerhalb von Datenbeständen gehört wie das Sortieren zu den wichtigsten Algo-
rithmen in der Informatik.
Der Wert nach dem gesucht wird, bezeichnet man meist als Schlüssel.
Dieser Algorithmus kann noch optimiert werden: Anstelle jedes mal innerhalb der Suchschlaufe zu
prüfen ob man das Ende des Feldes erreicht hat, kann man am Ende des Feldes den gesuchten Wert
anhängen. Damit wird die Schlaufe auf jeden Fall beendet, ohne dass ein zusätzlicher Test auf errei-
chen des Ende des Feldes nötig ist. Das kann einen merklichen Geschwindigkeitszuwachs zur Folge
haben.
Wenn der Datenbestand nicht sortiert ist, ist das Sequentielle Suchen die einzige Suchmöglichkeit, und
auch in Listen kann nur sequentiell gesucht werden
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 100/168
Eine Einführung in die Informatik und die Programmiersprache C
Wir vergleichen 68 zuerst mit dem mittleren Element (51) der Tabelle und stellen fest das wir in der
oberen Hälfte des Feldes weitersuchen müssen. Wir vergleichen 68 erneut mit dem mittleren Element
(77) des verbliebenen Feldes und stellen fest, das wir in dessen unterer Hälfte weitersuchen müssen.
Und so landen wir nacheinander noch bei 67 und 74 bis wir im fünften Schritt den gesuchten Wert 68
gefunden haben.
Die Effizienz dieses Algorithmus beruht darauf, dass das Restfeld in jedem Suchschritt halbiert wird
und die Suche sich so sehr schnell dem Ziel nähert.
Dieser Algorithmus lässt sich sehr schön rekursiv Implementieren: Solange das Element nicht gefun-
den ist, wendet die Funktion sich selbst auf die entsprechende übriggebliebene Hälfte an.
(Eine Implementation mit einer Schlaufe ist jedoch wesentlich effizienter.)
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 101/168
Eine Einführung in die Informatik und die Programmiersprache C
25 Rekursion
Rekursion bedeutet, dass sich ein Programm selbst aufruft. Viele Probleme lassen sich sehr elegant re-
kursiv Lösen, insbesondere alle, die sich auf einen vorgängig zu bestimmenden Wert beziehen.
Ein bekanntes, wenn auch eher sinnloses Beispiel ist das Berechnen der Fakultät nach der Rekursions-
formel n! = n * (n-1)! und der Anfangsbedingung 0! = 1.Dies würde wie folgt implementiert werden:
int Fakultaet(int n)
{
if (n > 0) {
return n*Fakultaet(n-1); /* Rekursiver Aufruf */
}else {
return 1;
}
}
Allerdings ist in diesem Fall eine Iteration (Schlaufe) eindeutig die bessere Lösung.
Achtung: Eine Rekursion kann sehr viel Stack-Speicher benötigen, und bei Systemen mit wenig
Stackspeicher zum Programmabsturz führen. Iterative Ansätze sind normalerweise (Geschwindig-
keits-) Effizienter als Rekursionen, Rekursionen sind dagegen oft einfacher zu Implementieren und zu
Verstehen.
Jede Rekursion kann mit dem entsprechenden Aufwand in eine Iteration umgewandelt werden. In si-
cherheitskritischen Anwendungen (z. B. Autopilot oder Medizin) darf Rekursion im allgemeinen nicht
eingesetzt werden.
Eine Rekursion muss eine garantierte Abbruchbedingung besitzen, damit sie ein Ende findet und nicht
ewig läuft (Und somit garantiert abstürzt).
Eine Rekursion ist im Gegensatz zu einer Schlaufe durch den verfügbaren Stack-Speicher limitiert.
Wenn eine Rekursion länger läuft als Speicher zur Verfügung steht, stürzt das Programm unweigerlich
ab.
Ein weiteres Beispiel für eine Rekursion ist die Ausgabe einer Zahl. Die Einerstelle einer Zahl kann
mit der Modulo-Operation sehr einfach bestimmt werden, jedoch müssen zuvor die übrigen Ziffern
der Zahl ausgegeben werden. Also wenn man 4285 Ausgeben will ergibt sich folgender Ablauf:
Ablauf Code
4285 muss noch ausgegeben werden void PrintZahl(int x)
5 ist die Einerstelle, 428 muss noch ausgegeben werden {
8 ist nun die Einerstelle, 42 muss noch ausgegeben werden if (x > 0) {
PrintZahl(x/10);
2 ist nun die Einerstelle, 4 muss noch ausgegeben werden putchar('0' + x %
4 ist nun die Einerstelle, keine weiteren Ausgaben nötig 10);
4 ausgeben 4 }
}
2 ausgeben 42 Die Einrückung entspricht
8 ausgeben 428 der Verschachtelung der int main (int c, char
5 ausgeben 4285 rekursiven Aufrufe *a[])
{
PrintZahl(4285);
Aufgabe 25.1: return 0;
Schreiben Sie ein Programm, das eine Zeile einliest und umgekehrt wieder ausgibt. Verwenden Sie
dazu Rekursion. (Tip: Lesen Sie die Zeile zeichenweise [getchar()]).
Eingabe: Hallo Welt Ausgabe: tleW ollaH
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 102/168
Eine Einführung in die Informatik und die Programmiersprache C
Um den Turm der Höhe n auf das rechte Feld zu bekommen, muss zuerst der Teilturm aus
den oberen (n-1) Scheiben in die Mitte plaziert werden, dann die unterste Scheibe auf den
rechten Platz verschoben werden und anschliessend wird der Teilturm von der Mitte auch
auf den rechten Platz verschoben.
Das bedeutet, um einen Turm der Höhe n zu verschieben, müssen wir zweimal einen Turm der Höhe
(n-1) und einmal eine Scheibe verschieben. Das Verschieben eines Teilturmes kann für sich alleine
wie das Verschieben des ganzen Turms betrachtet werden und deshalb auch mit der oben beschriebe-
nen Methode behandelt werden, ausser die Höhe des Teilturms ist 0, aber das Verschieben eines Tur-
mes der Höhe 0 ist trivial, es muss schlicht nichts gemacht werden.
Diese Vorschrift kann auch von menschlichen Spielern zum Lösen des Problems benutzt werden.
Die Vorschrift könnte in Pseudocode umgesetzt etwa wie folgt aussehen:
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 103/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 104/168
Eine Einführung in die Informatik und die Programmiersprache C
26 Dynamische Speicherverwaltung
Häufig ist beim Schreiben und Compilieren eines Programmes nicht bekannt, wieviel Daten dereinst
von diesem Programm bearbeitet werden. Deshalb ist es bei den internen Datenstrukturen oft auch
nicht möglich, im Voraus eine vernünftige, fixe Grösse für den Speicherplatz zu wählen.
Man kann natürlich alle Arrays auf Vorrat masslos überdimensionieren (wie zum Beispiel:
char TextVonEditor[1000000]), nur ist damit immer noch nicht garantiert dass nun in jedem
Fall genügend Platz vorhanden ist, und zudem wird dadurch im Normalfall viel zuviel Speicher ver-
schwendet.
Es muss deshalb möglich sein, zu Laufzeit des Programmes Speicher vom Betriebssystem anzufordern
wenn er benötigt wird, und diesen Speicher auch wieder zurückzugeben wenn er nicht mehr benutzt
wird.
Dieses dynamische Anfordern und Freigeben von Speicher bezeichnet man als dynamische Speicher-
verwaltung. In C stehen dazu grundsätzlich die Funktionen malloc() zum Allozieren von Speicher,
und free() zum Freigeben von Speicher zur Verfügung. (Zur Anforderung von Speicher gibt es für
spezielle Fälle noch die Funktionen realloc() und calloc()). Der Speicherbereich, aus dem
dieser dynamisch verwaltete Speicher entnommen wird, bezeichnet man als 'Heap'.
Der Funktion malloc() muss die Grösse des gewünschten Speichers übergeben werden, Sie liefert
einen Zeiger auf den nun reservierten Speicherbereich zurück. Wenn das System nicht mehr genügend
Speicher frei hat, um die Anforderung zu erfüllen, wird ein NULL-Zeiger zurückgegeben. Es ist des-
halb unerlässlich, nach jeder Anforderung von Speicher zu überprüfen, ob man den Speicher auch er-
halten hat.
Mit der Funktion realloc() kann ein bereits reservierter Speicherblock 'erweitert' werden. Dabei
wird vom System ein neuer Block mit der neuen Grösse reserviert, soviel Daten wie möglich (Maxi-
mal soviel wie der neue Block aufnehmen kann, resp. im alten enthalten ist) vom alten Block in den
neuen Block kopiert, der alte Block freigegeben und ein Zeiger auf den neuen Block zurückgeliefert.
Der Funktion muss ein Zeiger auf den alten Block, sowie die gewünschte neue Grösse mitgegeben
werden. Diese Funktion liefert ebenfalls einen NULL Zeiger zurück, wenn der angeforderte Speicher
nicht zur Verfügung gestellt werden konnte (Der alte Block wird in diesem Fall aber nicht freigege-
ben). Diese Funktion kann auch nachgebildet werden, indem man das Allozieren, Kopieren und Frei-
geben selbst programmiert, nur kann realloc in gewissen Fällen effizienter sein (Wenn z.B. der Spei-
cher hinter diesem Block noch frei ist, kann der Block vom Betriebssystem einfach erweitert werden,
und es muss nichts kopiert werden.)
Wenn man einen reservierten Speicherblock nicht mehr benötigt, muss man ihn mit der Funktion
free() wieder freigeben, damit er anderen Programmen zur Verfügung gestellt werden kann. Der
Funktion free() muss man den Zeiger auf den freizugebenden Speicherblock übergeben (Derselbe
Zeiger, den man von malloc(), calloc() oder realloc() erhalten hat).
Achtung, wenn ein Zeiger übergeben wird, der nicht auf einen von malloc() gelieferten Speicher-
block zeigt, oder denselben Block zweimal freigibt wird die Speicherverwaltung korrupt, und es muss
mit Absturz oder verfälschten Daten gerechnet werden. (Um solche Probleme zu finden gibt es spezi-
elle Debug-Speicherverwaltungen, welche zusätzliche Überprüfungen vornehmen und Fehlermeldun-
gen ausgeben.)
Wichtig, jeder Speicherblock, der angefordert wird, muss auch wieder freigegeben werden, sonst ent-
steht ein sogenanntes Speicherleck. Das heisst, dass der verfügbare Speicher ständig abnimmt, bis
plötzlich kein freier Speicher mehr übrig ist.
Innerhalb von Interruptroutinen und zeitkritischen Codestücken sollten malloc() und free()
nicht eingesetzt werden. (Als Workaround Speicher ausserhalb der Interruptroutine auf Vorrat allozie-
ren und auch ausserhalb der Interruptroutine wieder freigeben).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 105/168
Eine Einführung in die Informatik und die Programmiersprache C
#include <stdlib.h>
#include <stdio.h>
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 106/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 107/168
Eine Einführung in die Informatik und die Programmiersprache C
Wenn man eine sehr schnelle Speicherverwaltung benötigt, geht das am einfachsten mit einem Array,
dabei muss man aber die Einschränkung einer fixen Blockgrösse in Kauf nehmen.
static char MyMemory[50][128] = {0}; /* 50 Blöcke a 128 Bytes */
Bei dieser Version gilt die Einschränkung, das im ersten Byte des Speicherblocks nie der Wert 0 abge-
legt werden darf, weil sonst der Block als Frei angesehen wird. In vielen Anwendungen ist das kein
Problem, und sonst muss einfach ein Zeiger auf das zweite Byte des Blocks zurückgegeben werden,
dann hat der Anwender keinen Zugriff auf das Flag-Byte am Anfang:
/* Aenderung in FastMalloc: */
return &(MyMemory[i][1]); /* Zeiger auf Zweites Byte zurueck */
/* Aenderung in FastFree: */
*(((char *)(Mem))-1) = 0; /* Block als Frei markieren */
Wenn man mehrere Blockgrössen benötigt, kann man einfach für jede Blockgrösse ein eigenes Array
benutzen, und die grösse der Arrays dem zu erwartenden Speicherbedarf anpassen. Speicheranforde-
rungen des Anwenders werden jeweils auf die nächstgrösste Blockgrösse aufgerundet, und der Block
aus dem entsprechenden Array angefordert.
Bei grossen Arrays kann anstelle eines Flagbytes eine Indexliste aufgebaut werden. In jedem freien
Block ist im vordersten Byte/Wort der Index des nächsten freien Blocks eingetragen, so wird eine Lis-
te der freien Blocks aufgebaut welche schnelleres finden eines freien Blocks erlaubt. Die Speicherver-
waltung muss sich den Index des ersten freien Blocks merken (Head der Liste).
Freigegebene Blöcke werden zuvorderst in die Liste eingehängt, und angeforderte Blöcke werden wie-
derum vom Listenanfang genommen (Somit ist nur ein 'Head-Pointer' notwendig).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 108/168
Eine Einführung in die Informatik und die Programmiersprache C
27 Listen
Arrays sind die einfachste Form, um eine Menge von Daten zu verwalten. Sobald aber die einzelnen
Datenelemente eine gewisse Grösse erreichen oder häufig Daten zwischen bestehenden Elementen
eingefügt oder entfernt werden sollen, wird sehr viel Zeit mit kopieren von Daten verbracht.
Eine Lösung für dieses Problem bietet die Verwendung von Listen. Eine Liste kann man sich wie eine
Kette vorstellen, jeder Datensatz ist wie ein Kettenglied jeweils mit seinen direkten Nachbarn verbun-
den. Wenn man nun einen Datensatz einfügen oder entfernen will, muss dazu nicht die ganze Liste ge-
ändert werden, es betrifft nur die direkten Nachbarn.
Diese Verbindungen der Nachbarn untereinander werden in C normalerweise mit Zeigern realisiert.
Wenn man häufig auf Elemente am Listenende zugreifen will (Queue) empfiehlt sich der Einsatz eines
weiteren Zeigers, der ständig auf das letzte Element der Liste zeigt.
Head Element 1 Element 2 Element 3 Element 4 Element 5
Tail
int Wert;
Und nun die 'Verankerung' der Liste: Mindestens der Zeiger auf den Anfang ist unverzichtbar, sonst
weiss man ja nicht wo die Liste beginnt.
struct Listenelement * Head; /* Zeiger auf den Listenanfang */
struct Listenelement * Tail; /* Zeiger auf das Listenende */
Die beiden Zeiger müssen bei einer leeren Liste auf NULL gesetzt werden.
Neue Listenelemente werden bei bedarf mit malloc() erzeugt und nicht mehr benötigte mit
free() wieder freigegeben (Mit einem typedef könnte man sich einige Schreibarbeit ersparen):
struct Listenelement * Neues; /* Zeiger auf neues Element*/
Neues = (struct Listenelement *) malloc(sizeof(struct Listenelement ));
....
free(Neues);
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 109/168
Eine Einführung in die Informatik und die Programmiersprache C
Achtung: Der Speicherplatz von Listenelementen, die aus einer Liste entfernt werden und nicht mehr
benötigt werden, muss mit free() wieder freigegeben werden, falls zum Erzeugen der Listenele-
mente malloc() verwendet wurde. Das Freigeben oder Erzeugen von Listenelementen ist jedoch
nicht unbedingt Aufgabe der Einfüge- und Entfernfunktionen.
Aufgabe 27.1:
Entwerfen Sie zwei C-Funktionen (Inklusive Design/Struktogramm), die Elemente am Listenende ein-
fügen resp. entfernen.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 110/168
Eine Einführung in die Informatik und die Programmiersprache C
Bei einer einfach verketteten Liste lassen sich Elemente sehr einfach am Anfang oder am Ende anfü-
gen oder entfernen, es ist aber viel aufwendiger, Elemente in der Mitte hinzuzufügen oder zu entfer-
nen.
Aufgabe 27.2:
Überlegen Sie sich, wie Sie vorgehen müssten, um bei einer einfach verketteten Liste ein Element in
der Mitte zu entfernen, und wie um eines in der Mitte hinzuzufügen. Besprechen Sie sich mit Ihrem
Nachbarn.
Überlegen Sie sich, mit welchen Modifikationen der Listenstruktur man diese Funktionen vereinfa-
chen könnte.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 111/168
Eine Einführung in die Informatik und die Programmiersprache C
Ein Datenelement einer doppelt verketteten Liste könnte in C wie folgt definiert werden:
struct Datenelement {
int Wert;
Die Listenoperationen gestalten sich eigentlich ähnlich wie bei der einfach verketteten Liste, nur muss
jetzt immer auch der Rückwärtszeiger behandelt werden.
Struktogramm C-Code
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 112/168
Eine Einführung in die Informatik und die Programmiersprache C
Um ein Element hinter einem gegebenen Element anzuhängen, ist wie folgt vorzugehen:
Achtung: Der Speicherplatz von Listenelementen, die aus einer Liste entfernt werden und nicht mehr
benötigt werden, muss mit free() wieder freigegeben werden, falls zum Erzeugen der Listenele-
mente malloc() verwendet wurde. Das Freigeben oder Erzeugen von Listenelementen ist jedoch
nicht unbedingt Aufgabe der Einfüge- und Entfernfunktionen.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 113/168
Eine Einführung in die Informatik und die Programmiersprache C
Aufgabe 27.3: Überlegen Sie sich die Vorgehensweise zum Löschen eines beliebigen Elementes aus
der Liste. Sie erhalten einen Zeiger auf das zu löschende Element (Bitte Spezialfälle auch behandeln:
letztes, erstes, einziges).
Um jedes Element einer Liste zu untersuchen, bietet sich folgende Schlaufe an:
struct Datenelement* Current;
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 114/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 115/168
Eine Einführung in die Informatik und die Programmiersprache C
Um mit mehreren Listen gleichzeitig arbeiten zu können, kann das Modul dahingehend geändert wer-
den, das Tail und Head in eine Struktur gepackt werden, und allen Funktionen als zusätzliches Argu-
ment ein Zeiger auf eine solche Struktur übergeben wird.
Jede Liste hat so ihre eigene Listenverwaltung (Listenkopf und Ende).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 116/168
Eine Einführung in die Informatik und die Programmiersprache C
28 (Binäre) Bäume
Wenn man auf schnelles Suchen angewiesen ist, können Listen nicht verwendet werden. Als alternati-
ve dazu bieten sich Bäume an. Im Gegensatz zu einer Liste ist ein Baum eine Hierarchische Struktur,
bei der effizientes Suchen möglich ist. Ein Baum ist von Natur aus immer sortiert.
Bei einem binären Baum hat jedes Element zwei Nachfolger, auf der linken Seite ein Element welches
kleiner, und auf der rechten Seite ein Element welches grösser als das aktuelle Element ist. (Links und
Rechts sind willkürlich gewählte Bezeichnungen)
Soll ein Baum auch Mehrfach-Einträge erlauben (Das selbe Element mehrmals enthalten) werden glei-
che Elemente entweder immer links (Kleiner oder gleich), oder immer rechts (Grösser oder gleich) ab-
gelegt.
Beispiel für einen Baum Wurzel
Ebene 1 12
5 13
Ebene 2
Ebene 3 4 6 15
2 9 14 17
Ebene 4
8 10
Ebene 5 16 20
Der oberste Knoten eines Baumes heisst Wurzel. Ein Element, das keine Nachfolger hat, wird als Blatt
bezeichnet, ein Element mit Nachfolger als Knoten. Jedes Blatt und jeder Knoten ist genau einem
Knoten untergeordnet. Jeder Knoten bildet zusammen mit seinen untergeordneten Elementen einen
Teilbaum.
Bei einem optimal verteilten Baum haben bis auf die Knoten der letzten und der zweitletzten Ebene
alle Knoten zwei Nachfolger. Dies wird als ausgeglichener Baum bezeichnet. Bei einem ausgegliche-
nen Baum findet man jedes Element nach spätestens log2(N) vergleichen.
int Wert;
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 117/168
Eine Einführung in die Informatik und die Programmiersprache C
Und nun noch der Aufhänger für die Wurzel des Baumes:
Da ein Baum eine rekursive Datenstruktur ist (Ein Baum besteht aus Teilbäumen), lässt sich das Ein-
fügen von Elementen sehr elegant rekursiv lösen:
Das sortierte Ausgeben des Bauminhaltes lässt sich auch sehr leicht rekursiv lösen:
void PrintTree(TreeNode *Root)
{
/* Nur falls gültiges Element */
if (Root != NULL) {
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 118/168
Eine Einführung in die Informatik und die Programmiersprache C
Rekursiv:
TreeNode *FindNode(int Wert, TreeNode *Node)
{
/* Falls kein Element, nicht gefunden */
if (Node== NULL) {
return NULL;
} else {
/* Prüfen ob gefunden, sonst in Teilbaeumen weitersuchen */
if (Node->Value == Wert) {
return Node;
} else {
/* Prüfen ob im rechten oder im linken Teilbaum weitersuchen*/
if (Wert > Node->Value) {
/* Wert ist grösser, im rechten Teilbaum weitersuchen*/
return FindNode(Wert, Node->Right);
} else {
/* Wert ist kleiner, im linken Teilbaum weitersuchen*/
return FindNode(Wert, Node->Left);
}
}
}
Sequentiell:
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 119/168
Eine Einführung in die Informatik und die Programmiersprache C
Das Löschen eines Knotens ist deutlich komplizierter, insbesondere wenn ein Knoten mit zwei Nach-
folgern gelöscht werden soll, dann müssen beide Teilbäume dieses Knotens an der korrekten Stelle des
Restbaumes eingefügt werden. Bei Bäumen empfiehlt es sich deshalb besonders, auf bereits existie-
rende (und gründlich getestete) Bibliotheken zurückzugreifen.
/* Der Funktion muss die Addresse des Zeigers, der auf den zu löschenden */
/* Knoten zeigt, übergeben werden. Dies ist nötig, weil ebendieser Zeiger */
/* auch verändert werden muss. (Suchfunktion entsprechend ändern) */
ParentPtr ParentPtr
Zu löschendes gelöschtes
Element Element
void RemoveNode(TreeNode **ParentPtr) {
TreeNode *Node = *ParentPtr;
TreeNode *p, *p2;
if(Node == NULL) {
return; /* element not found; */
} else {
if((Node->left == NULL) && (Node->right == NULL)) {
/* Knoten hat keine Nachfolger, einfach entfernen */
free(Node);
*ParentPtr= NULL;
}
else if(Node->left==NULL) {
/* Knoten hat einen Nachfolger, einfach bei Parent anhaengen */
p = Node->right;
free(Node);
*ParentPtr= p;
}
else if(Node->right == NULL) {
/* Knoten hat einen Nachfolger, einfach bei Parent anhaengen */
p = Node->left;
free(Node);
*ParentPtr= p;
else {
/* hat zwei Nachfolger, zusammenfuegen und bei Parent anhaengen */
p2 = Node->right;
p = Node->right;
/*Zusammenfügeposition suchen (wo linken Zweig in rechten einfuegen)*/
while(p->left) p = p->left;
p->left = Node->left;
free(Node);
*ParentPtr= p2;
}
}
}
Bei binären Bäumen muss man darauf achten, dass sie nicht entarten (Viele Knoten haben nur einen
Nachfolger -> Baum wird zu einer Liste). Um das zu verhindern gibt es 'balanced Trees' (Ausgegli-
chene Bäume). Dabei wird bei jeder Einfüge und Löschoperation durch Überprüfung der Baumstruk-
tur und eventueller Umordnung des Baumes versucht, den Baum in einer möglichst optimalen Form
zu behalten.
Es gibt noch viele weitere Varianten von Bäumen, einige davon haben auch mehr als zwei Nachfolger
pro Knoten. Jede Variante hat ihre Vor- und Nachteile, welche jeweils gegeneinander abgewogen wer-
den müssen.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 120/168
Eine Einführung in die Informatik und die Programmiersprache C
29 Hashtabellen
Um Daten schnell zu finden werden oft Hashtabellen eingesetzt. Eine Hashtabelle ist ein gewöhnli-
ches Array, aber die Position eines jeden Elementes in diesem Array wird aus dem Schlüssel (= Wert
nach dem gesucht wird) des Elements selbst berechnet. Diese Berechnung wird als Hashfunktion be-
zeichnet. Bei Strings kann im einfachsten Fall einfach die Quersumme über alle ASCII-Codes der ent-
haltenen Buchstaben gebildet werden, oder auch aufwendigere Berechnungen ausgeführt werden, wel-
che auch Buchstabenverdreher berücksichtigen.
Es passiert selbstverständlich hin und wieder, das zwei unterschiedliche Einträge denselben Hashwert
erhalten, und so eigentlich am selben Platz in der Tabelle stehen müssten. Dies bezeichnet man als
Kollision. Das Problem kann auf verschiedene Arten umgangen werden.
Entweder bildet man Listen bei den einzelnen Tabellenplätzen (closed addressing), so können mehrere
Einträge 'am selben' Platz stehen, oder man nimmt einfach den nächsten freien Platz in der Tabelle,
(open addressing) wobei die Schrittweite für die Suche des nächsten freien Platzes statt fix 1 (Linear
Probing) besser durch eine weiteren Hashfunktion berechnet wird (Double hashing). Beim 'open ad-
dressing' darf die Tabelle nicht zu voll werden (<70%) und Einträge dürfen nicht einfach gelöscht
werden, sonst würden eventuell nachfolgende Kollisionsopfer später nicht mehr gefunden werden.
Deshalb werden gelöschte Einträge mit einem speziellen Marker (Tombstone) gekennzeichnet, dieser
wird beim Suchen als unpassender Eintrag, und beim Einfügen als Leerplatz interpretiert. (Beim 'clo-
sed addressing' können die Elemente direkt aus der Liste entfernt werden.). Welche Variante ('open'
oder 'closed') effizienter ist hängt von der Prozessorarchitektur (Cache) und der Datengrösse ab.
Die grösste Schwierigkeit bei Hashtabellen ist es, eine möglichst optimale Hashfunktion zu finden. Sie
sollte einerseits schnell sein, aber andererseits Kollisionen möglichst optimal vermeiden und die
Schlüssel möglichst gleichverteilt in die Tabelle abbilden. Zudem müssen die Schlüssel positiv sein,
da sie im Allgemeinen als Arrayindex verwendet werden.
Beispiele einiger Hash-Funktionen (Key ist der Wert, nach dem gesucht wird, Text oder Zahl):
#define FACTOR 7 /* Sollte Ungerade sein */
#define TABLESIZE 256 /* 2-er Potenzen sind schnell */
/* Primzahlen ideal */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 121/168
Eine Einführung in die Informatik und die Programmiersprache C
1983 4901
83
Beispielcode für eine Hashtabelle mit Einfüge und Suchfunktionen. (Doppelte Einträge werden nicht
gefunden, aber auch nicht verhindert. Dazu müsste man beim Einfügen zuerst nach dem Wert suchen,
und nur dann einfügen wenn der Wert nicht gefunden wurde.).
Suchfunktionen werden im optimalen Fall bei der Benutzung von Hashtabellen um den Faktor der Ta-
bellengrösse beschleunigt (Im Vergleich zum Suchen in linearen Listen, ist aber Prozessorabhängig).
Häufiges Vergleichen komplexer Datensätze oder ganzer Dateien kann mit Hashing ebenfalls be-
schleunigt werden, indem für jeden Datensatz / jede Datei ein Hashwert berechnet wird, und ansch-
liessend nur noch die Hashwerte verglichen werden. Jetzt muss nur noch im Falle einer Übereinstim-
mung der Hashwerte ein vollständiger Vergleich der Datensätze/Dateien durchgeführt werden, was
den Vorgang im Mittel deutlich beschleunigt. In Hashtabellen abgelegte Daten sind nie geordnet.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 122/168
Eine Einführung in die Informatik und die Programmiersprache C
O(1): Der Rechenaufwand ist unabhängig von der Anzahl Elemente in der Datenstruktur.
O(log n): Der Rechenaufwand wächst logarithmisch mit der Anzahl Elemente in der Datenstruktur.
O(n): Der Rechenaufwand wächst linear mit der Anzahl Elemente in der Datenstruktur.
X: Die Operation benötigt unverhältnismässigen Aufwand.
: Die Operation ist nicht möglich.
(*1): Es wird an einer beliebigen, resp. bei sortierten Daten an der korrekten Stelle eingefügt, d.h. es muss gegebenen-
falls die korrekte Einfügeposition gesucht werden
(*2): Es wird vorausgesetzt dass es noch freien Platz im Array hat.
(*3): Im Falle eines gefüllten Arrays wird beim Einfügen zusätzlich Zeit zum Allozieren eines grösseren Speicherblocks
mit anschliessendem Kopieren der Daten benötigt. Wenn dabei der Speicherblock jedes mal verdoppelt wird, ergibt
sich im Durchschnitt dennoch keine Änderung des Laufzeitverhaltens, d.h. K verschwindet ("amortized in time").
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 123/168
Eine Einführung in die Informatik und die Programmiersprache C
0 1 N-1
2
1 N-2
0 N-1
Mathematisch ausgedrückt, wird ein Array arr via Modulo-Funktion angesprochen, also anstelle
arr[i] wird das Array mit arr[i%N] angesprochen, wobei N der Arraylänge entspricht.
Da die Modulo-Operation recht aufwendig ist, wird diese möglichst vermieden und stattdessen mit
Vergleichen gearbeitet oder N als Zweierpotenz gewählt (So kann eine And-Verknüpfung anstelle ei-
ner Modulo-Operation benutzt werden).
Da das Array nun keinen Anfang und kein Ende mehr hat, werden Zeiger oder Indices benötigt, um
den Anfang oder das Ende der Daten zu kennzeichnen. Dafür müssen beim Entfernen oder Hinzufü-
gen von Einträgen am Anfang oder am Ende des Datenbereichs nicht mehr die restlichen Arrayinhalte
nachgerückt werden, es reicht wenn die entsprechenden Zeiger modifiziert werden.
0 1 N-1
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 124/168
Eine Einführung in die Informatik und die Programmiersprache C
0 1 N-1
Lesen Schreiben
Auch hier wird bei jedem Vorwärtsstellen eines Zeigers geprüft, ob das Arrayende überschritten wird,
und falls ja der Zeiger auf den Anfang des Arrays gesetzt.
Leider ist es bei einer direkten Implementation nicht möglich, zwischen Voll und Leer zu unterschei-
den, da in beiden Fällen der Lese- und der Schreibe-Zeiger auf denselben Platz zeigen. Eine Lösung
wäre, immer einen Platz leer zu lassen. So kann Gleichheit nur bei leerem Ringbuffer auftreten. Eine
andere Lösung wäre das Zählen der Anzahl enthaltenen Elemente. Der Zähler muss einfach bei jedem
Schreiben inkrementiert, und bei jedem Lesen dekrementiert werden.
Hier ein Beispiel für eine Ringbufferimplementation (Ohne Fehlerhandling):
#define MAX_SIZE 200
int LesePosition = 0; /* Zeiger auf naechstes zu lesende Element */
int SchreibePosition = 0; /* Zeiger auf naechsten freien Platz */
int NumberOfEntries = 0; /* Anzahl belegte Plaetze */
float Daten[MAX_SIZE];
void AddToRingbuffer(float Value) /* Ignoriert Value wenn Buffer voll */
{
if (NumberOfEntries < MAX_SIZE) {
Daten[SchreibePosition++] = Value;
if (SchreibePosition >= MAX_SIZE) {
SchreibePosition = 0;
}
NumberOfEntries++;
} else {
/* Errorhandling, Buffer voll */
}
}
float GetFromRingbuffer(void) /* Liefert 0 wenn Buffer leer ist */
{
float Value = 0.0;
if (NumberOfEntries > 0) {
Value = Daten[LesePosition++];
if (LesePosition >= MAX_SIZE) {
LesePosition = 0;
}
NumberOfEntries--;
} else {
/* Errorhandling, Buffer leer */
}
return Value;
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 125/168
Eine Einführung in die Informatik und die Programmiersprache C
Ein Stack kann mit einem Array oder einer Liste implementiert werden. Ein Array ist ideal, wenn die
maximale Anzahl abgelegter Elemente im Voraus bekannt ist, und die einzelnen Elemente nicht viel
Speicherbedarf haben.
Eine Liste kann benutzt werden, wenn die maximale Anzahl an Elementen nicht im Voraus bekannt
ist. Dabei werden die Datenelemente am Anfang der Liste hinzugefügt, und ebenfalls am Anfang der
Liste wieder entnommen.
Hier ein Beispiel für eine Stackimplementation mit einem Array (Ohne Fehlerhandling):
#define MAX_SIZE 200
int SchreibeLesePosition = 0;
float Daten[MAX_SIZE];
Ein wichtiger Einsatz für den Stack ist bei der Umgehung von Rekursion. Mit Hilfe eines Stacks kann
jede Rekursion in eine Schlaufe umgewandelt werden (Bei der Rekursion wird eigentlich der Stack
des Prozessors benutzt).
Dazu müssen an der Stelle des rekursiven Aufrufs die aktuellen lokalen Variablen (Der aktuelle Zu-
stand) auf den Stack abgelegt werden und durch die Parameter des rekursiven Funktionsaufrufs ersetzt
werden. Die ganze Funktion muss nun in eine Schleife eingepackt werden, die solange läuft wie Ele-
mente auf dem Stack sind. Zu Beginn der Schlaufe werden jeweils die lokalen Variablen (Der Zu-
stand) mit dem obersten Element auf dem Stack initialisiert und das oberste Element vom Stack ent-
fernt.
Die Benutzung eines Stacks anstelle einer Rekursion besitzt den Vorteil, die Kontrolle über den Spei-
cherverbrauch zu haben. Bei sauberer Programmierung ist es nicht möglich, dass der Stack quer durch
den Speicher läuft. Bei der Benutzung eines Arrays kann sogar ganz auf dynamischen Speicher ver-
zichtet werden (Bei Controllern nützlich). Dafür ist der Code meist weniger elegant.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 126/168
Eine Einführung in die Informatik und die Programmiersprache C
Ein wichtiger Einsatz für die Queue ist die Zwischenspeicherung von Daten. Insbesondere wenn Da-
ten von einer Quelle stossweise produziert werden, und vom Empfänger kontinuierlich abgearbeitet
werden. Alle Daten werden beim Empfang zuerst in einer Queue abgelegt, und anschliessend langsam
abgearbeitet. Das kann man sich wie bei einer Kaffeemühle vorstellen. Oben schüttet man denn Kaf-
fee sackweise (oder Lastwagenweise) in einen Vorratsbehälter, verarbeitet wird der Kaffee jedoch
kontinuierlich. Der Vorratsbehälter wird also Stossweise gefüllt, aber kontinuierlich entleert.
Eine Queue könnte man z. B. zum Zwischenspeichern von über die serielle Schnittstelle oder das
Netzwerk empfangenen Daten verwenden, wenn sie nicht sofort verarbeitet werden können. Damit
bleibt die Reihenfolge dennoch erhalten und es gehen keine Daten verloren.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 127/168
Eine Einführung in die Informatik und die Programmiersprache C
33 Software Engineering
Softwaresysteme werden immer komplexer und umfangreicher und bleiben oft lange im Einsatz. Um
die Entwicklungs- und Wartungskosten unter Kontrolle zu halten sowie zu einem stabilen und zuver-
lässigen System zu gelangen ist geplantes und strukturiertes Vorgehen beim Erstellen von Software
unumgänglich. Dieses ingenieurmässige Vorgehen bei der Programmierung von Softwaresystemen
wird als Software Engineering bezeichnet.
Qualitätpunkte: Kostenpunkte:
Wartbarkeit Entwicklungskosten
Korrektheit Personalkosten
Zuverlässigkeit Werkzeugkosten
Effizienz Wartungskosten
Ressourcen Amortisation
Ergonomie (Benutzerschnittstelle) Risiko
Portabilität
Wiederverwendbarkeit
Modularität
Flexibilität (Weiterentwicklung, Modifikation)
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 128/168
Eine Einführung in die Informatik und die Programmiersprache C
Wer macht was? Zuständigkeit und Verantwortlichkeiten für einzelne Aktivitäten und
Phasen regeln
Was ist gemacht worden? Projektfortschritt anhand erreichter Ergebnisse kontrollieren
Was ist noch zu tun? Aufwand für verbleibende Aktivitäten abschätzen
Diese fünf Phasen bilden nicht einen starren, streng linearen Ablauf, vielmehr sind sie Teile eines ite-
rativen Prozesses, bei dem einzelne oder mehrere Phasen bei negativen Resultaten mehrfach durchlau-
fen werden.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 129/168
Eine Einführung in die Informatik und die Programmiersprache C
Phasenablauf
Jede Phase wird zuerst initialisiert, das heisst die notwendigen Vorarbeiten werden ausgeführt, ansch-
liessend folgt der eigentliche Phasenaktivitätsteil gefolgt von den Abschlussarbeiten. Jede Phase hat
eine Schnittstelle zu der vorangehenden und der nachfolgenden Phase. Diese Schnittstellen bestehen
aus einer Reihe von Dokumenten und Anforderungen, die erfüllt, bzw. vorgelegt werden müssen (sie-
he dazu nachfolgende Graphik).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 130/168
Eine Einführung in die Informatik und die Programmiersprache C
Problem
4 1. Codierfehler .
Billig zu beheben
Analyse oder
Spezifikationsphase 2. Programmdesignfehler
3 Benötigen mehr Aufwand zur Kor-
rektur und sind mit mehr Kosten
Pflichtenheft verbunden.
3. Systemdesignfehler
System Design Phase Benötigen sehr viel Aufwand und
sind teuer zum Beheben.
Programm und
Datenstruktur
1
Codierung
(Implementation)
Programmcode
Modultest
Programmtest
Systemtest
System-Design-Test
Abnahme bzw. Test
der Spezifikation
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 131/168
Eine Einführung in die Informatik und die Programmiersprache C
Da sich das Wasserfallmodell in der Realität bei grösseren Projekten nicht vernünftig einsetzen lässt
(Man müsste das ganze System in einem Schritt realisieren), wurden weitere Modelle entworfen, eines
davon ist das nachfolgend dargestellte Spiraldiagramm. Das System wird dabei nicht in einem Durch-
gang, sondern schrittweise realisiert. In jedem Schritt wird dem System weitere Funktionalität hinzu-
gefügt, wobei in den ersten Schritten vorallem eine stabile und erweiterbare Systemarchitektur entwor-
fen wird. Am Ende eines jeden Zyklus steht ein getestetes (Teil-)System mit einer bestimmten Funkti-
onalität.
Das Spiraldiagramm zeigt die sukzessive Verbesserung und Erweiterung eines Softwaresystems, bis
die Zielvorstellung nahezu erreicht worden ist. Das Ziel wird nie erreicht, nur bei jedem Schritt besser
angenähert. Der Zyklus muss somit irgendwann einmal abgebrochen werden, damit das Projekt ein
Ende findet.
Problemanalyse
Problem
Leistungs-
beschreibung
Programm Entwurf
Implementierung
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 132/168
Eine Einführung in die Informatik und die Programmiersprache C
Das V-Modell:
Das V-Model ist dem Wasserfallmodell ähnlich, jedoch steht jeder Entwicklungsphase eine entspre-
chende Testphase gegenüber, und die Testspezifikationen werden in der jeweiligen Entwicklungspha-
se definiert. Damit wird sichergestellt, das Entwicklungsphase und Testphase zusammenpassen, und
umfassende Tests verfügbar sind, denn es muss für jede Anforderung oder Spezifikation eine dazuge-
hörige Testspezifikation erstellt werden.
Je nach Phase findet eine Validation (Wurde das richtige entwickelt) und/oder eine Verifikation (Ist
das Ergebnis korrekt) statt.
Für das V-Model gibt es viele unterschiedliche Definitionen, je nach Standard und Einsatzgebiet.
Nachfolgend ein generisches Beispiel, welches die grundsätzlichen Eigenschaften des V-Modells auf-
zeigt:
Validation
Testspezifikationen
Anforderungsdefinition Systemtest
Modulspezifikation Modultest
Implementierung Codereview
Verifikation
Grundsätzlich sind in diesem Modell auch Zyklen ähnlich dem Wasserfalldiagramm möglich, auch
iteratives oder paralleles Vorgehen (Bereits mit der nächsten Phase beginnen, obwohl die aktuelle
Phase noch nicht abgeschlossen ist) wird von einigen Varianten erlaubt.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 133/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 134/168
Eine Einführung in die Informatik und die Programmiersprache C
Bei einem TDD-Projekt beginnt man somit mit einem Test und einem Modul, welches nur aus den
leeren Schnittstellenfunktionen besteht, also noch keine Funktionalität besitzt. Dieses wird nun dem
Test unterworfen (Wobei der Test fehlschlägt), anschliessend wird die Funktionalität hinzugefügt und
erneut getestet (Wobei der Test nun bestanden werden muss). Anschliessend wird wieder ein Test und
allfällige Schnittstellenfunktionen hinzugefügt, getestet, Funktionalität hinzugefügt und wieder getes-
tet. Dieser Prozess wird sooft wiederholt, bis das System als ganzes die gewünschte Funktionalität
aufweist.
Bei jedem Test werden immer alle bisher entworfenen Tests abgearbeitet, so wird sichergestellt, dass
eine neu hinzugefügte Funktionalität nichts bestehendes beschädigt.
33.5 Scrum
Scrum ist ein weiteres Vorgehen zur Realisierung von Projekten. Scrum definiert eine Menge von Re-
geln, verschiedene Rollen, Aktivitäten und Artefakte.
Die drei Hauptrollen sind:
Product Owner:Er möchte ein möglichst optimales Produkt aus Sicht der Firma
Projektteam: Liefert Funktionalität in der vom Produkt Owner gewünschten Reihenfolge.
Scrum Master: Wacht über die Einhaltung der Scrum-Regeln und sorgt für optimales Arbeitumfeld.
Bei Scrum wird die Projektlaufzeit in kleine Intervalle (Sprints) von 1 bis 4 Wochen Dauer zerlegt. Zu
Beginn eines jeden Sprints wird zwischen Product Owner und Entwicklungsteam vereinbart, welche
Funktionalität im diesem Sprint zu realisieren ist. Während des Sprints wird an dieser Vereinbarung
nichts mehr geändert. Am Ende eines jeden Sprintes steht ein funktionierendes, getestetes Produkt.
Wärend des Sprints trifft sich das Projektteam täglich (Daily Scrum Meeting) um sich zu Synchroni-
sieren und Besprechen.
Scrum gibt genau vor, welche Dokumente, Sitzungen und Abläufe im Rahmen des Scrumprozesses
auftreten, und welche Auswirkungen diese haben.
Dazu gehören tägliche Sitzungen, Sitzungen zu Beginn und Ende eines jeden Sprints, Backlog (Tabel-
le mit erledigten, in Arbeit stehenden und unerledigten Arbeiten), Burndown-Chart (Diagramm mit
noch ausstehenden Arbeiten gegen die Zeit aufgetragen, üblicherweise eine (linear) fallende Kurve,
die den Projektverlauf anzeigt).
33.6 Refactoring
Beim Refactoring geht es darum, die Qualität bestehender Software zu verbessern, ohne (wichtig!) an
der Funktionalität etwas zu ändern. Refactoring gehört grundsätzlich zur Codeentwicklung, normaler-
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 135/168
Eine Einführung in die Informatik und die Programmiersprache C
weise wechseln sich die Schritte 'Funktionalität hinzufügen/ändern' und 'Refactoring' während der
Softwareimplementation ab.
Im Normalfall setzt man eine gewisse Mindestqualität der zu verbessernden Software voraus, je
schlechter die Ursprungsqualität, desto mehr Aufwand ist für das Refactoring notwendig, und ab einer
bestimmten Schwelle ist ein kompletter Neuentwurf sinnvoller.
Wenn ein Neuentwurf aus Zeit- oder Kostengründen nicht möglich ist, versucht man, das System
durch eine Kombination von Refactoring und Ersetzen von Modulen schrittweise in einen Neudesign
zu überführen.
Dazu entwirft man in einem ersten Schritt ein Neudesign des Gesamtsystems, und ersetzt anschlies-
send im Laufe der Zeit Modul für Modul. Dabei wird jeweils ein Refactoring durchgeführt, um
Schnittstellen zu erhalten, welche den Ersatz des jeweiligen Moduls erlauben, anschliessend wird die-
ses Modul ersetzt und dieser Schritt mit einem weiteren Refactoring abgeschlossen.
So kann in einem Zeitschritt jeweils ein Modul ersetzt werden, was pro Schritt weniger Zeit benötigt
als ein kompletter Neuentwurf.
Beim Refactoring geht man Schrittweise vor, und testet nach jedem Schritt, ob immer noch alles funk-
tioniert. (Wenn nur wenig geändert wurde, ist es viel einfacher, einen neu eingebauten Fehler zu loka-
lisieren).
In einem Schritt kann eine (oder mehrere, aber nicht zuviele) der folgenden Tätigkeiten ausgeführt
werden (Anpassen und ergänzen von Kommentar gehört selbstverständlich in jedem Schritt dazu):
- Zuerst bestimmen, wo die schlimmsten/dringendsten Baustellen sind. (Module, welche man sowieso
ersetzen oder wegwerfen will, braucht man nicht zu behandeln)
- Wenn im ganzen Projekt kein Design erkennbar ist, muss meist von vorne begonnen werden (Inklu-
sive Design), es können eventuell. Funktionen oder Codestücke übernommen oder als Anregung
verwendet werden. (Initialisierungen und ähnliches). Das ist aber nicht mehr Refactoring und sollte
in einem eigenen Projekt nicht vorkommen
- Bestehende Funktionen anders auf Module verteilen oder in neue Module auslagern, bestehende
Module umbenennen oder zusammenfassen.
- Schlecht dokumentierten Code nachkommentieren.
- Lange Funktionen in Teilfunktionen aufteilen.
- 'EierlegendeWollMilchSau'-Funktionen in mehrere Funktionen mit klarer Aufgabe und Zuständig-
keit aufteilen.
- Unstrukturierten Code strukturieren.
- Komplexer/Komplizierten Code vereinfachen.
- Unnötigen Code entfernen.
- Unpassende Namensgebung überarbeiten.
- Globale Elemente lokalisieren.
- Zusammenführen was zusammengehört.
- Code von dem man nicht weiss was er macht zu verstehen versuchen, oder mit Kommentar als 'Un-
bekanntes verhalten' kennzeichnen (Für Behandlung zu einem späteren Zeitpunkt, wenn der gesamte
Code besser bekannt und verstanden ist). Sollte in einem eigenen Projekt nicht vorkommen
- In schlimmen Fällen ganze Module durch neu geschriebenen Code ersetzen. In Extremfällen muss
der ganze Code neu erstellt werden, mit dem Risiko das für einige Zeit gar nichts mehr geht. (Wenn
möglich versuchen, Schrittweise (Funktion für Funktion oder Modul für Modul) vorzugehen. Sollte
in einem eigenen Projekt nicht vorkommen
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 136/168
Eine Einführung in die Informatik und die Programmiersprache C
34 Analyse/Design
Durch die Analyse wird das Problem untersucht und erfasst. Das Lösungskonzept entsteht durch Syn-
these während der Designphase. Die Implementierung (Codierung, Realisierung) stellt die Umsetzung
der Problemstellung in eine Programmiersprache dar.
Beim Übergang von Analyse zu Design ergeben sich häufig Lücken, weil nicht alles genau spezifiziert
ist oder werden konnte. Softwareentwickler müssen diese Lücke im Normalfall durch ihre Erfahrung
überbrücken.
Analysephase
Das wichtigste Resultat dieser Phase ist der Anforderungskatalog:
• Die Beschreibung der Gesamtaufgabe des Systems (Inklusive Sicherheitsanforderungen, Zuver-
lässigkeit, Lebensdauer, Energiebedarf, ...)
• Die Beschreibung aller Verarbeitungen, die das System erbringen soll.
• Die Beschreibung der Anforderungen an das Zeitverhalten des Systems
• Die Beschreibung aller Daten, die dafür einzugeben sind .
• Die Beschreibung aller dafür bereitzuhaltenden Daten.
• Die Beschreibung wesentlicher Konsistenzbedingungen wie Wertebereiche und Plausibilitäten
• Die Beschreibung der Zugriffsrechte auf die Daten: Wer darf was lesen, eingeben, verändern
oder löschen
• Die Beschreibung der Mensch-Maschinen-Schnittstelle (MMS/MMI): Welche Eingaben hat der
Benutzer wie zu tätigen.
• Die Beschreibung aller Ausgaben des Systems mit Ausgabemedium und Format.
• Die Beschreibung der Daten, die über Schnittstellen übernommen oder geliefert werden.
Unter Daten verstehen wir dabei auch Dinge wie Werte von Sensoren, Zustände von Schaltern oder
Steuersignale, dabei sind auch Eigenschaften wie Format, Wertebereich, Genauigkeit oder Zeitanfor-
derungen zu definieren.
Anforderungen müssen grundsätzlich Test- und Messbar sein. (Schlecht: 'Das System soll sofort Ant-
worten'. Gut: 'Das System soll in 99% der Fälle innerhalb 100ms Antworten')
Designphase
In der Designphase werden ausgehend vom Anforderungskatalog ein oder mehrere Entwürfe für das
Gesamtsystem erstellt.
Objektorientierter Entwurf:
Das System als Ansammlung von miteinander kommunizierenden Objekten betrachten, Bezie-
hungen und Zusammenarbeit zwischen den Objekten finden und modellieren.
Datengesteuerter Entwurf:
Das System widerspiegelt die Struktur der zu verarbeitenden Daten.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 137/168
Eine Einführung in die Informatik und die Programmiersprache C
34.2 Methoden
Eine Kombination von Methoden zur Lösung eines Problems wird als Methodologie bezeichnet. Eine
Analyse/Design-Mehodologie umfasst typisch die folgenden Elemente:
Projektmanagement-Technik
Dokumentationsprozeduren
Designwerkzeuge (ev. CASE-Tools)
Standards, Regeln
Die Anwendung einer Methodologie ergibt folgende Vorteile:
Aufdeckung von Widersprüchen
Erhöhung der Qualität
verbesserte Kommunikation zwischen den Projektpartnern
kürzere Einarbeitungszeit
überprüfbare Resultate
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 138/168
Eine Einführung in die Informatik und die Programmiersprache C
Die Verwendung von Methoden / Methodologien soll nicht als ein einengendes Korsett verstanden
werden, sondern als Hilfsmittel / Werkzeug um den Denkprozess zu unterstützen, eine klare Vorge-
hensweise zu definieren, die Kommunikation mit Kunden und Mitarbeitern zu erleichtern und das Ab-
weichen von geordneten Bahnen (Abdriften ins Chaos) zu vermeiden.
Wichtig ist, dass allen Beteiligten die Regeln der verwendeten Methoden mit ihren Vor- und Nachtei-
len bekannt sind.
Konzept und Systemdesign Structure Chart (Modul Hierarchie), Modul Life Cycles
CRC-Cards (Class, Responsibilities and Collaboration)
Jackson (Datenstrukturen)
Nassi Schneidermann (Struktogramm)
DFD, MASCOT (Prozesse und Prozesskommunikation)
State-Event (Automaten, reaktive Systeme)
Entity-Relationship-Modelle
UML (OOD)
Detailliertes Systemdesign, Pro- Structure Charts (Modul Hierarchie), Modul Life Cycles
grammdesign CRC-Cards (Class, Responsibilities and Collaboration)
Jackson (Datenstrukturen)
Nassi Schneidermann (Struktogramm)
Petri-Netze (Automaten, reaktive Systeme, Kommunikation
von Prozessen, Erkennen von Deadlocks)
Entscheidungstabellen
State-Event (Automaten, reaktive Systeme)
Entity-Relationship-Modelle (Beschreibung von Datenbanken,
Datennormalisierung)
UML (OOD)
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 139/168
Eine Einführung in die Informatik und die Programmiersprache C
34.4 Fehlerhandling
Ein oft unterschätzter Punkt ist der Umgang mit Fehlern oder unerwarteten Situationen in einem Sys-
tem. Es ist wichtig, sich bereits in der Designphase darüber Gedanken zu machen, welche Fehler oder
unerwarteten Situationen im zu entwerfenden System auftreten könnten, und wie damit umzugehen
ist.
Eine gute Möglichkeit ist das klassifizieren von Fehlern. Mögliche Klassen wären:
• Fehler die ignoriert werden können (Warnungen, allenfalls loggen).
• Fehler die lokal gleich beim Auftreten behandelt werden können (z. B. Retry).
• Fehler die vom System auf höherer Ebene behandelt werden können. (z. B. Neustart eines Teil-
prozesses).
• Fehler die durch Benutzerinteraktion behoben werden können. (z. B. Auswechseln von Ver-
schleissteilen, entfernen von Verunreinigungen auf Sensoren)
• Fehler die Teile des Systems ausser Betrieb setzten, das restliche System jedoch nicht beein-
trächtigen.
• Fehler die ein (geordnetes) Anhalten des Systems erfordern.
• Fehler die ein automatisches Ausschalten des Systems erfordern.
• Fehler die ein sofortiges und notfallmässiges Ausschalten des Systems erfordern.
• Fehler die das Starten oder Vorhandensein von Backup- oder Sicherheitssystemen erfordern.
Zu Designbeginn muss die Systemarchitektur so entworfen werden, dass die entsprechenden Fehler-
kategorien an den entsprechenden Stellen in den (Teil-)Systemen behandelt werden können, und Feh-
lerzustände erkannt und signalisiert werden können.
Grundsätzlich sollten Fehler immer dort behandelt werden, wo es am besten möglich ist, normalerwei-
se möglichst nahe beim Auftreten.
Fehler, welche nicht am Ort des Auftretens behandelt werden können, werden an die nächsthöhere In-
stanz weitergeleitet, welche diese ihrerseits weiterleitet, wenn sie nicht damit umgehen kann. So wer-
den schwerwiegende Fehler von Teilsystemen an das übergeordnete System weitergemeldet. Unterge-
ordnete Systeme können sich bei Bedarf auch abschalten, bevor der Fehler weitergemeldet wird.
Das Fehlerhandling sollte innerhalb eines Systems systematisch und immer gleich erfolgen. Beispiels-
weise immer durch Rückgabewerte von Funktionen, dabei sollte die Bedeutung der Rückgabewerte
einheitlich sein (Z.B. immer 0 für keinen Fehler). Es können auch Strukturen verwendet werden, dann
können zusätzliche Informationen zum Fehler gemeldet werden.
Bei grösseren Systemen kann der Code für das Fehlerhandling mehr Codezeilen beanspruchen als der
Code für den normalen Ablauf. Schon alleine aus diesem Grund ist es erforderlich, dass der Umgang
mit Fehlern im Design eingeplant wird, ansonsten besteht Gefahr, des der Code durch das Fehlerhand-
ling unverständlich und schwer wartbar wird.
34.5 Softwarearchitektur
Wie ein Haus braucht auch ein Programm eine Architektur, insbesondere wenn die Software sehr um-
fangreich wird. Mit der Architektur wird das Grundgerüst der Software definiert, durch sie wird auch
bestimmt, wir gut zukünftige Erweiterungen möglich sind. Die Architektur legt den grundsätzlichen
Aufbau des Systems fest und lässt sich nur sehr schwer ändern. Es empfiehlt sich, genug Zeit in den
Entwurf einer guten Architektur zu investieren, und diesen auch zu Hinterfragen, bevor mit der Imple-
mentation begonnen wird. Voraussetzung für eine gute Architektur sind klar definierte Anforderungen
(Requrements), welche vorgängig spezifiziert werden müssen. Anforderungen sollen nur das WAS,
aber keinesfalls das WIE festlegen. Anforderungen sind so zu spezifizieren, dass sie auch getestet wer-
den können. Eine gute Architektur soll für Tests ausgelegt sein und das fortlaufende Testen erleich-
tern, und (sinnvolle) zukünftige Anpassungen und Änderungen erleichtern.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 140/168
Eine Einführung in die Informatik und die Programmiersprache C
35 Designmethoden
Jede Designmethode ist an eine spezifische Modellierungssprache gebunden, welche meist aus graphi-
schen Elementen besteht. Die Syntax und Grammatik dieser Sprachen ist genau definiert ('Zeich-
nungsvorschrift') und muss vom Anwender eingehalten werden. (Sonst treten Verständigungsproble-
me auf). Es gibt meist keinen plausiblen Grund, eine eigene Methode zu erfinden, oder bestehende
Methoden zu ergänzen
Verzweigung if (A > B) {
Ist A > B?
Ja Anweisung1
Nein
} else {
Anweisung1 Anweisung2 Anweisung2
}
Fallunterscheidung, switch(a) {
Falls a Mehrfachverzweigung case 1: Anweisung1 break;
=1 =2 =3 Sonst case 2: Anweisung2 break;
case 3: Anweisung3 break;
Anw. Anw. Anw. 3 Anw. 4 default: Anweisung4 break;
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 141/168
Eine Einführung in die Informatik und die Programmiersprache C
Achtung: Struktogramme unterstützen das früher oft eingesetzte "GoTo" nicht. Dies ist eine beabsich-
tigte Konsequenz aus den Ideen der strukturierten Programmierung. Dadurch wird ein unbegrenztes,
willkürliches Verzweigen (GoTo!) in der Programmlogik von vornherein unmöglich gemacht und
strukturiertes Vorgehen gefördert.
Aufbau von Struktogrammen
Alle Elementar-Strukturblöcke können auf einfache Weise zusammengesetzt werden. Das Ergebnis ist
wiederum ein überschaubarer Strukturblock mit genau einem Eingang (Oberkante) und einem Aus-
gang (Unterkante). Für die Konstruktion zusammengesetzter Strukturblöcke gibt es dem Block-Kon-
zept folgend zwei einfache Regeln:
1. Ein Strukturblock wird an einen anderen gereiht, indem die gesamte Ausgangskante
des vorstehenden Strukturblock mit der gesamten Eingangskante des nachfolgenden
Strukturblocks zusammengelegt wird. Ein durch eine solche Aneinanderreihung ent-
standener Strukturblock wird Sequenz genannt.
2. In die Block-Felder (Anweisungs-Felder) der (elementaren) Strukturblöcke kann jeder
beliebige elementare oder zusammengesetzte Strukturblock eingesetzt werden.
Vorgehensweise
In Struktogrammen wird meist Pseudocode verwendet (Eine Art formale Umgangssprache), damit sie
Sprachunabhängig sind. Ein Struktogramm sollte noch nicht festlegen, in welcher Programmierspra-
che der entsprechende Code implementiert wird.
Bei grösseren Problemen erstellt man zuerst ein grobes Struktogramm, welches nur den grundsätzli-
chen Programmablauf darstellt, und verfeinert es anschliessend. Es können dabei mehrere Verfeine-
rungsstufen folgen. In der feinsten Stufe kann bei Bedarf bereits Code der Zielsprache anstelle von
Pseudocode eingesetzt werden.
Beispiel:
Aufgabe: Es soll ein Programm entworfen werden, das eine beliebige Anzahl Werte einliest und dar-
aus den Durchschnitt berechnet, negative Zahlen sollen ignoriert werden, eine 0 zeigt den Abschluss
der Eingaben an.
Grobstruktogramm: Feinstruktogramm
Durchschnitt Durchschnitt
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 142/168
Eine Einführung in die Informatik und die Programmiersprache C
Entscheidung
Getränk
auswählen
Ausgelagerter Teilablauf
Bargeld
zählen
Einfache Anweisung
Getränk
entnehmen
Ende
35.1.3 Aktivitätsdiagramme
Sind ein Bestandteil der UML und eine Art erweiterter Flussdiagramme, die es erlauben, auch paralle-
le und auf mehrere Systeme verteilte Abläufe darzustellen.
Startknoten
Endknoten
Aktivität
Verzweigung
Zusammenführung
Parallele Ausführung
Synchronisation
(Warten bis alle fertig)
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 143/168
Eine Einführung in die Informatik und die Programmiersprache C
ÜbrigeTasten /
OnTaste /
OffTaste / Nichts
Ausgeschaltet Einschalten
Ausschalten
OffTaste / OffTaste /
Ausschalten Ausschalten Keine Eingabe
Resultat Anzeigen OffTaste /
Ausschalten
Operatortaste / Ziffertasten /
ClearTaste /
Operation und Zahl merken Ziffertasten / Zahl = Ziffer
Alles löschen
Zahl = Ziffer
= Taste /
Zahl einlesen
Resultat berechnen ClearTaste /
Alles löschen
Ziffertasten /
Operatortaste / Ziffer zu Zahl hinzufügen
2. Zahl einlesen
Operation und Zahl merken
Operatortaste /
Resultat berechnen, anzeigen und Ziffertasten /
merken, Operation merken Ziffer zu Zahl hinzufügen
Als Tabelle (Nicht aufgeführte Ereignisse ändern weder den Zustand noch haben sie Aktionen zur Folge):
Zustand Ereignis Aktion Neuer Zustand
Ausgeschaltet OnTaste Einschalten Keine Eingabe
Keine Eingabe Off Taste Ausschalten Ausgeschaltet
Ziffertaste Zahl auf Wert von Ziffer setzen Zahl einlesen
Zahl Einlesen Off Taste Ausschalten Ausgeschaltet
Ziffertaste Ziffer zu Zahl hinzufügen Zahl einlesen
ClearTaste Zahl auf 0 setzen Keine Eingabe
Operatortaste Operation & Zahl merken 2. Zahl einlesen
2. Zahl Einlesen Off Taste Ausschalten Ausgeschaltet
Ziffertaste Ziffer zu Zahl hinzufügen 2. Zahl einlesen
ClearTaste Zahl auf 0 setzen Keine Eingabe
Operatortaste Resultat berechnen, anzeigen und merken, Operation merken 2. Zahl einlesen
= Taste Resultat berechnen und anzeigen Resultat anzeigen
Resultat anzeigen Off Taste Ausschalten Ausgeschaltet
Ziffertaste Zahl auf Wert von Ziffer setzen Zahl einlesen
ClearTaste Zahl auf 0 setzen (Fehlt in Diagramm) Keine Eingabe
Operatortaste Operation & Zahl merken 2. Zahl einlesen
Eine solche Tabelle lässt sich in C wartungsfreundlich als ein Array von Funktionspointern umsetzen
(NextState = HandlerTabelle[State][Event](Args);)
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 144/168
Eine Einführung in die Informatik und die Programmiersprache C
35.1.5 Datenflussdiagramme
Es wird gezeigt, woher die Daten kommen, wohin sie gehen und wo sie wie verarbeitet werden. Diese
Darstellung eignet sich auch gut für parallele Prozesse.
Datenquelle / Datensenke
n
Aktivität / Prozess (n = Identifikationsnummer)
Datenfluss
Datenspeicher
Kontext Diagramm
Das Kontextdiagramm stellt das System in seiner Umgebung dar. Es enthält nur einen einzigen
Prozess, der das System enthält. Das Kontextdiagramm wird immer als erstes Diagramm ent-
worfen, die weiteren Diagramme entstehen aus 'Verfeinerungen' des Kontaxtdiagramms.
Rufknöpfe
Knopfzustände Motoransteuerung
Motorsteuersignale
Türschalter
TürSchalter
Positionsschalter 0 Türansteuerung
PosSchalter Türsteuersignale
Lift-
Überlastsensor Sensorzustände steuerung Position
Positionsanzeige
Motorzustand Lampenzustände
Motorüberwachung
Brandkontrolle Rufknopfbestätigung
Brandfallsteuerung
Im nächsttieferen Diagramm (Dem 0-Diagramm) wird der Prozess aus dem Kontextdiagram weiter
aufgeteilt. Sämtliche Datenflüsse aus dem Kontextdiagramm müssen hier auch enthalten sein.
Fahraufträge
Lampenzustände
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 145/168
Eine Einführung in die Informatik und die Programmiersprache C
Die einzelnen Prozesse können wiederum in detailliertere Datenflussdiagramme zerlegt werden. Dabei
müssen jeweils alle Datenflüsse vom oder zum übergeordneten Diagramm aufgeführt werden (Als
Flüsse die von ausserhalb kommen oder nach ausserhalb gehen). Diese Verfeinerung wird solange
wiederholt, bis die Funktion des Systems klar wird.
2 Fahrtenhandler
Fahrbefehl
ZielErreicht
2.2
Fahren
Fahrquittung
Fahrkommando
2.4
Fahrsequenz Ablauf
2.3
Fahrauftrag steuerung Türbefehl
auswerten
TürQuittung Türsteuersignale
2.1
Türsteuerung
Fahraufträge
Brandkontrolle
TürSchalter
- Jeder Prozess muss eine (hierarchische) Nummer sowie einen aussagekräftigen Namen besitzen..
- Jeder Datenfluss muss einen Namen besitzen, der Auskunft über seine Daten gibt.
- Datenflüsse welche mit Datenspeichern verbunden sind müssen nicht benannt werden.
- Datenflüsse, welche in einem übergeordneten Diagramm in mit eine Prozess verbunden sind, müs-
sen im Detaildiagramm dieses Prozesses als Datenflüsse gegen aussen (Datenflüsse die 'im Leeren'
enden) aufgeführt sein.
- Ein Datenspeicher kann nur Daten einer 'Sorte' aufnehmen, es können also nicht unterschiedliche
Datenflüsse mit demselben Datenspeicher verbunden werden.
- Jeder Datenspeicher muss einen Namen besitzen, der Auskunft über seine Daten gibt.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 146/168
Eine Einführung in die Informatik und die Programmiersprache C
35.1.6 CRC
Für jedes Modul (Eigentlich Klasse = Class) wird klar definiert wie es heisst, es wird beschrieben für
was es verantwortlich ist (Responsibility), und mit welchen anderen Modulen (Klassen) es zusammen-
arbeitet (Collaborators). Eine CRC-Card sollte nicht grösser als eine Karteikarte (A6) werden, sonst ist
das Modul zu gross oder zu komplex. Mit CRC-Cards kann man sehr gut Planspiele betreiben, indem
man sie an eine Wand heftet oder auf einem Tisch auslegt und mit Linien oder Schnüren miteinander
verbindet. So kann ein Design interaktiv in einer Gruppe besprochen, verfeinert oder erstellt werden.
(CRC Cards neu gruppieren, Zuständigkeiten zwischen Modulen verschieben, Module zusammenfas-
sen oder Aufteilen).
Modulname
Modulaufgaben/ Benötigte Module
Funktionalitäten/
Verantwortlichkeit
Beispiel Liftsteuerung
Motorsteuerung
Motor auf definierte Position bewegen. MotorHardwareAnsteuerung
Motor auf Überlast überwachen Sensorhandler
Motor auf Blockieren überwachen Strommessung
Motorgeschwindigkeit regeln
Liftkabine Positionieren
Sollposition bestimmen. Motorsteuerung
Optimalen Geschwindigkeitsverlauf bestim- Sensorhandler
men.
Lift auf Sollposition bewegen
Istposition verfolgen
Im Notfall auf nächstem Stock oder sofort
halten, je nach Dringlichkeit.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 147/168
Eine Einführung in die Informatik und die Programmiersprache C
35.1.7 Pseudocode
Die Lösung des Problems wird in einer Art Pseudoprogrammiersprache beschrieben:
Funktion FindeAusreisser
Liftsteuerung
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 148/168
Eine Einführung in die Informatik und die Programmiersprache C
35.1.9 Sequenzdiagramm
Mit dem Sequenzdiagramm werden zeitliche Abläufe dargestellt. Das kann auf dem Niveau von Funk-
tionsaufrufen sein, aber auch auf höheren Abstraktionsebenen. Auch Kommunikationsabläufe in Sys-
temen lassen sich so darstellen. Für ein System kann es beliebig viele Sequenzdiagramme geben. (Für
jede mögliche Abfolge von Ereignissen eines). Das Sequenzdiagramm ist Bestandteil der UML.
Die Zeitachse bei diesem Diagramm läuft von oben nach unten. Es werden alle beteiligten Module/
Systeme mit ihrer Lebenslinie gezeichnet, anschliessend wird die Kommunikation in Form von gegen-
seitigen Funktionsaufrufen oder Nachrichtenaustausch dargestellt.
Nachrichten können auch mit externen Systemen ausgetauscht werden, diese werden üblicherweise als
Aktoren gezeichnet. Ein Aktor kann selbstverständlich auch Funktionen aufrufen. (Z.b. Wenn durch
die Interaktion eines Benutzers eine Funktion aufgerufen wird).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 149/168
Eine Einführung in die Informatik und die Programmiersprache C
Beispiel Liftsteuerung
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 150/168
Eine Einführung in die Informatik und die Programmiersprache C
Bei der objektorientierten Analyse sucht man nach Objekten, deren Eigenschaften und Fähigkeiten so-
wie den Beziehungen der Objekte untereinander. Der objektorientierte Ansatz ist eine Weiterentwick-
lung der modularen Programmierung.
Bei diesem Vorgehen steht nun nicht mehr die Funktionalität, also das wie, sondern die Daten, also
das was im Vordergrund. Ein Objekt ist eine Einheit, die bestimmte Eigenschaften (Attribute) auf-
weist, und bestimmte Befehle (Methoden) ausführen kann. Ein Objekt kann eine konkrete Entspre-
chung in der realen Welt haben, aber auch etwas abstraktes wie einen Algorithmus umfassen.
Beim objektorientierten Entwurf sucht man zuerst alle am Problem beteiligten Objekte, bestimmt an-
schliessend deren Eigenschaften (Attribute) und Aufgaben und findet heraus, wie diese Objekte mit-
einander in Beziehung stehen. Die zu den Daten gehörenden Funktionen (Aufgaben) werden dabei in
die Objekte integriert. Man spricht dabei nicht mehr von Funktionen, sondern von Memberfunktionen
oder Methoden. Die Daten werden entsprechend als Attribute (Eigenschaften) oder Member bezeich-
net. Die Objekte kommunizieren untereinander um die Aufgabe des Gesamtsystems zu erledigen.
Funktionsaufrufe resp. Methodenaufrufe werden in der Literatur oft als 'Botschaften' an ein Objekt be-
zeichnet. (Z. B. Objekt A sendet die Botschaft 'ZeichneDich()' an Objekt B).
Jedes Objekt gehört zu einer Klasse, gleichartige Objekte gehören zur selben Klasse. (Gleichartige
Objekte besitzen alle die gleichen Eigenschaften, nur können sie anders ausgeprägt sein. Zwei Objekte
können z. B. die Eigenschaft Farbe besitzen, wobei sie beim einen rot, und beim anderen grün ist)
Die objektorientierte Methode kann in jeder Programmiersprache angewendet werden (Sogar Assem-
bler), aber es gibt Sprachen, die speziell dafür entwickelt worden sind wie Smalltalk, C++ oder Java.
(Das heisst aber noch nicht, dass man bereits objektorientiert Programmiert, sobald man eine dieser
Sprachen benutzt, die Sprache ist nur das Werkzeug)
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 151/168
Eine Einführung in die Informatik und die Programmiersprache C
Vererbung Ein weiterer wichtiger Aspekt der OOP ist die Möglichkeit der Vererbung. Um
nicht jedesmal eine komplett neue Klasse entwerfen zu müssen, kann man festle-
gen, dass eine Klasse alle Elemente und Methoden einer bereits existierenden Klas-
se 'erben' soll, d.h. die neue Klasse enthält von vornherein schon alle Attribute und
Methoden dieser Klasse.
Die Attribute und Methoden der neuen Klasse können anschliessend um weitere er-
gänzt oder bei Bedarf auch überschrieben (überschreiben = für die neue Klasse neu
definiert) werden.
Damit wird eine Beziehung wie z.B. 'ein Apfel ist eine Frucht' realisiert, durch zu-
sätzliche Elemente und Methoden wird die neue Klasse genauer spezifiziert, dieser
Vorgang wird als Spezialisieren' bezeichnet. Die alte Klasse wird dabei als Basis-
klasse bezeichnet, die neue als abgeleitete Klasse und die Beziehungsstruktur, die
durch die Vererbung entsteht, als Klassenhierarchie.
Man kann auch den umgekehrten Weg gehen, indem man Gemeinsamkeiten von
Klassen in einer Oberklasse (Basisklasse) zusammenfasst und anschliessend von
dieser erbt. Man spricht in diesem Fall von Generalisierung.
Polymorphismus Wie erwähnt kann man in einer abgeleiteten Klasse Methoden der Basisklasse
überschreiben. Dadurch hat die abgeleitete Klasse eine neue Methode, welche aber
immer noch den gleichen Namen trägt wie die Methode der Basisklasse. Polymor-
phismus bedeutet, dass nun überall, wo ein Objekt der Basisklasse erwartet wird,
auch ein davon abgeleitetes Objekt eingesetzt werden kann. Beim Aufruf von Me-
thoden werden aber die Methoden des tatsächlichen Objektes, und nicht die der Ba-
sisklasse benutzt. Es wird somit nicht wie sonst üblich zur Compilezeit bestimmt,
welche Methode aufgerufen wird, sondern erst zur Laufzeit. (Es ist zur Compilezeit
ja nicht bekannt, welches Objekt später einmal bearbeitet wird, da sowohl ein Ob-
jekt der Basisklasse oder ein davon abgeleitetes zum Einsatz kommen kann. Somit
wird zur Laufzeit bei jedem Methodenaufruf geprüft, welches Objekt den nun ef-
fektiv vorhanden ist, und die entsprechende, zu diesem Objekt gehörende Mothode
aufgerufen).
.
(Z. B. für eine graphische Ausgabe wird ein Ausgabeobjekt benötigt. Dabei ist zur
Compilezeit vielleicht gar nicht bekannt, auf welches Objekt genau ausgegeben
wird, es muss nur den Befehl Zeichne() verstehen. Es könnte ein Laserdrucker, ein
Plotter oder nur ein Fenster auf dem Bildschirm sein, je nach dem, wo der Benutzer
gerade seine Ausgabe haben möchte).
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 152/168
Eine Einführung in die Informatik und die Programmiersprache C
Beispiel:
In einem Graphikprogramm können Zeichnungen erstellt werden. Die Zeichnung kann Figuren wie
Linien, Rechtecke und Ellipsen enthalten. Bei allen Figuren kann die Strichdicke, die Strichfarbe und
die Füllfarbe eingestellt werden. Mehrere Figuren können zu einer Gruppe zusammengefasst werden,
welche anschliessend wie eine Figur behandelt werden kann. Figuren können gezeichnet, verschoben,
kopiert, gespeichert, geladen und gelöscht werden. Die Eigenschaften der Figuren (Farbe, Strichdicke)
können jederzeit verändert werden. Die Zeichnung wird auf dem Bildschirm dargestellt, kann aber
auch auf einem Drucker ausgegeben werden. Eine Zeichnung kann abgespeichert und wieder geladen
werden. Das Ausgabegerät (Bildschirm, Drucker) stellt Funktionen zum Zeichnen der Grundfiguren
(Linie, Rechteck, Ellipse) zur Verfügung.
Es können mehrere Zeichnungen gleichzeitig geladen sein. Jede Zeichnung hat einen eindeutigen Na-
men, unter dem sie auch abgespeichert wird.
Klassendiagramm
Im Klassendiagramm werden die Klassen mit ihren Beziehungen untereinander, sowie ihre Methoden
und Attribute dargestellt.
Kollaborationsdiagramm
Im Kollaborationsdiagramm wird ein bestimmter Zustand des Systems mit allen beteiligten Objekten,
sowie ein Kommunikationsablauf für ein bestimmtes Ereignis dargestellt. Damit können Abläufe im
System durchgespielt und der Entwurf auf Korrektheit und Vollständigkeit überprüft werden.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 153/168
Eine Einführung in die Informatik und die Programmiersprache C
Grundsätzlich unterscheidet man zwei Arten von Tests: Statische Tests und Funktionstests.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 154/168
Eine Einführung in die Informatik und die Programmiersprache C
36.2 Funktionstests
Beim Funktionstest wird geprüft, ob ein System oder Teilsystem korrekt funktioniert und seinen Spe-
zifikationen entspricht, also das tut was von ihm gefordert wird. Das Ziel des Tests ist es Fehler zu
finden.
Es ist nur bei kleinen Programmen möglich, mit vertretbarem Aufwand die Fehlerfreiheit des Codes
zu beweisen. Bei grösseren Programmen wird der Aufwand unrealistisch hoch. Daraus folgt auch, das
vollständiges Testen kaum möglich ist. Aber durch systematisches Vorgehen kann man ein optimales
Ergebnis erreichen.
Testen ist eine kreative und anspruchsvolle Tätigkeit. Der Tester muss das Produkt gut kennen, und
sein Ziel muss sein, Fehler zu finden, nicht die Korrektheit zu bestätigen. Das Testen ist selbst ein Pro-
jekt, welches geplantes Vorgehen erfordert. Der Tester muss sich überlegen was getestet werden soll,
und wie es getestet werden kann. Er muss sich zudem überlegen, was alles schiefgehen kann. Dabei
wird für jede in Frage kommende Möglichkeit ein Testfall spezifiziert.
Für jeden Testfall muss definiert werden, in welchem Ausgangszustand das System sein soll, welche
Eingabedaten an das System erfolgen, und was die erwartete Reaktion des Systems ist. Tests müssen
reproduzierbar sein, d.h. dass der gleiche Test wiederholt ausgeführt werden kann (Nur so kann eine
Fehlerbehebung verifiziert werden). Tests können oft automatisiert werden, so dass der Tester von
Fleissarbeit entlastet wird. Für Tests muss häufig eigene Hard- und Software entwickelt werden.
Wichtig: Testen und Fehlerbehebung sind zwei verschiedene und getrennte Tätigkeiten. Das Ergebnis
eines Tests ist ein Fehlerprotokoll, die Fehlerkorrektur erfolgt anschliessend durch den(die) Program-
mierer aufgrund des Protokolls.
Für das Amt des Testers eignen sich besonders Leute, die ein grosses Interesse daran haben, möglichst
viele Fehler zu finden. Deshalb sind im Allgemeinen die Entwickler des Systems ungeeignet. Besser
eignen sich Leute, die das System später Warten oder in Betrieb nehmen müssen, zukünftige Anwen-
der oder Leute aus der Qualitätssicherung.
36.3 Blackboxtest
Beim Blackboxtest sind die Interna des Systems unbekannt, das System wird als Blackbox angesehen,
und die Tests werden aus den Anforderungen (Use Cases) an das System gewonnen. Blackbox Tests
werden meist gegen Ende des Projektes zum Test von Teilsystemen und des Gesamtsystems einge-
setzt. Da Blackboxtests auf den Anforderungen an das System basieren, können die Testfälle parallel
zur Entwicklung des Gesamtsystems entworfen werden. Das Testteam muss nicht bis zum Projektende
mit der Ausarbeitung der Tests warten.
36.4 Whiteboxtest
Beim Whitebox Test sind die Interna das Systems (Programmcode) bekannt. Beim Erstellen der Test-
fälle wird versucht, möglichst alle Programmflusspfade abzudecken, also die Testfälle so zu wählen,
dass alle Fälle eines Cases oder einer Verzweigung mindestens einmal durchlaufen werden, und dass
Schlaufen an ihren Grenzfällen betrieben werden (Einmal, keinmal, maximal). Bei Berechnungen soll-
te überprüft werden, ob Überläufe oder andere Fehler auftreten. Jede Codezeile sollte mindestens ein-
mal ausgeführt werden. Diese Art von Tests werden vor allem bei Modultests eingesetzt.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 155/168
Eine Einführung in die Informatik und die Programmiersprache C
36.6 Testabbruch
Die schwierigste Entscheidung beim Testen ist immer, wann die Testphase beendet wird. Es gibt im
Allgemeinen keine Möglichkeit mit Sicherheit zu wissen, ob alle Fehler beseitigt sind, also müssen
andere Kriterien herangezogen werden.
Einfach eine bestimmte Zeitdauer festzulegen ist sicherlich das schlechteste Kriterium. Bessere Mög-
lichkeiten basieren auf einer Restfehler-Abschätzung, d.h. versuchen zu schätzen, wieviele Fehler noch
übrig sind.
Dazu bietet sich die Fehlerfinderate an. (Gefundene Fehler pro Zeiteinheit, ev. noch nach Schwere-
grad gewichtet). Der Test wird abgebrochen, wenn die Fehlerfinderate eine gewisse Grenze unter-
schreitet. Diese Grenze wird vor dem Testbeginn festgelegt.
Eine anderer Ansatz ist die Kostenrechnung, der Test wird abgebrochen, wenn der durchschnittliche
Aufwand (Die Kosten) um einen Fehler zu finden, eine vorbestimmte Grenze überschreitet.
Eine Möglichkeit, die Restfehler zu schätzen besteht daran, mehrere Gruppen unabhängig voneinander
dasselbe System testen zu lassen. Aus den Ergebnissen der unterschiedlichen Gruppen lassen sich
Rückschlüse auf die nicht gefundenen Fehler ziehen.
Beispiel:
Von beiden Gruppen gefundene Fehler: 80
Nur von Gruppe A gefundene Fehler: 10
Nur von Gruppe B gefundene Fehler: 8
Von keiner Gruppe gefundene Fehler: ??
Aus diesen Werten kann man schliessen, dass noch ein unentdeckter Fehler im System steckt.
Idee: Gruppe B hat von den 90 Fehlern der Gruppe A 80 gefunden, die Erfolgsquote von B ist also
8:9. Wenn man die selbe Erfolgsquote auf die nur von Gruppe B gefundenen Fehler anwendet, ergibt
sich, dass noch ein Fehler unentdeckt ist. Diese Methode funktioniert aber nur bei grossen Zahlen und
wenn beide Gruppen völlig unabhängig voneinander testen (Keine Kommunikation, auch nicht in der
Kaffeepause oder nach Feierabend).
Damit solche Abschätzungen wirklich aussagekräftig sind, muss statistisch korrekt vorgegangen und
ausgewertet werden, also die Wahrscheinlichkeitsrechnung herangezogen werden.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 156/168
Eine Einführung in die Informatik und die Programmiersprache C
36.7 Testprotokolle
Jeder Test muss mit einem Testprotokoll belegt sein. In einem Testprotokoll muss genau festgelegt
werden, was getestet wird, welche Eingaben getätigt werden, welche Antworten vom System erwartet
werden, und welche Antworten das System tatsächlich gegeben hat.
Ein Testprotokoll muss so verfasst sein, dass der Test jederzeit reproduziert werden kann, nur so kön-
nen Fehler behoben und die Korrektur verifiziert werden.
Sinnvollerweise enthält das Testprotokoll zudem Zeitpunkt des Testes, was getestet wurde (Software-
version, Hardwareversion, Testeinrichtung) und wer getestet hat.
Testprotokolle sollten vorgefertigte Formulare sein, bei denen während des Tests nur noch die Ergeb-
nisse des Testes eingefüllt werden. Es sollten keine spontanen Testideen umgesetzt werden, sondern
der ganze Testablauf soll geplant, und entsprechend Dokumentiert werden.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 157/168
Eine Einführung in die Informatik und die Programmiersprache C
37.1 Zeitplanung
Um ein Projekt erfolgreich durchführen zu können, ist eine Zeitplanung unerlässlich. Dazu wird das
Projekt in einzelne Arbeitspakete aufgeteilt, welche nacheinander oder nebeneinander ausgeführt wer-
den können. Anschliessend wird für jedes Arbeitspaket der benötigte Zeitaufwand abgeschätzt. Unter
der Berücksichtigung von gegenseitigen Abhängigkeiten und verfügbaren Mitarbeitern wird anschlies-
send der Gesamtzeitaufwand des Projektes ersichtlich, oder wenn der Einführungszeitpunkt bekannt
ist, kann die Anzahl der benötigten Mitarbeiter abgeschätzt werden. (Achtung: Eine Verdoppelung der
Mitarbeiter hat nicht eine Verdoppelung der Kapazität zur Folge, da der Kommunikations- und Orga-
nisationsaufwand zunimmt. Zudem können nicht alle Arbeiten parallel zueinander ausgeführt werden.)
Unter Berücksichtigung der Randbedingungen (Abhängigkeiten, Mitarbeiterauslastung, Verfügbarkeit
von Resourcen, Termine) können nun die einzelnen Arbeitspakete in einem Zeitrahmen so eingeordnet
werden, so dass eine optimale Auslastung der Mitarbeiter und eine möglichst kurze Projektlaufzeit er-
reicht wird.
Wenn ein Projekt zuviele Unbekannte (Meist sind schon 2 eine zuviel) enthält, ist es oft gar nicht
möglich, eine zuverlässige Zeitschätzung zu machen, es muss auf Erfahrungswerte zurückgegriffen
werden. Aber auch in diesem Fall ist der Terminplan ein wichtiges Hilfsmittel, weil Schätzfehler da-
mit schon früh erkannt werden und frühzeitig die nötigen Massnahmen ergriffen werden können.
Innerhalb eines Projektes werden zudem Meilensteine definiert, das sind Zeitpunkte, zu welchen be-
stimmte Ziele des Projektes erreicht worden sein sollten. Spätestens zu diesen Zeitpunkten findet ein
Vergleich zwischen Soll und Ist statt, und bei Abweichungen wird über das weitere Vorgehen ent-
schieden (Teile weglassen, auf später verschieben, vorziehen, Team vergrössern/Verkleinern, Budget
überarbeiten, im Extremfall Projektabbruch). Meilensteine dienen bei grösseren Projekten auch als
überblickbare Zwischenziele, um nicht nur den Endtermin in vielen Jahren vor Augen zu haben.
Bei einer guten Zeitplanung können Abweichungen von der kalkulierten Projektzeit sehr früh erkannt
werden, ohne Zeitplanung wird meist erst viel zu spät erkannt, dass ein Projekt aus dem Ruder läuft.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 158/168
Eine Einführung in die Informatik und die Programmiersprache C
19 Software allgemein
24 GUI
36 Quellenfenster
40 Senkenfenster
44 Symbole JH
45 Sprachanpassung JH
46 Hilfe MZ
47 DSK
50 Verifikation / Tests
61 Dokumentation
Die einzelnen Punkte werden soweit verfeinert, bis eine Abschätzung des Zeitaufwandes möglich ist.
Die Länge eines Balkens ist durch den Zeitaufwand, die Anzahl der gleichzeitig daran arbeitenden
Personen und den Beteiligungsgrad dieser Personen gegeben. Es muss beachtet werden, das die Ge-
samtarbeitslast einer Person zu keinem Zeitpunkt 100% überschreitet.
Kosten
1 Klassischer Ansatz
2 Methoden Ansatz
Einführung Zeit
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 159/168
Eine Einführung in die Informatik und die Programmiersprache C
37.3 Versionsverwaltung
Ein weiterer wichtiger Punkt in grösseren und/oder langlebigen Softwareprodukten ist die Versions-
verwaltung. Ohne Versionsverwaltung ist es oft nicht mehr möglich, den Stand von älteren Programm-
versionen wiederherzustellen. Aber gerade zu Wartungs- und Fehlerbehebungszwecken ist es oft un-
umgänglich, die der Releaseversion des fehlerverursachenden Programmes entsprechenden Sourcefi-
les zur Verfügung zu haben.
Mit einer guten Versionsverwaltung ist es jederzeit möglich, jeden beliebigen Versionstand des Sour-
cecodes wiederherzustellen.
Im einfachsten Fall wird bei jedem Release schlicht der gesamte Sourcecode archiviert. Unter Ver-
wendung von Versionsverwaltungssystemen wie z. B. GIT, SVN, RCS oder CVS kann das Verwalten
von Versionen vereinfacht und automatisiert werden, es wird dabei auch überwacht, dass nicht mehre-
re Personen gleichzeitig an derselben Datei arbeiten. Solche Systeme speichern meist nicht den ge-
samten Code jedesmal vollständig ab, sondern jeweils nur die Differenzen zum letzten Release.
Grosse oder langlebige Softwareprojekte können ohne Versionsverwaltung kaum vernünftig verwaltet
werden, insbesondere weil ständig an irgend einem Teil weiterentwickelt wird und es kaum möglich
ist, den Überblick zu behalten wenn jeder unkontrolliert den Sourcecode ändert.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 160/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 161/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 162/168
Eine Einführung in die Informatik und die Programmiersprache C
{
Result; /* Compiler nimmt 'int result' an */
Result = a+b;
return Result;
}
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 163/168
Eine Einführung in die Informatik und die Programmiersprache C
Datentyp Makro für Printf Makro für scanf Makro für Literal Bedeutung
<inttypes.h> <inttypes.h> <stdint.h>
intN_t PRIdN SCNdN - Genau N Bits
PRIiN SCNiN
uintN_t PRIoN SCNoN - Genau N Bits
PRIuN SCNuN
PRIxN SCNxN
PRIXN
int_leastN_t PRIdLEASTN SCNdLEASTN INTN_C(value) Mindestens N Bits
PRIiLEASTN SCNiLEASTN
uint_leastN_t PRIoLEASTN SCNoLEASTN UINTN_C(value) Mindestens N Bits
PRIuLEASTN SCNuLEASTN
PRIxLEASTN SCNxLEASTN
PRIXLEASTN
int_fastN_t PRIdFASTN SCNdFASTN - Schnellster Typ mit mindestens N
PRIiFASTN SCNiFASTN Bits
uint_fastN_t PRIoFASTN SCNoFASTN - Schnellster Typ mit mindestens N
PRIuFASTN SCNuFASTN Bits
PRIxFASTN SCNxFASTN
PRIXFASTN
intptr_t PRIdPTR SCNdPTR - Gross genug für Pointer
PRIiPTR SCNiPTR
uintptr_t PRIoPTR SCNoPTR - Gross genug für Pointer
PRIuPTR SCNuPTR
PRIxPTR SCNxPTR
PRIXPTR
intmax_t PRIdMAX SCNdMAX INTMAX_C(value) Grösster int
PRIiMAX SCNiMAX
uintmax_t PRIoMAX SCNoMAX UINTMAX_C(va- Grösster int
PRIuMAX SCNuMAX lue)
PRIxMAX SCNxMAX
PRIXMAX
Bei den Makros für printf und scanf gibt es für jeden der möglichen Formatbezeichener (d, i, o, u, x, X) ein eigenes Makro.
Beispiele:
int_fast16_t Schnell = INT16_C(240); /* 240 erhält den korrekten Suffix */
printf ("Wert ist: %" PRIdFAST16 "!\n", Schnell); /* Ausgabe: Wert ist 240! */
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 164/168
Eine Einführung in die Informatik und die Programmiersprache C
Makros:
complex Wird zum Schlüsselwort _Complex, dient als Vereinfachung
_Complex_I Wird zu einer float _Complex Konstanten mit dem Wert der imaginären Ein-
heit i.
imaginary und _Imaginary_I Werden zu einer float _Complex Konstanten mit dem Wert der imaginären
Einheit i.
double complex CMPLX(double r, double i); Makro, Erzeugt ein komplexe Zahl aus Realteil r und Imaginärteil i
Funktionen:
Alle Funktionen sind jeweils in Varianten für float, double und long double gemäss folgendem Schema verfügbar (Winkel
werden in Radiant angegeben).:
double complex cfn(double complex z); // Für Double Werte: Normaler Funktionsname
float complex cfnf(float complex z); // Für Float Werte: Funktionsname mit suffix f
long double complex cfnl(long double complex z); // Für Long Double Werte: Funktionsname mit suffix l
So gibt es zum Beispiel die drei Varianten csin(), csinf() und csinl() der komplexen Sinus Funktion.
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 165/168
Eine Einführung in die Informatik und die Programmiersprache C
42 Index
A case 40 free() 111
Castoperator 32, 34 Funktionen 14, 46
Adresse 9, 24, 63
char 19 Funktionsdefinition 46
Adressoperator 32
Circular Array 129 Funktionsdeklaration 48
Algorithmus 11
Combsort 100 Funktionspointer 69
Alternation 11
Commandline 97 Funktionsprototyp 48
Analyse 134, 141
Compiler 10 Funktionsrumpf 46
AND 32
Compilern 8 Funktionstest 159
ANSI C99 8
Compilersystemen 13 Funktionszeiger 69
ANSI-C 89 17
complex.h 169 G
ANSI-C 99 17
compound statement 37
ANSI-Standard 8 Ganze Zahlen 19
const 20
Anweisung 36 Ganzzahlige Erweiterung 33
continue 45
Anweisungen 9, 16 generic selection 36
Äquivalenzklassen 160 D generischen Auswahl 36
Arbeiten mit Dateien 93 Daily Scrum Meeting 139 generisches Programmieren 36
Arbeitspaket 162 Datengesteuerter Entwurf 141 gets() 54
Arbeitsspeicher (RAM) 9 Datenstrukturen 9 GIT 164
argc 16, 97 Datentypen 19, 21 globale 90
Argumentenliste 46 Debugger 14 Globale Variablen 25
argv 16, 97 definiert 24 goto 45
Arithmetische Operatoren 30 Definition eines Arrays 50 H
Array-Literale 52 Deklaration 25
Hardware 20
Arrayliterale 52 dereferenziert' 63
Hashfunktion 127
Arrays 50 Dereferenzierungsoperator 32
Hashing 128
Arrayzuweisungen 52 Designphase 141
Hashing, beschleunigtes Suchen 128
ASCII-Code 22 Direkte Initialisierer 51, 57
hashing, closed addressing 127
Assembler 9 direkten Speicherzugriffe 71
hashing, open addressing 127
Assignement 31 do while 42
hashing, Tombstone 127
Assoziativität 35 Doppelt verkettete Liste 118
Hashtabellen 127
Aufzählungstyp 62 double 19
Hauptprogramm 16
Ausdrücke 36 dynamische Speicherverwaltung 111
Head 115
Ausgeben 27 E Headerdateien 14, 48
Ausgeglichene Bäume 126
einfach verketteten Liste 115 Heap 111
ausgeglichener Baum 123
einfache Datentypen 19 Hello World 14
auto 25
Eingabepuffer 28 Hierarchische Struktur 123
Automatic 25
Einlesen 27 Hochsprache 9
B Ellipse 49 I
Backlog 139 else 38
IDE 13
balanced Trees 126 Endlosschlaufe 43
identifier 17
Bäume 123 entarten 126
if 38
Bedingter Ausdruck 32 Entwurf 134
Implementierung 141
Betrieb 134 Enum 62
Implizite Typumwandlung 33
Bezeichner 17 Enumkonstanten 62
Index 50
Bibliothek 13 Escape-Sequenzen 17
Indexoperation 66
Bibliotheksfunktionen 74 Escapesequenzen 22
Indexoperator 50
binärer Operator 33 Explizite Typumwandlung 34
Initialisierer 50
Binäres Suchen 107 Expressions 36
Initialisiererliste 52
Bitfelder 61 extern 25
initialisiert 25
Bitweise Operatoren 30 externem Sortieren 98
Initialisierung 25, 56
Blackboxtest 159 Extreme Programming (XP) 138
Initialisierung einer Struktur 56
Blatt 123 F inline 49
Block 37
Fakultät 48 instabilen Sortieren 98
Bottom-Up 142
Falsch 32 int 19
break 45
Fehler, klassifizieren von 144 Integer 19
Bubblesort 99
Fehlerfinderate 160 Integerkonstanten 23
Buchstabenkonstante 22
Fehlerhandling 144 internem Sortieren 98
Budgets 162
Fehlerkategorien 144 Interpreter 10
Burndown-Chart 139
Felder 50 Interruptroutinen 20
by Reference 46
FIFO 132 Iteration 11
by Value 46
Fliesskomma 19 Iterative Phasenmodell 136, 137
C Fliesskommakonstanten 23 J
C 10 float 19
Java 10
C++ 10 flüchtig 9
Call by Reference 64 for-Schleife 42 K
Callback 69 Formatbezeichner 29 Klammern 35
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 166/168
Eine Einführung in die Informatik und die Programmiersprache C
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 167/168
Eine Einführung in die Informatik und die Programmiersprache C
Whiteboxtest 159
Wurzel 123
X
XP 138
Z
Zeichenketten 19, 54
Zeichenkonstante 22
Zeichensatz 16
Zeiger 63
Zeiger auf eine Funktion 69
Zeigervariable 63
Zeilenkommentar 15
Zeitplans 162
Zeitplanung 162
Zirkuläres Array 129
Zugriff auf die Elemente einer Struk-
turvariablen 58
Zusammengesetzte Array-Literale 52
zusammengesetzte Literale 59
zusammengesetzten Datentypen 19
Zuweisungs Operatoren 31
Zwischencode 10
Zwischenziele 162
_
__func__ 26
__STDC_ANALYZABLE__ 167
__STDC_IEC_559__ 167
__STDC_IEC_559_COMPLEX__ 167
__STDC_LIB_EXT1__ 167
__STDC_NO_ATOMICS__ 167
__STDC_NO_COMPLEX__ 167
__STDC_NO_THREADS__ 167
__STDC_NO_VLA__ 49, 51, 167
_Generic, 36
Gedruckt am 10.09.2021 15:42:08 Letzte Änderung am: 13. September 2021 Version 2.5.6, I. Oesch 168/168