Einführung in C++ (Teil 3)





Strukturen

Eine Struktur ist eine Ansammlung mehrerer Variablen verschiedener Typen unter einem Namen, um die Daten besser organisieren zu können. Eine Struktur wird wie eine ganz normale Variable definiert und kann danach beliebig oft im Programm verwendet werden. Auf die Struktur kann komponentenweise oder komplett zugegriffen werde. Der komponentenweise Zugriff erfolgt mit Hilfe des Punktoperators. Dabei wird der Name der Struktur, gefolgt von einem Punkt und dem Namen der gewünschten Komponente angegeben.

Beispiel 1:

#include <iostream.h>
#include <string.h>

struct Student
{
 char Vorname[20];
 char Nachname[20];
 int Matrikelnummer;
 char Studienfach[20];
 char Abschluss[20];
 int Fachsemester;
};

void main()
{
 Student Jan = {"Jan","Mueller", 984178, "Physik", "Diplom", 5};
 cout << "Name: " << Jan.Vorname << " " << Jan.Nachname << endl;
 cout << "Matrikelnummer: " << Jan.Matrikelnummer << endl
        << "Studienfach: " << Jan.Studienfach << endl
        << "Angestr. Abschluss: " << Jan.Abschluss << endl
        << "Fachsemester: " << Jan.Fachsemester << endl;
}

Output:
Name: Jan Mueller
Matrikelnummer: 984178
Studienfach: Physik
Angestr. Abschluss: Diplom
Fachsemester: 5

Dynamische Datenobjekte

Bisher  war der Speicherplatzbedarf immer schon fest vorgegeben.
Der Operator new bietet die Möglichkeit Speicherplatz in der richtigen Menge bereitzustellen.
Der Zugriff auf den Speicher erfolgt ausschließlich über Zeiger:

int *p;                              // Zeiger auf int
p = new int;                     // int-Objekt wird erzeugt
*p = 5;                            // int-Objekt bekommt den Wert 5 zugewiesen

n x m - Matrizzen:
Schritt 1: Mache die Zahl der Spalten variabel
Ausgangspunkt: O.b.d.A 4 x 3 - Matrix, also float   Matrix[4] [3]
-> 4 x m - Matrix:    float   *Matrix[4] ;                   // Array von 4 Zeigervariablen
                                    Matrix[i] = new float[m] ;      // Jede Spalte erhält m Einträge
                                    wobei i = 0 .. 3
Schritt 2: Mache zusätzlich noch die Zahl der Zeilen variabel
-> n x m - Matrix:    float  **Matrix;                        // Matrix ist ein Zeiger auf Zeiger auf float
                                    Matrix = new float* [n];         // Jede Zeile erhält n Einträge
                                    Matrix[i] = new float[m] ;      // Jede Spalte erhält m Einträge

float **Matrix ist gleichbedeutend mit (float*) *Matrix. Matrix ist also ein Zeiger auf Zeiger die auf Elemente vom Typ float zeigen.

Beispiel 2:

#include <iostream.h>

int i = 5;

void main()
{
 int i,j,dim1,dim2;
 cout << "Gib die Anzahl der Zeilen und Spalten der Matrix an: ";
 cin >> dim1 >> dim2;
 int **matrix;
 matrix = new int* [dim1];
 for (i = 0; i < dim1; i++)
   matrix[i] = new int[dim2];
 for (i = 0; i < dim1; i++)
 {
  for (j = 0; j < dim2; j++)
  {
   cout << "Geben sie das Matrixelement a" << i+1 << j+1 <<" ein: ";
   cin >> matrix [i] [j];
  }
 }
 cout << "Die eingegebene Matrix lautet: " << endl;
 for (int i = 0; i < dim1; i++)
  {
    for (int j = 0; j < dim2; j++)
      cout << matrix [i] [j] << " ";
    cout << endl;
  }
}

Output:
Gib die Anzahl der Zeilen und Spalten der Matrix an: 2  3
Geben sie das Matrixelement a11 ein: 4
Geben sie das Matrixelement a12 ein: 5
Geben sie das Matrixelement a13 ein: 1
Geben sie das Matrixelement a21 ein: 8
Geben sie das Matrixelement a22 ein: 2
Geben sie das Matrixelement a23 ein: 3
Die eingegebene Matrix lautet:
4 5 1
8 2 3

