~~SLIDYSHOW~~

Programmierung

Strukturen

Strukturen sind vereinfachte Klassen und fassen somit Eigenschaften (Variablen) zusammen.

Definition

Typdefinition

einfache Def. eines neuen Typs:

struct CDatum {
    int Tag;
    int Monat;
    int Jahr;
}; // Das Semikolon beendet die Definition
 
CDatum variable;

Typdef. und Erstellung einer Variable

struct CDatum {
    int Tag;
    int Monat;
    int Jahr;
} d; // d ist eine Variable des Datentyps CDatum.
 
CDatum variable;

anonyme Variablendefinition

struct { // Dieser Datentyp hat keinen eigenen Namen.
    int Tag; 
    int Monat;
    int Jahr;
} d; // Der Datentyp von d ist anonym

Verschachtelung von Strukturen

struct Kontobewegung {
    int KontoNr;
    char NameInhaber[20];
    struct {
       int Tag;
       int Monat;
       int Jahr;
    } Datum;
    char BewArt;
    double Betrag;
};

Zugriffe

Zugriff ist per '.' Punkt möglich.

test.variable;
Kontobewegung k;
 
// kann man die Elemente der Variablen k folgendermaßen ansprechen: 
k.KontoNr // ein Ausdruck des Datentyps int
k.Datum // ein Ausdruck des Datentyps CDatum
 
// Die drei Elemente von K.Datum lassen sich einzeln ansprechen durch 
k.Datum.Tag // ein Ausdruck des Datentyps int
k.Datum.Monat
k.Datum.Jahr

Definitionen sind auch mit geschwungenen Klammern möglich:

struct CDatum {
    int Tag;
    int Monat;
    int Jahr;
} d;
 
CDatum test = {1, 2, 3};
cout << test.Monat; // 2

Zeiger

Ein Zeiger speichert eine Speicheradresse und verweist so auf einen bestimmten Platz im Speicher. Mit der Dereferenzierung wird auf die unter der gespeicherten Adresse gespeicherten Daten zugegriffen.

Definition

int *p;

Erstellt einen Zeiger auf eine Ganzzahl.

Dereferenzierung

Mit der Dereferenzierung kann auf den Wert der referenzierten Variable zugreifen.

int *p;
*p = 17;

von Strukturen und Klassen

Mit dem → Operator

struct T {
    int a, b;
}
T *var;
(*var).a = 1;
var->b = 2;

Referenzierung

Gibt die Adresse einer regulären Variable zurück.

int i=17; double d=18; char c='A';
 
&i // Zeiger auf int
&d // Zeiger auf double
&c // Zeiger auf char

Beispiel

int* pi; double* pd;
pi = &i; // pi wird die Adresse von i zugewiesen
pd = &d; // pd wird die Adresse von d zugewiesen
char* pc=&c;// initialisiere pc mit &c

generische Zeiger

Der Datentyp void* wird als generischer Zeigertyp bezeichnet. Einem generischen Zeiger kann ein Zeiger auf einen beliebigen Zeigertyp zugewiesen werden.

Ein generischer Zeiger zeigt aber auf keinen bestimmten Datentyp. Deshalb ist es nicht möglich, einen generischen Zeiger ohne explizite Typkonversion zu dereferenzieren. Mit einer expliziten Typkonversion kann er aber in einen beliebigen Zeigertyp konvertiert werden.

int* pi;
void* pv;
 
// ist die erste und die dritte der folgenden Zuweisungen möglich: 
pv = pi;
pi = pv; // Fehler: Konvertierung nicht möglich
pi = (int*)pv; // explizite Typkonversion
 
// Die Dereferenzierung eines generischen Zeigers ist ohne explizite Typkonversion nicht möglich: 
*pi = *pv; // Fehler: Kein zulässiger Typ
int i = *((int*)pv); // das geht

dynamische Erzeugung

Für einen Datentyp T reserviert „new T“ so viele Bytes auf dem Heap, wie für eine Variable des Datentyps T notwendig sind. Der Ausdruck „new T“ hat den Datentyp „Zeiger auf T“. Sein Wert ist die Adresse des reservierten Speicherbereichs und kann einer Variablen des Typs „Zeiger auf T“ zugewiesen werden:

int* pi;
pi=new int; //reserviert sizeof(int) Bytes und weist pi die Adresse dieses Speicherbereichs zu
*pi=17;     // initialisiert den reservierten Speicherbereich
 
// Am besten initialisiert man eine Zeigervariable immer gleich bei ihrer Definition: 
int* pi=new int (3445234); // initialisiert pi mit der Adresse
*pi=17;

