Dynamische Arrays

Einführung

Arrays sind ja bereits bekannt. Dennoch haben die bisher bekannt gewordenen einen wesentlichen Nachteil. Ihre Größe ist unveränderbar. Einmal angelegt ist ihre Größe für immer fest bestimmt. Da Zeichenketten bisher als statische 'char'-Arrays bekannt gemacht wurden gilt diese Beschränkung auch hier. Die Größe ist auch hier fest vorgegeben. Angenommen ein Array wird mit 100 Einträgen in seiner Größe fest bestimmt Auf einmal braucht das Programm dann aber 101 Elemente. Schon kann der Array nicht mehr benutzt werden. Außerdem ist nicht immer vorauszusehen, wann und wo ein Objekt benötigt wird. Angenommen ein Benutzer hat die Möglichkeit in einem Programm beliebig viele Dateien zu öffnen, deren Informationen jeweils in einer Variablen irgendeines Datentyps stehen. Es besteht dann die Notwendigkeit mit jedem Öffnen einer neuen Datei neuen Speicher bereitzustellen, in dem dann die Informationen gespeichert werden können. Solange der Speicher benötigt wird, kann man auf ihn mithilfe eines entsprechenden Zeigers zugreifen. Wird der Speicher nicht mehr benötigt, dann muss er wieder freigegeben werden, um sogenannte Speicherlecks zu vermeiden.

Bereitstellen und freigeben von Speicher via malloc und free

In der Datei "malloc.h" werden die Methoden 'malloc' und 'free' definiert. Die erste Methode ermöglicht das Bereitstellen eines Speichers auf den dann mithilfe eines Zeigers zugegriffen werden kann. Der Programmierer muss lediglich die gewünschte Größe des Speichers (in Byte) angeben. Der allgemeine Aufruf der Funktion sieht wie folgt aus:

void* <zeigername> = malloc(<speichergröße>);

Sollen beispielsweise 16 Byte Speicher reserviert werden, auf den dann mithilfe der Variablen 'pv' zugegriffen werden kann, dann könnte der Aufruf wie folgt aussehen:

void *pv = malloc(16);

Anschließend sollte zunächst einmal überprüft werden ob die Speicherzuweisung überhaupt erfolgreich war. Es ist durchaus denkbar, dass gar nicht genügend Speicher vorhanden ist. In diesem Fall wird dem Zeiger der Wert null übergeben. Der Programmierer hat dafür zu sorgen, dass dieser Fall berücksichtigt wird. Normalerweise folgt direkt auf einen Aufruf via 'malloc' eine Überprüfung des Zeigers via 'if':

//Speicher bereitstellen
void *pv = malloc(16);

//Zuweisung erfolgreich?
if (pv)
{
	/* Speicher steht zur Verfügung und kann nach belieben editiert werden
	...
	*/
}
else
{
	/* es steht nicht genügend Speicher zur Verfügung
	...
	*/
}

Die Freigabe erfolgt schließlich mithilfe der Methode 'free'. Einem Aufruf dieser Funktion sollte mit dem Zurücksetzen des Zeigers einhergehen. Allgemein könnte dieser Aufruf in etwa so aussehen:

//Speicher freigen
free(<zeigername>);

//Zeiger zurücksetzen
<zeigername> = 0;

Und übertragen auf das vorige Beispiel:

//Speicher freigeben
free(pv);

//Zeiger zurücksetzen
pv = 0;

Wie Sie wahrscheinlich festgestellt haben, wurde jeweils ein 'void*'-Zeiger verwendet. Es ist nun aber kein Problem diesen Zeiger in einen Zeiger beliebigen Datentyps zu casten. Dies kann man auch sofort beim Aufruf der Funktion 'malloc' tun. Dann braucht man gar nicht erst einen 'void*'-Zeiger zu deklarieren. Der entsprechende Aufruf sähe dann im allgemeinen so aus:

<datentyp>* <zeigername> = (<datentyp>*)malloc(<speichergröße>);

Auf diese Weise lassen sich dynamische Arrays erstellen, also Arrays deren Größe erst zur Laufzeit des Programms bestimmt wird. Angenommen man möchte einen Array 'pArr' des Datentyps 'int' mit 'n' Elementen erstellen. Zunächst muss allerdings bestimmt werden, wie groß der bereitzustellende Speicher sein muss, da 'malloc' eine Größenangabe in Bytes (und nicht evtl. in Anzahl der Elemente) verlangt. Es soll ein Array mit 'n' Elementen erstellt werden. Da jedes Element vom Typ 'int' ist, benötigt jedes Element 'sizeof(int)' (also vier) Bytes. Bei 'n' Elementen bedeutet das eine Gesamtgröße von 'n * sizeof(int)' Bytes für den Array. Der komplette Aufruf könnte dann in etwa so aussehen:

int *pArr = (int*)malloc(n * sizeof(int));

Am Ende muss der gesamte Array wieder freigegeben werden. Dies geschieht wiederum mit der Methode 'free':

free(pArr);
pArr = 0;

Das Erstellen eines dynamischen Arrays mit 'n' Elementen mithilfe von 'malloc' und das anschließende Freigeben mithilfe von 'free' sieht im allgemeinen dann so aus:

//Erstellen
<datentyp>* <arrayname> = (<datentyp>*)malloc(n * sizeof(<datentyp>));

//Freigeben
free(<arrayname>);
<arrayname> = 0;

Bereitstellen und freigeben von Speicher via new und delete

Für das Anlegen von dynamischen Arrays lassen sich auch die Operatoren 'new' und 'delete' verwenden. Im Gegensatz zu der Methode 'malloc' gibt man beim Operator 'new' nicht die Größe des gewünschten Speichers in Byte an. Stattdessen gibt man die Anzahl der Elemente des zu erstellenden Arrays an. Der allgemeine Aufruf zum Erstellen des Arrays (mit 'n' Elementen) sieht in etwa so aus:

<datentyp>* <arrayname> = new <datentyp>[n];

Vorteilhaft ist u.a. auch, dass man im Gegensatz zu der Methode 'malloc' bei dem Operator 'new' kein Typecasting vornehmen muss. In Anlehnung an das obige Beispiel einen dynamischen Array (mit 'n' Elementen) des Datentyps 'int' mithilfe von 'malloc' anzulegen folgt hier nun der Aufruf der nötig ist den gleichen Array mit dem Operator 'new' zu erzeugen:

int* pArr = new int[n];

Die Freigabe eines mit dem Operator 'new' angelegten Arrays geschieht mit 'delete'. Im allgemeinen sähe der Aufruf dann so aus:

delete[] <arrayname>;
<arrayname> = 0;

In Bezug auf das letzte Beispiel sähe dann das Freigeben des Arrays so aus:

delete[] pArr;
pArr = 0;

Auch wenn der Array nur ein ('n = 1') Element besitzt, lassen sich die Aufrufe genauso durchführen wie oben gezeigt. Es existiert allerdings noch eine andere Möglichkeit lediglich ein einzelnes "Element" zu erstellen. Man lässt einfach bei dem Operator 'new' die eckigen Klammern weg ('[...]'):

<datentyp>* <zeigername> = new <datentyp>;

Die Freigabe erfolgt dann auch wieder mithilfe des Operators  'delete' aber auch diesmal ohne die eckigen Klammern ('[]'):

delete <zeigername>;
<zeigername> = 0;

Zugriff auf die Elemente eines dynamischen Arrays

Der Vorteil an dynamischen Arrays (egal ob mit 'new' oder mit 'malloc' angelegt) ist, dass man nach der Erstellung auf die einzelnen Elemente genauso zugreifen kann, wie es schon bei den bereits zuvor kennen gelernten, üblich war. Um also auf das 'm'. Element ('m < n') zugreifen (lesen und schreiben) zu können, wobei auch hier wieder das erste Element im Array den Index null trägt, sind folgende Aufrufe zugelassen:

element_m = <arrayname>[m];

oder

element_m = *(<arrayname> + m);

Angenommen es soll ein Array (Datentyp 'int') mit 'n' Elementen erstellt werden, wobei 'n' vom Benutzer eingegeben wird, und anschließend vom Benutzer Element für Element gefüllt werden, dann könnte dies wie folgt aussehen:

"main.cpp":

#include <iostream.h>
#include <malloc.h>

