| #include <pronix.de> |
|
|
Bisher wurde die Speicherverwaltung in Variablendefinitionen versteckt. Sie mussten sich so zwar keine Gedanken bezüglich der Verwaltung machen, aber spätestens, wenn Sie neuen Speicher für weitere Daten benötigten, musste der Code umgeschrieben werden.
17.1. Das Speicherkonzept
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Speicherbereich | Verwendung |
| Code | Maschinencode des Programms |
| Daten | Statische und globale Variablen |
| Stack | Funktionsaufrufe und lokale Variablen |
| Heap | Dynamisch reservierter Speicher |
Code-Speicher
Dieser wird in den Arbeitsspeicher geladen, und von dort aus werden die Maschinenbefehle der Reihe nach in den Prozessor (genauer in die Prozessor-Register) geschoben und ausgeführt.
Daten-Speicher
Darin befinden sich alle statischen Daten, die bis zum Programmende verfügbar sind (globale und statische Variablen).
Stack-Speicher
Im Stack werden die Funktionsaufrufe mit ihren lokalen Variablen verwaltet. In Kapitel 12, Funktionen, wurde auf den Stack ja schon näher eingegangen.
Heap-Speicher
Dem Heap-Speicher gebührt in diesem Kapitel das Hauptinteresse. Über ihn wird die dynamische Speicherreservierung mit Funktionen wie malloc() erst realisiert. Der Heap funktioniert ähnlich wie der Stack. Bei einer Speicheranforderung erhöht sich der Heap-Speicher, und bei Freigabe wird er wieder verringert. Wenn ein Speicher angefordert wurde, sieht das Betriebssystem nach, ob sich im Heap noch genügend zusammenhängender freier Speicher dieser Größe befindet. Bei Erfolg wird die Anfangsadresse des passenden Speicherblocks zurückgegeben.
Es wurde bereits kurz erwähnt, mit welcher Funktion Speicher dynamisch reserviert werden kann. Es wird dabei auch von einer Speicherallozierung (allocate, dt. zuweisen) gesprochen. Hier die Syntax dieser Funktion:
#include <stdlib.h> void *malloc(size_t size);
Bei erfolgreichem Aufruf liefert die Funktion malloc() die Anfangsadresse der Größe size Bytes vom Heap zurück. Da die Funktion einen void-Zeiger zurückliefert, ist diese nicht abhängig von einem Datentyp. Hierzu ein Beispiel:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p;
p=(int *)malloc(sizeof(int));
if(p != NULL)
{
*p=99;
printf("Allozierung erfolgreich ... \n");
}
else
printf("Kein Speicherplatz vorhanden!!!\n");
return 0;
}
Nach der Deklaration eines int-Zeigers wurde diesem mit
p=(int *)malloc(sizeof(int));
eine Anfangsadresse eines Speicherbereichs der Größe int zugewiesen. Bei Erfolg zeigt der Zeiger p auf den Anfang des reservierten Speicherbereichs. Ist dabei etwas schief gegangen, zeigt der Zeiger auf NULL, und es wird ausgegeben, dass kein Speicherplatz reserviert werden konnte. Hierzu der Programmablauf bildlich:

Abbildung 17.1: Dynamisch reservierter Speicher vom Heap
|
Hinweis |
|
Das Typencasting der Funktion malloc() ist laut ANSI C-Standard nicht notwendig und kann auch weggelassen werden. ANSI C++ schreibt allerdings ein Casten des Typs void * vor. Da es aber kaum noch reine C-Compiler gibt, wurde und wird in den Beispielen das Typencasting durchgeführt. |
|
Na gut, ich denke, das beeindruckt Sie nicht besonders. Zur Laufzeit eines Programms Speicherplatz für einen int-Wert reservieren mit solch einem Aufwand? Gut, dann reservieren Sie eben mehr Speicherplatz für mehrere int-Werte:
p=(int *)malloc(2*sizeof(int));
Hiermit reservieren Sie Speicherplatz für zwei int-Werte vom Heap. Das Beispiel als Listing:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p;
p=(int *)malloc(2*sizeof(int));
if(p != NULL)
{
*p=99; /* Alternativ auch p[0] = 99 */
*(p+1) = 100; /* Alternativ auch p[1] = 100 */
printf("Allozierung erfolgreich ... \n");
}
else
printf("Kein Speicherplatz vorhanden!!!\n");
printf("%d %d\n",p[0],p[1]);
/* Sie können die Werte auch so ausgeben lassen */
printf("%d %d\n",*p, *(p+1));
return 0;
}
Folgende Abbildung soll den Sachverhalt veranschaulichen:

Abbildung 17.2: Speicher für mehrere Elemente dynamisch anfordern
Der Sachverhalt, warum *p und p[0] oder *(p+1) und p[1] auf dasselbe Element zugreifen, wurde in Kapitel 15, Zeiger, geklärt. Blättern Sie notfalls einfach zu den Tabellen am Ende des Kapitels 15 zurück.
17.2.1 NULL-Zeiger bei der Speicherreservierung
Ein NULL-Zeiger wird zurückgeliefert, wenn malloc() nicht mehr genügend zusammenhängenden Speicher finden kann. Der NULL-Zeiger ist ein vordefinierter Zeiger, dessen Wert sich von einem regulären Zeiger unterscheidet. Er wird vorwiegend bei Funktionen zur Anzeige und Überprüfung von Fehlern genutzt, die einen Zeiger als Rückgabewert zurückgeben.
17.2.2 Möglichkeiten der Speicherreservierung und ihre Probleme
Bei einem Aufruf der Funktion malloc() muss die Größe des zu reservierenden Speichers in Bytes angegeben werden. Damit ist die Größe des Speicherobjekts gemeint, das durch einen Zeiger referenziert werden soll. Diese Größe können Sie auch mit numerischen Werten selbst bestimmen:
p=(int *)malloc(sizeof(2));
Hiermit werden zwei Bytes reserviert, auf deren Anfangsadresse der Zeiger p verweist. Wird dieser Codeabschnitt jetzt auf ein anderes System portiert, bei dem der Datentyp int einer Größe von vier Bytes entspricht, sind Probleme vorprogrammiert. Daher ist es oft besser, die Größe des Zielobjekts vom Compiler selbst bestimmen zu lassen. Für die dynamische Speicherzuweisung haben Sie folgende drei Möglichkeiten:
#include <stdio.h>
#include <stdlib.h>
int main()
{
double *p1,*p2;
p1=(double *)malloc(sizeof(p1));
p2=(double *)malloc(sizeof(p2));
if(p1 != NULL && p2 != NULL)
{
*p1=5.15;
printf("p1 = %f\n",*p1);
*p2=10.99;
printf("p2 = %f\n",*p2);
}
return 0;
}
Wenn Sie "Glück" haben, stürzt das Programm ab. Im schlimmsten Fall funktioniert das Programm und gibt die richtigen Zahlen aus. Das wäre aber purer Zufall. Denn ohne den Dereferenzierungsoperator wird nicht ein double-Wert an malloc() übergeben, sondern die Größe des Zeigers. Und diese beträgt immer vier statt der erforderlichen acht Bytes. Wenn jetzt ein anderer Wert an diese Adresse gelangt, ist der weitere Verlauf des Programms nicht absehbar. Es kommt zu einer so genannten Überlappung der Speicherbereiche.
17.2.3 free() - Speicher wieder freigeben
Wenn Sie Speicher vom Heap angefordert haben, sollten Sie diesen auch wieder zurückgeben. Der alllozierte Speicher wird mit folgender Funktion freigegeben:
#include <stdlib.h> void free (void *p);
Der Speicher wird übrigens auch ohne einem Aufruf von free() freigegeben, wenn sich das Programm beendet. Ein Beispiel zu free():
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p;
p=(int *)malloc(sizeof(int));
if(p != NULL)
{
*p=99;
printf("Allozierung erfolgreich ... \n");
}
else
printf("Kein Speicherplatz vorhanden!!!\n");
if(p != NULL)
free(p);
return 0;
}
Es wird hier auch überprüft, dass nur wirklich reservierter Speicherplatz wieder freigegeben wird. Der mit free() freigegebene Speicherplatz wird danach zwar als frei markiert, aber p zeigt immer noch auf die ursprüngliche Speicherstelle. Hier das Beispiel:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p;
p=(int *)malloc(sizeof(int));
if(p != NULL)
{
*p=99;
printf("Allozierung erfolgreich ... \n");
}
else
printf("Kein Speicherplatz vorhanden!!!\n");
printf("vor free() *p = %d\n",*p);
if(p != NULL)
free(p);
printf("nach free() *p = %d\n",*p);
return 0;
}
Wollen Sie absolut sicher sein, dass der Zeiger nichts mehr zurückgibt, dann übergeben Sie dem Zeiger nach der Freigabe von Speicher einfach den NULL-Zeiger:
free(p); *p=NULL;
Dies können Sie in ein Makro wie folgt verpacken:
#define free(x) free(x); *x=NULL
Was malloc() und die weiteren Speicherallozierungs-Funktionen so bedeutend und wichtig macht, ist die Möglichkeit, von jedem beliebigen Datentyp Speicher anfordern zu können - sind es nun einfache Datentypen wie Strings, Arrays oder komplexe Strukturen.
Natürlich können Sie auch Speicherplatz für ein char-Array zur Laufzeit anfordern. Das folgende Beispiel demonstriert dies:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define BUF 80
int main()
{
char puffer[BUF];
char *dyn_string;
printf("Ein Text mit max. 80 Zeichen: ");
fgets(puffer, BUF, stdin);
dyn_string = (char *)malloc(strlen(puffer)+1);
if(dyn_string != NULL)
strncpy(dyn_string, puffer, strlen(puffer)+1);
else
printf("Konnte keinen Speicherplatz reservieren\n");
printf("%s",dyn_string);
free(dyn_string);
return 0;
}
Wobei erwähnt werden muss, dass diese Art, dynamisch Speicher für einen Text zu reservieren, noch recht unflexibel ist. Mit
dyn_string = (char *)malloc(strlen(puffer)+1);
wird exakt so viel Speicher angefordert wie zuvor mit fgets() in den String puffer eingelesen wurde. Im Verlauf des Buchs werden Sie erfahren, wie Sie viel effektiver dynamischen Speicher für Text anfordern. Denn bei diesem Beispiel hätten Sie es ja gleich beim char-Array puffer belassen können.
Ohne mich hier zu sehr in die Details der Freispeicherverwaltung zu verstricken, soll dieses Thema kurz behandelt werden. Als Programmierer kann es Ihnen im Prinzip egal sein, wie ein Betriebssystem seinen Speicher reserviert. Aber wenn Sie irgendwann professionelle Programme schreiben, die häufig Speicher vom Heap anfordern, wäre manches Mal ein wenig Hintergrundwissen wünschenswert.
Außer dem Hauptspeicher und dem Festplattenspeicher gibt es noch weitere Möglichkeiten, Daten zu speichern und zu verarbeiten. Es wird dabei von einer Speicherhierarchie gesprochen, die sich in die folgenden sechs Schichten aufteilt:
Als C-Programmierer bedienen Sie sich allerdings vorwiegend vom Hauptspeicher. Das Betriebssystem verwendet dabei das so genannte Paging zur Verwaltung des Speichers. Als Paging wird die Unterteilung des virtuellen Speichers in Seiten (pages) und des physischen Speichers in Seitenrahmen (page frames) bezeichnet. Die Größe einer Seite beträgt bei den gängigen Betriebssystemen 512 KB oder 1024 KB. Ein virtuelles Speichersystem ist erforderlich, damit auch mehrere Programme laufen können, die nicht alle in den physischen Speicher (echter vorhandener Speicher, RAM) passen würden. Dafür stellt Ihnen das Betriebssystem eine so genannte Adresskonvention zur Verfügung, womit aus einer virtuellen Adresse wieder eine physische Adresse wird. Denn mit virtuellen Adressen allein könnte kein Programm laufen.
|
Hinweis |
|
Jedem Prozess steht ein eigener virtueller Adressraum und darin eine eigene Speicherverwaltung zur Verfügung. Meistens wird hier auch noch das Swapping benutzt. Dabei wird ein Prozess in den Hauptspeicher geladen und läuft eine gewisse Zeit, bis dieser anschließend auf die Festplatte ausgelagert wird. Dieses Swapping findet z.B. statt, wenn nicht mehr genügend Speicherplatz vorhanden ist, um einen Prozess ausführen zu können. |
|
In Abschnitt 17.1 haben Sie bereits etwas über den Heap erfahren. Sie wissen, dass der Heap ein zusammenhängender Speicherplatz im Arbeitsspeicher ist, von dem Sie als Programmierer Speicher allozieren können. Das Betriebssystem verwaltet diesen Speicherbereich als eine Kette von freien Speicherblöcken, welche nach aufsteigenden Speicheradressen sortiert ist. Jeder dieser Blöcke enthält Informationen wie die Gesamtlänge oder den nächsten freien Block. Benötigen Sie jetzt Speicherplatz, durchläuft das Betriebssystem diesen Speicherblock nach verschiedenen Verfahren. Dabei wird von einer prozessinternen Freispeicherverwaltung gesprochen.
17.3.1 Prozessinterne Freispeicherverwaltung
Durch einen Aufruf von malloc() sucht das Betriebssystem jetzt einen zusammenhängenden Speicherblock, welcher den Anforderungen entspricht. Auf der Suche nach diesem Speicherplatz gibt es verschiedene Strategien bzw. Algorithmen, die im Betriebssystem (genauer: im Kernel) implementiert sind.
Als Beispiel dienen freie Speicherbereiche, die folgendermaßen im System angeordnet sind:

Abbildung 17.3: Freie Speicherbereiche im System
Sie fordern jetzt von diesen freien Speicherbereichen mit der Funktion malloc() folgende Speicherblöcke an:
ptr1 = malloc(10); ptr2 = malloc(12); ptr3 = malloc(9);
Anhand des freien Speicherbereichs (siehe Abbildung 17.3) und den drei Speicheranforderungen will ich Ihnen die einzelnen Verfahren erklären.
First-Fit-Verfahren
Die Speicherverwaltung durchläuft die Liste der Reihe nach und alloziert den erstbesten freien Bereich, der groß genug ist. Somit sieht die Speicherbelegung im First-Fit-Verfahren folgendermaßen aus:

Abbildung 17.4: First-Fit-Verfahren
Next-Fit-Verfahren
Next-Fit funktioniert wie First-Fit, nur merkt sich das Next-Fit-Verfahren die aktuelle Position und fährt bei der nächsten Suche nach freiem Speicher von dieser Position aus fort.
Best-Fit-Verfahren
Es wird die gesamte Speicherliste durchsucht, bis ein kleinstmögliches Loch gefunden wird. Mit diesem Verfahren wird eine optimale Speicherausnutzung garantiert.

Abbildung 17.5: Best-Fit-Verfahren
Worst-Fit-Verfahren
Das Gegenteil von Best-Fit; es wird in der Liste nach dem größten verfügbaren freien Bereich gesucht, und dieser wird verwendet.