Separate Compilation

Man kann den Quellcode von umfangreichen Programmen auf mehrere Dateien aufteilen. Um den Mechanismus der Aufteilung zu verstehen, muß zunächst einmal erläutert werden, wie ein C++ Programm aus den Quelldateien erzeugt wird. Das Umsetzen der Quelltexte in ein ausführbares Programm erfolgt in drei Schritten: Präporzessor -> Compiler -> Linker.

Der Präprozessor sucht nach speziellen Anweisungen, die mit einem Doppelkreuz (#) gekennzeichnet sind. Die wichtigste Funktion des Präprozessors ist das Einbinden von Dateien durch den Befehl #include.
Dabei werden andere Quelltexte in den Quelltext des Programms eingebunden.
Der Compiler setzt den vom Präprozessor überarbeiteten Quelltext in den Objektcode um. Der Objektcode ist ein maschinennaher Code, der aber noch nicht ausgeführt werden kann, da er noch unaufgelöste Funktionsaufrufe enthält. Er wird in Dateien mit der Endung .o gespeichert.
Der Linker setzt die vom Compiler generierten Objektdateien zu einem vollständigen, aufführbaren Programm zusammen. Er verbindet die Funktionsaufrufe mit den zugehörigen Funktionen.

Beispiel 3:

//Programm: Modul1.cpp

#include <iostream.h>

void vertausche(int *a, int *b);

void main()
{
  int x, y;
  x = 5;
  y = 7;
  cout << "x = " << x << " und " << "y = " << y << endl;
  vertausche (&x, &y);
  cout << "x = " << x << " und " << "y = " << y << endl;
}
 

//Programm: Modul2.cpp

#include <iostream.h>

void vertausche(int *a, int *b)
{
  int h;
  h = *a;
  *a = *b;
  *b = h;
}
 

Separate Compilation:
> c++ -c Modul1.cpp
> c++ -c Modul2.cpp

Der Schalter -c unterdrückt den Aufruf des Linkers. Die Quelldateien werden nur  übersetzt, es entstehen die Objektdateien Modul1.o und Modul2.o.

Das Linken erfolgt über den Befehl

>  c++ -o vertausche Modul1.o  Modul2.o

Der Aufruf des Linkers bildet aus den Objektdateien das ausführbare Programm unter dem Namen vertausche.

Man kann das Compilieren und Linken auch in einem Aufruf veranlassen:

>  c++ -o vertausche Modul1.cpp Modul2.cpp

Allgemeine Regeln:

Unter Definition versteht man die Bereitstellung von Speicherplatz für Daten und den Programmcode. Variablen und Funktionen dürfen nur einmal in einem Programm definiert werden. Deklarationen sind nur Informationen für den Compiler. Sie sind in beliebiger Anzahl möglich.

Beispiel 4:

// Programm file1.cpp

#include <iostream.h>

int  a = 3, b = 5, c = 1;                //Definition externer Variablen
int f();                                       //Deklaration Funktionsprototyp

void main()
{
 cout << a << " " << b << " " << c << endl;
 cout << f() << endl;
 cout << a << " " << b << " " << c << endl;
}
 

// Programm file2.cpp

int f()
{
  extern int a;                            //Verweis auf globales (externes) a
  int b;                                      //b ist lokal

  a = b = 2;
  return (a + b);
}

> c++ -c file1.cpp
> c++ -c file2.cpp
> c++ -o test file1.o file2.o
> ./test

Output:
3 5 1
4
2 5 1

Vorteil der separaten Complilation: Will man in einem Modul etwas verändern, braucht man nicht das gesamte Programm erneut zu compilieren, sondern nur das Modul, in dem die Änderung vorgenommen wurde.

Beispiel 5:

//Veränderung von file2.cpp. Setze a = b = 1.

int f()
{
  extern int a;                   //Verweis auf globales (externes) a
  int b;                              //b ist lokal

  a = b = 1;
  return (a + b);
}

> c++ -c file2.cpp
>  c++ -o vertausche file1.o  file2.o

Output:
3 5 1
2
1 5 1
 

Ein- und Ausgabe mit Dateien

Die Ein- und Ausgabe muß nicht unbedingt über die Tastatur oder den Bildschirm erfolgen. Mit

> Programmname < Quelldateiname > Zieldateiname

werden als Eingabe in das Programm Programmname die Einträge der Datei Eingabe verwendet und die Ausgabe des Programms in die Datei Zieldatei umgelenkt.

Beispiel 6: Schreibe die Ausgabe des Programms vertausche1 in die Datei vertausche.dat.

// Programm: vertausche1

#include <iostream.h>

void vertausche(int *a, int *b)
{
  int h;
  h = *a;
  *a = *b;
  *b = h;
}

void main()
{
  int x, y;
  x = 5;
  y = 7;
  cout << "x = " << x << " und " << "y = " << y << endl;
  vertausche (&x, &y);
  cout << "x = " << x << " und " << "y = " << y << endl;
}

> c++ -o vertausche vertausche1.cpp
> ./vertausche > vertausche.dat
> cat vertausche.dat
x = 5 und y = 7
x = 7 und y = 5

Beispiel 7: Einlesen der Werte für x und y aus der Datei  eingabe.dat

// Programm: vertausche2

#include <iostream.h>

void vertausche(int *a, int *b)
{
  int h;
  h = *a;
  *a = *b;
  *b = h;
}

void main()
{
  int x, y;
  cin >> x;
  cin >> y;
  cout << "x = " << x << " und " << "y = " << y << endl;
  vertausche (&x, &y);
  cout << "x = " << x << " und " << "y = " << y << endl;
}

> echo 4 5 > eingabe.dat
> c++ -o vertausche2 vertausche2.cpp
> ./vertausche1 < eingabe.dat

Output:
x = 4 und y = 5
x = 5 und y = 4

Will man die Werte für x und y aus der Datei eingabe.dat einlesen und das Ergebnis der Vertauschung in die Datei ausgabe.dat schreiben, so gibt man folgenden Befehl im Terminal ein:

> ./vertausche2 < eingabe.dat > ausgabe.dat
> cat ausgabe.dat
x = 4 und y = 5
x = 5 und y = 4

Klassen und Objekte

Eine Klasse ist ein abstrakter Datentyp. Sie enthält Daten mit den zugehörigen Funktionen.

Abstrakter Datentyp = Daten + Funktionen
Eine Klasse enthält also zusätzlich zu den Daten Funktionen, die beschreiben, wie man mit den Daten umzugehen hat. Die Daten und Funktionen sind Elemente eines Objekts.
Die Daten nennt man auch Elemente. Die Funktionen werden oft als Elementfunktionen oder Methoden bezeichnet.

Die Klasse dient dazu, dem Compiler die Beschreibung von später zu definierenden Objekten mitzuteilen.
Eine Variable oder Konstante dieses Datentyps nennt man eine Instanz der Klasse.

Beispiel 8:

#include <iostream.h>

class vektor
{
 public:
 vektor spiegeln() const
 { vektor a;
   a.x = -x;
   a.y = -y;
   return a; }
 void print() const
 {  cout << x << " " << y << endl; }
 float x,y;
};

void main()
{
 vektor u = {2,4}, v;
 cout << "Vektor u: "; u.print();
 u.x = 5;
 v = u.spiegeln();
 cout << "Vektor u: "; u.print ();
 cout << "Vektor v: "; v.print ();
}

Output:
Vektor u: 2 4
Vektor u: 5 4
Vektor v: -5 -4

Der Datentyp Vektor enthält zwei Datenelemente x und y (Komponenten des Vektors) und zwei Funktionen spiegeln (Spiegeln eines beliebigen Vektors am Ursprung) und print (Ausgabe eines beliebigen Vektors auf den Bildschirm).
Das Schlüsselwort const, das z.B. im Ausdruck void print() const verwendet wird, deutet an, daß die Funktion print beispielsweise  das Objekt u durch den Aufruf u.print () nicht verändern wird.

Es ist sinnvoll, die Elementfunktionen in der Klassendefinition nur zu deklarieren, und sie an einer anderen Stelle zu definieren. Dort werden sie durch Voranstellen von Klassenname:: gekennzeichnet.

Beispiel 9:

#include <iostream.h>

class vektor
{
 public:
 vektor spiegeln() const;
 void print() const;
 float x,y;
};

vektor vektor::spiegeln() const
 {
   vektor a;
   a.x = -x;
   a.y = -y;
   return a;
 }

void vektor::print() const
 {
   cout << x << " " << y << endl;
 }

void main()
{
 vektor u = {2,4}, v;
 cout << "Vektor u: "; u.print();
 v = u.spiegeln();
 cout << "Vektor v: "; v.print ();
}

Output:
Vektor u: 2 4
Vektor v: -2 -4

Das objektorientierte Programmieren basiert unter anderem auf dem Konzept der Datenkapselung. Dadurch ist der Zugriff auf bestimmte Objekte eingeschränkt und nur über wohldefinierte Schnittstellen erlaubt. Auf alle private Elemente einer Klasse ist kein direkter Zugriff möglich.
Beispiel:
class vektor
{
 public:
 vektor spiegeln() const;
 void print() const;
 private:
 float x,y;
};

Es ist kein direkter Zugriff auf die Elemente x und y möglich. Die Zuweisung u.x = 5 führt zu folgender Fehlermeldung: member `x' is a private member of class `vektor'.
private: nur in Elementfunktionen der Klasse selbst benutzbar.
public: allgemein benutzbar.
Um private Mitglieder einer Klasse initialisieren zu können braucht man einen Konstruktor.

Konstruktoren

Ein Konstruktor ist eine besondere Methode, die den Namen der Klasse trägt und keinen Rückgabewert besitzt (auch kein void).
Der Konstruktor reserviert  den für das zu instanzierende Objekt nötigen Speicherplatz.
Beispiel: vektor(float xx = 0, float yy = 0): x(xx), y(yy){}

Durch den Aufruf des Konstruktors mittels  u(2,4), v(1,2), w(5)  werden die Objekten u, v und w initialisiert. Durch u(2,4) erhält x den Wert 2 und y den Wert 4. x(xx) ist hier gleichbedeutend mit x=xx=2 .
Da durch w(5)  nur ein Parameter für x übergeben wird,  nimmt y den durch yy = 0 definierten defaultwert an. w(5) ist gleichbedeutend mit x=xx=5 und y=yy=0.

Beispiel 10:

#include <iostream.h>

class vektor
{
 public:

 vektor(float xx = 0, float yy = 0): x(xx), y(yy){}        //Konstruktor

 vektor spiegeln() const;
 void print() const;

 private:
 float x,y;
};

vektor vektor::spiegeln() const
 { vektor a;
   a.x = -x;
   a.y = -y;
   return a;
 }

void vektor::print() const
 {
  cout << x << " " << y << endl;
 }

void main()
{
 vektor u(2,4), v(1,2), w(5);
 cout << "Vektor w: "; w.print ();
 cout << "Vektor v: "; v.print ();
 cout << "Vektor u: "; u.print ();
 v = u.spiegeln();
 cout << "Gespiegelter Vektor u: "; v.print ();
}

Output:
Vektor w: 5 0
Vektor v: 1 2
Vektor u: 2 4
Gespiegelter Vektor u: -2 -4
 

Eigene Operatoren definieren

Um die Lesbarkeit von Programmen zu erhöhen und das Programmieren einfacher und übersichtlicher zu machen, definiert man sich eigene Operatoren. In C++ stehen eine Reihe von vordefinierten Operatoren zur Verfügung (+, -, *, /, += ...) wobei man folgendes beachten muß:

Die Operatoren einer Klasse werden innerhalb der Klassendefinition über das Schlüsselwort operator definiert.
Syntax der Operatordefinition:    Returndatentyp    operatorOperator (Argumentenliste) {Funktionscode}
Beispiel:    vektor    operator+(const vektor &a, const vektor &b);

Auf diese Weise kann man die gewohnte Schreibweise für die Addition/Subtraktion zweier Vektoren und die Spiegelung eines Vektors am Ursprung benutzen (Beispiel   ):
Addition zweier Vektoren: u + v = (u1,u2) + (v1,v2)=(u1+v1,u2+v2)
Subtraktion zweier Vektoren: u - v = (u1,u2) - (v1,v2)=(u1-v1,u2-v2)
Spiegelung eines Vektors v am Ursprung: v=-(v1,v2)=(-v1,-v2)

Beispiel 11:

#include <iostream.h>

class vektor
{
 public:

 vektor(float xx = 0, float yy = 0): x(xx), y(yy){}
 friend vektor operator+(const vektor &a, const vektor &b);
 friend vektor operator-(const vektor &a, const vektor &b);
 friend vektor operator-(const vektor &a);
 void print() const;

 private:
 float x,y;
};

vektor operator+(const vektor &a, const vektor &b)  //Addition von Vektoren
{
 return vektor(a.x + b.x, a.y + b.y);
}

vektor operator-(const vektor &a, const vektor &b)  //Subtraktion von Vektoren
{
 return vektor(a.x - b.x, a.y - b.y);
}

vektor operator-(const vektor &a)                                //Vektor spiegeln
{
 return vektor(-a.x, -a.y);
}

void vektor::print() const
 {
  cout << x << " " << y << endl;
 }

void main()
{
 vektor u(2,4), v(1,2), Summe, Differenz, vs;
 cout << "Vektor u: "; u.print ();
 cout << "Vektor v: "; v.print ();
 Summe = u + v;
 cout << "Summenvektor: "; Summe.print();
 Differenz = u - v;
 cout << "Differenzvektor: "; Differenz.print();
 vs = -v;
 cout << "Gespiegelter Vektor: "; vs.print();
}

Output:
Vektor u: 2 4
Vektor v: 1 2
Summenvektor: 3 6
Differenzvektor: 1 2
Gespiegelter Vektor: -1 -2
 

Ein selbstdefinierter Operator ist eine Funktion in einer syntaktisch ansprechenden Verkleidung.
Der Ausdruck  u + v ist eine verkürzte Schreibweise des Funktionsaufrufs operator+(u,v).
Bei der Deklaration der drei Operatorfunktionen innerhalb der Klasse Vektor wurde das Schlüsselwort friend benutzt. Nur dadurch erhielten die Operatorfunktionen Zugriff auf die privaten Klassenelemente x und y.

Hinter Operatoren verbergen sich also letztlich Elementfunktionen. Der Ausdurck
vektor operator+(const vektor &a, const vektor &b) ist somit äquivalent zu
vektor addieren(const vektor &a, const vektor &b).
Man hätte das Programm auch folgendermaßen schreiben können:

Beispiel 12:

#include <iostream.h>

class vektor
{
 public:

 vektor(float xx = 0, float yy = 0): x(xx), y(yy){}
 friend vektor addieren(const vektor &a, const vektor &b);
 friend vektor subtrahieren(const vektor &a, const vektor &b);
 friend vektor spiegeln(const vektor &a);
 vektor spiegeln();
 void print() const;

 private:
 float x,y;
};

vektor addieren(const vektor &a, const vektor &b)                 //Addition von Vektoren
{
 return vektor(a.x + b.x, a.y + b.y);
}

vektor subtrahieren(const vektor &a, const vektor &b)           //Subtraktion von Vektoren
{
 return vektor(a.x - b.x, a.y - b.y);
}

vektor spiegeln(const vektor &a)                                                //Vektor spiegeln
{
 return vektor(-a.x, -a.y);
}

void vektor::print() const
 {
  cout << x << " " << y << endl;
 }

void main()
{
 vektor u(2,4), v(1,2), Summe, Differenz, vs;
 cout << "Vektor u: "; u.print ();
 cout << "Vektor v: "; v.print ();
 Summe = addieren(u,v);
 cout << "Summenvektor: "; Summe.print();
 Differenz = subtrahieren(u,v);
 cout << "Differenzvektor: "; Differenz.print();
 vs = spiegeln(v);
 cout << "Gespiegelter Vektor: "; vs.print();
}

Output:
Vektor u: 2 4
Vektor v: 1 2
Summenvektor: 3 6
Differenzvektor: 1 2
Gespiegelter Vektor: -1 -2
















Last update: 1.Januar 2001
E-Mail: rcbruens@astro.uni-bonn.de