void main()
{
	int n = 0;

	//Arraygröße eingeben
	cout << "Arraygroesse: "; cin >> n;

	//n ungültig -> Programm beenden
	if (n <= 0) return;

	//Array erstellen
	int* pArr = new int[n];			//bzw. int* pArr = (int*)malloc(n * sizeof(int));

	//Array ungültig -> Programm beenden
	if (!pArr) return;

	//Element für Element eingeben
	for (int i = 0; i < n; i++)
	{
		cout << i << ". Element: ";
		cin >> pArr[i];			//bzw. cin >> *(pArr + i);
	}

	//Elemente in umgekehrter Reihenfolge wieder ausgeben
	for (i = n - 1; i >= 0; i--)
	{
		cout << i << ". Element: ";
		cout << pArr[i] << endl;	//bzw. cout << *(pArr + i) << endl;
	}

	//Array freigeben
	delete[] pArr;				//bzw. free(pArr);
	pArr = 0;
}

Nachdem die Elemente mit den einzelnen Werten gefüllt wurden, werden sie in umgekehrter Reihenfolge wieder ausgegeben. Wenn ein ungültiger Wert für die Arraygröße eingegeben wird oder der Speicher nicht reserviert werden kann, dann wird das Programm durch einen Aufruf von 'return' (siehe Kapitel Funktionen) beendet.

Funktionen für den Umgang mit dynamischen Arrays

Wie bereits bei den "statischen" Arrays lassen sich natürlich auch bei dynamischen Arrays die Funktionen 'memcpy' und 'memset' (siehe Kapitel Arrays) verwenden, um den Array mit Daten zu füllen. Allerdings muss man bei der Größenangabe darauf achten, dass man die tatsächliche Größe der Daten und nicht die des Datentyps angibt. So wahr folgender Aufruf bei den "statischen" Arrays, um diese zu initialisieren, vollkommen korrekt:

int Arr[50];
memset(Arr, 0, sizeof(Arr));		//sizeof(Arr) liefert den Wert 200

Überträgt man diesen Aufruf auf dynamische Arrays, dann muss man die tatsächliche Datengröße anhand der Anzahl der Elemente bestimmen, also:

int *pArr = new int[50];
memset(pArr, 0, 50 * sizeof(int));	//sizeof(int) liefert den Wert 4

Da 'pArr' in diesem Fall ein Zeiger ist, ist die Größe, welche man mit dem Operator 'sizeof' durch einen Aufruf von 'sizeof(pArr)' bestimmen kann, genau vier Bytes und nicht etwa 200 (50 * 4) Bytes. Indem man die Anzahl der Elemente mit  der Größe des Datentyps (in diesem Fall 'int') multipliziert erhält man schließlich die Größe der gesamten Daten. Während man bei dynamischen Arrays die Größe der Daten auf diese Weise bestimmen muss, ist es bei "statischen" Arrays auch mit der anderen Variante möglich. Sinnvoll wäre es natürlich immer die zweite zu verwenden, denn dann ist es in jedem Fall die richtige. 

Im folgenden ist entscheidend, dass sich dynamische Arrays nach der Deklaration in ihrer Anwendung kaum noch unterscheiden. Ein wesentlicher Unterschied ist, dass es sich bei dynamischen Arrays wirklich um Zeiger handelt. Die Konsequenz ist z.B., dass sich aufgrund dessen die Größe des Datenbereichs, auf den ein solcher Zeiger dann zeigt nicht mithilfe des 'sizeof'-Operators bestimmen lässt. Aus diesem Grunde sollte die Größe eines Arrays immer extra gespeichert werden. Neben diesem Unterschied liegt der wesentliche Unterschied in der Deklaration und in der Eigenschaft, dass der Speicher für dynamische Arrays je nach Bedarf dynamisch angelegt wird.

Ändern der Arraygröße

Angenommen ein Array reicht nicht mehr aus um weitere Elemente aufzunehmen oder es soll ein Element entfernt werden. Dann muss die Größe des Arrays verändert werden. Die allgemeingültige Methode wäre, einen neuen Array mit der neuen Größe zu erstellen, die Daten von dem alten in den neuen zu kopieren und im Falle eines Hinzufügens von Elementen, diese Elemente in dem neuen zu speichern. Anschließend wird dann einfach der alte Array gelöscht und der Zeiger auf den des neuen gesetzt. Dazu folgendes Programmbeispiel: In Anlehnung an das letzte soll nun wieder ein Array vom Benutzer gefüllt werden. Allerdings muss dieser die Größe des Arrays nicht bei Beginn festlegen. Solange er irgendwelche gültigen Werte eingibt wird der Array , wie eben beschrieben, vergrößert. Als ungültige Eingabe gelten hierbei alle negativen Zahlen. Das Programm könnte dann wie folgt aussehen:

"main.cpp":