Abbildung 17.6: Worst-Fit-Verfahren
Quick-Fit-Verfahren
Das Quick-Fit-Verfahren unterhält getrennte Listen für freie Bereiche gebräuchlicher Größe.
Buddy-Verfahren
Das Buddy-Verfahren verwendet für jede Speichergröße eine eigene Liste. Die Zeiger auf die Listenköpfe werden dabei in einem Array zusammengefasst. Bei diesem Verfahren werden nur Blöcke von Zweierpotenzen verwendet (1 Byte, 2 Byte, 4 Byte, 8 Byte, 16 Byte, 32 Byte, 64 Byte … 512 Byte, 1 KB … 512 KB, 1 MB …). Wird Speicher angefordert, der nicht diesem Block entspricht, wird ein Block mit der nächsten Zweierpotenz verwendet. Die Blöcke werden außerdem dahin gehend markiert, ob sie zur Anwendung frei sind. Ist bei Speicheranforderung kein gewünschter Block frei, wird ein Block in zwei gleich große Blöcke aufgespalten.
|
Hinweis |
|
Solche Strategien der Freispeicherverwaltung haben natürlich auch ihren Sinn. Vorwiegend dienen solche Verfahren dazu, einen Verschnitt des Speichers zu vermeiden. Das bedeutet, der Speicher wird schlecht ausgenutzt, wenn sehr viele unterschiedliche Stücke verwaltet werden müssen. So kann es passieren, dass einzelne Fragmente des Speichers wahrscheinlich nicht mehr verwendet werden können. Ein zweiter Grund für diese Freispeicherverwaltung ist natürlich die Geschwindigkeit. Je schneller wieder auf einen Speicherblock zurückgegriffen werden kann, umso besser. |
|
Freigabe von Speicher
Der Speicherplatz, der wieder freigegeben wird, wird nicht an das Betriebssystem zurückgegeben, sondern in die Freispeicherliste eingehängt.
Nach diesem Ausflug, der schon mehr in Richtung Programmierung von Betriebssystemen ging, nun wieder zurück zur Praxis.
Wenn mit der Funktion malloc() ein zusammenhängender Speicherbereich reserviert werden kann, dann muss es auch möglich sein, Speicher für ein Array während der Laufzeit zu reservieren. Bei einem zusammenhängenden Speicher können Sie davon ausgehen, dass dieser in einem Block (lückenlos) zur Verfügung gestellt wird. In dem folgenden Beispiel wird ein solches dynamisches Array erzeugt:
#include <stdio.h>
#include <stdib.h>
#include <string.h>
int main()
{
int *value;
int size;
int i=0;
printf("Wie viele Werte benötigen Sie : ");
scanf("%d",&size);
value=(int *)malloc(size*sizeof(int));
if( NULL == value )
{
printf("Fehler bei malloc....\n");
exit(0);
}
while( i<size )
{
printf("Wert für value[%d] eingeben : ",i);
scanf("%d",&value[i]);
i++;
}
printf("Hier Ihre Werte\n");
for(i=0; i<size; i++)
printf("value[%d] = %d\n",i,value[i]);
return 0;
}

