![]() |
Algorithmen & Datenstrukturen
|
In der kommerziellen Datenverarbeitung treten sehr oft Problemstellungen auf, bei denen es weniger auf die Algorithmen ankommt (weil diese sehr einfach sind), sondern auf die Umformung der gegebenen Daten. So gibt es zum Beispiel Listengeneratoren; das sind Programme, die Listen erzeugen, also nichts anderes tun als Daten, die bereits bekannt sind, in eine übersichtliche Form zu bringen. Für die Aufgabenstellung ist der Algorithmus selbst von geringer Bedeutung. Hier kommt es in erster Linie auf die Ein- und Ausgabedaten sowie deren Umformung an. So hat sich neben der problemorientierten Programmentwicklung die datenorientierte Programmentwicklung herausgebildet. Hier wird zuerst die Datenstruktur für jede zu verarbeitende Datenmenge definiert. Danach stellt man fest, welche Arten der Zuordnung existieren. Dabei sollen
unterschieden werden. Dann werden alle Einzeloperationen in ungeordneter Reihenfolge aufnotiert und anschließend jede Operation den Datenstrukturdiagrammen und den daraus entwickelten Programmstrukturen zugeordnet. Dieses Entwurfsprinzip ist aber ausschließlich für umformende Problemstellungen geeignet.
Wichtig für die allgemeinen Probleme ist, daß die Eingabe- und Ausgabedaten eines Programms genau analysiert und strukturiert werden. Das führt meist auch zu kürzeren und übersichtlicheren Programmen. Bei der Datenanalyse können Sie folgendermaßen vorgehen:
Es hat sich auch beim Entwurf von Programmiersprachen schnell herausgestellt, die zu verarbeitenden Daten nach ihrer Art, ihrem Typ, zu unterscheiden. In strukturierten Sprachen ist es üblich, alle verwendeten Variablen am Anfang eines Programmes summarisch aufzuführen. Das trägt zu dessen Verständlichkeit und Leserlichkeit wesentlich bei. Insbesondere gehört zu einer guten Dokumentation die explizite Angabe des Wertebereiches einer jeden Variablen. Die wichtigsten Gründe dafür sind die folgenden:
Der Wertebereich einer Variablen spielt eine derart wichtige Rolle in der Charakterisierung einer Variablen, daß er als deren Typ bezeichnet wird. Die Werte eines Bereichs werden als Konstanten von diesem Typ bezeichnet. Es wird daher die bindende Konvention eingeführt, daß in jedem Programm alle Variablen vereinbart werden. Dies geschieht durch Angabe einer Liste der gewählten Variablen-Bezeichnungen und deren Typen. Eine Variablen-Vereinbarung habe allgemein die Form
TYP variable
wobei "variable" eine Variablen-Bezeichnung und "TYP" der Typ der Variablen ist. Falls mehrere Variablen vom gleichen Typ vereinbart werden sollen, wird die Kurzform
TYP v1, v2 ,..., vn (n > 1)
verwendet, wobei v1 ... vn Variablenbezeichnungen sind und TYP wiederum entweder eine Typenbezeichnung oder eine Typenbeschreibung ist. Die Konvention, alle verwendeten Bezeichnungen am Anfang eines Programms zu vereinbaren, hat zudem den großen Vorteil, daß ein Compiler bei jeder im Programm vorkommenden Bezeichnung prüfen kann, ob sie überhaupt vereinbart ist. Im negativen Fall, der z. B. durch Schreibfehler verursacht wird, kann der Compiler durch eine entsprechende Fehlermeldung auf den Lapsus aufmerksam machen. Die zusätzliche Redundanz wird also wiederum zur Erhöhung der Programmiersicherheit ausgenutzt. Wie werden Datentypen in einem Programm eingeführt, und welche Wertbereiche können durch Computer zweckmäßig dargestellt werden? Vorerst sei bemerkt, daß es üblich ist, Datentypen in verschiedene Arten einzuteilen. Das wesentliche Merkmal eines Typs ist die Struktur seiner Werte. Ist ein Wert unstrukturiert, also nicht in einzelne Komponenten zerlegbar, so wird er - und damit auch sein Typ - als skalar bezeichnet. Ist er dagegen in einzelne Komponenten zerlegbar, nennt man ihn strukturiert.
Konstante sind Datenwerte, die direkt in der Anweisungsfolge eines Programms eingetragen werden können (Standardbezeichnung, z. B. Zahlen). Als Konstante bezeichnet man üblicherweise auch Namen für Datenwerte (frei wählbare Bezeichnung). Durch die Vereinbarung von Konstanten-Namen wird ein ganz bestimmter Datenwert mit einem Namen versehen und ist über diesen Namen jederzeit zugreifbar. Der Wert einer Konstanten kann nicht geändert werden. Konstante dienen der Übersichtlichkeit und Lesbarkeit von Programmen; z. B.:
Pi = 3,1415 Maximum = 10000 Autor = "Johann Wolfgang von Goethe"
Bei der Programmiersprache C findet man zwei Varianten der Konstantenvereinbarungen:
const float Pi = 3.1415 #define Pi 3.1415
Die erste Form entspricht dem aktuellen ANSI-Standard und ist zu bevorzugen. Die zweite Variante ist älter und definiert eigentlich nur ein Makro. Vor der eigentlichen Übersetzung wird im gesamten Text die Zeichenkette "Pi" durch "3.1415" ersetzt.
In C gibt es zwei verschiedene Arten von Vereinbarungen, Definitionen und Deklarationen.Der Begriff der Vereinbarung umfasst sowohl die Definition als auch die Deklaration.
Definitionen
Eine Deklaration umfasst stets den Namen eines Objektes und seinen Typ. Damit weiß der Compiler, mit welchem Typ er einen Namen verbinden muß. Kurz ausgedrückt:
| Definition = Deklaration + Reservierung des Speicherplatzes |
Die Zuweisung eines Wertes an eine Variable ist eine fundamentale Operation in Computerprogrammen. Eine Variable zeigt eine Verhaltensweise, die einer Wandtafel ähnlich ist: Sie kann jederzeit gelesen werden (sie liefert den Wert) andererseits ausgewischt und überschrieben werden. In fast allen höheren Programmiersprachen müssen Variablen vor ihrer Verwendung deklariert werden - so auch in C. Es wird dabei der Typ der Variablen (siehe unten: Datentypen) und ein Bezeichner angegeben. Z. B.:
int Anzahl, Summe, I, J; float DM_Betrag;
Die Zuweisung eines Wertes an eine Variable wird im allgemeinen durch das Operatorzeichen = bezeichnet (zur Unterscheidung vom Gleichheitszeichen verwenden manche Programmiersprachen auch einen Linkspfeil oder ":="):