und Löschung

Zur Vermeidung von Speicherlecks ist eine explizite Löschung empfohlen.

delete pi;

Listen

Listen sind Strukturen, die einen Wert speichern und auf ihrem Nachfolger verweisen.

struct CWaggon {
    AnsiString data;      // die Nutzdaten
    CWaggon *next;        // Zeiger auf den nächsten Knoten
};

Ein solcher Datentyp wird auch als rekursiver Datentyp bezeichnet.

bck-292.jpg

Im Programm muss nur ein Zeiger gespeichert werden, der auf den ersten Knoten der Liste verweist:

CWaggon *first; // Zeiger auf den ersten Knoten

Den letzten Knoten der Liste kennzeichnet man durch einen Zeiger next mit dem Wert 0. Grafisch wird dieser Wert oft durch einen schwarzen Punkt dargestellt. Dann hat die verkettete Liste einen eindeutigen Anfang und ein eindeutiges Ende:

bck-293-1.jpg

Einhängen eines neuen Knotens

Als nächstes sollen Anweisungen gesucht werden, mit denen man vor einem Knoten, auf den ein Zeiger n0 zeigt,

bck-293-2.jpg

einen neuen Knoten einfügen kann, auf den n0 dann zeigt:

bck-293-3.jpg

Das ist mit den Anweisungen unter 1. und 2. möglich:

1. Ein Zeiger tmp soll auf einen neuen Knoten zeigen, der mit new erzeugt wird

CWaggon *tmp = new CWaggon; // 1.1

und dem die Daten durch eine Anweisung wie

tmp->data = d2; // 1.2

zugewiesen werden. Dadurch ergibt sich:

bck-293-4.jpg

2. Der neue Knoten *tmp wird dann mit den beiden Anweisungen

tmp->next = n0; // 2.1
n0 = tmp;       // 2.2

in die Liste eingehängt:

bck-294-1.jpg

Diese Anweisungen werden mit der Funktion

CWaggon *newWaggon(T &data, CWaggon *next) // gibt Zeiger auf neuen Listenknoten{d0,nx0} zurück,  
{                                         // wobei d0 und nx0 die Argumente für data und next sind.
 CWaggon *n = new CWaggon;                // 1.1
 n->data = data;                          // 1.2
 n->next = next;                          // 2.2
 return n;                                // Nachbedingung: Der Rückgabewert zeigt auf 
}                                         // einen neuen Knoten {d0,nx0}

durch den Aufruf

 n0 = newWaggon(d2,n0);                   // 2.1

ausgeführt. n0 zeigt danach auf einen neu erzeugten Knoten, dessen Element next auf den Knoten zeigt, auf den das Argument für next zeigt. Falls das Argument für next den Wert 0 hat, zeigt der Funktionswert auf einen Knoten mit next==0.

Das Ergebnis der ersten drei Ausführungen der Anweisung

 first = newWaggon(di,first);

mit den Daten d1, d2 usw., wobei der Wert von first zuerst 0 sein soll, ist in dem folgenden Ablaufprotokoll dargestellt. Dabei sind die Zeiger auf die von newWaggon erzeugten Knoten mit n1, n2 usw. bezeichnet, und ein Knoten, auf den ein solcher Zeiger zeigt, mit →{di,nj}. Der Ausdruck →{d2,n1} in der Spalte n2 ist also nichts anderes als eine Kurzschreibweise für

bck-294-2.jpg

Die Werte in den Spalten n1, n2 usw. erhält man einfach durch Einsetzen der Argumente in die Nachbedingung von newWaggon:

                                   // first         n1          n2        n3
first = 0;                               0
first = newWaggon(d1,first);             n1        ->{d1,0}
first = newWaggon(d2,first);             n2                    ->{d2,n1}
first = newWaggon(d3,first);             n3                              ->{d3,n2}

Dieses Ablaufprotokoll illustriert, wie der erste dieser Aufrufe einen ersten Knoten mit next==0 erzeugt, auf den first dann zeigt, und wie jeder weitere Aufruf einen neuen Knoten am Anfang in die verkettete Liste einhängt, auf die first zeigt.

Die Funktionsweise von newWaggon beruht insbesondere darauf, dass eine mit new erzeugte Variable bis zum Aufruf von delete existiert. Im Unterschied zu einer gewöhnlichen Variablen wird ihr Speicherplatz nicht mit dem Verlassen des Blocks wieder freigegeben, in dem sie erzeugt wurde.

– Deshalb existiert die Variable *tmp, die mit CWaggon *tmp=new CWaggon; erzeugt wurde, auch noch nach dem Verlassen der Funktion newWaggon.

