Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

9 Variablen III

9 Variablen III

In diesem Kapitel geht es um s.g. abstrakte Datentypen und Sie sollten sich wirklich Zeit nehmen für diesen Abschnitt, da die ganze Thematik nicht ganz einfach ist. Ich hatte Ihnen bereits erzählt, dass eine Variable aus drei Teilen besteht, nämlich ihrer Speicheradresse, ihrem Namen und ihrem Wert.

Darstellung einer Variable

Genau dieses Konzept müssen Sie sich in diesem Kapitel immer genau vor Augen führen. Jede Wertangabe mit einem "0x" deutet auf eine hexadezimale Speicheradresse hin, wobei diese Werte in der Realität immer ein Vielfaches von vier bzw. acht und je nach System entweder 32 oder 64 Bit groß sind. Der Einfachheit halber, habe ich mich aber auf besser übersichtlichere Werte beschränkt, welche nicht mit "0x" beginnen, kein vielfaches sind und noch dazu kleiner. Allerdings benutze ich in den folgenden Grafiken immer Adressen mit Buchstaben, damit man schneller sieht, dass es sich hierbei nicht um einen normalen Wert handelt.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.6 Übungsaufgaben VII

9.6 Übungsaufgaben VII

  1. Erstellen Sie eine Funktion "Create", welche keine Übergabeparameter besitzt und ein Zeiger auf ein zweidimensionales Array mit Zeigern auf Integer zurück gibt. Speichern Sie die Dimensionen global ab.
  2. Die Funktion "Create" soll den Benutzer auffordert, zwei positive Zahlen einzugeben. Anschließend soll damit ein dynamisches zweidimensionales Array erzeugen und zurückgeben werden.
  3. Erstellen Sie eine Funktion "Initialize", welche einen Zeiger auf das Array übergeben bekommt, welches die Funktion "Create" erzeugt hat.
  4. Die Funktion "Initialize" soll das Array befüllen, wobei der Wert das Produkt aus Zeilen und Spaltenwert sein soll.
  5. Erstellen Sie eine Funktion "GetInnerArray", welches anhand eines Indexwertes, ein Pointer auf ein Unterarray des zweidimensionalen Arrays zurückgibt. Überlegen Sie selbst, welche Parameter sie benötigen und was sinnvoll wäre, wenn ein ungültiger Index angegeben wurde.
  6. Erstellen Sie eine Funktion "OutputInnerArray", welches ein übergebenes Unterarray ausgibt.
  7. Erstellen Sie eine Funktion "Finalize", welche das zweidimensionale Array sauber wieder freigibt.
  8. Erstellen Sie eine "main" Funktion, welche die erstellten Funktionen aufruft und implementieren Sie ggf. zusätzliche Ein- bzw. Ausgaben.
  9. Optimieren Sie Ihren Quelltext so, dass Sie zum einen gezielte Typdefinitionen zur besseren Verständlichkeit einsetzen und zum anderen Referenzen als Übergabeparameter einsetzen. Versuchen Sie auch wenn möglich das Schlüsselwort "const" bei Funktionsargumenten zu verwenden, bei denen es sich anbietet.
Zum Seitenanfang
Zum Inhaltsverzeichnis

9.3 Dynamische Felder

9.3 Dynamische Felder

Mit den bisherigen Mitteln war es nur möglich, zur Entwicklungszeit die Größe von Feldern statisch festzusetzen. Nun möchte ich Ihnen zeigen, wie ein solcher Datentyp dynamisch, zur Laufzeit verwaltet werden kann. Dynamische Felder lassen sich nicht in normalen Variablen speichern. Hierfür benötigt man Zeiger auf einen beliebigen Datentyp, nur dass man noch eine Anzahl mit angibt. Intern werden dann entsprechende Variablen im Speicher, unmittelbar nebeneinander, reserviert. Das Anlegen eines Zeigers auf einen Integer, unterscheidet sich also nicht von einem dynamischen Array mit einem einzigen Integer.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.3.2 Die Funktion realloc()

9.3.2 Die Funktion realloc()

Mit dieser Funktion ist es möglich, den Speicherbedarf einer Variable oder besser gesagt den Bereich, worauf ein Pointer zeigt, zu verändern. Dabei wird aber im seltensten Fall der Bereich einfach größer gemacht. Meistens wird ein ganz neuer Bereich im RAM reserviert, der alte Inhalt dorthin verschoben und der alte Bereich freigegeben. Das scheint wie eben bereits erwähnt, ziemlich Zeitaufwendig zu sein, aber ganz so schlimm ist dies auch nicht, da intern viel bessere Mechanismen benutzt werden, als ,am das von Hand hinbekommen würde.

Das Schema sieht so aus:
<Zeigervariable> = (<VariablenTypeDesZeigers>)realloc(<Zeigervariable>, <Größe>)

Im folgenden Beispiel sehen Sie, wie ich zunächst ein Array anlege und mich später dafür entscheide, es doch größer haben zu wollen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
					
// Ein Array mit Platz für drei Integer
int* pArray	= (int*)malloc(3 * sizeof(int));
pArray[0]	= 10;
pArray[1]	= 20;
pArray[2]	= 30;

// Array vergrößern, da noch ein vierter Integer rein muss
pArray		= (int*)realloc(pArray, 4 * sizeof(int));
pArray[3]	= 40;

printf("%i %i %i %i\n", pArray[0], pArray[1], pArray[2], pArray[3]);

free(pArray);
					

Ausgabe:

4
8.7
		
Zum Seitenanfang
Zum Inhaltsverzeichnis

9.3.1 Unveränderliche dynamische Felder

9.3.1 Unveränderliche dynamische Felder

Als erstes möchte ich Ihnen zeigen, wie man schnell und einfach dynamische Arrays mit "new" anlegt, mit ihnen arbeitet und anschließend mit "delete" wieder freigibt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
					
int iAnzahl;

printf("Bitte geben Sie an, wie viele Floats im Array sein sollen: ");
scanf("%i", &iAnzahl);
printf("\n");

// Wenn ein ungültiger Bereich angegeben wurde
if (iAnzahl < 1) {
	iAnzahl			= 1;
} // end of if

// Pointer auf ein dynamisches Array mit Floats anlegen
float* pArrayOfFloats		= new float[iAnzahl];

// Durchlaufe die Elemente im Array
for (int iCount=0; iCount<iAnzahl; iCount++) {
	float fEingabe;

	printf("Bitte ein float eingeben: ");
	scanf("%f", &fEingabe);

	pArrayOfFloats[iCount]	= fEingabe;
} // end of for

