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.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

© Copyright by Thomas Weiß, 2009 - 2012