– Der Zeiger tmp ist dagegen eine „gewöhnliche“ lokale Variable, deren Speicherplatz mit dem Verlassen des Blocks wieder freigegeben wird. Da der Wert von tmp dem Element next des Funktionswerts zugewiesen wird, kann man die lokal erzeugte Variable *tmp über den Funktionswert auch außerhalb der Funktion verwenden, in der sie erzeugt wurde.

Durchlaufen

Um die Liste zu Durchlaufen, kann man sich mit einem Hilfszeiger durch die Liste bis zu Ende durchhangeln:

CWaggon *tmp=start;
while (tmp != 0) {
    Form1->Memo1->Lines->Add(tmp->data);
    tmp = tmp->next;
}

dopplete Verkettung

Dabei wird nicht nur auf das folgende, sonder auch auf das vorherige Element verwiesen.

struct CWaggon {
    AnsiString data;
    CWaggon *next;
    CWaggon *previous;
};
 
CWaggon *first=0, *last=0;

Einhängen

vorne
CWaggon *tmp = new CWaggon;
tmp->data = Edit1->Text;
tmp->next = first;
tmp->previous = 0;
if (tmp->next!=0)
    tmp->next->previous = tmp;
else last = tmp;
first = tmp;
Edit1->Text  = "";
hinten
CWaggon *tmp = new CWaggon;
tmp->data = Edit2->Text;
tmp->previous = last;
tmp->next = 0;
if (tmp->previous!=0)
     tmp->previous->next = tmp;
else first = tmp;
last = tmp;
Edit2->Text = "";

Bäume

Baumstrukturen werden aus Knoten aufgebaut, die einen Zeiger auf einen linken und rechten Teilbaum enthalten:

Binärbäume

Ein Baumknoten kann durch den Datentyp CKnoten dargestellt werden:

struct CKnoten {
  int data;       // die Nutzdaten sind in diesem Beispiel nur Zahlen
  CKnoten* left;
  CKnoten* right;
};

Der Zeiger auf den obersten Knoten des Baums wird oft als root bezeichnet:

CKnoten *root=0;

Einhängen eines neuen Knotens

void einhaengen(CKnoten *&parent, CKnoten *neu) {
    if (parent==0) {
        parent=neu;
    } else {
        if (neu->data<=parent->data) {
            einhaengen(parent->left,neu);
        } else {
            einhaengen(parent->right,neu);
        }
    }
}
 
void __fastcall TForm3::Button1Click(TObject *Sender) {
    CKnoten *neu = new CKnoten;
    if (Edit1->Text != "") { 
        neu->data = StrToInt(Edit1->Text);
        neu->left = 0;
        neu->right = 0;
        Edit1->Text = "";
        einhaengen(root,neu);
    }
    Edit1->Focused();
}

Löschen eines Knotens

Knoten h wird gelöscht. Es werden 3 Fälle unterschieden:

In den ersten beiden Fällen wird einfach der Teilbaum nach oben gerückt, im 3 Fall muss der Wert der Wurzel verglichen werden.

if (h!=0) {
    if (StrToInt(Form1->Edit3->Text) <= h->data)
        loeschen(h->left);
    else if (StrToInt(Form1->Edit3->Text) > h->data)
        loeschen(h->right);
    if (StrToInt(Form1->Edit3->Text) == h->data) {
        if (h->left == 0 && h->right == 0) {   //Blatt
            delete(h);
            h = 0;
        } else if (h->left != 0 && h->right == 0) {    //Halbblatt  links
            // CKnoten *h1 = h;
            // h wird h->left;
            delete(h);   //delete(h1) if not works
            h = h->left;   //to delete if not works
        } else if (h->left == 0 && h->right != 0) {    //Halbblatt rechts
            // CKnoten *h1 = h;
            // h wird h->right;
            delete(h);   //delete(h1) if not works
            h = h->right;   //to delete if not works
        } else if (h->left != 0 && h->right != 0) {   //2 Kinder
            CKnoten *h1 = h;     //Hilfsknoten
            CKnoten *h2 = h->left;
            h = h->right;              //größerer Teil rückt nach oben
            delete(h1);
            h1 = h;
            while (h1->left != 0)
                h1 = h1->left;  //weiterwandern, immer links bis 0
            h1->left = h2;
        }
    }
}

Objektorientierung

Klassen funktionieren ähnlich wie Strukturen, bieten aber mehr Möglichkeiten. Sie bestehen aus 2 Arten von Elementen:

Deklaration

Funktionen können innerhalb als auch außerhalb deklariert werden:

class Punkt {
    double x, y;
    void Init(double x_, double y_) {
        x = x_;
        y = y_;
    }
    AnsiString toStr() {
        return "(" + FloatToStr(x) + "|" + FloatToStr(y) + ")";
    } // z.B. (2,35|3,46)
    void show() {
        Form1->Memo1->Lines->Add(toStr());
    }
};

Eine Deklaration von außen ist mit :: möglich:

void C2DPunkt::Init(double x_, double y_) {
    x = x_;
    y = y_;
}
 
AnsiString C2DPunkt::toStr() {
    return "(" + FloatToStr(x) + "|" + FloatToStr(y) + ")";
} // z.B. (2,35|3,46)
 
void C2DPunkt::show() {
    Form1->Memo1->Lines->Add(toStr());
}

Datenkapselung

Drei verschiedene Zugriffsrechte

Mit den Zugriffsrechten private, protected und public kann man für jedes Klassenelement explizit festlegen, ob man über ein Objekt darauf zugreifen kann.

Diese Spezifizierer definieren ab ihrer Angabe einen Abschnitt mit Zugriffsrechten, die für alle folgenden Elemente bis zum nächsten solchen Spezifizierer oder bis zum Ende der Klasse gelten. Ein Element aus einem private, protected oder public Abschnitt heißt auch private, protected oder public Element.

Ohne die Angabe eines Zugriffsrechts sind alle Elemente einer mit class definierten Klasse private, während alle Elemente einer mit struct definierten Klasse public sind. Dieses voreingestellte Zugriffsrechte ist der einzige Unterschied zwischen einer mit class und einer mit struct definierten Klasse.

Alle Elemente der Klassen C0 und C1 haben dieselben Zugriffsrechte:

class C0 {
    int x;
    int f() { return x; }
};
 
struct C1 {
  private:
    int x;
    int f() { return x; }
};

Jeder Zugriff auf diese Elemente über ein Objekt führt zu einer Fehlermeldung:

void test(C0 a, C1 b) {
    a.x=1;//Fehler: Zugriff auf 'C0::x' nicht möglich
    a.f();//Fehler: Zugriff auf 'C0::f()' nicht mögl.
    b.x=1;//Fehler: Zugriff auf 'C1::x' nicht möglich
    b.f();//Fehler: Zugriff auf 'C1::f()' nicht mögl.
}

Mit dem Zugriffsrecht public sind alle diese Zugriffe zulässig:

class C0 {
  public:
    int x;
    int f() { return x; }
};
 
struct C1 {
    int x;
    int f() { return x; }
};

Eine Klasse kann eine beliebige Anzahl von Abschnitten mit verschiedenen Zugriffsrechten in einer beliebigen Reihenfolge enthalten. Die Reihenfolge der Abschnitte ist dabei ohne Bedeutung.

Die beiden Klassen C1 und C2 sind gleichwertig. Oft werden die verschiedenen Abschnitte wie bei C1 in der Reihenfolge private, protected und public aufgeführt. Wenn man sie aber wie bei C2 in der umgekehrten Reihenfolge anordnet, kommen die Elemente, die für das breiteste Publikum interessant sind, am Anfang:

class C1 {
    int x; 
  public:
    int f(C p);
};
 
class C2 {
  public: 
    int f(C p);
  private:
    int x;
};

Konstruktoren

Ein Konstruktor ist eine Methode der Klasse, die denselben Namen wie die Klasse hat. Der Konstruktor hat keinen Rückgabetyp. Eine Klasse kann mehrere Konstruktoren haben, welche sich überladenen, also durch ihre Parameter unterscheiden. Die Parameter können auch Default-Argumente haben.

Ein Konstruktor wird in den folgenden Situationen automatisch aufgerufen:

class C2DPunkt {
    double x, y;
  public:
    C2DPunkt (double x_) { // ein Parameter: x-Wert
        x = x_;
        y = 0;
    }
    C2DPunkt (double x_, double y_) {
        x=x_;
        y=y_;
    }
};

Damit können folgendermaßen Objekte dieser Klasse definiert werden:

C2DPunkt p(3); // p=(3,0)
C2DPunkt q(5,6); // q=(5,6)

Dagegen wird diese Definition vom Compiler nicht akzeptiert:

C2DPunkt p; //Fehler: Keine Übereinstimmung für 'C2DPunkt::C2DPunkt()' gefunden

Die beiden Konstruktoren von C2DPunkt können auch durch einen einzigen mit einem Default-Argument ersetzt werden:

class C2DPunkt{
    double x,y;
  public:
    C2DPunkt(double x_, double y_ = 0) {
        x = x_;
        y = y_;
    }
};