printf("\n");

// Durchlaufe die Elemente im Array
for (int iCount=0; iCount<iAnzahl; iCount++) {
	printf("[%i] = %g\n", iCount, pArrayOfFloats[iCount]);
} // end of for

// Reservierten Speicher wieder freigeben
delete [] pArrayOfFloats;
					

Ausgabe:

Bitte geben Sie an, wie viele Floats im Array sein sollen: 3

Bitte ein float eingeben: 1.2
Bitte ein float eingeben: 2.3
Bitte ein float eingeben: 3.4

[0] = 1.2
[1] = 2.3
[2] = 3.4
		

Allerdings ergibt sich jetzt hier bereits eines der erwähnten Nachteile von "new" und "delete". Man kann im Nachhinein keine weiteren Elemente hinzufügen bzw. entfernen. Um dies zu realisieren, müsste man den Inhalt des gesamten Feldes irgendwo zwischenspeichern, dass original mit "delete" freigeben und mit "new" und einer neuen Größenangabe neu anlegen. Dieser Mechanismus ist nicht nur umständlich, sondern auch Zeitaufwändig.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.5 Vereinfachter Umgang mit Pointern

9.5 Vereinfachter Umgang mit Pointern

Wie Sie gesehen haben, ist der Umgang mit Pointern wirklich etwas gewöhnungsbedürftig, zumal man ständig mit diesen Sternen arbeiten muss und man immer überlegen muss, wie viele man braucht. In diesem Teilkapitel soll es darum gehen, wie man sich das alles vereinfachen kann.

Folgende Tabelle soll noch einmal zeigen, wie man sich merken kann, wann man wie viele Sterne braucht.

Deklarierter Datentyp Dereferenzierter Datentyp Resultierender Datentyp
Typ*** pppVariable *pppVariable Typ** ppVariable
Typ** ppVariable *ppVariable Typ* pVariable
Typ* pVariable *pVariable Typ tVariable
Typ*** pppVariable **pppVariable Typ* pVariable
Typ** ppVariable **ppVariable Typ tVariable
Typ*** pppVariable ***pppVariable Typ tVariable

Als Faustregel gilt also, mit jedem Stern vor einem Variablenname, resultiert der Datentyp mit einem Stern weniger.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.5.2 Vereinfachter Funktionsaufruf

9.5.2 Vereinfachter Funktionsaufruf

Ich habe bisher mehrfach erwähnt, dass bei einem Funktionsaufruf mehrere Sachen im Hintergrund passieren und auch was dies für den Speicherbedarf bedeutet. Gerade wenn man viele Funktionsparameter benötigt, wird der Stack schnell sehr voll. Hier kommen Pointer ins Spiel. Schauen Sie sich folgenden Quelltext an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
					
// Zeichnet ein Rechteck //////////////////////////////////////////////////////
void Draw(int iWidth, int iHeight, int iPosLeft, int iPosTop) {
	//...
} // Draw /////////////////////////////////////////////////////////////////////

//...

int iWidth		= 200;
int iHeight		= 10;
int iPosLeft		= 10;
int iPosTop;

// 10 Rechtecke erzeugen
for (int iCount = 0; iCount < 10; ++iCount) {
	iPosTop		= (iCount + 1) * 20;
	Draw(iWidth, iHeight, iPosLeft, iPosTop);
} // end of for
					

Im Grunde sollen hier nur 10 Rechtecke erzeugt und gezeichnet werden. Wie das eigentliche Zeichnen aussieht, spielt hier keine Rolle, aber schauen wir doch mal an, was auf dem Stack passiert.

Funktionsaufruf mit Primitivtypen

Wie Sie sich bestimmt denken konnten, werden also ständig viele Werte auf den Stack gepackt und wieder abgeräumt, nur um sie dann wieder darauf zu packen und wieder abzur/auml;umen. Hier ist es also sinnvoller, sich für viele Parameter eine Struktur zu bauen und anschließend nur noch einen Pointer darauf zu übergeben.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
					
struct SRectangle {
	int iWidth;
	int iHeight;
	int iPosLeft;
	int iPosTop;
};

typedef SRectangle* PRectangle;



// Zeichnet ein Rechteck //////////////////////////////////////////////////////
void Draw(PRectangle& pArea) {
	//...
} // Draw /////////////////////////////////////////////////////////////////////

// ...

PRectangle	pRect	= new SRectangle;
pRect->iWidth	= 200;
pRect->iHeight	= 10;
pRect->iPosLeft	= 10;

// 10 Rechtecke erzeugen
for (int iCount = 0; iCount < 10; ++iCount) {
	pRect->iPosTop	= (iCount + 1) * 20;
	Draw(pRect);
} // end of for

delete pRect;
					

Schauen wir uns zunächst auch hier an, was im Speicher passiert.

Funktionsaufruf mit referenziertem Pointer auf Struktur

Wenn Sie sich das jetzt anschauen, sehen Sie, dass wieder ständig etwas auf den Stack gepackt wird. Allerdings ist es jetzt nur noch die Rücksprungadresse. Der Pointer selbst benötigt keinen zusätzlichen Platz, da wir ihn ja als Referenz übergeben und die eigentlichen Nutzdaten liegen im Heap. Somit wird der Funktionsaufruf wesentlich schneller.

Wenn Sie jetzt noch ein wenig weiter denken, fragen Sie sich bestimmt, warum man nicht einfach die ganzen Nutzdaten als Referenz übergibt? Das hätte doch den gleichen Effekt? Nun, bei erster Betrachtung stimmt dies, aber mit diesem Lösungsweg haben wir wesentlich mehr Vorteile.

Was passiert, wenn wir irgendwann noch die Rahmen- und Hintergrundfarbe mit übergeben wollten oder evtl. noch die Art des Stiftes, mit welchem das Rechteck gezeichnet werden soll? Der Funktionskopf würde sich nicht nur weiter Aufblähen und noch unübersichtlicher werden, nein, man müsste ggf. noch den Funktionsprototypen ändern und noch jede Stelle im Code, an welcher diese Funktion aufgerufen wird (wenn man nicht gerade Standardparameter festgelegt hat). Mit der Vorgeschlagenen Lösung ist man da viel flexibler, da man lediglich die Struktur und das Erzeugen erweitern müsste.