Wichtig ist die Unterscheidung zwischen Wertzuweisung und Gleichung im mathematischen Sinn. So würde die mathematische Gleichung
X = X + 1
wenig Sinn machen (es gibt nämlich keine Lösung), in einer Programmiersprache bedeutet der Ausdruck jedoch "Addiere 1 zum Wert von X und speichere das Ergebnis wieder in X" oder kürzer "Erhöhe X um 1". Noch ein Beispiel:

In C ist eine Wertzuweisung auch bei der Definition erlaubt. Es handelt sich um ein sehr sinnvolles Feature, denn eine Variable besitzt nach der Deklaration noch keinen definierten Wert. Nicht initialisierte Variablen (d. h. Variablen ohne Anfangswert) sind oft die Ursache von Programmfehlern. Beispiel:
int I = 0, Anzahl = 111; float X = 0.0;Merke: Steht die Variable auf der rechten Seite einer Zuweisung, bezeichnet sie ihren Inhalt. Steht die Variable auf der linken Seite einer Zuweisung bezeichnet Sie einen Behälter.
Sofern Ausdrücke mit mehr als einem Operator vorkommen, ist die Reihenfolge ihrer Ausführung eindeutig festzulegen. Dies kann durch drei Möglichkeiten erfolgen:
Die Hierarchie nimmt in der oben aufgeführten Liste von oben nach unten zu, d. h. Klammern binden stärker als Vorrangregeln und diese stärker als die Reihenfolge. Der Begriff "Ausdruck" beschränkt sich nicht auf arithmetische Ausdrücke (z. B. a * y + b), es gibt auch logische oder mengentheoretische Ausdrücke oder Ausdrücke, die Zeichen, Zeichenfolgen oder Speicheradressen zum Ergebnis haben.
In C gibt es
int i = 0; /* Zuweisung eines Anfangswertes an die Variable */
5 * 5; /* zulaessig, aber nicht besonders sinnvoll */
/* Der Rueckgabewert von 5 * 5 wird nicht verwendet */
i++; /* Sinnvoll - i wird um 1 erhoeht (siehe spaeter) */
Generell gilt:
| In C kann jeder Ausdruck eine Anweisung werden und einen Rückgabewert haben. |
a = a + 5*cBeim Namen a ist rechts vom Zuweisungsoperator der Wert gemeint, der in der Speicherzelle a gespeichert ist, und links ist die Adresse der Speicherzelle a gemeint, in der der Wert des Gesamtausdrucks auf der rechten Seite gespeichert werden soll. Aus dieser Stellung links oder rechts des Zuweisungsoperators wurden auch die Begriffe L-Wert und R-Wert abgeleitet.
|
Ein Ausdruck stellt einen L-Wert ("lvalue" oder "left value") dar, wenn er sich
auf ein Speicherobjekt bezieht. Ein solcher Ausdruck kann links und rechts des
Zuweisungsoperators stehen.
Ein Ausdruck, der keinen L-Wert darstellt, stellt einen R-Wert ("rvalue" oder "right value") dar. Er darf nur rechts des Zuweisungsoperators stehen. Einem R-Wert kann man also nichts zuweisen. |
Es gilt:
Bei jedem Computer spezifiziert der Typ "Integer" nur eine Untermenge der ganzen Zahlen im mathematischen Sinn, und zwar die Menge aller Zahlen, die betragsmäßig kleiner als ein bestimmter Maximalwert sind. Dies bedeutet, daß die Axiome der Arithmetik nur innerhalb dieser Untermenge gelten. Normalerweise wird eine Länge gewählt, die einem Vielfachen der Wortläge des Prozessors entspricht. Es gilt fü die Wortlänge: short <= int <= long int. In ANSI-C wird als Minimum definiert:
| Typ | Wertebereich |
|---|---|
| int | -32768 ... 32767 |
| long int | -2147483648 ... 2147483647 |
| short int | -32768 ... 32767 |
| unsigned int | 0 ... 65535 |
| unsigned long int | 0 ... 4294967295 |
| unsigned short int | 0 ... 65535 |
| Typ | Wertebereich |
|---|---|
| char | -127 ... 128 oder 0 ... 255 |
| unsigned char | 0 ... 255 |
Achtung: Um die verschiedenen nationalen Zeichensätze unter einen Hut zu bringen, wurde UNICODE entwickelt. Das hat zur Folge, daß char auch 16 Bilt lang sein kann.
Anmerkung: Eine explizite Konversion von Integer nach Real existiert nicht. Diese Konversion wird implizit bei der Übersetzung des Ausdrucks vorgenommen. Es gilt fü die Wortlänge: float <= double <= long double. In ANSI-C wird als Minimum definiert:
| Typ | Wertebereich Vorzeichen beliebig |
Mantissen- stellen |
|---|---|---|
| float | 3,4*10-38 ... 3,4*10+38 | >=6 |
| double | 1,7*10-308 ... 1,7*10+308 | >=10 |
| long double | 1,7*10-308 ... 1,7*10+308 | >=10 |
Komponententyp Variablenname [Dimension]
Der Index wird dabei immer von 0 gezählt. Die Dimension gibt an, wieviele Komponenten das Feld haben soll. Die Indexmenge reicht also von 0 bis (Dimension - 1). Beispiel:
int A [100]
Danach besitzt die Variable A 100 Komponenten vom Typ Integer: A[0], A[1], A[2], A[3], ..., A[98], A[99]
Wie man sieht, werden die Komponenten der Array-Variablen A mit dem Index I durch A[I] bezeichnet. Anstelle des Indexwertes kann, abhängig von Indextyp, auch ein beliebiger Ausdruck stehen, z. B.: A[i + j].
Zwei Array-Variablen werden als gleich bezeichnet, wenn sie vom selben Typ sind (d. h. identische Vereinbarung besitzen) und alle Komponenten paarweise gleich sind (d. h. A = B, wenn A[i] = B[i] für alle i des Indexbereichs).
Als Komponententyp eines Array kann auch wieder ein Arraytyp verwendet werden --> mehrdimensionale Arrays. Die Vereinbarung wird in den Programmiersprachen zu einer verkürzten Vereinbarung zusammengezogen:
int A [100][50]
struct Recordbezeichner
{
Komponententyp Komponentenbezeichner;
Komponententyp Komponentenbezeichner;
Komponententyp Komponentenbezeichner;
Komponententyp Komponentenbezeichner;
};
Man kann nach der schließenden Klammer auch gleich noch eine
Variable deklarieren. Beispiele:
struct Kundensatz
{
int Kundennummer;
char Name[20];
char Vorname [20];
float Umsatz;
} Kunde;
struct Datumstyp
{
int Tag;
int Monat;
int Jahr;
} Datum;
Deklarier man die Variablen getrennt von der Record-Definition, sieht das
so aus:
sruct Recordbezeichner Variablenbezeichner;Als Record-Komponenten können wieder beliebige Datentypen stehen, also auch wieder Array- oder Recordtypen. Umgekehrt kann auch ein Array Records als Komponenten besitzen (z. B. ein Array mit Komponenten vom oben gezeigten Kundentyp).
Auf einzelne Komponenten des Records wird über die Kombination des Recordnamens und der gewünschten Recordkomponente zugegriffen. Als Trennzeichen zwischen den Namen wird ein Punkt verwendet (hier gibt es Unterschiede bei den einzelnen Programmiersprachen), z. B.:
Datum.Tag oder Kunde.Name
Auch hier kann - je nach Aufbau des Records - eine mehrstufige Referenz notwendig sein.
Dateien sind ein allgemeines Konzept für die permanente Speicherung bzw. Ein-/Ausgabe. Dateien bestehen aus beliebig vielen Komponenten eines bestimmten Datentyps. Je nach Dateiart können die einzelnen Komponenten sequentiell oder wahlfrei gelesen werden. Am Ende einer Datei können weitere Komponenten hinzugefügt werden. Die Abbildung der abstrakten Dateistruktur auf reale Speichergeräte (Platte, Band, Drucker, etc.) erfolgt durch das Betriebssystem.
Eine Datei besitzt also lauter Komponenten gleichen Typs. Zusammen mit der Dateivariable wird implizit auch ein Pufferbereich im Arbeitsspeicher definiert, der mindestens eine Dateikomponente (--> aktueller Wert) aufnehmen kann. Auf diesen Datenwert wird dann über die Dateivariable zugegriffen. Zusammen mit dem Typ "Datei" müssen einige Standardoperationen definiert sein, die den Zugriff auf die Datei erlauben:

Nach Art des Zugriffs auf die Komponenten wird unterschieden in sequentielle Dateien und Dateien mit wahlfreiem Zugriff.
Um eine Datei mehrfach lesen zu können, gibt es bei vielen Systemen eine Standardoperation zum Zurücksetzen auf den Dateianfang (Rewind, Reset).
Nun sind Texte i. a. in Zeilen unterteilt, und es stellt sich die Frage, wie diese Zeilenstruktur auszudrücken ist. In der Regel enthält der in einem DVS verwendete Zeichencode spezielle Steuerzeichen, von denen eines als Zeilenende-Zeichen verwendet werden kann. Bedauerlicherweise verhindert die Realisierung von Textdateien auf Betriebssystemebene eine einfache Realisierung der Textdatei als "FILE OF Character".

Wichtig ist daß die Erfüllung der o.g. Erfordernisse quantitativ beschreibbar vorgegeben und somit objektiv überpüfbar ist. Gebräuchlich sind hier sechs Qualitätsmerkmale die in der Softwarebeurteilung zum Tragen kommen:
Für jedes dieser Merkmale existieren unterschiedliche Maßstäbe und Verfahren um eine quantitative Erfüllung zu überprüfen. Jede Abweichung vom gewünschten Ergebnis ist ein Fehler. Ideal wäre es Fehler zu vermeiden bevor sie entstehen. Prävention ist eine Strategie die sich auszahlt: Betrachten wir einmal wo Fehler entstehen, wenn man den Bogen von der Anforderungsdefinition bis hin zum Test und Integration hin spannt und wie sich diese auswirken:

Das wichtigste ist also mit dem Kunden, dem Softwarenutzer, zu reden und die Anforderungen möglichst genau und widerspruchsfrei zu erfassen. Aber wie findet man nun die Fehler, die man vermeiden möchte?
Hier gibt es verschiedene Strategien. Zum Einen kann man durch Vorgaben, wie z.B. Programmierrichtlinien, Checklisten und den Einsatz von entsprechenden Werkzeugen, Programmiersprachen und Methoden im Vorfeld den Rahmen für den einzelnen Softwareentwickler festlegen (sog. konstruktives Qualitätsmanagement) . Gewisse Tricks in der Programmierung werden untersagt, die Art und der Umfang der Kommentierung des Quellcodes vorgegeben und eine Programmiersprache gewählt, die passend zur zu lösenden Aufgabe ist und dem Ausbildungsstand der beteiligten Programmierer entsprechen.
Zum Anderen prüft man mittels Tests ob das was Geschaffen wurde mit dem übereinstimmt was vorgegeben war (sog. analytisches Qualitätsmanagement). Was Vorgegeben ist richtet sich nach der Phase in der Softwareentwicklung in der man sich befindet. Wie getestet werden kann richtet sich nach den Möglichkeiten und dem Aufwand den man investieren möchte. Welche Arten von Tests gibt es eigentlich?
Stillschweigend wurde hier vorausgesetzt, daß sinnvolle Testfälle bekannt sind. Das heißt für einen Satz Eingabedaten ist das Ergebnis, die Ausgabedaten, bekannt. Nur dann kann ein Test auf richtiges/falsches Funktionieren des Algorithmus und seiner Implementierung als Computerprogramm überhaupt durchgeführt werden. In der Regel können auf Grund der Komplexität nicht alle möglichen Variationen der Eingabedaten für Tests verwendet werden. Hat z.B. eine Programmfunktion zwei Eingangsvariable deren jeweiliger Wertebereich 2^64 ist, so wären theoretisch 2^128 unterschiedliche Eingabedaten-Möglichkeiten zu testen. Selbst wenn nur eine Picosekunde für jeden einzelnen Test veranschlagt wäre, so käme man auf knapp 4 * 10^21 Tage Rechenzeit. Damit ist klar, daß nicht alle Fehler durch Tests aufgedeckt werden können. Deshalb ist es nicht verwunderlich, wenn Tests des Softwareentwicklers keine Fehler mehr zu Tage bringen, Tests des Softwareanwenders aber schon. Die sinnvolle Auswahl der Testfälle ist entscheidend. Dazu gibt es unterschiedliche Sichtweisen, was getestet werden soll. Hier zwei Beispiele:
Problematisch beim Black-Box-Testverfahren ist die sogenannte Testüberdeckung. Die Internas des Programms werden nicht berücksichtigt. So kann es vorkommen, daß gewisse Programmteile, wie Schleifen oder Abfragen, bei solchen Tests nie oder immer mit 'harmlosen' Daten durchlaufen werden. Sinnvolle Testfälle, die sich nicht aus der Verwendung, sondern einzig aus dem Aufbau und der Formulierung des Programms ergeben, werden beim Black-Box-Test nicht systematisch durchgeführt. Die Konsequenz: unendeckte Fehler.
Um dies zu vermeiden gibt es das Prinzip des wesentlich aufwendigeren White-Box-Tests.
Eine Variante des White-Box-Tests sieht vor, daß die Testfälle so generiert werden, daß zumindest alle Programmanweisungen mindestens einmal durchlaufen werden. Dieser wird Anweisungsüberdeckungtest oder auch Co -Test genannt.
Durch Verzweigungen im Programm kann eine Anweisung auf mehreren Wegen erreicht werden. Der Anweisungsüberdeckungstest nimmt hier keine Rücksicht. Hauptsache jede Anweisung ist einmal durchlaufen:

Im Bild steht jeder Kreis für eine Anweisung des in der Bildmitte aufgeschriebenen C++-Programms und jeder Pfeil für einen möglichen Pfad den das Programm während seiner Ausführung nehmen kann. Diese Art der Graphik nennt man Kontrollflußgraph. Wie links im Bild zu erkennen ist wird bei einem Anweisungsüberdeckungstest von n-Start bin n-Final jede Anweisung n1 - n6 durchlaufen aber nicht unbedingt jeder Pfeil.
Beim sog. Zweigüberdeckungtest, auch C1-Test genannt, werden Testfälle so konstruiert, daß alle Zweige durchlaufen werden. Dieser ist einsichtig aufwendiger als der Anweisungsüberdeckungstest. Aber selbst dieser reicht nicht aus um alle Fälle abzudecken.
Schleifen (while..) und datenabhängige Verzweigungen (if..else..) führen zu unterschiedlichen Wegen durch das Programm, diese werden Pfade genannt. Die Kombination der beschrittenen Zweige, im Bild von oben nach unten betrachtet, hat unterschiedliche Auswirkungen und kann somit Fehlerquelle sein. Dieses berücksichtigen die sehr aufwendigen Pfadüberdeckungtests. Selbst hier ist noch nicht alles getan. Die datenabhängigen Verzweigungen haben meist komplexe Entscheidungsbedingungen:
if (A && B || C ) {...} else { ...}
Das Ergebnis True/False der Bedingung A && B || C hängt von den Ergebnissen der Bewertung der Teilausdrücke A, B, C ab. Eigentlich müßten für alle möglichen Wertekombinationen diese Teilausdrücke die Verzweigung getestet werden. Im Pfadüberdeckungstest wird hierauf nicht Rücksicht genommen. Erst der sog. Bedingungsüberdeckungstest kümmert sich darum.
Betrachtet man nun noch die möglichen Daten die ein Programm erzeugt und die es eingespeist bekommt, so finden noch die sogenannten datenflußorientierten Testverfahren Anwendung. Statt nur den Kontrollfluß zu betrachten wird hier der Datenfluß mit den möglichen Werten und Wertebereichen der einzelnen Variablen im Programm untersucht.
In der Praxis wird man sich für einen sinnvollen realisierbaren Mix aus den vorgestellten White-Box-Testverfahren und dem Black-Box-Testverfahren entscheiden.
Bei der Auswahl der Testdaten kann man sich von folgenden Gesichtspunkten leiten lassen:
Es soll ein Programm geschrieben werden, daß aus einer beliebigen reelen Zahl A und einer positiven ganzen Zahl B die Potenz AB ermittelt. Ein Programm das dieses Problem löst, ist dann korrekt, wenn folgende Beziehungen gelten:

Alle zulässigen Eingabewerte für die Basis werden durch A und für den Exponenten durch B dargestellt. Im Gegensatz zum Testen werden hier keine konkreten Werte wie z. B. A = 3.14 und B = 5 angenommen. Die in der Graphik als Anfangs- und Endebedingung gekennzeichneten Stellen werden als Zusicherungen (Assertions) bezeichnet. Das im nachstehenden Bild gezeigte Programm Potenzieren löst das Problem auf Grund folgender mathematischer Zusammenhänge:
BasisExponent = Basis * Basis(Exponent-1) für ungerade Exponenten

Die Anfangs- und Endebedingung kann sofort hingeschrieben werden. Nun sind alle Anweisungen und Pfade des Programmablaufplans durchzugehen und zu zeigen, daß man aus der Anfangsbedingung durch das Programm die Endebedingung erhält. Die Schleife möge man sich vorläufig als aufgeschnitten denken. Die an der Stelle (2) gegebene Zusicherung ist der Angelpunkt des ganzen Verfahrens. Diese muß zunächst intuitiv gefunden werden und in geeigneter Weise formuliert werden. Ausgehend von dieser Behauptung (2) können nun weitere Zusicherungen abgeleitet werden. Zusicherung (3) ergibt sich aus (2), da Exponent = 0 ist und Basis0 = 1 gilt. (4) folgt unmittelbar aus (2). Um (7) aus (5) abzuleiten, wird (5) in die äquivalente Form (6) umgeschrieben. Durch Ersetzen in (6) von Ergebnis * Basis durch Ergebnis und Exponent-1 durch Exponent ergibt sich dann (7). Der Exponent muß in (7) gerade sein! (8) ist interessanterweise identisch zu (7). (7)/(8) kann in (9) umgeformt werden ( (x*x)(y/2) = x(2*y/2) = xy ). Durch Ersetzen von Exponent/2 durch Exponent und Basis * Basis durch Basis in (9) erhält man (10). Danach ist in (10) der Exponent gerade oder ungerade aber größer gleich Null. Schließt man nun die Rückwärtsschleife von (10) nach (2), so sieht man die Übereinstimmung der Zusicherungen (10) und (2). Damit ist die Schleife widerspruchsfrei geschlossen.
Die Zusicherung (2) gilt offenbar für den ersten Durchlauf der Schleife, da mit Ergebnis = 1 , mit Basis = A und Exponent = B der Ausdruck Ergebnis*BasisExponent = AB wahr ist. Aufgrund des geführten Beweises gilt die Zusicherung auch für den zweiten Durchlauf und allen folgenden Durchläufen. Damit ist bewiesen, daß das Programm tatsächlich AB liefert, wenn es das Ende erreicht.
Wird jetzt noch nachgewiesen (nicht hier!) daß das Programm auch nach endlich vielen Wiederholungen aufhört (terminiert), so ist die Korrektheit des Programms formal bewiesen und es sind keine Tests nötig.
Der Aufwand den Verifikationsweg zu beschreiten ist enorm und wird in ausgesuchten Situationen beschritten. Es gibt abgewandelte Verfahren w.z.B. das symbolische Testen die öfter in der Praxis eingesetzt werden. Das Fehlerfinden mittles Testens mittels Testfälle ist das bisher am häufigsten eingesetzte Verfahren.
Auf die Möglichkeit von Zusicherungen zum Programmtest wird im folgenden Kapitel nochmals sprachbezogen eingegangen.
Damit ist es möglich nachzuvollziehen was getan und getestet wurde. Idealerweise setzt man heute Testwerkzeuge ein, die es gestatten Tests aufzuzeichnen und beliebig oft automatisch zu wiederholen. Diese Feature ist dann besonders gefragt wenn die Software erweitert wird und damit wieder vollständig getestet werden muß. Manuell wäre das sehr aufwendig bei großen Softwaresystemen.
Ist beim Testen ein Fehler erkannt worden, geht es an seine Lokalisierung und sodann an die Therapie. Typische Fehler:
Die überprüfung eines Programms auf Erfüllung seiner Spezifikation kann also nach zwei verschiedenen Methoden erfolgen:
|
Ermittle die Lösung einer quadratischen Gleichung der Form
A*X2 + B*X + C = 0