#include <iostream.h>	//für cout und cin
#include <memory.h>	//für memcpy
#include <malloc.h>	//für free und malloc

void main()
{
	//Größe des Arrays
	int n = 0;

	//Array
	int* pArr = 0;

	int i = 0;

	//Element für Element eingeben
	while (true)
	{
		cout << n << ". Element: ";
		cin >> i;

		//Eingabe ungültig -> Schleife beenden
		if (i < 0) break;

		//Neuen Array mit entsprechender Größe erstellen
		int* pArrTmp = new int[n + 1];	//bzw. int* pArrTmp = (int*)malloc((n + 1) * sizeof(int));

		//Erstellung nicht möglich -> Schleife beenden
		if (!pArrTmp) break;

//--- 1. Markierung für nachträgliche Änderung ---

		//n > 0 -> n Elemente kopieren
		if (n > 0)
			memcpy(pArrTmp, pArr, n * sizeof(int));

		//Die Eingabe des Benutzers als letztes Element (Index n) hinzufügen
		pArrTmp[n] = i;			//bzw. *(pArrTmp + n) = i;

//--- 2. Markierung für nachträgliche Änderung ---

		//Alten Array freigeben und Zeiger neu setzen
		delete[] pArr;			//bzw. free(pArr);
		pArr = pArrTmp;
		pArrTmp = 0;

		//Arraygröße anpassen
		n++;
	}

	//Elemente in umgekehrter Reihenfolge wieder ausgeben
	for (i = n - 1; i >= 0; i--)
	{
		cout << i << ". Element: ";
		cout << pArr[i] << endl;	//bzw. cout << *(pArr + i) << endl;
	}

	//Array freigeben
	delete[] pArr;				//bzw. free(pArr);
	pArr = 0;
}

Der Array wird also mit jedem Schleifendurchlauf um ein Element vergrößert, welches durch den Benutzer eingegeben wird. Der Array wird nachher in umgekehrter Reihenfolge ausgegeben. Im Prinzip wird hier das eingegebne Element immer hinten angehängt. Da der Array aber nachher in umgekehrter Reihenfolge ausgegeben werden soll, wäre es naheliegend, die Elemente gleich in umgekehrter Reihenfolge zu speichern und den Array nachher richtig herum auszugeben. In diesem Fall müssen die Elemente allerdings vorne angehängt werden. Dazu müsste der Quelltext zwischen den beiden Markierungen lediglich wie folgt abgeändert werden:

		//n > 0 -> n Elemente kopieren
		if (n > 0)
		memcpy(pArrTmp + 1, pArr, n * sizeof(int));

		//Die Eingabe des Benutzers als erstes Element (Index 0) hinzufügen
		pArrTmp[0] = i; 		//bzw. *(pArrTmp + 0) = i; oder *pArrTmp = i;

Bei dieser Variante werden die 'n' Elemente des alten Arrays, in die letzten (und nicht, wie zuvor,  in die ersten) Elemente des neuen Arrays kopiert. Anschließend wird in dem ersten (und nicht, wie zuvor, im letzten) Element (Index null) des neuen Arrays noch die Benutzereingabe gespeichert.

Damit das Programm genauso läuft wie zuvor, sollte noch die Schleife, welche für die Ausgabe zuständig ist, entsprechend angepasst werden, d.h. der Array sollte wieder von vorne nach hinten und nicht mehr von hinten nach vorne durchlaufen werden:

	//Elemente in umgekehrter Reihenfolge (bezüglich der Eingabe) wieder ausgeben
	for (i = 0; i < n; i++)
	{
		cout << i << ". Element: ";
		cout << pArr[i] << endl;	//bzw. cout << *(pArr + i) << endl;
	}

Dynamische Zeichenketten

Da es sich, wie bereits mehrmals erwähnt, bei Zeichenketten ebenfalls um Arrays handelt lassen sich auch Zeichenketten in Form dynamischer Arrays erstellen. So könnte ein Programm bevor es eine vom Benutzer eingegebene Zeichenkette in einer Variablen speichert ert einmal überprüfen, wie groß sie Zeichenkette überhaupt ist. Dann müsste es genügend Speicher bereitstellen (inkl. ein Byte für das '\0'-Zeichen) um anschließend die tatsächlichen Daten zu ermitteln. Bei einer Konstruktion mit 'cin' ist dies allerdings nicht möglich. Mit dieser Methode werden nur Daten in übergebenen Zeichenketten, ohne Berücksichtigung auf deren Größe, gespeichert. Der Benutzer Programmierer sollte beim Aufruf von 'cin' also gewährleisten, dass die Zeichenkette groß genug ist. 

