Kapitel Matrizen in C++ In der Computersprache C ist die Standardmethode zur Behandlung von Matrizen durch 1 const int n=10; 3 double a[n][n]; gegeben. Allerdings gibt es bei dieser Methode eine Reihe von Schwierigkeiten, insbesondere wenn wir das Array a an eine Funktion übergeben. Eine deutlich zufriedenstellendere Lösung kann mit Hilfe des Klassenkonzeptes erzielt werden. In den folgenden Abschnitten wollen wir eine Matrixklasse programmieren. Diese Klasse wird Ausgangspunkt für die meisten Programme dieses Kurses darstellen..1 Die Matrixklasse matrix Beginnen wir damit, die wichtigsten Elemente dieser Matrixklasse anzuführen. Der Einfachheit halber wollen wir nur reelle Matrizen (Variablentyp double) der Größe n n (Ordnung n) betrachten. Zuerst soll es möglich sein: eine Matrix einer bestimmten Größe zu erstellen (d.h., genügend Speicherplatz für die n n Matrixelemente zu reservieren); auf die Matrixelemente zuzugreifen (d.h., sie zu lesen oder zu verändern). Ein typisches Programm könnte dann von der Form 7
8 KAPITEL. MATRIZEN IN C++ 1 matrix a(3); // 3 x 3 Matrix 3 a(1,)=1; 4 a [0][1]=; sein. In der ersten Zeile wird eine 3 3 Matrix deklariert, wobei an dieser Stelle genügend Speicherplatz reserviert werden soll. Zeile 3 zeigt einen Zugriff auf das Matrixelement a 1. Weiters wäre es wünschenswert, dass wir auch in klassischer C-Form a[ i ][ j ] auf das Matrixelement a ij zugreifen können. Wir beginnen nun mit der Implementierung der Matrixklasse. Zuerst öffnen wir zwei Dateien matrix.h und matrix.cc. matrix.h ist die header-datei, in der die Klassendefinition (sowie kürzere Klassenfunktionen) stehen sollen. In matrix.cc werden die längeren Klassenfunktionen ausprogrammiert. Im Folgenden soll angenommen werden, dass Sie die Grundkonzepte von Klassen in C++ kennen und wissen, wie Sie mehrere Dateien kompilieren und binden können (z.b. unter Verwendung von Makefile)..1.1 Grundelemente der Matrixklasse Beginnen wir mit der Klasse matrix.h. Das Gerüst ist von der Form 1 #ifndef matrix h #define matrix h 3 4 class matrix { 5 public: 6 matrix(int n=0); 7 matrix(); 8 9 double& operator() (int i, int j); 10 double operator[] (int i); 11 1 int size () const {return n; } 13 14 private: 15 int n; 16 double a; 17 }; 18 19 #endif Die Bedeutung der Zeilen ist wie folgt:
.1. DIE MATRIXKLASSE matrix 9 Zeilen 1,, 19. Die übliche Struktur von header files, die verhindert, das eine header- Datei öfters als einmal eingelesen wird. Zeile 6. Konstruktor für die Matrix der Ordnung n. Zeile 7. Destruktor. Zeile 9. Zugriff auf Matrixelemente über a(i, j). Zeile 10. Zugriff auf Matrixelemente über a[ i ][ j ]. Zeile 1. Funktion, die die Ordnung n der Matrix liefert. Zeile 15. Ordnung n der Matrix (d.h., n n-matrix). Zeile 16. Zeiger auf Matrixelemente..1. Speicherung der Matrixelemente Betrachten wir die Matrix A = a 00 a 01 a 0 a 03 a 10 a 11 a 1 a 13 a 0 a 1 a a 3 a 30 a 31 a 3 a 33 wobei wir von der C-Notation Gebrauch gemacht haben, dass Indizes mit 0 beginnen. Wie können wir diese 4 4 Matrix abspeichern? Eine Möglichkeit besteht darin, die Matrixelemente hintereinander in einem Vektor der Länge n n = 16 zu speichern {a 00, a 01, a 0, a 03, a 10, a 11, a 1, a 13, a 0, a 1, a, a 3, a 30, a 31, a 3, a 33 }. Konstruktor und Destruktor. Im Konstruktor der Matrixklasse müssen wir somit einen Speicherplatz der Länge n n reservieren, der dann vom Destruktor wieder freigegeben wird 6 matrix(int n=0) : n( n ) { if (n) a=new double[n n]; } 7 matrix() { if (n) delete[] a; } operator(). Wie greifen wir auf das Matrixelement a ij zu? Offensichtlich über a[ i n+j]. Wir können somit die Klassenfunktion operator() implementieren. Weil sie so kurz ist, können wir sie gleich nach matrix.h schreiben. Zuerst fügen wir vor der Klassendefinition die Zeile
10 KAPITEL. MATRIZEN IN C++ #define index(i,j) (( i) n+(j)) ein. Und weiters 9 double& operator() (int i, int j) { return a[index(i,j )]; } operator[]. Schließlich wollen wir noch den normalen Indexoperator implementieren. Wie gehen wir vor? Wenn wir wollen, dass wir mit Hilfe von a[ i ][ j ] auf das Matrixelement a ij einer Matrix A zugreifen können, so muss operator[i] einen Zeiger liefern. Betrachten wir nochmals die Aufteilung des Speicherplatzes a a+n a+ n a+3 n { a 00, a 01, a 0, a 03, a 10, a 11, a 1, a 13, a 0, a 1, a, a 3, a 30, a 31, a 3, a 33 } Wir erkennen, dass der Zeiger a+i n auf den Beginn der i-te Zeile der Matrix A zeigt. Durch (a+i n)[j] können wir also auf a ij zugreifen. Somit können wir die Klassenfunktion 10 double& operator[] (int i) { return a+i n; } vervollständigen. Aufgabe.1 Vervollständigen Sie die Matrixklasse. Schreiben Sie ein Programm, das eine 3 3-Matrix deklariert und sie mit den Matrixelementen besetzt. A = 1 0 4 3 7 1. Aufgabe. Betrachten Sie die folgende Funktion: 1 void f(const matrix& a) { 3 cout << a(0,0) << endl; 4 } Warum kommt es beim Kompilieren zu einer Fehlermeldung? Wie kann das Problem behoben werden?
.. ALGORITHMEN 11. Algorithmen In diesem Abschnitt wollen wir einige nützliche Funktionen der C++-Standardbibliothek besprechen, die in den header-dateien #include <algorithm> #include <numeric> definiert sind. Ausgansgpunkt sind die sogenannten Iteratoren. Das sind beliebige Objekte, für die gelten soll, dass sie: über den Zuordnungsoperator operator= auf einen bestimmten Anfangswert gesetzt werden können; über die Vergleichsoperatoren operator== und operator!= mit einem anderen Iterator verglichen werden können; über operator++ inkrementiert werden können; über operator einen Wert liefern. Offensichtlich sind Zeiger Iteratoren, da alle Anforderungen 1 double a[10], p; 3 for (p=a; p!=a+n; p++) p=0; erfüllt sind (es gibt allerdings noch eine Reihe weiterer Iteratoren, die Zugriff auf verschiedenst Objekte z.b. Container erlauben). Was können wir mit diesen Iteratoren machen? All die kleinen Aufgaben, die wir im Programmieralltag stets benötigen. In dem obigen Beispiel hätten wir auch 1 fill (a,a+n,0.); schreiben können unter Benutzung der Funktion 1 copy(inputiter first, InputIter last, const double& value); Hierbei wird der Speicherplatz in dem Bereich first bis last mit dem Wert value belegt. Zwei Dinge sind zu beachten, die allgemein für alle Funktionen von numeric und algorithm gelten: es erfolgt keine Überprüfung, ob der Speicherplatz, auf den das Programm zugreift, zuvor reserviert wurde;
1 KAPITEL. MATRIZEN IN C++ der letzte Iterator last zeigt auf das Element, das unmittelbar nach dem letzten Element steht, auf das zugegriffen werden soll (in dem obigen Beispiel a+n, d.h. das Element a[n]). Die am häufigsten verwendeten Funktionen sind 1 // aus numeric T accumulate(inputiter first, InputIter last, T init ); 3 T inner product(inputiter first1, InputIter last1, InputIter first, T init ); 4 5 // aus algorithm 6 count(inputiter first, InputIter last, const T& value); 7 copy(inputiter first, InputIter last, OutputIter result ); 8 bool equal(inputiter first1, InputIter last1, InputIter first ); 9 fill (OutputIter first, OutputIter last, const T& value); 10 InputIter max element(inputiter first, InputIter last ); 11 InputIter max element(inputiter first, InputIter last ); 1 replace(forwarditer first, ForwardIter last, const T& old value, const T& new value); 13 reverse(forwarditer first, ForwardIter last ); 14 sort(forwarditer first, ForwardIter last ); Hierbei bezeichnet T den Variablentyp (z.b. double), InputIter ist ein Iterator der nur gelesen wird, OutputIter einer der verändert wird, und ForwardIter einer der gelesen und verändert wird. Die Wirkung der Funktionen ist: accumulate: summiert den Inhalt von InputIter zu init ; inner product: bildet das innere Produkt ab von zwei Vektoren a und b; count: zählt, wie oft value vorkommt; copy: kopiert einen Bereich in einen anderen; equal: überprüft zwei Bereiche auf Gleichheit; fill: kopiert den Wert value in alle Elemente eines Bereiches; max element: liefert den Iterator auf das größte Element; min element: liefert den Iterator auf das kleinste Element; replace: ersetzt in einem Bereich old value durch new value; reverse: kehrt den Inhalt eines Bereiches um; sort: sortiert den Inhalt eines Bereiches in aufsteigender Reihenfolge. Wann immer möglich, wollen wir in den folgenden Programmen von diesen Funktionen Gebrauch machen.
.3. DIE VOLLSTÄNDIGE MATRIXKLASSE 13 Aufgabe.3 Betrachten Sie das Programm 1 #include <numeric> #include <algorithm> 3 4 void main() 5 { 6 int a [6]={0,1,,0,3,}; 7 int b [6]; 8 } Benutzen Sie die zuvor eingeführten Funktionen und kopieren Sie den Inhalt von a nach b; überprüfen Sie die beiden Arrays auf Gleichheit; drehen Sie die Reihenfolge von b um; bestimmen Sie die Summe 5 i=0 a i; bestimmen Sie das größte Element von b; sortieren Sie a; setzen Sie alle Elemente von b auf 0..3 Die vollständige Matrixklasse Damit wir die Funktionen von numeric und algorithm auch für die Matrixklasse verwenden können, führen wir zwei weitere Klassenfunktionen 1 double begin() const { return a; } double end() const { return a+n n; } ein, die auf den Beginn bzw. das Ende des Matrixspeicherplatzes zeigen. Mit Hilfe dieser zusätzlichen Funktionen, können wir dann z.b. alle Matrixelement auf einen bestimmten Wert setzen 1 matrix a(5); 3 copy(a.begin(),a.end (),0); Aufgabe.4 Schreiben Sie ein Programm, das den Inhalt einer Matrix A in eine zweite Matrix B kopiert.
14 KAPITEL. MATRIZEN IN C++ Für die häufigsten Matrixverknüpfungen wie Zuordnung A = B, Addition A + B, Subtraktion A B oder Multiplikation A B (entsprechend den Regeln der Matrixmultiplikation) ist es vernünftig, die entsprechenden Klassenoperatoren zu implementieren. Eine solche Implementierung ist in den Dateien http://physik.uni-graz.at/~uxh/lineare-algebra/matrix.h http://physik.uni-graz.at/~uxh/lineare-algebra/matrix.cc zu finden. Die vollständige Klasse beinhaltet folgende Funktionen: 1 class matrix { 3 public: 4 matrix(int n); 5 matrix(const matrix& m); 6 matrix(const char filename); 7 matrix(); 8 9 double opertor[] (int i ); 10 const double operator[] (int i) const; 11 double& operator() (int i, int j); 1 double operator() (int i, int j ) const; 13 14 const matrix& operator= (const matrix& m); 15 16 const matrix& operator+= (const matrix& m); 17 const matrix& operator = (const matrix& m); 18 const matrix& operator = (const matrix& m); 19 0 matrix operator+ (const matrix& m) const; 1 matrix operator (const matrix& m) const; matrix operator (const matrix& m) const; 3 4 double begin() const; 5 double end() const; 6 7 int size (); 8 void write(); 9 }; Die Wirkung der Funktionen ist wie folgt: Zeile 4. Standardkonstruktor. Zeile 5. Konstruktor, wobei die Matrix dieselbe Ordnung n und Matrixelemente wie m erhält.
.3. DIE VOLLSTÄNDIGE MATRIXKLASSE 15 Zeile 6. Konstruktor, bei dem die Ordnung n und Matrixelemente aus einer Datei filename eingelesen werden. Der erste Eintrag in der Datei soll die Ordnung n sein, gefolgt von den n n Matrixelementen. Beispielsweise erhält mit der Datei matrix.dat 1 3 3 4 3 1 4 0 4 8 7 1 die Matrix a in dem Programm 1 matrix a( matrix.dat ); die Form. 3 4 1 4 0 8 7 1 Zeilen 9 1. Zugriff auf Matrixelement über a(i, j) und a[ i ][ j ]. Zeile 14. Zuordnungsoperator, der es erlaubt, Ordnung n und Inhalt einer Matrix m in die aktuelle Matrix zu kopieren. Zeilen 16. Addition, Subtraktion und Multiplikation (entsprechend c ij = k a ikb kj ) von Matrizen; z.b. 1 matrix a(3), b(3), c (3); 3 a+=b; c=a+b; 4 a =b; c=a b; 5 a =b; c=a b; Bei all diesen Operationen wird überprüft, ob die Matrizen dieselbe Ordnung n haben. Zeilen 4, 5. Funktionen, die den Beginn begin() bzw. das Ende end() des Speicherplatzes liefern, an dem die Matrixelemente abgespeichert sind. Zeile 7. Ordnung n der Matrix. Zeile 8. Einfache Ausgaberoutine für Matrix. Aufgabe.5 Erstellen Sie zwei Dateien matrix.dat und diag.dat mit Inhalt und : 3 4 1 4 0 8 7 1 1 0 0 0 1 0 0 0 1 lesen Sie die Matrizen m und e in einem Programm ein, und geben Sie ihren Inhalt am Bildschirm aus; multiplizieren Sie die beiden Matrizen; zeigen Sie, dass gilt m e = e m; addieren Sie m und e.
16 KAPITEL. MATRIZEN IN C++.4 Matrizen in Fortran90 Es gibt eine Reihe anderer Programmiersprachen, in denen die wichtigsten Matrixoperationen bereits implemetiert sind. Besonders einfach ist das Rechnen mit Matrizen in fortran90, auf das wir hier kurz eingehen wollen. Beispielsweise werden in dem Programm 1 program main 3 real, dimension(0:,0:) :: a, b, c 4 5 a=0 6 b=1 7 8 a(0,0)=1 9 10 c=matmul(a,b) 11 1 print,c 13 14 end program in der Zeile 3 die Matrizen a,b,c der Größe 3 3 definiert (Indizes von 0 bis ), deren Matrixelemente in Zeilen 5 und 6 auf eine Konstante gesetzt werden; in Zeile 8 wird das Element a 00 gesetzt; in Zeile 10 erfolgt eine Matrixmultiplikation von a und b; und schließlich wird die Matrix c in Zeile 1 ausgegeben. Wir sehen, dass all die Funktionen (und noch einige mehr), die wir in diesem Kapitel programmiert haben, in fortran90 bereits vorhanden sind. Dies ist auch einer der Gründe, weshalb sich die Programmiersprache fortran in der Physik großer Beliebtheit erfreut. Dennoch wird sich zeigen, dass wir mit der selbst erstellten Matrixklasse matrix alle Probleme vollständig befriedigend lösen können.