Abbildung 17.7: Dynamisch erzeugtes Array
Mit
value=(int *)malloc(size*sizeof(int));
wird ein zusammenhängender Speicherbereich mit size int-Werten reserviert. Danach werden mit
while(i<size)
{
printf("Wert für value[%d] eingeben : ",i);
scanf("%d",&value[i]);
i++;
}
diesem Speicherbereich Werte zugewiesen. Zum besseren Verständnis dasselbe Programm nochmals, aber statt mit Arrays nun mit Zeigern:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int *value;
int size;
int i=0;
printf("Wie viele Werte benötigen Sie : ");
scanf("%d",&size);
value=(int *)malloc(size*sizeof(int));
if(NULL == value)
{
printf("Fehler bei malloc...!!\n");
exit(0);
}
while(i<size)
{
printf("Wert für value[%d] eingeben : ",i);
scanf("%d",(value+i));
i++;
}
printf("Hier Ihre Werte\n");
for(i=0; i<size; i++)
printf("value[%d] = %d\n",i,*(value+i));
return 0;
}
Da *value, value[0] und *(value+1), value[1] immer auf dieselbe Speicheradresse verweisen, ist es egal, wie darauf zugegriffen wird.
Das Programm ist jetzt etwas unflexibel. Was ist, wenn Sie für fünf weitere Elemente Speicherplatz benötigen? Mit der Funktion realloc() wäre dies recht einfach zu realisieren. Aber diese steht jetzt noch nicht zur Debatte. Es ist auch mit malloc() möglich, wenn auch etwas umständlicher. Hier das Beispiel:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
static int size;
static int merker;
int main()
{
int *value,*temp;
int i=0;
int more;
printf("Wie viele Werte benötigen Sie : ");
scanf("%d",&size);
merker=0;
value=(int *)malloc(size*sizeof(int));
if(NULL == value)
{
printf("Fehler bei malloc...!! n");
exit(0);
}
do{
while(merker<size)
{
printf("Wert für value[%d] eingeben : ",merker);
scanf("%d",&value[merker]);
merker++;
}
printf("Neuen Platz reservieren (0=Ende) : ");
scanf("%d",&more);
temp=(int *)malloc(size*sizeof(int));
if(NULL == temp)
{
printf("Kann keinen Speicher mehr reservieren!\n");
exit(0);
}
for(i=0; i<size; i++)
temp[i]=value[i];
size+=more;
value=(int *)malloc(size*sizeof(int));
if(NULL == value)
{
printf("Kann keinen Speicher mehr reservieren!\n");
exit(0);
}
for(i=0; i<size; i++)
value[i]=temp[i];
}while(more!=0);
printf("Hier Ihre Werte\n");
for(i=0; i<size; i++)
printf("value[%d] = %d\n",i,value[i]);
return 0;
}

Abbildung 17.8: Dynamisch reserviertes Array dynamisch erweitern
Bevor Sie für das bereits dynamisch reservierte Array erneut Speicherplatz reservieren können, müssen Sie die bereits eingegebenen Werte erst einmal in ein temporär alloziertes Array zwischenspeichern. Danach kann neuer Speicherplatz für das Array reserviert werden, worein anschließend die Werte aus dem temporären Array zurückkopiert werden. Das alles ist ziemlich aufwändig. Ihnen das jetzt anhand eines char-Arrays (Strings) zu demonstrieren, erspare ich mir zunächst.