Grundlagen der Informatik - 6. Praktikum In diesem Praktikum soll es neben der Anwendung von Funktionsdefinitionen auch um einfache Prinzipien der verteilten Quelltext-Strukturierung gehen. Diese spielt vor allem dann eine Rolle, wenn mehrere Entwickler an einem Projekt arbeiten oder das Projekt so groß wird, daß es nicht sinnvoll ist, nur eine oder zwei Quelltext-Dateien zu verwenden. In der letzten Übung haben wir zwischen Prototypen und Definitionen unterschieden. Diese Trennung hat sich als sinnvoll erachtet, um Konflikten gegenseitiger Abhängigkeiten aus dem Weg zu gehen. Wir haben diese Trennung jedoch nur innerhalb einer Quelltext-Datei vorgenommen, was für kleine Aufgaben ausreichend ist, bei größeren aber zu Unübersichtlichtkeit führt. Zur Verdeutlichung der Zusammenhänge sei folgendes C++ Programm gegeben: using namespace std; void hallo(); void welt(); char ausrufezeichen(); char leerzeichen(); int main() hallo(); cout << leerzeichen(); welt(); cout << ausrufezeichen(); cout << endl; return 0; Prototypen Hauptfunktion void hallo() cout << "Hello"; void welt() cout << "Welt"; char ausrufezeichen() return '!'; Definitionen char leerzeichen() return ' ';
Wie schon zuvor erwähnt, werden Prototypen in Header-Dateien ausgelagert und über die #include Compiler-Anweisung in die jeweilige Quelltext-Datei eingebunden, welche Funktionen aus diesem Header verwendet. Es entstehen also nach diesem Schritt folgende 2 Dateien: // Datei: functions.h void hallo(); void welt(); char ausrufezeichen(); char leerzeichen(); Prototypen // Datei: main.cpp #include "functions.h" using namespace std; int main() hallo(); cout << leerzeichen(); welt(); cout << ausrufezeichen(); cout << endl; return 0; Anführungszeichen = im aktuellen Verzeichnis nach Header Datei suchen Hauptfunktion void hallo() cout << "Hello"; void welt() cout << "Welt"; char ausrufezeichen() return '!'; Definitionen char leerzeichen() return ' ';
Dies allein ist natürlich nicht sehr geschickt, da es zwar die Abhängigkeitsschwierigkeiten löst, aber nicht den Code abspeckt. In der Praxis fasst man deshalb alle Funktionen, Klassen, Makros etc., die sich gemeinsame Aufgabenbereiche teilen, in eigenen Quelltext- Dateien zusammen, um sie später in Form von Bibliotheken zu sammeln. Es ergeben sich in diesem Fall folgende 3 Dateien: // Datei: functions.h void hallo(); void welt(); char ausrufezeichen(); char leerzeichen(); // Datei: main.cpp #include "functions.h" using namespace std; int main() hallo(); cout << leerzeichen(); welt(); cout << ausrufezeichen(); cout << endl; return 0; // Datei: functions.cpp #include "functions.h" void hallo() std::cout << "Hello"; Hier fehlt das using namespace std;, weshalb der Programmierer nun festlegen muss, aus welchem Namensraum cout zu verwenden ist. (nämlich std ) Die zwei Doppelpunkte dienen als Trennelement zum Namensraum. void welt() std::cout << "Welt"; char ausrufezeichen() return '!'; char leerzeichen() return ' ';
Dies ist eine konventionelle Aufteilung der Funktionen und Prototypen entsprechend ihrer Aufgabenbereiche. Es spricht nichts dagegen, mehrere Header-Dateien und mehrere Module zu verwenden, wenn dies der Projektübersicht dient. - Es ist sogar erforderlich, wenn es darum geht, realen Anforderungen gerecht zu werden. Zur Verdeutlichung des Ganzen, soll es im folgenden um das Zusammenfügen aller Komponenten eines Projektes gehen, um daraus das eigentliche Programm zu erstellen. Dabei wenden wir das Prinzip des getrennten Compilierens an, was wir zunächst per Hand machen, im nächsten Semester aber dann automatisieren per Skript. Präprozessor a.h a.cpp b.cpp c.cpp <iostream> <iomanip> Compilieren gcc -c a.cpp gcc -c b.cpp gcc -c c.cpp a.o b.o c.o Linken gcc a.o b.o c.o automatisch Standard- C++- Bibliothek programm Diese Abbildung zeigt eine Vorgehensweise des getrennten Compilierens und Linkens unter der Voraussetzung, dass alle Dateien in einem Verzeichnis liegen. Aus einer Quelltext-Datei a.cpp wird mit dem Befehl gcc -c a.cpp das zugehörige Objekt-Modul a.o erstellt. Während der Ausführung dieses Schrittes bindet der Präprozessor noch eventuell benötigte Header-Dateien ein, was durch die #include -Direktive ausgedrückt wird. (sowohl eigens erstellte als auch Standard-Header wie z.b. iostream ) Der vom Präprozessor zusammengestellte Quelltext wird an den Compiler übergeben und schließlich compiliert. Das fertige Modul enthält dann den Maschinencode der Funktionen aus dem Quelltext a.cpp. Nutzen diese Funktionen wiederrum Funktionen anderer Module (z.b. cout der Standard-C++-Bibliothek), so existiert im Modul nur ein Verweis auf Adressen dieser Funktionen. Diese Adressen sind aber noch nicht belegt - dies ist die Aufgabe des Linkers.
Der Linker bindet alle Module (Objekt-Dateien) zusammen zu einer ausführbaren Programmdatei. Der Befehl gcc <liste aller Module> veranlasst dies. Die Standard- C++-Module werden automatisch eingebunden, was natürlich viel Schreibaufwand spart. Am Ende all dieser vielleicht zu Beginn kompliziert anmutenden Prozedur halten wir das fertige Programm in der Hand. Wollen wir nachträgliche Änderungen im Programm vornehmen, so genügt es hier, nur das jeweilige Modul neu zu compilieren und alles neu zu binden. Der Binde-Vorgang geschiet sehr schnell das Compilieren ist ein nicht zu unterschätzender Zeitfaktor. (ein weiterer Vorteil dieser Methode) Allein das Compilieren eines Mozilla-Webbrowsers nimmt 30min bis 60min in Anspruch. Müsste der Entwickler bei jeder Änderung stets alles neu compilieren, so wäre der Zeitverlust imens. Soweit zur Theorie: nun wollen wir selbst loslegen und das Konzept anhand einer Aufgabe üben. Aufgabe In dieser Aufgabe soll es darum gehen, ein Programm mycalc zu erstellen, welches 2 Zahlen und einen Operator einliest und dann diverse Operationen ausführt und ausgibt. Dabei soll das Programm so aufgebaut sein, dass alle mathematischen Funktionen in das eine Modul kommen und alle Ein-/Ausgabe-Operationen in das andere. Vorschlag für die Projekt-Struktur: main.cpp... Hauptprogramm mymath.h... Mathe Header mymath.cpp... Mathe Modul myinout.h... E/A Header myinout.cpp... E/A Modul Das Mathe-Paket soll folgende Funktionen enthalten: long mycalc(int a, char op, int b); Diese Funktion soll Summe, Differenz oder Produkt von a und b berechnen und zurückgeben. a und b sind also die Operanden und op der Operator. Wird ein unzulässiger Operator eingelesen, wird Programm beendet: exit(-1); long mypow(int a, int b); Diese Funktion soll die Potenz von a und b berechen und zurückgeben. a ist die Basis und b ist der Exponent. b soll stets größer /gleich 0 sein. void myneg(const int& src, int& dst); Diese Funktion soll src negieren und das Ergebnis an dst geben. (Call By Reference) Implementieren Sie die Berechnung selbst ohne auf die C-Mathe-Bibliothek zurückzugreifen!
Das Ein-/Ausgabe-Paket soll folgende Funktionen enthalten: void mywritestr(char* c); Eine Zeichenkette c ausgeben. void mywritech(char c); Ein Zeichen c ausgeben. void mywriteval(long zahl); Eine Zahl zahl ausgeben. void myreadval(int& zahl); Einen Wert in Referenz zahl lesen. void myreadop(char& op); Einen Operator in Referenz op lesen. Im Hauptprogramm sind alle so erstellten Funktionen anzuwenden. Dabei sollen nur die Header mymath.h und myinout.h über #include eingebunden werden, keine weiteren. Die Ausgabe des Programms soll folgende Form haben: Geben Sie die 1. Zahl ein : 2 Geben Sie den Operator ein: * Geben Sie die 2. Zahl ein : 3 2*3 = 6 2^3 = 8 neg(2) = -2 Die Erstellung und Ausführung des Programms geschieht bei Verwendung o.g. Dateinamen so: g++ -c main.cpp g++ -c mymath.cpp g++ -c myinout.cpp g++ main.o mymath.o myinout.o -o mycalc./mycalc Bei Fragen können Sie sich wenden an: toka@freebits.de.