Ziel dieser Übung ist es, ein "Gefühl für Größe" zu bekommen. Wie gestaltet man eine Bibliothek so, dass sie in ihrer Codegröße mit der verwendeten Funktionalität skaliert? Was macht der Compiler aus meinem Code und wo kommen die vielen KBs eigentlich her?
Aufgabenbeschreibung
In dieser Aufgabe soll eine im Speicherplatzverbrauch skalierbare Bibliothek zur formatierten Ausgabe von Zeichen, Zeichenketten, Ganzzahlen und Zeigern erstellt werden.
Die Bearbeitung der Aufgabe soll wie angekündigt in einer Dreiergruppe erfolgen. Spätestens jetzt solltet ihr euch also eine Gruppe suchen. Die Abgabe der Aufgabe erfolgt bei Wanja im Büro durch ein Mitglied der Gruppe. Wir werden uns dann per ssh im CIP-Pool anmelden, wo eurer funktionstüchtiges Implement abgabereif liegen sollte. Dann müsst ihr auch die Gruppenzusammensetzung mitteilen können.
Bitte beachtet die unten genannten Anforderungen an den Speicherplatzverbrauch!
Vorgabe
Die Vorgabe umfasst einen Verzeichnisbaum, in dem ihr eure Entwicklung durchführen könnt. Die Makefiles sind für die Anwendung auf den Debian/Linux-PCs im CIP-Pool gedacht. Wer zu Hause arbeiten will, muss sie gegebenenfalls anpassen.
Des weiteren umfasst die Vorgabe drei Testprogramme test1.cc bis test3.cc, mit deren Hilfe ihr am Ende die Skalierbarkeit der Bibliothek nachweisen sollt.
Die einzige vorgegebene C++-Klasse ist OStreamDock. Sie hat eine Methode void out (char), die mit Hilfe des Systemaufrufs write () implementiert wird. Alle Aufgabefunktionen der zu erstellenden Bibliothek sollen darauf aufsetzen.
Weiterhin vorgegeben ist der Startup-Code, der u. a. für die Ausführung der Konstruktoren und Destruktoren aller globalen Objekte sorgt. Außerdem misst er den Stackverbrauch und gibt diesen als Exit-Code des Programms zurück.
Die Datei config.h wird in den g++-Optionen per forced include in jede Übersetzungseinheit eingebunden. Sie ist damit der ideale Ort für Konfigurierungs-Makros und ähnliches.
Funktionsumfang
Bei der I/O-Library sind lediglich Funktionen zur Ausgabe bereitzustellen. Eingabefunktionen sind nicht nötig. Außerdem werden auch Fließkommazahlen nicht unterstützt.
Wie man den drei Testprogrammen ansieht, sollen Anwendungsprogramme die Bibliothek mit Hilfe des Datentyps OutputStream aus "OutputStream.h" nutzen. Der Typ stellt die von cout her bekannten Ausgabeoperatoren operator<< für Zeichen, Zeichenketten, Ganzzahlen und Zeiger zur Verfügung. Ob die Funktionen dieses Typs vielleicht aus Basisklassen geerbt werden oder mit Hilfe von Hilfsklassen implementiert werden, bleibt euch überlassen.
Neben den Ausgabeoperatoren sollen folgende Methoden auf einem OutputStream-Objekt anwendbar sein:
void width (int)
Setzt die Feldbreite für die nächste Ausgabe.
void fill (char)
Setzt das Füllzeichen, das ausgegeben wird, wenn das Ausgabefeld aufgefüllt werden muss (Default: Leerzeichen).
void left ()
Ausgaben sollen ab sofort im Feld linksbündig erfolgen.
void right ()
Ausgaben sollen ab sofort im Feld rechtsbündig erfolgen.
void showbase (bool)
Stellt ein, ob bei Integer-Zahlen die Basis ausgegeben wird ("0x" bei Basis 16, "0" bei Basis 8, "%" bei Basis 2).
Die Funktionen orientieren sich an der C++-Standardbibliothek. Neben den Ausgabeoperatoren und den hier genannten Funktionen sind die bekannten Manipulatoren hex, bin, dec, oct und endl zu implementieren.
Speicherplatzverbrauch
Die folgende Tabelle zeigt den Speicherplatzverbrauch der drei Testprogramme in der Lehrstuhl-Implementierung und den von eurer Implementierung nicht zu überschreitenden Wert (Angaben in Bytes). (Die Obergrenze entspricht dem Verbrauch der Lehrstuhl-Implementierung plus 5%.) Die statische Größe wird dabei mit dem Befehl size ermittelt. Der Stackverbrauch wird als Exit-Code zurückgegeben und kann somit nach Ausführung von der Shell ausgegeben werden (./test1; echo $?). Automatisiert ermittelt und addiert wird dies durch ein im Tar-Archiv mitgeliefertes Perl-Skript, welches durch make size ausgeführt werden kann.
Testprogramm
Text
Data
BSS
Stack
Summe
Eure Obergrenze
test1
559
0
12
152
723
759
test2
973
4
20
184
1181
1240
test3
1702
4
20
248
1974
2072
Diese Werte gelten für g++ (GCC) 4.2.3 (Debian 4.2.3-3), der im CIP-Pool der Standardcompiler ist.
Tipps
Um diese harten Anforderungen bezüglich des Speicherplatzverbrauchs zu erfüllen, sind folgende Hinweise vielleicht hilfreich:
Virtuelle Funktionen sind zu vermeiden.
Die Aufrufbeziehungen so ordnen, dass möglichst viel unbenutzte Funktionalität durch das "Function-Level-Linking" rausfällt.
Wo das Function-Level-Linking nicht reicht, kann man auch anwendungsspezifisch konfigurieren.
Mit konfigurierbaren Klassen arbeiten (typedefs, die eine bestimmte Variante einer Klassenimplementierung wählen).
Mit #ifdefs innerhalb des Codes arbeiten.
Außerdem sollte man sich immer mal wieder ansehen, was Compiler und Linker eigentlich aus dem Programmcode machen. Dazu eignet sich der Befehl objdump. Mit objdump -D --demangle test1 erhält man z. B. ein relativ gut lesbares disassembliertes Listing seiner Applikation.