Tätigt der Benutzer aber in einem Windowsprogramm eine Eingabe in einem Eingabefeld, dann könnte man als Programmierer vor dem Abfragen der Zeichenkette erst einmal die Länge der Zeichenkette bestimmen um dann eine geeignete Zeichenkette zu erstellen. Für ein Programm in dem man mehrzeilige Texte verfassen soll, sollte natürlich keine Begrenzung der Zeichenlänge vorliegen. Da wären dann dynamische Zeichenketten unumgänglich. 

An dieser Stelle ist allerdings nur interessant, wie sie erstellt und wieder freigegeben werden. Dies unterscheidet sich im Prinzip nicht von Erstellung und Freigabe eines Arrays beliebigen Datentyps. Man könnte beispielsweise das Beispielprogramm aus dem Kapitel über Zeichenketten noch einmal aufgreifen. Dieses Mal werden vorher vom Benutzer Angaben über die maximale Länge von Vor- und Nachnamen gemacht. Der Quellcode könnte wie folgt aussehen:

"main.cpp":

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

void main()
{
	//Zeichenkette mit den Vorlagen
	char strTemplate[] = "%s %s (%d) ist %.2f Meter gross.";

	//Längen der Zeichenketten
	int n1 = 0, n2 = 0, n = 0;

	//Abfrage vom Benutzer
	cout << "max. Zeichenlaenge" << endl;
	cout << "1.) des Nachnamen: "; cin >> n2;
	cout << "2.) des Vornamen: "; cin >> n1;

	//Überprüfung der Eingaben
	if (n1 < 0) n1 = 30;
	if (n2 < 0) n2 = 20;

	//Daten der Person
	char *strName1 = new char[n1 + 1];	//Vorname
	char *strName2 = new char[n2 + 1];	//Nachname
	int iAge = 0;
	double dSize = 0;

	//Erstellung fehlgeschlagen -> Programmende
	if (!strName1) {return;}
	if (!strName2) {delete[] strName1; return;}
	
	//Eingeben der Daten
	cout << "Nachname: "; cin >> strName2;
	cout << "Vorname: "; cin >> strName1;
	cout << "Alter: "; cin >> iAge;
	cout << "Groesse: "; cin >> dSize;

	//Bestimmen der Länge des Zielstrings
	n = strlen(strName1) + strlen(strName2) + strlen(strTemplate) +
		3 /*Alter < 1000*/ + 4 /*Größe d.kk*/ - 10 /*Formatspezifikationen*/;
	

	//Zielstring
	char *str = new char[n + 1];

	//Erstellung fehlgeschlagen -> Programmende
	if (!str) {delete[] strName1; delete[] strName2; return;}

	//Daten zusammenfassen
	sprintf(str, strTemplate, strName1, strName2, iAge, dSize);
	
	//Ausgabe
	cout << str << endl;

	//Differenz
	cout << (n - strlen(str)) + (n1 - strlen(strName1)) + (n2 - strlen(strName2))
		<< " Bytes zu viel reserviert" << endl;

	//Freigeben
	delete[] str;
	delete[] strName1;
	delete[] strName2;
	

}

Als Zugabe wird neben dem gewünschten Satz die Anzahl der Bytes angegeben, die zuviel reserviert wurden. Wenn man z.B. die Größen der Vor- und Nachnamen so eingibt, dass sie mit den anschließenden Eingaben übereinstimmen und man ein zweistelliges Alter angibt, dann wird ein Byte zuviel reserviert, da für das Alter drei Stellen reserviert werden. Wählt man zu lange Namen aus oder gibt ein vierstelliges Alter oder eine Körpergröße von zehn Metern oder mehr ein, dann kommt es zu Absturz des Programms. Dafür werden bei richtiger Eingabe maximal ein Byte zuviel reserviert (Personen unter 100 Jahre).

Für die nächsten Lektionen

Für die nächsten Lektionen sollten die Eigenschaften und die Anwendung, insbesondere in Bezug auf Deklaration, Zugriff auf die Elemente und die Verwendung der Methoden 'memcpy' und 'memset', bekannt sein. Außerdem sollte der Bezug zu "statischen" Arrays hergestellt sein.

Zurück Nach oben Weiter