Prof. Dr. D rer. natt. Peer Johannsen Prrof. Dr.-Ing g. Andreas s Mazura Diipl. Ing. (F FH) Peter Bitterlich B Diipl. Ing. (F FH) Andrea as Reber Dipl. Inf. C Christoph h Ußfeller FAKULT TÄT FÜR TECHNIK T STUDIEN NGÄNGE MECHA ATRONIK MEDIZINT TECHNIK Labor So oftwa are-E Entwicklu ung 2 V Vorbere itungsa aufgabe en für Versuch V h2 C C-Prog gramm mierung g – Vertiefun g Somme ersemesterr 2013 Seite 1 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura Vorbemerkungen Literatur Sie können gerne Literatur und Bücher zur Programmierung in C verwenden und mit in das Labor bringen. Insbesondere die Nachschlagewerke „C – Die Programmiersprache C. Ein Nachschlagewerk“ „C Programmierung – Eine Einführung“ des Regionalen Rechenzentrums für Niedersachsen / Leibnitz Universität Hannover (RRZN) sind inklusive handschriftlicher Ergänzungen sowohl im Labor als auch in der Prüfung als Hilfsmittel zugelassen. Was Sie im 1. Semester gelernt haben Sie sollten vertraut und geübt sein im Umgang mit der graphischen Darstellung von Algorithmen durch Programmablaufpläne und Struktogramme, mit der Definition und Initialisierung von Variablen in der Programmiersprache C, mit dem Umgang mit den Datentypen int, float und double mit dem Zuweisungsoperator und den mathematischen Operatoren, mit den Betriebssystemfunktionen zur Eingabe und Ausgabe und mit der mathematischen Bibliothek, mit der Blockanweisung, den Schleifen und den Auswahlanweisungen, mit der Deklaration und dem Aufruf von Funktionen und Unterprogrammen und mit der Verwendung von Funktionsparametern und Rückgabewerten Wiederholen Sie ggf. diese Inhalte zur Vorbereitung auf die Laborversuche! Allgemeine Vorbereitung auf den Laborversuch Wiederholen Sie die neuen Inhalte der Vorlesung und machen Sie sich vertraut mit Feldern (Arrays) und mit den Datentypen enum und struct mit der Eingabe und Speicherung von Text in C (Strings) mit Zeigern und deren Verwendung mit dem Konzept der Rekursion und rekursiven Funktionen mit der Wirkungsweise der Präprozessor-Direktiven Lesen Sie weiterhin die Hinweise auf der Internetseite des Labors. Hinweise zu den Vorbereitungsaufgaben Die Bearbeitung der Vorbereitungsaufgaben sowie die Teilnahme an den Softwarelaboren dienen Ihrer persönlichen Übung im Programmieren mit der Programmiersprache C. Hierbei ist die selbstständige Bearbeitung der jeweiligen Aufgaben eine wichtige Prüfungsvorbereitung für die Klausur. Die Vorbereitung auf das Labor und das Bearbeiten der Vorbereitungsaufgaben sind Bestandteil der Prüfungsleistung für das Labor! Eine unvorbereitete Teilnahme an den Laborterminen ist nicht möglich! Versuchen Sie nicht, diese Vorbereitung durch Nachschlagen der Lösungen im Internet oder Abschreiben abzukürzen. Spätestens in der Prüfung werden Sie sich darüber freuen können, dass Sie die Aufgaben selbst gelöst haben. Die Lösungen der Vorbereitungsaufgaben sind zum Labortermin mitzubringen, C-Programme in digitaler Form (z.B. USB-Stick, so dass sie im Labor weiterverwendet werden können), Struktogramme in handschriftlicher Form. Labor Software-Entwicklung 2 – Versuch 1 Seite 2 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura Hinweis: Zur Informations-Beschaffung kann auch das Internet verwendet werden, hier ein Beispiel: http://www.youtube.com/watch?v=_vcCQkCM6mo Bitte bearbeiten Sie mindestens die Aufgaben 1b, 1c, 2a und 2c. Aufgabe 1: Zur Speicherung und Verwaltung gleichartiger Daten unbekannter Anzahl eignet sich die Datenstruktur „Vektor“. Der Begriff Vektor wird hierbei nicht im mathematischen Sinn verwendet, sondern bezeichnet eine Datenstruktur zur Verwaltung dynamisch allokierter Felder. Hierbei ist wahlfreier Zugriff auf die Elemente des Feldes möglich. Eine Besonderheit der Datenstruktur ist, dass es möglich ist, Elemente am Ende anzufügen und vom Ende zu entnehmen. Damit stellt der Vektor auch die Funktionalität eines Stack zur Verfügung. Damit nicht bei jeder Einfüge- und Entnahmeoperation die Kapazität des dynamisch allokierten Feldes angepasst werden muss, hat der Vektor die Information über seinen Füllstand. Sobald der Füllstand die Kapazität des Feldes übersteigt, wird eine Größenanpassung für das Feld durchgeführt. Ein solcher Vektor kann also mit den 3 Komponenten „Adresse des ersten Feldelements“ (Adresse des ersten Datums), Anzahl der für das Feld bereitgestellten Elemente „Kapazität“ und der Anzahl der momentan gültigen Elemente „Füllstand“ beschrieben werden. Die Datenstruktur „Vektor“ sei in der Programmiersprache C definiert durch: typedef struct{ int* data; unsigned int capacity; unsigned int size; }Vector; // Zeiger auf das erste Element des Feldes // Anzahl der Elemente des Feldes // Anzahl gueltiger Elemente Hierbei stehen bereits die Funktionen zur int* vector_get_mem( unsigned int capacity ); Erzeugung eines Feldes der Elementanzahl capacity und void vector_free_mem( int* data ); zur Freigabe eines dynamisch erzeugten Feldes zur Verfügung. Labor Software-Entwicklung 2 – Versuch 1 Seite 3 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura a) Entwerfen Sie einen Programmablaufplan für die Operation void vector_resize( Vector* vector, unsigned int new_capacity ); Diese Funktion soll die Kapazität von vector auf new_capacity setzen. Hierfür muss neuer Speicher für das Feld bereitgestellt werden. Anschließend werden die nach der Größenanpassung noch gültigen Elemente aus dem alten in den neuen Speicher kopiert und danach wird der nun nicht mehr benötigte alte Speicher freigegeben. Weiterhin müssen die Komponenten capacity und size auf die entsprechenden Werte gesetzt werden. Das Bereitstellen des Speichers für ein Feld der Elementzahl capacity soll durch Aufruf der Funktion int* vector_get_mem( unsigned int capacity ); und das Freigeben nicht mehr benötigten Speichers soll durch Aufruf der Funktion void vector_free_mem( int* data ); erfolgen. b) Entwerfen Sie einen Programmablaufplan für die Operation void vector_push_back( Vector* vector, int value ); Diese Funktion soll den Wert value an das Ende des gültigen Speichers anfügen. Falls hierbei die Kapazität von vector überschritten würde, so muss zuerst eine Größenanpassung durch den Aufruf von void vector_resize( Vector* vector, unsigned int new_capacity ); erfolgen. Die Kapazität von vector sollte sich dabei verdoppeln, darf aber 1 nicht unterschreiten. Beim Anfügen eines Wertes wird der Füllstand von vector um 1 erhöht. c) Entwerfen Sie einen Programmablaufplan für die Operation int vector_pop_back( Vector* vector ); Diese Funktion soll den Wert der letzten gültigen Speicherstelle von zurückgeben, und den Füllstand von vector um 1 verkleinern. vector Der C-Header in Anhang 1 darf zur Bearbeitung der Aufgabe herangezogen werden. Labor Software-Entwicklung 2 – Versuch 1 Seite 4 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura Aufgabe 2: Zur Speicherung und Verwaltung gleichartiger Daten unbekannter Anzahl eignet sich die Datenstruktur „verkettete Liste“. Eine Liste ist eine lineare Struktur, deren Elemente das jeweilige zu speichernde Datum sowie Verweise auf Vorgänger- und Nachfolgerknoten tragen. Existieren nur die Verweise auf die jeweiligen direkten Nachfolger, so spricht man von einer „einfach verketteten Liste“. Gibt es auch Verweise auf die direkten Vorgänger, so wird die Struktur „doppelt verkettete Liste“ genannt. Im Gegensatz zu dynamisch verwalteten Feldern ist das Verändern der Elementanzahl mit sehr wenig Aufwand (ohne Umkopieren) zu erreichen. Weiterhin ist es sehr einfach möglich, Elemente an einer beliebigen Position einzufügen oder zu löschen. Ein Nachteil gegenüber Feldern ist jedoch, dass es keinen direkten Zugriff auf die einzelnen Elemente gibt. Die Verweise auf Vorgänger- und Nachfolgerknoten können bei einer Implementierung in der Programmiersprache C durch „Zeiger auf Knoten“ realisiert werden. Hierbei zeigt die ungültige Adresse „0“ den Start bzw. das Ende der Liste an -- der Startknoten hat keinen Vorgänger und der Endknoten hat keinen Nachfolger. In folgender Abbildung sehen Sie eine doppelt verkettete Liste, die aus den drei Knoten „A“, „B“ und „C“ besteht. Dabei steht „next*“ für den Verweis auf den Nachfolger und „prev*“ Für den Verweis auf den Vorgänger. In einer Beispiel-Implementation sei der Typ „Node“ zur Darstellung der einzelnen Knoten folgendermaßen definiert: struct _Node{ struct _Node * next; struct _Node * previous; int data; }; typedef struct _Node Node; // // // Labor Software-Entwicklung 2 – Versuch 1 Zeiger auf den Nachfolgerknoten Zeiger auf den Vorgaengerknoten Das Datum Seite 5 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura Mit Hilfe der Funktion Node* node_new( int value ); können Sie einen neuen Knoten mit dem Wert value anlegen, die Funktion liefert einen Zeiger auf den neuen Knoten zurück. Vorgänger und Nachfolger sind als ungültig markiert. Die zu nun von Ihnen zu bearbeitende Aufgabe möge an folgender Abbildung verdeutlicht werden: a) Entwerfen Sie einen Programmablaufplan für die Operation Node* node_insert_after( Node* parent, NodeData value ); Diese Funktion soll einen neuen Knoten mit dem Wert value anlegen und hinter dem Knoten parent in die Liste eintragen. Die Funktion soll einen Zeiger auf den neu erstellten Knoten zurückgeben. (Füge neuen Knoten „D“ hinter Knoten „B“ ein.) b) Entwerfen Sie einen Programmablaufplan für die Operation Node* node_insert_before( Node* parent, NodeData value ); Diese Funktion soll einen neuen Knoten mit dem Wert value anlegen und vor dem Knoten parent in die Liste eintragen. Die Funktion soll einen Zeiger auf den neu erstellten Knoten zurückgeben. (Füge neuen Knoten „D“ vor Knoten „B“ ein.) Labor Software-Entwicklung 2 – Versuch 1 Seite 6 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura c) Entwerfen Sie einen Programmablaufplan für die Operation void node_delete( Node* node ); Diese Funktion soll den Knoten node aus der Liste löschen, indem sie die Verweise in Vorgänger- und Nachfolgerknoten von node anpasst. Anschließend soll der nun nicht mehr verwendete Speicher durch den Aufruf free(node) wieder freigegeben werden. (Lösche Knoten „D“.) Der C-Header in Anhang 2 darf zur Bearbeitung der Aufgabe herangezogen werden. Labor Software-Entwicklung 2 – Versuch 1 Seite 7 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura Anhang 1: #ifndef ___VECTOR_H___ #define ___VECTOR_H___ #include <stdio.h> #include <stdlib.h> #include <assert.h> //#define _VECTOR_PAYLOAD_CHAR #define _VECTOR_PAYLOAD_INT //#define _VECTOR_PAYLOAD_DOUBLE #if defined _VECTOR_PAYLOAD_CHAR #define __FORMAT "%c" typedef unsigned char VectorData; #elif defined _VECTOR_PAYLOAD_INT #define __FORMAT "%i" typedef int VectorData; #elif defined _VECTOR_PAYLOAD_DOUBLE #define __FORMAT "%lf" typedef double VectorData; #endif typedef unsigned int uint; /** Definition des Typs "Vector" zur Modellierung eines Feldes variabler Elementzahl. ‐> Die Elementzahl eines Vectors kann im Gegensatz zu statisch allokierten Feldern zur Laufzeit des Programms festgelegt und veraendert werden. ‐> Eine Variable des Typs "Vector" hat die Komponenten ‐ "data" vom Typ "Zeiger auf VectorData" ‐> Dies ist der Verweis auf den dynamisch allokierten Speicher ‐> Dort befindet sich die Nutzlast in Form eines Feldes von "VectorData" ‐ "capacity" vom Typ "uint" ‐> Allokierte Elementzahl des Feldes "data" ‐> Das Feld "data" umfasst "capacity" Elemente ‐> Maximaler Fuellstand des Feldes ‐ "size" vom Typ uint ‐> Aktueller Fuellstand des Feldes ‐> Fuer das Feld kann mehr Speicher reserviert worden, als momentan benutzt wird. */ typedef struct{ VectorData* data; uint capacity; uint size; }Vector; /** Allokation eines Feldes von "VectorData". Das Feld umfasst capacity Elemente. Gibt Addresse des ersten Elementes des Feldes zurueck. */ VectorData* vector_get_mem( uint capacity ); Labor Software-Entwicklung 2 – Versuch 1 Seite 8 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura /** Freigabe des dynamisch erzeugten Feldes data. */ void vector_free_mem( VectorData* data ); /** Initialisierung von vector zur Aufnahme von capacity Elementen. ‐ Allokation eines Feldes von capacity Elementen ‐ Aktueller und maximaler Fuellstand wird auf capacity gesetzt ‐ Die einzelnen Elemente sind noch nicht initialisiert. (Zufaelliger Inhalt der einzelnen Eintraege) Bsp: Vector x; vector_init(&x,17); // fuer x.data wurden 17 Elemente reserviert // x.capacity == 17 // x.size == 17 */ void vector_init( Vector* vector, uint capacity ); /** Loeschen der Inhalte von vector. ‐ Freigabe des dynamisch allokierten Speichers. ‐ v‐>size = 0, v‐>capacity = 0 */ void vector_clear( Vector* vector ); /** Abfrage, ob vector leer ist. ‐> leer: Gibt 0 zurueck ‐> nicht leer: Gibt 1 zurueck */ int vector_is_empty( Vector* vector ); /** Veraenderung der Groesse der Nutzlast von vector. ‐ Falls new_capacity == vector‐>capacity, beendet sich diese Funktion sofort. ‐ Andernfalls ‐ wird mit "vector_get_mem" neuer Speicher new_data fuer new_capacity Elemente bereitgestellt. ‐ werden alle noch gueltigen Eintraege von vector‐>data nach new_data kopiert (vector‐>size und new_capacity beachten) ‐ wird vector‐>data mittels "vector_free_mem" freigegeben ‐ wird mit den Zuweisungen ‐ vector‐>data = new_data ‐ vector‐>capacity = new_capacity ‐ vector‐>size = Anzahl der gueltigen Eintraege die Groessenanpassung von vector abgeschlossen Bsp: Vector x; vector_init(&x,17); // x hat 17 Elemente vector_resize(&x,39); // x hat 39 Elemente vector_resize(&x,4); // x hat 4 Elemente */ void vector_resize( Vector* vector, uint new_capacity ); Labor Software-Entwicklung 2 – Versuch 1 Seite 9 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura /** Anhaengen des Wertes value an das Ende des verwendeten Speichers von vector. ‐ Falls der reservierte Speicher ist schon vollstaendig belegt ist Dann vector_resize(vector, 2*vector‐>capacity) Hinweis: Sonderfall vector‐>capacity==0 beachten ‐ vector‐>data[vector‐>size]=value ‐ vector‐>size um 1 erhoehen Bsp: Vector x; uint i; // x.size==4, x.capacity==4 vector_init(&x,4); for( i = 0; i < x.size; ++i ){ x.data[i] = i; } vector_push_back(&x,12); // x.size==5, x.capacity=8 */ void vector_push_back( Vector* vector, VectorData value ); /** Rueckgabe des letzen Elementes von vector, dieses wird dabei aus vector entfernt. ‐ Wert des letzten Elementes von vector in temporaerer Variable speichern ‐ Optional: Groessenanpassung fuer vector, beispielsweise bei Unterschreiten einer bestimmten Auslastung (size pro capacity) ‐ vector‐>size um 1 verkleinern ‐ Rueckgabe der temporaeren Variable Bsp: Vector x; uint i; VectorData tmp; vector_init(&x,4); // x.size==4, x.capacity==4 for( i = 0; i < x.size; ++i ){ x.data[i] = i; } while( !vector_is_empty(&x) ){ tmp = vector_pop_back(&x); // Alle Elemente von x extrahieren } */ VectorData vector_pop_back( Vector* vector ); /** Abfrage des Elementes an Stelle index. */ VectorData vector_get( Vector* vector, uint index ); /** Setzen des Wertes von Element index auf value. */ void vector_set( Vector* vector, uint index, VectorData value ); /** Ausgabe von vector auf der Standardausgabe. */ void vector_print( Vector* vector ); #endif/*___VECTOR_H___*/ Labor Software-Entwicklung 2 – Versuch 1 Seite 10 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura Anhang 2: #ifndef ___NODE_H___ #define ___NODE_H___ #include <stdlib.h> #include <stdio.h> //#define _NODE_PAYLOAD_CHAR #define _NODE_PAYLOAD_INT //#define _NODE_PAYLOAD_DOUBLE #if defined _NODE_PAYLOAD_CHAR #define __FORMAT "%c" typedef unsigned char NodeData; #elif defined _NODE_PAYLOAD_INT #define __FORMAT "%i" typedef int NodeData; #elif defined _NODE_PAYLOAD_DOUBLE #define __FORMAT "%lf" typedef double NodeData; #endif /** Deklaration des Typs "struct _Node" ‐> Es ist jetzt bekannt, dass es einen Typ "struct _Node" gibt, aber noch nicht, was dieser Typ ist. (Der Typ wurde noch nicht definiert.) */ struct _Node; /** Fuer bessere Lesbarkeit soll auf den Typ "struct _Node" mit dem Namen "Node" zugegriffen werden koennen. */ typedef struct _Node Node; /** Definition des Typs "struct _Node" zur Modellierung einer doppelt verketteten Liste ‐ Der Typ "struct _Node" bzw. "Node" dient hierbei als Element einer solchen Liste ‐ Ein solches Element traegt als Nutzlast ein Datum data und haelt Verweise auf seine direkten Vorgaenger und Nachfolger in der Liste ‐ Eine Variable vom Typ "struct _Node" bzw. "Node" besteht aus den Komponenten ‐ "next" vom Typ "Zeiger auf Node" ‐> Verweis auf den direkten Nachfolger ‐ "previous" vom Typ "Zeiger auf "Node" ‐> Verweis auf den direkten Vorgaenger ‐ "data" vom Typ "NodeData" ‐> Die Nutzlast */ struct _Node{ Node* next; Node* previous; NodeData data; }; Labor Software-Entwicklung 2 – Versuch 1 Seite 11 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura /** Allokiert Speicher fuer eine Variable knoten vom Typ "Node" und initialisiert knoten.next = 0, knoten.previous = 0, knoten.data = value Knoten traegt also schon die Nutzlast, ist aber noch nicht in die Liste eingebunden Gibt die Addresse dieser Variablen knoten zurueck. */ Node* node_new( NodeData value ); /** Erzeugt eine Variable knoten vom Typ "Node" mit der Nutzlast value. Knoten wird in die durch parent gegebene Liste direkt hinter parent eingetragen. Hierzu wird zuerst durch knoten.next = parent‐>next parent‐>next‐>previous = &knoten (falls parent‐>next valide ist) der vorherige direkte Nachfolger von parent nach knoten eingehangen. Danach wird mit parent‐>next = &knoten knoten.previous = parent knoten nach parent eingehangen. Gibt Addresse von knoten zurueck */ Node* node_insert_after( Node* parent, NodeData value ); /** Erzeugt eine Variable knoten vom Typ "Node" mit der Nutzlast value. Knoten wird in die durch parent gegebene Liste direkt vor parent eingetragen. Hierzu wird zuerst durch knoten.previous = parent‐>previous parent‐>previous‐>next = &knoten (falls parent‐>previous valide ist) der vorherige direkte Vorgaenger von parent vor knoten eingehangen. Danach wird mit parent‐>previous = &knoten knoten.next = parent knoten vor parent eingehangen. Gibt Addresse von knoten zurueck */ Node* node_insert_before( Node* parent, NodeData value ); /** Findet durch Rueckaertstraversion das erste Element knoten der durch node gegebenen Liste. Fuer knoten gilt: &knoten!=0 knoten.previous==0 Gibt Addresse von knoten zurueck */ Node* node_find_begin( Node* node ); /** Findet durch Vorwaertstraversion das letzte Element knoten der durch node gegebenen Liste. Fuer Knoten gilt: &knoten!=0 knoten.next==0 Gibt Addresse von knoten zurueck */ Labor Software-Entwicklung 2 – Versuch 1 Seite 12 von 13 Prof. Dr. rer. nat. Peer Johannsen Prof. Dr.-Ing. Andreas Mazura Node* node_find_end( Node* node ); /** Loescht das Element node aus der durch node gegebenen Liste und deallokiert den nicht mehr benoetigten Speicher. Hierbei werden direkte Vorgaenger und Nachfolger von node aktualisiert. */ void node_delete( Node* node ); /** Loescht in der durch node gegebenen Liste alle Elemente, die vor node stehen. */ void node_delete_head( Node* node ); /** Loescht in der durch node gegebenen Liste alle Elemente, die nach node stehen. */ void node_delete_tail( Node* node ); /** Loescht in der durch node gegebenen Liste alle Elemente. */ void node_delete_all( Node* node ); /** Gibt die Nutzlast aller auf node folgenden Elemente untereinander auf der Standardausgabe aus. */ void node_print_forward( Node* node ); /** Gibt die Nutzlast aller vor node stehenden Elemente untereinander auf der Standardausgabe aus. */ void node_print_reverse( Node* node ); /** Gibt die Nutzlast von node auf der Standardausgabe aus. */ void node_print_self( Node* node ); /** Gibt die Nutzlasten aller Elemente der durch node gegebenen Liste untereinander auf der Standardausgabe aus. */ void node_print( Node* node ); #endif/*___NODE_H___*/ Labor Software-Entwicklung 2 – Versuch 1 Seite 13 von 13