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