12 Templates - ZAIK - Universität zu Köln
Transcription
12 Templates - ZAIK - Universität zu Köln
Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 12 12.1 Templates Funktions-Templates In vielen Fällen ist es sinnvoll, Code der für einen bestimmten Datentyp entwickelt wurde, auch für andere Datentypen zu implementieren. Beispiel (Maximum zweier Zahlen): Folgender Code ermittelt das Maximum zweier Ganzzahlen: int max ( int z1, int z2 ) { if ( z1 < z2 ) return z2; else return z1; } Um diese Funktion auch für andere Datentypen zugänglich zu machen, müssen wir für diese auch eigenen Quelltext schreiben: double max (double z1, double z2); unsigned max (unsigned z1, unsigned z2); bruch max (bruch z1, bruch z2); WS 2001/02 Programmierkurs C++ Seite 210 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Besser wäre es, diese Funktion mit einem „virtuellen“ Datentyp zu implementieren, der dann bei Bedarf durch einen real existierenden Datentyp ersetzt wird. Die Lösung hierfür sind Templates bzw. Schablonen. • Die Definition einer Template-Funktion beginnt immer mit dem Schlüsselwort template • gefolgt von einer in die Zeichen < > eingeschlossenen Liste von Typ-Platzhaltern. • Jede Definition eines Typ-Platzhalters besteht aus dem Schlüsselwort class gefolgt von dem Platzhalter (oft der Buchstabe T). Der Platzhalter (T) kann nun überall eingesetzt werden, wenn der gewünschte Datentyp gemeint ist: <template T> T max ( T z1, T z2 ) { if ( z1 < z2 ) return z2; else return z1; } WS 2001/02 Programmierkurs C++ Seite 211 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Zugreifen kann man auf diese Funktion dann auf natürliche weise durch: int m1 = max ( 1, 2 ); double m2 = max ( 1.0, 2.0 ); //... • Der Compiler bestimmt automatisch, welcher Datentyp für T eingesetzt werden soll. • Wichtig hierbei ist, daß alle Funktionen und Operatoren, die innerhalb der Templatefunktion benötigt werden, auch für den Datentyp implementiert wurden. • Für Typen die keinen Operator < kennen, läßt sich natürlich auch nicht das Maximum mit obiger Funktion bestimmen. Zusätzlich ist es möglich dem Compiler zu sagen, welcher Typ gemeint ist, indem man diesen zwischen die Zeichen <> hinter den Funktionsnamen schreibt: double m3 = max<char> (12345, 67890); Dieser Aufruf wandelt die Eingabeparameter 12345 und 67890 in Zahlen vom Typ char um, ruft die entsprechende max-Funktion auf und wandelt den Rückgabewert dann in eine double-Zahl um. WS 2001/02 Programmierkurs C++ Seite 212 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 12.2 Klassen-Templates Analog zu den Funktionen lassen sich auch für Klassen-Templates definieren: Beispiel (Matrix): In Aufgabe 26 wurde eine Matrix-Klasse implementiert, deren Elemente durch double-Werte dargestellt werden. Mit einem Klassen-Template kann diese auch für andere Datentypen genutzt werden: template <class T> class Matrix { private: T **matrix; //... public: T det () const; Matrix operator* ( T d ) const; T& operator() (unsigned n,unsigned m); //... }; WS 2001/02 Programmierkurs C++ Seite 213 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Es können nun verschiedene Matrizen verwendet werden: matrix<double> doubleMatrix (4); matrix<int> intMatrix (3); • Die Datentypen müssen hierbei natürlich auch wieder die von der Klasse Matrix benötigten Operatoren und Funktionen unterstützen. • Aus diesem Grund ist es z.B. nicht möglich eine Matrix vom Typ char* zu erstellen: matrix<char *> charPtrMatrix (3); führt zu einem Compilerfehler, da z.B. keine Multiplikation mit diesem Datentyp möglich ist. • Bei matrix<double> und matrix<int> handelt es sich um zwei verschiedene Klassen. Aus diesem Grund sind Zuweisungen wie doubleMatrix=intMatrix nicht möglich. WS 2001/02 Programmierkurs C++ Seite 214 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 12.3 Templates mit Wertparametern Neben Typparametern können wir auch sogenannte Wertparameter innerhalb von Templates festlegen. Eine mit template <class T, int N = 3> class Matrix { T matrix [N][N]; //... } deklarierte Klasse würde neben dem Datentyp auch einen Wert für N erwarten. • Wird kein Parameter angegeben, so wird auf den Standartwert 3 zurückgegriffen. • N wird innerhalb der Klasse als Konstante behandelt und bestimmt die Größe der Matrix. Diese kann also nicht mehr Dynamisch geändert werden. • Matrizen unterschiedlicher Größe haben automatisch auch unterschiedliche Typen. • Rechenoperationen, Zuweisungen usw. sind nur noch bei Matrizen gleicher Größe möglich. Eine entsprechende Überprüfung ist deshalb nicht mehr nötig. WS 2001/02 Programmierkurs C++ Seite 215 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 13 Strings Wir haben schon Zeichenketten als Felder einzelner Zeichen kennengelernt: char char s2 [ s2 [ *s1 = "Zeichenkette\0"; s2 [15]; 0 ] = ’Z’; 1 ] = ’\0’; Diese Art der Strings wurde von der Sprache C übernommen. In C++ gibt es eine Klasse, welche den Umgang mit Zeichenkette erheblich erleichtern. Die Definition der Klasse string steht im gleichnamigen Standard-Header und kann mit #include <string> in eigene Programme eingebunden werden. Initialisieren kann man ein string-Objekt durch eine C-String oder ein anderes string-Objekt. string s1 ( "Zeichenkette" ); string s2 ( s1 ); WS 2001/02 Programmierkurs C++ Seite 216 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln In der Klasse string sind verschiedene Funktionen und überladene Operatoren enthalten, welche den Umgang erheblich vereinfacht: Zuweisungen: s3 = s2; s4 = "Zeichenkette"; Verkettung: s5 = s1 + " " + s2 + "!"; Länge ermitteln: int len = s2.length (); Ein- und Ausgabe: cin >> s6; cout << s6 << endl; Zugriff auf einzelne Buchstaben: s7 [ 1 ] = ’X’; Vergleich: if ( s1 == s2 ) ... else if ( s1 < s2 ) ... WS 2001/02 Programmierkurs C++ Seite 217 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 14 Streams Wir haben schon eine Möglichkeit kennengelernt Meldungen an den Benutzer auszugeben und Eingaben von ihm anzufordern. Mit den Befehlen cout << "Geben Sie eine Zahl ein: "; cin >> zahl; cout << "Eingabe: " << zahl << endl; wird der Benutzer aufgefordert, eine Zahl einzugeben, welche dann direkt wieder ausgegeben wird. Bei cout und cin handelt es sich um Objekte vom Typ ostream bzw. istream. Allgemein laufen alle Ein- und Ausgaben über Streams, Ströme von Daten von Objekten zu anderen Objekten. Der Transfer der Daten läuft hierbei über die überladenen Operatoren << für die Aus- und >> für die Eingabe. Das Stream-Objekt steht hierbei auf der linken Seite gefolgt von einem oder mehreren Objekten und Manipulatoren (getrennt durch die überladenen Operatoren). WS 2001/02 Programmierkurs C++ Seite 218 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 14.1 Ein- und Ausgabe über die Konsolen Für die Ein- und Ausgabe über die Konsole stehen ein Eingabeobjekt (cin) vom Typ istream und zwei Ausgabeobjekte (cout und cerr) vom Typ ostream. Zusätzlich gibt es noch den Objekttyp iostream, welcher sowohl lesenden (z.B. von Tastatur) als auch schreibenden Zugriff (auf den Bildschirm) ermöglicht. Um diese Objekte nutzen zu können, muß die Headerdatei iostream eingebunden werden. Die Verwendung haben wir in vielen Beispielen schon gesehen. Alle folgenden Stream-Varianten sind (je nachdem ob es sich um Ein- oder Ausgabestreams handelt) von diesen drei Klassen abgeleitet. Bei den Varianten handelt es sich um • Filestreams (Zugriff auf Dateien) und • Stringstreams (Zugriff auf Zeichenketten) Die in den folgenden Folien vorgestellten Techniken lassen sich auf alle Typen von Streams anwenden. WS 2001/02 Programmierkurs C++ Seite 219 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 14.2 Formatierung Globale Formatierungen Mit Hilfe der Methode flags von Streams lässt sich die Ausgabe abändern. Als Parameter wird ein Zahlenwert übergeben, welcher (binär kodiert) das gewünschte Format enthält. Dieser Parameter kann durch Verknüpfung mehrer Konstanten mit Hilfe des Operators | gebildet werden. Die Konstanten werden in der Klasse ios_base definiert und werden mit ios_base::konstante angesprochen. Um z.B. alle boolschen als Strings (true und false) ausgeben zu lassen anstatt 1 und 0, kann man cout.flags ( ios_base::boolalpha ); aufrufen. Definiert wird die Klasse ios_base in der Headerdatei <ios>. Die bisher gesetzten Flags lassen sich ermitteln, indem man die Methode flags ohne Parameter aufruft. WS 2001/02 Programmierkurs C++ Seite 220 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Um alle bisher gesetzten Flags beizubehalten und nur ein neues zu setzen kann der Rückgabewert mit hinzugefügt werden: cout.flags(cout.flags()|ios_base::showpos); Das gleiche Verhalten erzielen wir durch Aufruf der Methode setf. Das übergebene Flag wird den schon bestehenden hinzugefügt: cout.setf ( ios_base::showpos ); Um ein einmal gesetztes Flag wieder zurückzunehmen kann man auf die Methode unsetf zurückgreifen: cout.unsetf ( ios_base::showpos ); Die Flags im Überblick: Flag Bedeutung boolalpha Boolensche Werte ausschreiben showbase 0 vor oktalen und 0x vor hexadez. Zahlen showpos + vor positiven Zahlen anzeigen uppercase Grußbuchstaben bei scientific und showbase showpoint Nullen hinter dem Dezimalpunkt anzeigen WS 2001/02 Programmierkurs C++ Seite 221 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Die vorangegangene Flags werden immer als einziger Parameter oder als Verknüpfung mit dem Operator | der Methode setf übergeben Andere Flags treten jedoch immer zusammen mit einem Bereichsflag an, um zu kennzeichnen in welchen Kontext diese stehen. Um z.B. Zahlen in verschiedenen Zahlensystemen ausgeben zu lassen muß setf mit einem zweiten Parameter aufgerufen werden, welcher immer ios_base::basefield lautet. Eine hexadezimale Ausgabe erhält man mit cout.setf (ios_base::hex, ios_base::basefield); ios_base::basefield muß bei folgenden Flags immer mit angegeben werden: WS 2001/02 Flag Bedeutung dec Zahlen dezimal kodieren hex Zahlen hexadezimal kodieren oct Zahlen oktal kodieren Programmierkurs C++ Seite 222 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Für Gleitkommazahlen kann man folgende Flags in verbindung mit ios_base::floatfield verwenden: Flag Bedeutung scientific Wissenschafliche Darst. (z.B. 4.2e-3) fixed Festkommadarstellung 0.0042 Zur Ausrichtung der einzelnen Ausgaben kann man in Verbindung mit ios_base::adjustfield diese Flags benutzen: WS 2001/02 Flag Bedeutung left Linksbündig ausrichten right Rechtsbündig ausrichten internal Am Dezimalpunkt ausrichten Programmierkurs C++ Seite 223 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Manipulatoren Mit Hilfe der Methoden flags, setf und unsetf können wir das Verhalten von Streams global beeinflussen. Meist ist jedoch nur eine Formatierung der als nächstes auzugebenden Elemente gewünscht. Um dieses zu erreichen bietet C++ Manipulatoren an, welche genauso wie Daten mit den Opratoren << und >> an einen Stream geschickt werden. Um diese Manipulatoren zu nutzen, muß die Headerdatei <iomanip> eingebunden werden. Um z.B. eine Gleitkommazahl mit 4 Ziffern (nicht Nachkommastellen) auf einer Breite von insgesamt 10 Zeichen auszugeben sind die Manipulatoren setw und setprecision nötig: cout << << << << setw ( 10 ) setprecision ( 4 ) 12.3456789 endl; liefert die Ausgabe 12.35, welcher 5 Leerzeichen vorausgehen. WS 2001/02 Programmierkurs C++ Seite 224 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Den Manipulator endl haben wir in Beispielen schon kennengelernt. Er fügt einen Zeilenumbruch hinzu. Analog dazu liefert ends das Zeichen ’\0’, welches einen String abschließt. Folgende Manipulatoren sind gleichbedeutend zu den entsprechenden Flags (s.o.) bzw. deren Negation und werden ohne Parameter und Klammern an die Streams übergeben: boolalpha noboolalpha showbase noshowbase showpoint noshowpoint showpos noshowpos uppercase nouppercase fixed scientific internal left right dec hex oct Diese Manipulatoren benötigen einen Parameter: Manipulator Funktion setbase (n) Zahlen zur Bases n ausgeben setfill (c) Füllzeichen c verwenden setprecision (n) Insgesamt n Ziffern anzeigen setw (n) Gesamtbreite der Ausgabe festlegen WS 2001/02 Programmierkurs C++ Seite 225 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 14.3 Weitere Methoden zur Ein- und Ausgabe Neben den Operatoren << und >> gibt es noch verschiedene Methoden um Daten an Streams zu übergeben bzw. von diesen zu erhalten. So ist es mit den Operatoren z.B. nicht möglich, einzelne Zeichen oder eine bestimmte Anzahl von Zeichen zu lesen bzw. zu schreiben, oder eine komplette Zeile zu lesen. Die wichtigsten Methoden zum Lesen von Daten sind • get (char& c) Die erste Variante von get ließt einfach ein Zeichen aus dem Stream ein und schreibt es in c. • get (char* c, unsigned n, char t) Die zweite Variante schreibt solange Zeichen in den Puffer c bis entweder das zeichen t gefunden wird oder n Zeichen eingelesen wurden. Wird der Parameter t nicht mit angegeben wird das Zeilenendezeichen verwendet. • getline (char* c, unsigned n, char t) Diese Methode arbeitet genauso wie das vorangegangene get. Einziger Unterschied ist, daß das Zeichen t nicht in die Zeichenkette aufgenommen wird. WS 2001/02 Programmierkurs C++ Seite 226 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln • read (char* c, unsigned n) read liest n Zeichen ein und schreibt sie in die Zeichenkette c. Zeilenenden werden als normale Zeichen behandelt und in c mit hineingeschrieben. • ignore (unsigned n, char t) Die Methode ignore verhält sich wie die Methode getline. Die eingelesenen Daten werden jedoch nicht gespeichert und zurückgegeben sondern verworfen. Bei den Methoden get, getline und read muß darauf geachtet werden, daß die Zeichenkette auch ausreichend Platz für die zu lesenden Daten bereitstellt. Zum Ausgeben von Daten stehen uns die beiden Methoden • put (char c) • write (const char* c, unsigned n) zur Verfügung. put schickt ein Zeichen an den Stream während write genau n Zeichen schreibt. Methode zum Schreiben von Zeilen (analog zu getline und der zweiten get-Variante) sind nicht vorhanden. Sie können auf einfache Weise mit dem Operator << und dem Manipulator endl nachgebildet werden. WS 2001/02 Programmierkurs C++ Seite 227 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 14.4 Dateien Daten lassen sich auch aus Dateien ein- und auslesen. Hierzu muß ein Objekt vom folgenden Typ angelegt werden: Datentyp Verwendung ifstream Daten einlesen ofstream Daten schreiben fstream Daten lesen und schreiben Die Ein- und Ausgabe erfolgt auf die bekannte Weise: ofstream os; ifstream is; fstream ios; os << "Ausgabe auf os" << endl; is >> zahl; // Hier ist beides Möglich ios >> zahl; // zuerst schreiben ios << "und dann lesen"; mit den Streams is, os und ios. WS 2001/02 Programmierkurs C++ Seite 228 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Um dem Compiler mitzuteilen wie die Datei heißt, kann man den Namen an den Konstruktor übergeben: ofstream os ("ausgabe.txt"); ifstream *is; is = new ifstream ( "eingabe.txt" ); Dies ist jedoch nicht immer möglich. Es kann z.B. vorkommen, daß man (nacheinander) zwei verschiede Dateien mit einem Stream öffnen will. Hierfür haben die Streams eine Methode open. Dieser kann man genau wie beim Konstruktor einen Dateinamen übergeben: ofstream os; os.open ( "ausgabe.txt" ); Geschlossen wird eine Datei automatisch, wenn das entsprechende Objekt zerstört wird. Will man dies vorher erledigen, so kann man dies mit der Methode close tun. os.close (); WS 2001/02 Programmierkurs C++ Seite 229 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Dateiattribute Beim Öffnen kann man einer Datei zusätzlich zum Namen auch Flags übergeben, um dem Compiler mitzuteilen, wie er die Datei zu behandeln hat. Diese werden in der Klasse ios_base festgelegt und können mit dem Operator | verknüpft und dem Konstruktor bzw. der Funktion open übergeben werden. Flag Bedeutung in Datei zum lesen öffnen out Datei zum schreiben öffnen app Daten an Dateiende anfügen ate Öffnen und ans Ende springen trunc Datei leeren Vor jedem Flag muß die Klasse ios_base in der die Konstanten definiert wurden mit angegeben werden: // Datei zum lesen öffnen fstream f1 ( "Datei1", ios_base::in ); // Daten an Datei anhängen fstream f2("Datei2",ios_base::app); WS 2001/02 Programmierkurs C++ Seite 230 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Wahlfreier Zugriff Bisher hatten wir nur die Möglichkeit sequentiell auf Dateien zuzugreifen, ohne innerhalb der Datei hin- und herzuspringen um dort gezielt Daten zu lesen oder zu schreiben. Bei Ausgabestreams kann mit dem Befehl seekp an eine bestimmte Poition gesprungen werden. Mit fstream fout ("Datei"); fout seekp ( 10 ); cout << "zehn"; fout seekp ( 100 ); cout << "hundert"; gelangt man auf das 10-te Zeichen innerhalb der Datei, schreibt dort die Zeichenkette „zehn“ hin, springt dann auf Zeichen 100 und schreibt da die „hundert“ hinein. Als zusätzlichen Parameter kann man noch angeben von wo aus gesprungen werden soll: WS 2001/02 Parameter Bezugspunkt ios_base::beg Dateianfang ios_base::end Dateiende ios_base::cur aktuelle Position Programmierkurs C++ Seite 231 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln So kann man z.B. mit fout.seekp (-42, ios\_base::cur) 42 Zeichen zurückgehen und mit fout.seekp (42, ios\_base::cur) dann wieder an die alte Stelle zurück. Der Befehl fout.seekp (1, ios\_base::end)} läßt den Dateizeiger auf das vorletzte Zeichen innerhalb der Datei springen. Mit der Methode tellp kann man die aktuelle Position innerhalb der Datei herausfinden. Für Eingabestreams heißen die Methoden seekg und tellg. Die Funktionsweise ist identisch. In Streams in denen man sowohl lesen als auch schreiben kann, werden alle vier Methoden zur Verfügung gestellt und die beiden Positionen getrennt voneinander verwaltet. WS 2001/02 Programmierkurs C++ Seite 232 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 14.5 Der Status eines Streams Beim Arbeiten mit Streams können verschiedene Probleme auftreten. So ist es z.B. möglich, daß eine Zahl eingelesen werden soll aber nur eine Zeichenkette verfügbar ist. Aus diesem Grund ist es wichtig, daß man den Zustand eines Streams vor und nach kritischen Operstionen abfragt. Hierzu stehen verschiedene Methoden zur Verfügung: Methode Rückmeldung eof Dateiende erreicht fail Datenfehler ohne Datenverlust bad Datenfehler mit Datenverlust good weder Dateiende noch Datenfehler Der Erfolg einer Operation kann auch direkt geprüft werden: if ( cin >> zahl ) cout << "Zahl gelesen"; else cout << "Fehler beim lesen"; WS 2001/02 Programmierkurs C++ Seite 233 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln Beispiel: Einlesen von Zahlen Es sollen alle Zahlen aus einer Datei eingelesen und ausgegeben werden. Unser erster Versuch while ( infile >> zahl ) cout << zahl; bricht jedoch ab, wenn z.B. Buchstaben verwendet werden. Der zweite Versuch while ( true ) if ( infile >> zahl ) cout << "=> " << zahl << endl; führt zu einer Endlosschleife, da der Buchstabe den Eingabestream „verstopft“. Erst die dritte Variante while ( true ) if ( infile >> zahl ) cout << zahl; else if ( ! infile.eof () ) { infile.clear (); infile.ignore (); } else break; welche mit Hilfe der Methoden clear und ignore den Status des Streams zurücksetzt und das fehlerhafte Zeichen entfernt, führt zum Erfolg. WS 2001/02 Programmierkurs C++ Seite 234 Zentrum für Angewandte Informatik Köln Arbeitsgruppe Faigle / Schrader Universität zu Köln 14.6 Stringstreams Oft ist es nötig, Daten aus Strings genau so wie mit Streams auszulesen bzw. einen String auf diese Weise zu erzeugen: • Unterteilen einer Zeichenkette in Wörter • Umwandeln von Zahlen in Zeichenketten und umgekehrt • Formatierte Zeichenkette erzeugen Die benötigten Klassen lauten istringstream, ostringstream und stringstream. Den Konstruktoren dieser Klassen kann man einen Variable vom Typ string übergeben mit denen sie initialisiert werden. Mit den für die übrigen Streams beschriebenen Funktionen lassen sich Daten in die Streams hineinschreiben und wieder auslesen. Die aktuelle Zeichenkette kann mit der Methode str ermittelt werden. istringstream is ( "4711 42 1 2 3 4 5" ); int zahl; while (is >> zahl) cout << zahl << endl; ostringstream os ( "letzte zahl=" ) os << zahl; cout << os.str () << endl; WS 2001/02 Programmierkurs C++ Seite 235