Eine Frage bleibt aber noch offen. Wann übergibt man Pointer und wann Referenzen? Nun das ist eigentlich ganz einfach zu beantworten. Bedenken Sie, dass Pointer auch auf nichts sinnvolles zeigen können. Wenn Sie also einer Funktion einen puren Pointer übergeben, müssen Sie immer damit rechnen, dass "NULL" übergeben wird. Dem zur Folge müssen Sie in der Funktion erst prüfen, ob es sich um einen gültigen Pointer handelt. Wenn Sie allerdings eine Referenz übergeben, schließen Sie diesen Fall aus und benötigen in der Funktion keine extra Abfrage. Sie müssen also immer überlegen, ob es tatsächlich gewollt passieren kann (beispielsweise, wenn Sie den Inhalt noch erzeugen wollen, wenn "NULL" übergeben wird), dass ein ungültiger Zeiger übergeben wird oder nicht. Wenn dies Ihrer Meinung nach nie passieren kann/darf, benutzen Sie Referenzen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.5.1 Vereinfachung durch Typdefinitionen

9.5.1 Vereinfachung durch Typdefinitionen

In C hat man die Möglichkeit, sich neue Typen zu erzeugen bzw. für Bestehende einen Aliasnamen zu vergeben. Damit erspart man sich eine komplizierte Schreibweise

Das Schema sie so aus: typedef <ZuErsetzenderTyp> <ZuErsetzenderTyp>;

Angenommen man hat ein Array mit Pointern auf Integer, dann benötigt man folgende Schreibweise.

 1
 2
 3
					
int*	aArray[];
// oder
int**	aArray;
					

Gerade letztere kann Verwirrung stiften und deswegen gibt es von mir an dieser Stelle die Empfehlung, dies durch geeignete Typdefinitionen zu verschönern.

 1
 2
					
typedef	int*		PInteger;	// Pointer auf ein Integer
typedef	PInteger*	AIntArray;	// Array mit Pointern auf Integer
					

Wichtig ist, dass bei einer Typdefinition keine eckigen Klammern benutzt werden können, aber was bringt einem das jetzt genau? Schauen Sie sich dafür folgende zwei Funktionen und deren Aufruf an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
					
// Variante 1 /////////////////////////////////////////////////////////////////
int** CreateArray1(int iLength) {
	int** pResult		= new int*[iLength];

	// Alle Integer erzeugen
	for (int iCount = 0; iCount < iLength; ++iCount) {
		pResult[iCount]	= new int;
	} // end of for

	return pResult;
} // CreateArray1 ///////////////////////////////////////////////////////////////



// Variante 2 /////////////////////////////////////////////////////////////////
AIntArray CreateArray2(int iLength) {
	AIntArray pResult	= new PInteger[iLength];

	// Alle Integer erzeugen
	for (int iCount = 0; iCount < iLength; ++iCount) {
		pResult[iCount]	= new int;
	} // end of for

	return pResult;
} // CreateArray2 ///////////////////////////////////////////////////////////////



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main(int argc, char** argv) {
	int**		pArray1	= CreateArray1(10);	// Variante 1
	AIntArray	pArray2	= CreateArray2(10);	// Variante 2

	delete [] pArray1;
	delete [] pArray2;
	
	return 0;
} // main ///////////////////////////////////////////////////////////////////////
					

Wie Sie sehen können, ist die zweite Variante wesentlich besser lesbar und es wurde kein Stern mehr benutzt. Zudem kann es keine Verwirrung mehr darüber geben, ob man ein Array mit Pointern oder ein Pointer auf einen Pointer hat. Beim Kompilieren wandelt der Präprozessor alle Typdefinitionen um und es resultiert der gleiche Code. Es passiert bei beiden Varianten also haargenau das gleiche. Setzen Sie also ruhig des Öfteren Typdefinitionen ein, um sich den Umgang zu erleichtern. Achten Sie aber darauf, dass die von Ihnen vergebenen Aliasnamen aussagekräftig sind.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.2 Komplexe Zeigerstrukturen

9.2 Komplexe Zeigerstrukturen

Achtung! Jetzt lege ich noch einen drauf und ich empfehle Ihnen, extrem gut hin zuschauen, damit Sie zum einen versteht was passiert und zum anderen auch mitbekommt, warum etwas so läuft und nicht anders. Falls Sie etwas nicht auf Anhieb verstehen, dann sollten Sie das letzte Unterkapitel noch einmal lesen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.2.2 Die Funktionen malloc(), calloc() und free()

9.2.2 Die Funktionen malloc(), calloc() und free()

Wenn man Zeiger auf Werte erstellen möchte, ohne dass man vorher eine normale Variable definiert hat und dann auf jene verweist, muss man sich zunächst Speicher im RAM reservieren und zum Ende wieder freigeben. Allerdings ist diese eigenständige und dynamische Speicherverwaltung etwas Trickreich und man muss gut aufpassen, dass man alles wieder freigibt, was man reserviert hat, da sonst entsprechender Speicher nicht mehr benutzt werden kann, solange das Programm läuft. Wenn das Programm schließlich beendet wird, sorgt der s.g. Garbage Collector dafür, dass auch noch der Rest freigegeben wird, welchen man vergessen hat. An dieser Stelle sei aber erwähnt, dass dies erst seit Windows 2000 so ist. Dies ist u.a. ein Grund dafür, warum Windows 98 nie länger als 78 Stunden am Stück lief, da es irgendwelche Programme gab, die s.g. Speicherlecks verursachten. Irgendwann war der Speicher voll und der PC somit handlungsunfähig (andere Faktoren spielten allerdings auch eine Rolle).

Mit "malloc" kann man sich Speicher reservieren, wobei man angeben muss, wie viel Speicher man haben möchte. Zudem muss ggf. das Ergebnis von "malloc" gecastet werden, da manche Compiler oben erklärten "void" Pointer zurückgeben. Falls beim Reservieren des Speichers etwas schief läuft, gibt die Funktion "NULL" zurück.

Das Schema sieht so aus: <Zeigervariable> = (<VariablenTypeDesZeigers>)malloc(<Größe>)

 1
 2
 3
 4
 5
 6
 9
10
11
12
13
14
15
16
					
// Zeiger auf einen float vorbereiten
float* pZeigerAufFloat;

// Speicher für eine Float-Variable reservieren
pZeigerAufFloat	= (float*)malloc(sizeof(float));

// Wenn das Reservieren des Speichers funktioniert hat (ganz paranoid)
if (pZeigerAufFloat != NULL) {
	// Reservierung hat funktioniert
} else {
	// Reservierung ist fehlgeschlagen, weil z.B. der RAM voll ist
} // end of if

// ...
					