| B = 0: | falls C != 0 --> Wiederspruch ( != bedeutet "ungleich") |
| falls C = 0 --> Allgemeingültig | |
| B != 0: | Lösung: X = -C/B |

Anmerkungen zum Struktogramm:
/* Quadratische Gleichung */
#include <stdio.h>
#include <math.h>
void main(void)
{
float a, b, c, d, xl, x2; /* Variablenvereinbarung */
printf("Bitte geben Sie die Werte fuer a, b und c ein: ");
scanf("%f %f %f", &a, &b, &c); /* Einlesen der Parameter */
if (a == 0.0)
{ /* Lineare Gleichung */
if (b != 0.0)
{
x1 = -c/b;
printf("Lineare Gleichung: x = %f \n", x1);
}
else
{
if (a != 0.0)
printf("Widerspruch!\n");
else
printf("Allgemeingültig!\n");
}
}
else /* Quadratische Gleichung */
{
d = b*b - 4*a*c; /* Diskriminante berechnen */
if (d == 0.0)
{
x1 = -b/(2*a);
printf("eine reelle Lösung x = %f \n", x1);
}
else
{
if (d > 0.0)
{
x1 = (-b + sqrt(d)/(2*a);
x2 = (-b - sqrt(d)/(2*a);
printf("Zwei reelle Lösungen: x1 = %f, x2 = %f \n", x1, x2);
}
else /* d < 0 */
printf("Keine reelle Lösung\n");
}
}
}
Programm-Dokumentation ist wie Sex - ist er gut, dann ist es sehr, sehr gut; ist er schlecht, ist es besser als gar nichts.
Zum Inhaltsverzeichnis |
Zum nächsten Abschnitt |