Alternativ zur Funktion "malloc" kann man auch "calloc" benutzen, wobei sich ein paar kleine Vorteile ergeben. Die Funktion calloc benötigt zwar eine zusätzliche Variable, welche die Anzahl der zu reservierenden Elemente angibt, aber (gerade bei Arrays interessant) zudem werden alle Elemente mit dem Wert 0 initialisiert.

Das Schema sieht so aus:
<Zeigervariable> = (<VariablenTypeDesZeigers>)calloc(<AnzahlDerFelder>, <Größe>)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
					
// Zeiger auf einen float vorbereiten
float* pZeigerAufFloat;

// Speicher für eine Float-Variable reservieren
pZeigerAufFloat	= (float*)calloc(1, sizeof(float));

// Wenn das Reservieren des Speichers funktioniert hat (ganz paranoid)
if (pZeigerAufFloat != NULL) {
	// Reservierung hat funktioniert
	// der Wert, worauf pZeigerAufFloat zeigt, ist 0
} else {
	// Reservierung ist fehlgeschlagen, weil z.B. der RAM voll ist
} // end of if

// ...
					

Wie bereits erwähnt, muss man reservierten Speicher auf wieder freigeben und dafür gibt es die Funktion "free". Sie übergeben einfach den Zeiger des Elementes, dessen Speicher gelöscht werden soll.

Das Schema sieht so aus: free(<Zeigervariable>)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
					
// Zeiger auf einen float vorbereiten
float* pZeigerAufFloat;

// Speicher für eine Float-Variable reservieren
pZeigerAufFloat	= (float*)calloc(1, sizeof(float));

// Nur wenn der Speicher auch reserviert wurde
if (pZeigerAufFloat != NULL) {
	free(pZeigerAufFloat)
} // end of if
					
Zum Seitenanfang
Zum Inhaltsverzeichnis

9.2.5 Das komplexe Beispiel

9.2.5 Das komplexe Beispiel

Normalerweise beginnt man mit der main Funktion, aber die spielt in diesem Beispiel eine untergeordnete Rolle, von daher werde ich den Quelltext von oben nach unten erklären. Damit Sie wissen, um was es geht, habe ich eine Grafik erstellt, die zum einen zeigt wie es im Arbeitsspeicher aussehen könnte und zum anderen, wie die Zusammenhänge sind.

Veranschaulichung des folgenden Beispieles

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
					
#include <stdio.h>

struct SPunkt3D {
	int iX;			// X - Koordinate
	int iY;			// Y - Koordinate
	int iZ;			// Z - Koordinate
};

struct SDreieck3D {
	SPunkt3D* pPunktA;	// Zeigt auf einen SPunkt3D
	SPunkt3D* pPunktB;	// Zeigt auf einen SPunkt3D
	SPunkt3D* pPunktC;	// Zeigt auf einen SPunkt3D
};
					

Ich denke mal, dass bis hierher noch alles klar sein sollte was passiert. Wenn nicht, dann schlage ich vor, noch einmal Kapitel 8.4 und 9.1 zu lesen.

 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
					
// Kopiert eine SDreieck3D mit all seinen SPunkt3D //////////////////////////(())
// und deren Koordinaten in eine komplett neues SDreieck3D
SDreieck3D* Dreieck3DDeepCopy(SDreieck3D* pSource) {
	// Neues "leeres" Dreieck erzeugen
	SDreieck3D*	pTarget	= new SDreieck3D;



	// "leere" Punkte für das Dreieck erzeugen ///////////////////////////

	// Variante 1
	pTarget->pPunktA	= new SPunkt3D;

	// Variante 2
	(*pTarget).pPunktB	= new SPunkt3D;
	(*pTarget).pPunktC	= new SPunkt3D;



	// Werte Kopieren /////////////////////////////////////////////////////

	// Variante 1
	*(pTarget->pPunktA)	= *(pSource->pPunktA);
	
	// Variante 2
	*((*pTarget).pPunktB)	= *(*pSource).pPunktB;
	
	// Variante 3
	pTarget->pPunktC->iX	= pSource->pPunktC->iX;
	pTarget->pPunktC->iY	= (*pSource->pPunktC).iY;
	pTarget->pPunktC->iZ	= (*(*pSource).pPunktC).iZ;



	return pTarget;
} // Dreieck3DDeepCopy ////////////////////////////////////////////////////////
					

In Dieser Funktion wird diese Dreiecksstruktur mit samt ihren Inhalt in eine neue Variable kopiert und zurückgegeben. Der Einfachheit halber arbeite ich in diesem Beispiel nur mit "new" und "delete". Die fett gedruckten Zeilen sind die Zeilen, welche am einfachsten verständlich sind und deren Stiel Sie sich angewöhnen sollten.

Die Zeile 20 sollte klar sein. Interessanter ist schon Zeile 27 und 31 bzw. 32. Hier werden die einzelnen Punkte des Dreiecks angelegt bzw. es wird Speicherplatz für sie reserviert. Ich habe hier absichtlich zwei verschiedene Schreibweisen gewählt, damit Sie sehen, dass sie äquivalent sind.

Die Zeilen 39 und 42 sind wieder äquivalent. Man ließt dies folgendermaßen: "Die Struktur, die sich dort befindet, worauf "PunktA" zeigt, die zu dem Dreieck gehört, worauf "pSource" zeigt, wird dort hin kopiert, wo "PunktA" hin zeigt, welcher zu dem Dreieck gehört, worauf "pTarget" zeigt".

Alternativ dazu könnte man auch, wie in Zeile 45 bis 47, jeden einzelnen Wert per Hand kopieren. Dies ist zwar einfacher nachzuvollziehen, aber spätestens wenn man dies für eine Struktur mit 100 Elementen machen möchte/muss, wird man davon Abstand nehmen. Auf der rechten Seite des "=" habe ich wieder drei verschiedene Schreibweisen verwendet, welche zwar alle äquivalent sind, sich aber mehr oder weniger gut handhaben bzw. lesen lassen.

 52
 53
 54
 58
 59
 60
 61
 62
 63
					
// Gibt die Koordinaten eines SPunkt3D aus ////////////////////////////////////
void PrintPunkt3D(SPunkt3D* pPunkt) {
	// Variante 1
	printf("%i ", pPunkt->iX);

	// Variante 2
	printf("%i ", (*pPunkt).iY);
	printf("%i\n",(*pPunkt).iZ);
} // PrintPunkt3D /////////////////////////////////////////////////////////////
					

Auch hier sehen Sie wieder wunderschön, die zwei Schreibweisen mit dem "*" und dem "->" und wieder habe ich die leichtere Variante fett hervorgehoben.

 67
 68
 69
 70
 71
 72
 73
 74
 75
					
// Gibt ein SDreieck3D mit all seinen SPunkt3D aus ////////////////////////////
void PrintDreieck3D(SDreieck3D* pDreieck) {
	// Variante 1
	PrintPunkt3D(pDreieck->pPunktA);

	// Variante 2
	PrintPunkt3D((*pDreieck).pPunktB);
	PrintPunkt3D((*pDreieck).pPunktC);
} // PrintDreieck3D ///////////////////////////////////////////////////////////
					

In dieser Funktion möchte ich schon einmal einen kleinen Hinweis dafür geben, wie man ein objektorientiertes Programm aufbaut. Statt eine Ausgabefunktion zu haben, welche sich durch jede der Strukturen hangelt und die einzelnen Elemente ausgibt, verlagert man das Problem auf untergeordnete Funktionen (später auf untergeordnete Objektmethoden). Vereinfacht ausgedrückt heißt das, wenn ich von allen Studenten ein Foto machen soll, frag ich lieber die Studenten ob sie mir ein Bild von sich geben können.

 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
					
// Hauptfunktion des Programms ////////////////////////////////////////////////
int main(int argc, char** argv) {
	// Erzeugen der abstrakten Struktur
	SDreieck3D* pDreieck1	= new SDreieck3D;

	pDreieck1->pPunktA	= new SPunkt3D;
	pDreieck1->pPunktB	= new SPunkt3D;
	pDreieck1->pPunktC	= new SPunkt3D;



	// Befüllen mit Werten

	pDreieck1->pPunktA->iX	= 1;
	pDreieck1->pPunktA->iY	= 2;
	pDreieck1->pPunktA->iZ	= 3;

	pDreieck1->pPunktB->iX	= 4;
	pDreieck1->pPunktB->iY	= 5;
	pDreieck1->pPunktB->iZ	= 6;

	pDreieck1->pPunktC->iX	= 7;
	pDreieck1->pPunktC->iY	= 8;
	pDreieck1->pPunktC->iZ	= 9;



	// Ausgabe

	PrintDreieck3D(pDreieck1);
	printf("\n");

	SDreieck3D* pDreieck2;
	pDreieck2		= Dreieck3DDeepCopy(pDreieck1);

	PrintDreieck3D(pDreieck2);



	// Freigeben des Speichers ////////////////////////////////////////////

	// Variante 1
	delete pDreieck1;
	pDreieck1		= NULL;

	// Variante 2
	delete pDreieck2->pPunktA;
	(*pDreieck2).pPunktA	= NULL;
	
	delete (*pDreieck2).pPunktB;
	(*pDreieck2).pPunktB	= NULL;
	
	delete (*pDreieck2).pPunktC;
	(*pDreieck2).pPunktC	= NULL;

	delete pDreieck2;
	pDreieck2		= NULL;
	
	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

In den Zeilen 82 bis 86 lege ich zunächst das Dreieck an und dann jeweils die Punkte. Zu beachten ist, dass mit der Anlage des Dreiecks noch lange nicht die Punkte existieren, wohl aber die Zeiger (welche vorerst irgendwohin zeigen). In Zeile 92 bis 102 weiße ich den Punkten, mehr oder weniger sinnvolle Werte (Koordinaten) zu, denn mit der Anlage der Punkte existierten zwar bereits die Struktur, allerdings waren auch ihre Werte nicht definiert.

In den Zeilen 108 - 114 wird zuerst das erste Dreieck ausgegeben. Anschließend wird es kopiert und abschließend wird die Kopie zur Kontrolle ausgegeben.

Zuletzt müssen die Dreiecke wieder gelöscht werden. Am einfachsten geht dies wie in Zeile 121. Anschließend sollten Sie, der Form halber, den Zeiger auf "NULL" setzen, da er nun auf einen Bereich zeigt, der ihm nicht mehr gehört. Extrem vorteilhaft an dieser Variante ist, dass das "delete" sehr intelligent ist und nicht nur das Dreieck wieder freigibt, sondern auch die drei Punktstrukturen, mit den Koordinaten, auf welche die Zeiger in der Dreiecksstruktur zeigen. Möchte man, wie in Variante 2, nur einzelne Punkte löschen, weil man die anderen evtl. noch benötigt, ist es extrem wichtig die entsprechenden Zeiger nach dem "delete" auf "NULL" zu setzen. Würde man dies versäumen, gäbe es bei der Freigabe des Dreiecks einen Absturz, da der Zeiger des Punktes ja noch irgendwo hin zeigt, was ihm nicht gehört. Das Programm würde dann versuchen genau diesen Bereich freizugeben und dabei käme es zu einer Speicherzugriffsverletzung. Am Hässlichsten daran ist, dass Sie z.B. ganz weit Oben im Quelltext schon vergessen haben könntet, diesen Zeiger auf "NULL" zu setzen und der Fehler tritt erst bei Programmende auf, was einem nahe legt, den Fehler am Ende des Programms zu suchen, was aber vergeblich wäre.

Achtung! Wenn Sie mit den Funktionen "malloc" und "calloc" arbeitet, müssen Sie die einzelnen Elemente mit "free" freigeben wie in Variante 2. Die Funktion "free" besitzt diesbezüglich keinen Automatismus.

1 2 3
4 5 6
7 8 9

1 2 3
4 5 6
7 8 9
		
Zum Seitenanfang
Zum Inhaltsverzeichnis

9.2.1 VOID-Pointer

9.2.1 VOID-Pointer

Zeiger auf "void" sind s.g. Dummy-Zeiger. Sie dient dazu, gerade in der objektorientierten Programmierung, Zeiger auszutauschen, welche auf unterschiedliche Datentypen zeigen können, die erst zur Laufzeit festgelegt sind. So kann man beispielsweise universelle Sortierfunktionen schreiben, die für jede Art von Datentypen funktionieren. Hier mal ein kleines Anwendungsbeispiel, in welchem ich drei Pointer auf unterschiedliche Variablentypen definiere und sie dann in ein Array aus "void" Pointern packe. Somit ist es mir quasi möglich, verschiedene Variablentypen in einem Array abzulegen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
					
int	iWert			= 4;
int*	pZeigerAufInt		= &iWert;

float	fWert			= 8.2f;
float*	pZeigerAufFloat		= &fWert;

char	cZeichen		= 'c';
char*	pZeigerAufChar		= &cZeichen;

void*	aVoidArray[3];
aVoidArray[0]			= pZeigerAufInt;
aVoidArray[1]			= pZeigerAufFloat;
aVoidArray[2]			= pZeigerAufChar;

printf(	"%i\t%g\t%c\n",
	*(int*)aVoidArray[0],
	*(float*)aVoidArray[1],
	*(char*)aVoidArray[2]);
					

Ausgabe:

4	8.2	c
		

Bis einschließlich Zeile 8 sollte alles klar sein. In Zeile 10 erzeuge ich jetzt ein Array mit drei Pointern auf "void". Anschließend Speichere ich die Adressen der Zeiger im Array ab, wobei ich hier keine Typumwandlung vornehmen muss, was eine Besonderheit ist (ansonsten muss man immer diese Konvertierung vornehmen - auch bei Zeigern). In den Zeilen 15 bis 18 gebe ich dann den Inhalt des Arrays aus, wobei hier die Typumwandlung notwendig ist. Allgemein rate ich dringend davon ab, in einem Array, mehrere verschiedene Typen zu halten, wie ich dies gerade demonstriert habe, da man keine Kontrolle bzw. Information darüber hat, was wo gespeichert ist.

Zuletzt sei noch erwähnt, dass man "void" Pointer nicht dereferenzieren kann. Zudem kann man den Wert eines "void" Pointern, nicht ohne Typumwandlung an einem anderen Pointer übergeben, welcher selbst kein "void" Pointern ist (dies war ja in der Ausgabe von eben klar zu sehen).

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.2.4 Der -> Operator

9.2.4 Der -> Operator

Dieser Operator wurde mit C++ neu eingeführt und soll dem Programmierer erheblich das Leben erleichtern. Man kann mit ihm ein Zeiger und eine Struktur bzw. Objekte miteinander verbinden ohne den "*" Operator und Klammern benutzen zu müssen.

Unter der Annahme, dass A ein Zeiger auf eine Struktur X ist, welche B enthält, so gilt (*a).b ist das gleiche wie a->b.

Ich denke, die Vorteile die sich daraus ergeben sind nicht zu übersehen, da wirklich eine Vereinfachung stattfindet. Zudem ist zu beachten, dass der Pfeil stark bindet.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.2.3 Die Präfixoperatoren new und delete

9.2.3 Die Präfixoperatoren new und delete

Wie im Kapitel 9.2.2 gezeigt, ist das Reservieren von Speicher etwas umständlich und gewöhnungsbedürftig. Eine Vereinfachung wird durch "new" und "delete" erreicht (erst ab C++), wobei man aber strengstens darauf achten muss, dass man alle Sachen, die mit "new" angelegt wurden, auch mit "delete" freigegeben werden. Ebenso müssen alle Sachen die mit "malloc" bzw. "calloc" erstellt wurden, auch mit "free" wieder freigegeben werden!

Das Schema sieht so aus: <Zeigervariable> = new <ReferenzierterVariablentyp>

 1
 2
 3
 4
					
// Speicher für einen Float reservieren
float* pZeigerAufFloat = new float;

delete pZeigerAufFloat;
					

Allerdings ergeben sich auch Nachteile bei der Verwendung dieser Präfixoperatoren, da "new" und "delete" eigentlich für Objekte gedacht sind. Einmal angelegter Speicher kann nicht mehr verändert werden. Hat man also flexible Typen, wie dynamische Arrays oder Listen, kann man diese zwei nicht verwenden bzw. nur sehr eingeschränkt, aber dazu später mehr.

An dieser Stelle möchte ich Ihnen noch einen kleinen Rat mit auf den Weg geben. Vermeiden Sie es, ständig "new" und "delete" zu verwenden. Dadurch wird nicht nur ihr Hauptspeicher fragmentiert, sondern ggf. hängen an diesen Operationen auch Auslagerungsaktionen mit daran. Unter Umständen kann es also wirklich lange dauern, bis der Speicher tatsächlich zur Verfügung steht. Deshalb ist es sinnvoll, alle Sachen wenn möglich bei Programmstart zu erzeugen und dann bei Programmende wieder freizugeben. Das Programm wird zwar dann zur ganzen Laufzeit mehr Speicher benötigen als vielleicht im Moment notwendig, aber dafür läuft es zwischen drin schneller, da die Speicherverwaltung des Betriebssystems nicht angeleiert werden muss und die Daten zudem meist hintereinander im Speicher liegen, was sehr günstig für den Cache und somit ihr Programm ist.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.1 Einführung in Zeiger

9.1 Einführung in Zeiger

Wie der Name schon verrät, handelt es sich bei Zeigern nicht im klassischen Sinne um Variablen die Werte beinhalten, sondern um Verweise, die auf Speicherbereiche zeigen, an welchen Werte stehen (ähnlich einer Verknüpfung bei auf eine Datei). Natürlich haben Zeiger oder auch Pointer genannt, auch Werte, da sie erst einmal ganz normale Variablen sind. Jene Werte repräsentieren aber nur eine Adresse im Speicher, an welcher man einen Wert eines bestimmten Typs findet. Der Verweis kann, muss aber nicht, auf eine Variable zeigen.

Gerade Neulinge tun sich schwer mit Pointern oder haben auch in gewisser Weise Angst vor ihnen. Ich möchte versuchen diese Einstellung zu ändern, denn Pointer sind das wichtigste bei der Programmierung und intern wird fast ausschließlich mit ihnen gearbeitet. Wann immer der Prozessor Daten benötigt, benutzt er eine Speicheradresse, welche dann an die entsprechenden Steuerwerke geschickt werden. Jene holen dann genau die Daten aus dem Arbeitsspeicher, welche an der gewünschten Adresse stehen. Auch der Stack benötigt einen s.g. Stackpointer, welcher auf die nächste freie Stelle zeigt. Wenn eine Funktion aufgerufen wird, steht im Bytecode auch nur eine Speicheradresse, an welche gesprungen werden soll.

Wenn Sie einen Brief schreiben, dann schreiben Sie auf den Briefumschlag auch eine Adresse, damit die Post weiß, wohin der Brief geschickt werden soll. Wenn Sie jemanden anrufen, dann benutzen Sie auch eine Art Adresse, nämlich die Telefonnummer. Selbst beim Surfen im Internet benutzen Sie Adressen. Sie sehen also, dass das Prinzip einer Adressierung für Sie nicht neu sein dürfte. Der Umgang mit Adressen beim Programmieren ist zwar zu Beginn gewöhnungsbedürftig, aber erlernbar.

Zeiger verweist auf Variable

Hier sehen Sie, dass die Zeigervariable "pZeiger", welche sich selbst bei Adresse "0xFFFF4824" befindet, auf die Variable "iWert" verweist. Deshalb entspricht auch der Wert von "pZeiger" der Speicheradresse von "iWert", also "0xFFFF4828". Stellen Sie sich einfach vor, "pZeiger" wäre ihr Telefon und die Speicheradresse die Telefonnummer ihres Telefons, also "FFFF4828". Jetzt wollen Sie "iWert" anrufen. Sie geben also die Speicheradresse bzw. die Telefonnummer von "iWert" an. Nach dem wählen besteht eine Verbindung zwischen Ihrem Telefon und des anderen. Wenn man über Ihr Telefon fragt, wie alt bist du, kann der andere beispielsweise "15" antworten. Man bekommt also über Ihr Telefon heraus, wie alt der andere ist und so kann man auch über einen Pointer den Wert einer anderen Variable in Erfahrung bringen.

Das Schema sieht so aus: <Variablentyp> *<Zeigername> [= <KonstanterAusdruck>]

Wenn man einen Zeiger mit "nichts" initialisieren will, dann weist man ihm den Wert "NULL" zu (ähnlich eines Telefons, was noch nicht angemeldet wurde). Diesen Wert sollten Sie Zeigern immer zuweisen, wenn sie momentan nicht gebraucht werden. So kann man besser prüfen, ob der Zeiger auf etwas Sinnvolles zeigt oder nicht. Der Wert "0" hingegen, repräsentiert eine gültige Speicheradresse und ist somit nicht geeignet für nicht benutzte Pointer.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.1.3 Einfache Beispiele an Bildern erklärt

9.1.3 Einfache Beispiele an Bildern erklärt

Die Figuren "iThomas" und "iTobias" sind normale Variablen und dadurch gekennzeichnet, dass sie ihren Wert in der Hand halten. "pNils" hingegen ist ein Zeiger und dadurch kenntlich gemacht, dass er nichts in der Hand hält, sondern auf etwas zeigt.

 1
 2
 3
					
int iThomas;
int *pNils	= NULL;
int iTobias	= 18;
					

Initialisierung der Variablen und des Zeigers

Hier wurden zunächst zwei normale Integervariablen "iThomas" und "iTobias" definiert, wobei "iTobias" auch gleich einen Wert bekommt. Der Wert von "iThomas" ist erst einmal undefiniert. In Zeile 2, wird jetzt ein Zeiger namens "pNils" erzeugt, welcher zunächst auf nichts zeigt, also "NULL". Die Blöcke neben jeder Figur, sollen ihre Speicheradressen repräsentieren.

 1
					
pNils = &iTobias;
					

Zeiger verweist jetzt auf eine Variable

Hier wurde dem Zeiger "pNils" die Adresse von "iTobias" zugewiesen. "pNils" zeigt von nun an auf "iTobias" und hat also als Wert die Speicheradresse von "iTobias".

 1
					
iThomas = *pNils;
					

Variable bekommt Wert durch Dereferenzierung eines Zeigers

Jetzt möchte "iThomas" gerne den Wert haben, auf welchen "pNils" zeigt. "pNils" fragt jetzt quasi "iTobias" "Was hast du für einen Wert "iTobias"?", worauf "iTobias" ihm ins Ohr flüstert: "18!". Diese Information gibt "pNils" nun an "iThomas" weiter und der neue Wert von "iThomas" ist 18.

 1
					
iThomas = (int)pNils;
					

Vektor aus Pointer Kopieren

Weil "iThomas" neugierig ist, wohin "pNils" zeigt, bzw. sich "iTobias" im Speicher aufhält, fragt er "pNils" "Sag mal, worauf zeigst du eigentlich die ganze Zeit?", worauf ihm "pNils" antwortet: "Na dahin wo "iTobias" im Speicher steht!". Also bekommt "iThomas" als Wert den Vektor von "pNils" und somit die Adresse von "iTobias".

ACHTUNG! "iThomas" ist jetzt kein Zeiger, auch wenn man ihn mit einem "*" benutzten würde (evtl. weißt der Compiler diese Zuweisung sogar ab, da die formalen Typen nicht übereinstimmen). Er hat die Adresse zwar als Wert gespeichert, aber man kann/darf trotzdem keine Dereferenzierung durchführen. Allerdings könnte der Wert von "iThomas" einem neuen Zeiger zugewiesen werden, welcher dann auch auf "iTobias" zeigen würde. Jener Wert könnte z.B. "pJakob" zugewiesen werden und somit würde er auch auf "iTobias" zeigen.

 1
					
*pNils = 7;
					

Wertzuweisung einer Variable über Dereferenzierung eines Zeigers

Hier sagt das Programm "pNils": "Ey du, gib mal deinem Kollegen den Wert 7!", worauf "pNils" folgendes in das Ohr von "iTobias" flüstert: "Nur damit du Bescheid weißt, wenn man dich fragt, dann antworte von jetzt an immer mit 7!".

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.1.2 Der & Operator

9.1.2 Der & Operator

Diesen Operator hatte ich bereits kurz angesprochen. Immer wenn vor einer Variable ein "&" Operator steht, wird nicht der Wert der Variable betrachtet, sondern die Adresse der Variable im Speicher. Dies kommt oft vor, wenn man Pointer initialisiert bzw. einen Pointer auf eine Variable verweisen lassen will und wenn man Variablen (nicht ihren Wert) einer Funktion übergeben möchte, welche sie manipulieren soll (alternative zu call-by-reference, wie dies bereits bei der Funktion "scanf" angerissen wurde). Wie dies funktionieren könnte, sehen Sie auch in Kapitel 9.2.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.1.1 Der * Operator

9.1.1 Der * Operator

Immer wenn vor einer Variable ein * steht, dann wird ihr Wert als Adresse angesehen und somit dereferenziert. In Folge wird nun im Hintergrund zu dieser verwiesenen Adresse gesprungen und die gewünschte Aktion (Auslesen oder Beschreiben) durchgeführt (entspricht dem Doppelklick auf eine Windows-Verknüpfung - nicht die Verknüpfung wird geöffnet, sondern die Datei, auf welche verwiesen wird).

An dieser Stelle seine schon erwähnt, dass dieser Operator eine schwache Bindung hat, was ich in Kapitel 9.2 an einem Beispiel demonstrieren werde.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.4 Mehrdimensionale Felder

9.4 Mehrdimensionale Felder

Wie ich bereits erwähnte, kann man Arrays auch ineinander verschachteln und das beliebig oft. Für die statischen Arrays hatte ich dies bereits angedeutet. Allerdings ist der Umgang mit dynamischen Arrays ein wenig kniffliger, da es sehr stark davon abhängt, was man möchte bzw. was man meint. Deswegen werde ich nun vier verschiedene Herangehensweisen, für ein zweidimensionales Array, aufzeigen. Dabei werde ich auch jeweils etwas zur Speicherverwaltung sagen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.4.1 Statische mehrdimensionale Arrays

9.4.1 Statische mehrdimensionale Arrays

Wie bereits erwähnt, wird bei statischen Arrays die Größe zur Entwicklungszeit, durch den Programmierer, fest vorgegeben und kannen somit zur Laufzeit nicht mehr verändert werden. Nun kann man eine Matrix bauen, welche die Werte direkt enthält und eine, die nur Zeiger auf Werte enthält.

Variante 1 Variante 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
					
int  iZeilen							= 2;
int  iSpalten							= 3;
int  iCounter							= 0;

int  aStaticIntArray[2][3];		// Variante 1
int* aStaticPointerArray[2][3];		// Variante 2

// Erzeugen

// Durchlaufe die Zeilen
for (int iRowCount=0; iRowCount<iZeilen; iRowCount++) {
	// Erzeugen der Spalteninhalte
	for (int iColCount=0; iColCount<iSpalten; iColCount++) {
		aStaticPointerArray[iRowCount][iColCount]	= new int;
	} // end of for
} // end of for

// Befüllen

// Durchlaufen der Zeilen
for (int iRowCount=0; iRowCount<iZeilen; iRowCount++) {
	// Befüllen der Spalten
	for (int iColCount=0; iColCount<iSpalten; iColCount++) {
		aStaticIntArray[iRowCount][iColCount]		= iCounter;
		*aStaticPointerArray[iRowCount][iColCount]	= iCounter;

		iCounter++;
	} // end of for
} // end of for

// Ausgeben

// Durchlaufen der Zeilen
for (int iRowCount=0; iRowCount<iZeilen; iRowCount++) {
	// Ausgeben der Spalten
	for (int iColCount=0; iColCount<iSpalten; iColCount++) {
		printf("%i ", aStaticIntArray[iRowCount][iColCount]);
		printf("%i \t\t", *aStaticPointerArray[iRowCount][iColCount]);
	} // end of for

	printf("\n");
} // end of for

// Freigeben

// Durchlaufen der Zeilen
for (int iRowCount=0; iRowCount<iZeilen; iRowCount++) {
	// Löschen der Spalteninhalte
	for (int iColCount=0; iColCount<iSpalten; iColCount++) {
		delete aStaticPointerArray[iRowCount][iColCount];
	} // end of for
} // end of for
					

Ausgabe:

0 0        1 1        2 2
3 3        4 4        5 5
		

Die Variante 1 benötigt keinerlei Speicherverwaltung. Variante 2 benötigt für den Aufbau des Arrays ebenso keine Speicherverwaltung, jedoch für die Werte, auf welche die Pointer zeigen und genau diese müssen abschließend auch wieder freigegeben werden.

Zum Seitenanfang
Zum Inhaltsverzeichnis

9.4.2 Dynamische mehrdimensionale Arrays

9.4.2 Dynamische mehrdimensionale Arrays

Als wäre das nicht schon alles kompliziert genug, packe ich jetzt noch einen drauf. Falls zur Laufzeit die Größe des Arrays erst festgelegt werden soll, benötigt man dynamische Arrays. Hierbei gibt es wieder eine Fallunterscheidung. Man kann nur die zweite Dimension dynamisch machen, oder die komplette Matrix. Erstere Variante ist ein statisches Array mit Zeigern auf dynamische Arrays. Die zweite Variante ist ein Zeiger auf ein dynamisches Arrays, welches wiederum Zeiger auf dynamische Arrays beinhaltet.

Variante 1 Variante 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
					
int iZeilen							= 2;
int iSpalten							= 3;
int iCounter							= 0;

int* aStaticArrayWithDynArray[2];	// Variante 1
int** pDynArrayWithDynArray;		// Variante 2

// Erzeugen

pDynArrayWithDynArray						= new int*[iZeilen];

// Durchlaufe die Zeilen
for (int iRowCount=0; iRowCount<iZeilen; iRowCount++) {
	aStaticArrayWithDynArray[iRowCount]			= new int[iSpalten];
	pDynArrayWithDynArray[iRowCount]			= new int[iSpalten];
} // end of for

// Bef�llen

// Durchlaufen der Zeilen
for (int iRowCount=0; iRowCount<iZeilen; iRowCount++) {
	// Befüllen der Spalten
	for (int iColCount=0; iColCount<iSpalten; iColCount++) {
		aStaticArrayWithDynArray[iRowCount][iColCount]	= iCounter;
		pDynArrayWithDynArray[iRowCount][iColCount]	= iCounter;

		iCounter++;
	} // end of for
} // end of for

// Ausgeben

// Durchlaufen der Zeilen
for (int iRowCount=0; iRowCount<iZeilen; iRowCount++) {
	// Ausgeben der Spalten
	for (int iColCount=0; iColCount<iSpalten; iColCount++) {
		printf("%i ", aStaticArrayWithDynArray[iRowCount][iColCount]);
		printf("%i\t\t", pDynArrayWithDynArray[iRowCount][iColCount]);
	} // end of for

	printf("\n\n");
} // end of for

// Freigeben

// Durchlaufen der Zeilen
for (int iRowCount=0; iRowCount<iZeilen; iRowCount++) {
	delete [] aStaticArrayWithDynArray[iRowCount];
	delete [] pDynArrayWithDynArray[iRowCount];
} // end of for

delete [] pDynArrayWithDynArray;
					

Die Ausgabe ist wieder exakt das Gleiche wie eben (daher hab ich sie hier weggelassen), aber hinter den Kulissen läuft einiges anders. Das Befüllen und das Zugreifen auf die Werte ist weitestgehend gleich, aber die Speicherverwaltung ist grundlegend anders.

Bei Variante 1 ist das äußere Array statisch und somit müssen nur die Spalten erzeugt und abschließend wieder freigegeben werden. Bei Variante 2 muss auch noch das äußere Array erzeugt und ganz zum Schluss wieder freigegeben werden. Gerade hier ist die Reihenfolge wichtig, da man sich sonst den Ast absägt auf dem man sitzt und somit Speicherleichen produzieren würde.

Natürlich würden mir jetzt noch ein paar Möglichkeiten einfallen, aber ich denke mal, bei den vier gezeigten kann man es belassen, da in der Praxis weitere Variationen kaum bis gar nicht auftreten. Notfalls lassen sie sich aber aus den vier gezeigten Beispielen ab - bzw. herleiten.

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012