Die heutige Lektion behandelt einige der fortgeschritteneren Aspekte der Speicherverwaltung in C-Programmen. Heute lernen Sie:
In C gehört jedes Datenobjekt einem bestimmten Typ an. Eine numerische Variable
kann vom Typ int
oder float
sein, ein Zeiger kann ein Zeiger auf den Typ double
oder char
sein und so weiter. In vielen Programmen ist die Kombination von
verschiedenen Typen in Ausdrücken und Anweisungen unumgänglich. Wie verfährt
man in solchen Situationen? Manchmal werden die verschiedenen Datentypen von C
automatisch angeglichen und Sie brauchen sich um nichts zu kümmern. In anderen
Fällen müssen Sie explizit einen Datentyp in einen anderen umwandeln, um
fehlerhafte Ergebnisse zu vermeiden. Damit wurden Sie bereits in früheren Lektionen
konfrontiert, als Sie den Typ eines void
-Zeigers vor der Verwendung des Zeigers in
einen anderen Typ umwandeln beziehungsweise konvertieren mussten. In diesen und
anderen Situationen sollten Sie immer eine klare Vorstellung davon haben, wann eine
explizite Typumwandlung notwendig ist und welche Fehler auftreten können, wenn
diese Umwandlung nicht vorgenommen wird. Die folgenden Abschnitte beschreiben
automatische und explizite Typumwandlungen in C.
Wie der Name schon verrät, werden automatische Typumwandlungen vom C- Compiler automatisch vorgenommen, ohne dass es irgendeines Eingriffs Ihrerseits bedarf. Sie sollten aber wissen, wann und wo der Compiler automatische Typumwandlungen vornimmt, damit Sie verstehen, wie C Ausdrücke auswertet.
Wenn ein C-Ausdruck ausgewertet wird, hat das Ergebnis des Ausdrucks einen
bestimmten Datentyp. Sind dabei alle Komponenten des Ausdrucks vom gleichen
Typ, wird auch das Ergebnis diesen Typ aufweisen. Wenn zum Beispiel x
und y
beide
vom Typ int
sind, wird das Ergebnis des folgenden Ausdrucks vom Typ int
sein:
x + y
Was aber, wenn die Komponenten eines Ausdrucks unterschiedliche Typen aufweisen? In diesem Fall erhält der Ausdruck den gleichen Typ wie die Komponente mit dem umfangreichsten Wertebereich. Daraus ergibt sich folgende Reihenfolge der numerischen Datentypen:
char
int
long
float
double
Nach dieser Regel wird ein Ausdruck, der einen int
- und einen char
-Wert umfasst, als
Typ int
ausgewertet, ein Ausdruck, der einen long
- und einen float
-Wert umfasst, als
Typ float
und so weiter.
Innerhalb der Ausdrücke werden die Operanden der binären Operatoren paarweise aneinander angeglichen, wobei wiederum gilt, dass der Operand mit dem kleineren Wertebereich in den Typ des Operanden mit dem umfangreicheren Wertebereich umgewandelt wird. Im Englischen bezeichnet man diese Form der automatischen Typumwandlung daher auch als »Promotion« (Beförderung). Natürlich entfällt die Promotion, wenn beide Operanden den gleichen Typ aufweisen. Wenn nicht, folgt die Promotion den folgenden Regeln:
double
ist, wird der andere Operand in den
Typ double
umgewandelt.
float
ist, wird der andere Operand in den
Typ float
umgewandelt.
long
ist, wird der andere Operand in den
Typ long
umgewandelt.
Wenn zum Beispiel x
vom Typ int
und y
vom Typ float
ist, wird die Auswertung des
Ausdrucks x/y
dazu führen, dass x
vor der Auswertung des Ausdrucks in den Typ
float
umgewandelt wird. Das bedeutet jedoch nicht, dass der Typ der Variablen x
geändert wird. Es bedeutet nur, dass von x
eine Kopie des Typs float
angelegt und in
der Auswertung des Ausdrucks verwendet wird. Der Wert des Ausdrucks ist, wie Sie
gerade gelernt haben, ebenfalls vom Typ float
. Entsprechend würde, wenn x
vom
Typ double
und y
vom Typ float
wäre, y
in den Typ double
umgewandelt.
Eine Typumwandlung kann auch mit Hilfe des Zuweisungsoperators erfolgen. Der
Ausdruck auf der rechten Seite der Zuweisung wird immer auf den Typ des
Datenobjekts links des Zuweisungsoperators gesetzt. Beachten Sie, dass nach dieser
Regel die Typumwandlung auch leicht eine »Degradierung« sein kann. Wenn f
vom
Typ float
und i
vom Typ int
ist, wird i
in der folgenden Zuweisung in den Typ float
umgewandelt:
f = i;
Im Vergleich dazu wird in der Zuweisung
i = f;
f
zum Typ int
herabgestuft. Der Nachkommateil geht bei der Zuweisung an i
verloren. Denken Sie jedoch daran, dass f
selbst nicht verändert wird. Die
Typumwandlung betrifft nur die Kopie dieses Wertes. So hat die Variable i
nach der
Ausführung der folgenden Anweisungen:
float f = 1.23;
int i;
i = f;
den Wert 1
und f
immer noch den Wert 1.23
. Wie dieses Beispiel zeigt, geht der
Nachkommateil verloren, wenn eine Fließkommazahl in einen Integer-Typ konvertiert
wird.
Auch sollten Sie sich vor Augen halten, dass bei der Umwandlung eines Integer-Typs
in einen Fließkommatyp der resultierende Fließkommawert nicht immer genau mit
dem Integer-Wert übereinstimmen muss. Das liegt daran, dass das Fließkommaformat,
das intern vom Computer verwendet wird, nicht jede mögliche Integer-Zahl absolut
exakt darstellen kann. Deshalb möchte ich Ihnen raten, Integer-Werte in Variablen
vom Typ int
oder long
unterzubringen.
Diese Art von Typumwandlung verwendet den Umwandlungsoperator, mit dem man explizit steuern, kann, wo im Programm Typumwandlungen vorgenommen werden sollen. Eine explizite Typumwandlung besteht aus einem in Klammern gesetzten Typnamen vor einem Ausdruck. Umgewandelt werden können arithmetische Ausdrücke und Zeiger. Das Ergebnis ist, dass der Ausdruck in den Typ konvertiert wird, der in der Typumwandlung angegeben wurde. Auf diese Art und Weise können Sie die Typen der Ausdrücke selbst festlegen und müssen sich nicht auf die automatischen Typumwandlungen in C verlassen.
Durch die Typumwandlung eines arithmetischen Ausdrucks teilen Sie dem Compiler
mit, den Wert des Ausdrucks in einer bestimmten Form darzustellen. Man kann fast
sagen, dass die explizite Typumwandlung der oben besprochenen Promotion sehr
ähnlich ist. Allerdings haben Sie hierbei die Möglichkeit, selbst aktiv zu werden, und
müssen nicht alles dem Compiler überlassen. Wenn zum Beispiel i
vom Typ int
ist,
dann wird mit dem Ausdruck
(float)i
i
in den Typ float
umgewandelt. Mit anderen Worten, das Programm legt eine
interne Kopie des Wertes von i
im Fließkommaformat an.
Wann bietet es sich an, eine Typumwandlung für einen arithmetischen Ausdruck durchzuführen? Am häufigsten nutzt man die Typumwandlung, um zu verhindern, dass bei einer Integer-Division der Nachkommateil verloren geht. Listing 18.1 soll dies verdeutlichen. Kompilieren Sie das Programm und führen Sie es aus.
Listing 18.1: Wenn ein Integer durch einen anderen geteilt wird, geht der Nachkommateil der Antwort verloren.
1: #include <stdio.h>
2:
3: int main(void)
4: {
5: int i1 = 100, i2 = 40;
6: float f1;
7:
8: f1 = i1/i2;
9: printf("%f\n", f1);
10: return(0);
11: }
2.000000
Die vom Programm ausgegebene Antwort lautet 2.000000
, obwohl 100/40 den Wert
2,5 ergibt. Was ist passiert? Der Ausdruck i1/i2
in Zeile 8 enthält zwei Variablen vom
Typ int
. Nach den oben beschriebenen Regeln ist der Wert des Ausdrucks i1/i2
ebenfalls vom Typ int
, und da dieser Typ nur ganze Zahlen darstellen kann, geht der
Nachkommateil der Antwort verloren.
Sie könnten erwarten, dass durch die Zuweisung des Ergebnisses der Division i1/i2
an
eine float
-Variable das Ergebnis zu einem float
-Wert befördert wird. Das stimmt auch,
aber es ist leider zu spät, denn der Nachkommateil der Antwort ist bereits verloren.
Um diese Art von Fehler zu vermeiden, müssen Sie eine der int
-Variablen in den Typ
float
umwandeln. Und wenn Sie sich noch an unsere Regeln von oben erinnern,
dann hat die float
-Typumwandlung der einen Variablen die automatische
Umwandlung der anderen Variable in den Typ float
zur Folge. Auf diese Weise bleibt
der Nachkommateil der Antwort erhalten. Sie können sich dies veranschaulichen,
indem Sie die Zuweisung in der Zeile 8 des Quelltextes wie folgt ändern:
f1 = (float)i1/i2;
Anschließend wird das Programm die korrekte Antwort ausgeben.
Sie haben bereits eine Einführung in die Typumwandlung von Zeigern erhalten. Ein
void
-Zeiger ist ein generischer Zeiger, der auf alles zeigen kann. Bevor Sie einen void
-
Zeiger verwenden können, müssen Sie ihn jedoch in den entsprechenden Typ
umwandeln. Beachten Sie, dass Sie den Typ eines Zeigers nicht umwandeln müssen,
um ihm einen Wert zuzuweisen oder ihn mit NULL
zu vergleichen. Die Typumwandlung
wird allerdings erforderlich, wenn Sie den Zeiger dereferenzieren oder nach den
Regeln der Zeigerarithmetik manipulieren wollen.
Die C-Bibliothek enthält Funktionen für die Speicherallokation zur Laufzeit - eine
Technik, die auch dynamische Speicherallokation genannt wird und erhebliche
Vorteile gegenüber der Reservierung von Speicher durch die Deklaration von
Variablen, Strukturen und Arrays haben kann. Die letztgenannte Methode, auch
statische Speicherallokation genannt, setzt voraus, dass Sie schon beim Aufsetzen
des Programms wissen, wie viel Speicher Sie benötigen. Dank der dynamischen
Speicherallokation kann das Programm reagieren, wenn es - beispielsweise aufgrund
von Benutzereingaben - zur Laufzeit erforderlich wird, weiteren Speicher
anzufordern. Alle Funktionen, die mit der dynamischen Speicherallokation zu tun
haben, erfordern die Einbindung der Header-Datei stdlib.h
(je nach Compiler muss
zusätzlich malloc.h
eingebunden werden). Beachten Sie, dass alle
Speicherreservierungsfunktionen einen Zeiger vom Typ void
zurückgeben. Zeiger
dieses Typs müssen vor der Verwendung in den Typ der Objekte umgewandelt
werden, für die der Speicher reserviert wurde.
Bevor wir uns den Details widmen, möchte ich einige allgemeine Worte zu der Speicherallokation verlieren. Was genau ist unter der Reservierung von Speicher zu verstehen? Jeder Computer verfügt über fest installierten Arbeitsspeicher mit wahlfreiem Zugriff (RAM für Random Access Memory). Die Größe dieses Speicherbereichs variiert von Computer zu Computer. Wann immer Sie ein Programm ausführen, sei es ein Textverarbeitungsprogramm, ein Grafikprogramm oder ein selbst geschriebenes C-Programm, wird dieses Programm von der Platte in den Arbeitsspeicher des Computers geladen. Der Speicherbereich, den das Programm belegt, umfasst den Programmcode sowie die gesamten statischen Daten des Programms, das heißt die Datenelemente, die im Quelltext deklariert sind. Der übrig gebliebene Arbeitsspeicher steht für die dynamische Allokation mit Hilfe der in diesem Abschnitt beschriebenen Funktionen zur Verfügung.
Wie viel Speicher steht genau für die Allokation zur Verfügung? Das ist unterschiedlich. Wenn Sie ein großes Programm auf einem System ausführen, das nur eine begrenzte Speicherkapazität hat, ist der freie Speicher relativ klein. Wenn Sie jedoch ein kleines Programm auf einem Multi-Megabyte-System ausführen, steht jede Menge Speicher zur Verfügung. Letztendlich bedeutet dies, dass Ihre Programme nicht vorhersehen können, wie viel Speicher genau zur Verfügung steht. Wenn eine Funktion zur Speicherallokation aufgerufen wird, müssen Sie ihren Rückgabewert prüfen, um sicherzustellen, dass die Speicherreservierung erfolgreich war. Außerdem müssen Ihre Programme damit umgehen können, dass eine Anfrage auf Reservierung von Speicher fehlschlägt. Leider ist Linux ein Multitasking- und Mehrbenutzer- Betriebssystem, das von virtuellem Speicher Gebrauch macht (Festplattenspeicher, der als temporärer Speicher genutzt wird). Deshalb gibt es auch keine allgemeine, zuverlässige Methode, um genau festzustellen, wie viel Speicher verfügbar ist. Doch normalerweise stellt das kein Problem dar.
Beachten Sie auch, dass die Verfügbarkeit von Speicher vom jeweils verwendeten Betriebssystem abhängen kann. Linux und andere Mitglieder der Unix-Familie verhalten sich im Großen und Ganzen gleich. Andere Betriebssysteme handhaben die Speicherverwaltung unter Umständen etwas anders. Im Allgemeinen sollten die betriebssystemspezifischen Unterschiede bei der Speicherreservierung für Sie jedoch nicht relevant sein. Wenn Sie eine der C-Funktionen zur Speicherreservierung verwenden, kann der Aufruf entweder erfolgreich sein oder fehlschlagen. Sie selbst brauchen sich über die dahinter stehenden Details keine Gedanken zu machen.
An einem der vorangehenden Tage haben Sie bereits gelernt, wie man mit Hilfe der
Bibliotheksfunktion malloc()
Speicherplatz für Strings reserviert. Diese Funktion ist
jedoch nicht darauf beschränkt, Speicherplatz für Strings zu reservieren; sie kann
Speicher zu jedem Zweck reservieren. Der Trick ist, dass malloc()
den Speicherplatz
byteweise reserviert. Zur Erinnerung, der Prototyp von malloc()
lautet:
void *malloc(size_t num);
Das Argument size_t
ist in stdlib.h
als unsigned_int
oder unsigned
long
definiert.
Die malloc()
-Funktion reserviert num
Byte Speicherplatz und liefert einen Zeiger auf
das erste Byte zurück. Die Funktion liefert NULL
zurück, wenn der angeforderte
Speicherbereich nicht reserviert werden konnte. Gemäß dem ISO-Standard kann die
Bibliothek je nach System eine Reihe von Dingen machen, wenn num == 0
ist. So
kann die Bibliothek je nach Bedarf einen NULL-Zeiger oder einen gültigen Zeiger auf
einen bestimmten Bereich im Speicher zurückgeben. Die GNU-C-Bibliothek, die Teil
des GNU/Linux-Systems ist, verfolgt den zweiten Ansatz. Auch wenn ein gültiger
Zeiger zurückgegeben wurde, kann die Größe des Speicherbereichs, auf den der
Zeiger weist, nicht mit Sicherheit angegeben werden. Deshalb ist es nicht sicher, ihn
zu verwenden.
Die calloc()
-Funktion reserviert ebenfalls Speicher. Anstatt eine Gruppe von Byte zu
reservieren, wie das mit malloc()
geschieht, reserviert calloc()
eine Gruppe von
Objekten. Der Funktionsprototyp lautet:
void *calloc(size_t num, size_t size);
Das Argument num
ist die Anzahl der Objekte, die reserviert werden sollen, und size
ist die Größe (in Byte) eines jeden Objekts. Bei erfolgreicher Reservierung, wird der
reservierte Speicher initialisiert (auf 0
gesetzt), und die Funktion liefert einen Zeiger auf
das erste Byte zurück. Wenn die Reservierung fehlschlägt oder num
beziehungsweise
size
gleich 0
ist, liefert die Funktion NULL
zurück.
Listing 18.2 verdeutlich die Verwendung von calloc()
.
Listing 18.2: Mit der Funktion calloc() dynamisch Speicher reservieren.
1: /* Beispiel für calloc(). */
2:
3: #include <stdlib.h>
4: #include <stdio.h>
5:
6: int main(void)
7: {
8: unsigned anzahl;
9: int *zgr;
10:
11: printf("Für wie viele int-Werte soll Speicher reserviert werden: ");
12: scanf("%d", &anzahl);
13:
14: zgr = (int*)calloc(anzahl, sizeof(int));
15:
16: if (zgr != NULL)
17: puts("Die Speicherallokation war erfolgreich.");
18: else
19: puts("Die Speicherallokation ist fehlgeschlagen.");
20: return(0);
21: }
Für wie viele int-Werte soll Speicher reserviert werden: 100
Die Speicherallokation war erfolgreich.
Lassen Sie sich nicht dazu verführen, übermäßig große Werte am Befehlsprompt einzugeben. Wenn Sie Speicherplatz anfordern, der erheblich über der Größe des physisch verfügbaren Speichers liegt, kann das dazu führen, dass Linux alle gerade ausgeführten Anwendungen in den virtuellen Speicher auf der Festplatte umlagert. Anwendungen auf die Festplatte zu umlagern, kann etliche Minuten dauern. Während dieser Zeit wird Ihr Linux-Rechner nur sehr langsam auf Maus- und Tastaturbefehle reagieren. Doch nach einiger Zeit wird das Programm zu Ende sein und eine der beiden Meldungen ausgeben. Es kann jedoch einige Minuten dauern, bis Linux Ihr Programm beendet hat und die Anwendungen wieder vom virtuellen Speicher in den physischen Speicher schreibt.
Dieses Programm fordert Sie in Zeile 11 auf, einen Wert einzugeben. Diese Zahl legt
fest, wie viel Speicherbereich das Programm zu reservieren versucht. Das Programm
ist bemüht, so viel Speicher zu reservieren (Zeile 14), wie für die Aufnahme der
angegebenen Anzahl an int
-Variablen benötigt wird. Wenn die Speicherallokation
fehlschlägt, lautet der Rückgabewert von calloc()
NULL
. Andernfalls wird ein Zeiger
auf den reservierten Speicher zurückgeliefert. In unserem Programm wird der
Rückgabewert von calloc()
in den int
-Zeiger zgr
kopiert. Die if
-Anweisung in den
Zeilen 16 bis 19 prüft anhand des Wertes von zgr
, ob die Speicherallokation
erfolgreich war, und gibt eine entsprechende Meldung aus.
Mit der Funktion realloc()
können Sie die Größe eines Speicherblocks ändern, der
zuvor mit malloc()
oder calloc()
reserviert wurde. Der Funktionsprototyp lautet:
void *realloc(void *ptr, size_t size);
Das Argument ptr
ist ein Zeiger auf den ursprünglichen Speicherblock. Die neue
Größe in Byte wird in size
angegeben. Das Ergebnis von realloc()
hängt von
verschiedenen Faktoren ab:
ptr
zeigt, zu
erweitern, wird der zusätzliche Speicher reserviert, und die Funktion liefert ptr
zurück.
size
reserviert und die bestehenden Daten werden vom alten Block an den Anfang des
neuen Blocks kopiert. Der alte Block wird freigegeben und die Funktion liefert
einen Zeiger auf den neuen Block zurück.
ptr
-Argument NULL
ist, verhält sich die Funktion wie malloc()
, das
heißt, sie reserviert einen Block von size
Byte und liefert einen Zeiger darauf
zurück.
size
gleich 0
ist, wird der Speicher, auf den ptr
zeigt,
freigegeben und die Funktion liefert NULL
zurück.
NULL
zurück und der originale Block wird unverändert
beibehalten.
Listing 18.3 veranschaulicht den Einsatz der Funktion realloc()
.
Listing 18.3: Mit realloc() die Größe eines dynamisch reservierten Speicherblocks vergrößern.
1: /* Mit realloc() reservierten Speicher vergrößern. */
2:
3: #include <stdio.h>
4: #include <stdlib.h>
5: #include <string.h>
6:
7: int main(void)
8: {
9: char puffer[80], *meldung;
10:
11: /* Einen String einlesen. */
12:
13: puts("Geben Sie eine Textzeile ein.");
14: fgets(puffer,80,stdin);
15: puffer[strlen(puffer)-1] = 0; /* Entfernt Neue-Zeile-Zeichen. */
16:
17: /* Reserviert ersten Block und kopiert String dort hinein. */
18: meldung = realloc(NULL, strlen(puffer)+1);
19: strcpy(meldung, puffer);
20:
21: /* Meldung ausgeben. */
22:
23: puts(meldung);
24:
25: /* Liest einen weiteren String vom Anwender ein. */
26:
27: puts("Geben Sie eine weitere Textzeile ein.");
28: fgets(puffer,80,stdin);
29: puffer[strlen(puffer)-1] = 0; /*Entfernt Neue-Zeile-Zeichen.*/
30:
31: /* Vergrößert den Speicherblock und hängt dann den String an. */
32:
33: meldung = realloc(meldung,(strlen(meldung) + strlen(puffer)+1));
34: strcat(meldung, puffer);
35:
36: /* Gibt die neue Meldung aus. */
37: puts(meldung);
38: return(0);
39: }
Geben Sie eine Textzeile ein.
Dies ist die erste Textzeile.
Dies ist die erste Textzeile.
Geben Sie eine weitere Textzeile ein.
Dies ist die zweite Textzeile.
Dies ist die erste Textzeile.Dies ist die zweite Textzeile.
Dieses Programm liest in Zeile 14 einen eingegebenen String in ein Zeichenarray
namens puffer
ein. Anschließend wird der String an die Speicherstelle kopiert, auf die
meldung
zeigt (Zeile 19). Der Speicher, auf den meldung
verweist, wurde in Zeile 18 mit
Hilfe von realloc()
reserviert. Die Funktion realloc()
kann auch dann verwendet
werden, wenn es keine vorangehende Speicherallokation gab. Durch die Übergabe
von NULL
als ersten Parameter weiß realloc()
, dass dies die erste Allokation ist.
Zeile 28 liest einen zweiten String in den Puffer puffer
ein. Dieser String wird an den
bereits in meldung
abgelegten String angehängt. Da meldung
gerade groß genug ist, um
den ersten String aufzunehmen, muss der Speicherbereich neu allokiert werden, um
genügend Raum für den ersten und zweiten String zu enthalten. Dies geschieht in
Zeile 33. Am Ende des Programms wird der neue verkettete String ausgegeben.
Speicher der mit malloc()
oder calloc()
reserviert wird, wird vom dynamischen
Speicher-Pool abgezweigt. Dieser Pool wird manchmal auch Heap genannt und ist
vom Umfang begrenzt. Wenn Ihr Programm einen bestimmten dynamisch
reservierten Speicherbereich nicht mehr benötigt, sollten Sie ihn deallokieren oder
freigeben, damit er im weiteren Verlauf erneut verwendet werden kann. Um Speicher
freizugeben, der dynamisch reserviert wurde, gibt es die Funktion free()
. Ihr Prototyp
lautet:
void free(void *ptr);
Mit free()
geben Sie den Speicher, auf den ptr
zeigt, wieder frei. Dieser Speicher
muss mit malloc()
, calloc()
oder realloc()
reserviert worden sein. Wenn ptr
gleich
NULL
ist, bewirkt free()
nichts. Ein Beispiel für free()
finden Sie in Listing 18.4.
Listing 18.4: Mit free() zuvor dynamisch reservierten Speicher freigeben.
1: /* Mit free() dynamisch reservierten Speicher freigeben. */
2:
3: #include <stdio.h>
4: #include <stdlib.h>
5: #include <string.h>
6:
7: #define BLOCKGROESSE 30000
8:
9: int main(void)
10: {
11: void *zgr1, *zgr2;
12:
13: /* Einen Block reservieren. */
14:
15: zgr1 = malloc(BLOCKGROESSE);
16:
17: if (zgr1 != NULL)
18: printf("Erste Allokation von %d Byte erfolgreich.\n"
,BLOCKGROESSE);
19: else
20: {
21: printf("Allokation von %d Byte misslungen.\n",
BLOCKGROESSE);
22: exit(1);
23: }
24:
25: /* Weiteren Block allokieren. */
26:
27: zgr2 = malloc(BLOCKGROESSE);
28:
29: if (zgr2 != NULL)
30: {
31: /* Bei erfolgreicher Allokation Meldung ausgeben u. beenden */
32:
33: printf("Zweite Allokation von %d Byte erfolgreich.\n",
34: BLOCKGROESSE);
35: exit(0);
36: }
37:
38: /* Wenn nicht erfolgreich, ersten Block freigeben und
erneut versuchen.*/
39:
40: printf("Zweiter Versuch, %d Byte zu reservieren, misslungen.\n"
,BLOCKGROESSE);
41: free(zgr1);
42: printf("\nErster Block wurde freigegeben.\n");
43:
44: zgr2 = malloc(BLOCKGROESSE);
45:
46: if (zgr2 != NULL)
47: printf("Nach free(), Allokation von %d Byte erfolgreich.\n",
48: BLOCKGROESSE);
49: return(0);
50: }
Erste Allokation von 30000 Byte erfolgreich.
Zweite Allokation von 30000 Byte erfolgreich.
Dieses Programm versucht, zwei Speicherblöcke dynamisch zu reservieren. Die
Konstante BLOCKGROESSE
wird definiert, um festzulegen, wie viel Speicher zu
reservieren ist. Zeile 15 nimmt die erste Allokation mit malloc()
vor. Die Zeilen 17 bis
23 prüfen anhand des Rückgabewertes, ob die Speicherreservierung erfolgreich war
(Rückgabewert ungleich NULL
) oder nicht, und geben eine entsprechende Meldung aus.
Wenn die Allokation fehlgeschlagen ist, wird das Programm beendet. Zeile 27
versucht, einen zweiten Speicherblock zu reservieren, wobei erneut geprüft wird, ob
die Allokation erfolgreich war (Zeilen 29 bis 36). War die zweite Allokation
erfolgreich, wird das Programm mit einem Aufruf von exit()
beendet. Andernfalls
wird eine Meldung ausgegeben, die den Anwender über das Scheitern der
Speicherreservierung informiert. Der erste Block wird dann mit free()
wieder
freigegeben (Zeile 41), und es wird ein neuer Versuch, den zweiten Block zu
reservieren, gestartet.
Auf Systemen wie Linux, die über einen großen virtuellen Speicher verfügen, sollten die beiden Allokationen immer erfolgreich sein. Auf Systemen mit weniger Speicher könnte die Ausgabe des Programms wie folgt aussehen:
Erste Allokation von 30000 Byte erfolgreich.
Zweiter Versuch, 30000 Byte zu reservieren, misslungen.
Erster Block wurde freigegeben.
Nach free(), Allokation von 30000 Byte erfolgreich.
Die bisherigen Abschnitte haben Ihnen gezeigt, wie man Speicherblöcke reserviert und wieder freigibt. Die C-Bibliothek enthält aber auch Funktionen, mit denen man Speicherblöcke manipulieren kann - zum Beispiel alle Byte in einem Block auf einen bestimmten Wert setzen oder Informationen von einer Stelle zu einer anderen verschieben.
Um alle Bytes in einem Speicherblock auf einen bestimmten Wert zu setzen, können
Sie memset()
verwenden. Der Funktionsprototyp lautet:
void * memset(void *dest, int c, size_t count);
Das Argument dest
zeigt auf den Speicherblock. c
ist der Wert, der zugewiesen
werden soll, und count
ist die Anzahl Bytes, die ab dest
gesetzt werden soll. Beachten
Sie, dass, obwohl c
vom Typ int
ist, es wie vom Typ char
behandelt wird. Mit
anderen Worten, nur das niedere Byte wird verwendet, und Sie können für c
nur
Werte zwischen 0
und 256
angeben.
Nutzen Sie memset()
, um einen Speicherblock mit einem speziellen Wert zu
initialisieren. Da diese Funktion nur einen Initialisierungswert vom Typ char
verwenden kann, ist es nicht besonders sinnvoll, mit Blöcken zu arbeiten, die diesem
Datentyp nicht entsprechen, es sei denn, Sie wollen mit 0
initialisieren. Mit anderen
Worten, es wäre nicht besonders effizient, mit memset()
ein Array vom Typ int
mit
dem Wert 99
zu initialisieren, aber Sie könnten alle Array-Elemente mit dem Wert 0
initialisieren. Ein Beispiel für memset()
finden Sie in Listing 18.5.
memcpy()
kopiert Datenbytes zwischen Speicherblöcken, manchmal auch Puffer
genannt, hin und her. Die Funktion schenkt dem Typ der zu kopierenden Daten keine
Beachtung - sie erstellt einfach eine exakte Byte-für-Byte-Kopie. Der
Funktionsprototyp lautet:
void *memcpy(void *dest, void *src, size_t count);
Die Argumente dest
und src
zeigen jeweils auf den Ziel- und Quellspeicherblock. count
gibt die Anzahl der zu kopierenden Bytes an. Der Rückgabewert ist dest
. Wenn sich
die beiden Speicherblöcke überlappen, arbeitet die Funktion eventuell nicht
ordnungsgemäß - einige der Daten in src
können überschrieben werden, bevor eine
Kopie davon erstellt wird. Verwenden Sie deshalb für überlappende Speicherblöcke die
nachfolgend wird Funktion memmove()
. Ein Beispiel für memcpy()
finden Sie in Listing
18.5.
memmove()
ist memcpy()
sehr ähnlich. Auch hiermit wird eine angegebene Anzahl von
Byte von einem Speicherblock in einen anderen kopiert. Sie ist jedoch flexibler, da in
dieser Funktion überlappende Speicherblöcke keine Schwierigkeit darstellen. Da
memmove()
den gleichen Leistungsumfang wie memcpy()
hat, darüber hinaus aber auch
mit überlappenden Blöcken umgehen kann, werden Sie nur sehr selten, wenn
überhaupt, eine Veranlassung haben, memcpy()
zu verwenden. Der Prototyp von
memmove()
lautet:
void *memmove(void *dest, void *src, size_t count);
Die Argumente dest
und src
zeigen jeweils auf den Ziel- und Quellspeicherblock.
count
gibt die Anzahl der zu kopierenden Bytes an. Der Rückgabewert ist dest
. Wenn
sich die zwei Speicherblöcke überlappen, stellt diese Funktion zuvor sicher, dass die
Quelldaten in dem überlappten Bereich kopiert wurden, bevor sie überschrieben
werden. In Listing 18.5 finden Sie Beispiele für memset()
, memcpy()
und memmove()
.
Listing 18.5: Ein Beispiel für memset(), memcpy() und memmove().
1 : /* Beispiele für memset(), memcpy()und memmove(). */
2 :
3 : #include <stdio.h>
4 : #include <string.h>
5 :
6 : char meldung1[60] = " Vier Hunde und sieben kleine Katzen...";
7 : char meldung2[60] = "abcdefghijklmnopqrstuvwxyz";
8 : char temp[60];
9 :
10: int main(void)
11: {
12: printf("meldung1[] vor memset():\t%s\n", meldung1);
13: memset(meldung1 + 5, '@', 10);
14: printf("\nmeldung1[] nach memset():\t%s\n", meldung1);
15:
16: strcpy(temp, meldung2);
17: printf("\nOriginalmeldung: %s\n", temp);
18: memcpy(temp + 4, temp + 16, 10);
19: printf("Nach memcpy() ohne Überlappung:\t%s\n", temp);
20: strcpy(temp, meldung2);
21: memcpy(temp + 6, temp + 4, 10);
22: printf("Nach memcpy() mit Überlappung:\t%s\n", temp);
23:
24: strcpy(temp, meldung2);
25: printf("\nOriginalmeldung: %s\n", temp);
26: memmove(temp + 4, temp + 16, 10);
27: printf("Nach memmove() ohne Überlappung:\t%s\n", temp);
28: strcpy(temp, meldung2);
29: memmove(temp + 6, temp + 4, 10);
30: printf("Nach memmove() mit Überlappung:\t%s\n", temp);
31: return (0);
32: }
meldung1[] vor memset(): Vier Hunde und sieben kleine Katzen...
meldung1[] nach memset(): Vier@@@@@@@@@@ sieben kleine Katzen...
Originalmeldung: abcdefghijklmnopqrstuvwxyz
Nach memcpy() ohne Überlappung: abcdqrstuvwxyzopqrstuvwxyz
Nach memcpy() mit Überlappung: abcdefefefijijmnqrstuvwxyz
Originalmeldung: abcdefghijklmnopqrstuvwxyz
Nach memmove() ohne Überlappung: abcdqrstuvwxyzopqrstuvwxyz
Nach memmove() mit Überlappung: abcdefefghijklmnqrstuvwxyz
Die Funktionsweise von memset()
ist einfach. Beachten Sie, wie wir mit Hilfe des
Zeigerausdrucks meldung1 + 5
die Funktion memset()
anweisen, erst ab dem sechsten
Zeichen von meldung1[]
mit dem Setzen des Klammeraffen zu beginnen (zur
Erinnerung, Arrays beginnen mit dem Index Null
). Als Ergebnis werden das sechste
bis fünfzehnte Zeichen von meldung1[]
durch den Klammeraffen @
ersetzt.
Wenn Quelle und Ziel nicht überlappen, gibt es mit memcpy()
keine Probleme. Die
zehn Zeichen von temp[]
, die bei Position 17 beginnen (die Buchstaben q bis z)
wurden in die Positionen 5 bis 14 kopiert. An diesen Stellen haben sich zuvor die
Buchstaben e bis n befunden. Wenn sich jedoch Quelle und Ziel überlappen, sehen die
Dinge anders aus. Wenn die Funktion versucht, zehn Zeichen ab Position 4 an die
Position 6 zu kopieren, kommt es zu einer Überlappung an acht Positionen. Ihren
Erwartungen zufolge sollten die Buchstaben e bis n über die Buchstaben g bis p
kopiert werden, aber statt dessen werden die Buchstaben e und f fünfmal wiederholt.
Gibt es keine Überlappung, funktioniert memmove()
wie memcpy()
. Im Falle einer
Überlappung jedoch kopiert memmove()
die ursprünglichen Quellzeichen in das Ziel.
Wie Sie vielleicht wissen, ist die elementarste Speichereinheit für Computerdaten das Bit. Es gibt Zeiten, da ist es sehr nützlich, wenn Sie wissen, wie man die einzelnen Bits in den Daten Ihres C-Programms manipuliert. In C gibt es dafür einige Möglichkeiten.
Die Bit-Operatoren von C ermöglichen es Ihnen, die einzelnen Bits von Integer-
Variablen zu manipulieren. Zur Erinnerung, ein Bit ist die kleinstmögliche Einheit zur
Datenspeicherung und kann nur einen von zwei Werten annehmen: 0
oder 1
. Die Bit-
Operatoren können nur mit Integer-Typen verwendet werden: char
, int
und long
.
Bevor wir mit diesem Abschnitt fortfahren, sollten Sie mit der Binärnotation vertraut
sein - der Notation, die der Computer intern zur Codierung und Speicherung von
Integer-Werten verwendet.
Die Bit-Operatoren werden am häufigsten dann verwendet, wenn Ihr C-Programm direkt mit der Hardware Ihres Systems interagiert - ein Thema, das hier aus Platzgründen leider nicht besprochen werden kann. Es gibt aber auch noch andere Anwendungsbereiche, die ich in den folgenden Abschnitten vorstellen möchte.
Es gibt zwei Shift-Operatoren, die die Bits in einer Integer-Variablen um eine
angegebene Anzahl an Positionen verschieben. Der <<
-Operator verschiebt die Bits
nach links und der >>
-Operator nach rechts. Die Syntax für diese binären Operatoren
lautet:
x << n
x >> n
Jeder dieser Operatoren verschiebt die Bits in x
um n
Positionen in die angegebene
Richtung. Bei einer Rechtsverschiebung werden die n
höheren Bits der Variable mit
Nullen aufgefüllt. Bei einer Linksverschiebung werden die n
niederen Bits der Variable
mit Nullen aufgefüllt.
Binär 00001100 (dezimal 12) wird nach Rechtsverschiebung um 2 zu binär 00000011 (dezimal 3).
Binär 00001100 (dezimal 12) wird nach Linksverschiebung um 3 zu binär 01100000 (dezimal 96).
Binär 00001100 (dezimal 12) wird nach Rechtsverschiebung um 3 zu binär 00000001 (dezimal 1).
Binär 00110000 (dezimal 48) wird nach Linksverschiebung um 3 zu binär 10000000 (dezimal 128).
Unter bestimmten Umständen können die Shift-Operatoren für eine Division oder
Multiplikation einer Integer-Variablen um ein Vielfaches von 2 verwendet werden. Die
Linksverschiebung eines Integers um n Stellen hat den gleichen Effekt wie die
Multiplikation mit 2n, und die Rechtsverschiebung eines Integers hat den gleichen
Effekt wie die Division durch 2n. Die Ergebnisse einer Multiplikation durch
Linksverschiebung sind nur dann korrekt, wenn es keinen Überlauf gibt - das heißt,
wenn keine Bits aus den höheren Positionen geschoben werden und so »verloren
gehen«. Eine Division durch Rechtsverschiebung ist eine Integer-Division, in der der
Nachkommateil des Ergebnisses verloren geht. Wenn Sie zum Beispiel den Wert 5
(binär 00000101
) mit der Absicht, durch 2 zu dividieren, nach rechts verschieben, lautet
das Ergebnis 2
(binär 00000010
) anstatt des korrekten Wertes 2.5
, da der
Nachkommateil (.5
) verloren gegangen ist. Listing 18.6 zeigt den Einsatz der Shift-
Operatoren.
Listing 18.6: Die Shift-Operatoren.
1 : /* Beispiel für die Shift-Operatoren. */
2 :
3 : #include <stdio.h>
4 :
5 : int main(void)
6 : {
7 : unsigned char y, x = 255;
8 : int count;
9 :
10: printf("%s %15s %13s\n","Dezimal","Linkssverschiebung","Ergebnis");
11:
12: for (count = 1; count < 8; count++)
13: {
14: y = x << count;
15: printf("%6d %12d %16d\n", x, count, y);
16: }
17: printf("%s %16s %13s\n","Dezimal","Rechtsverschiebung","Ergebnis");
18:
19: for (count = 1; count < 8; count++)
20: {
21: y = x >> count;
22: printf("%6d %12d %16d\n", x, count, y);
23: }
24: return(0);
25: }
Dezimal Linksverschiebung Ergebnis
255 1 254
255 2 252
255 3 248
255 4 240
255 5 224
255 6 192
255 7 128
Dezimal Rechtsverschiebung Ergebnis
255 1 127
255 2 63
255 3 31
255 4 15
255 5 7
255 6 3
255 7 1
Es gibt drei logische Bit-Operatoren, mit denen die einzelnen Bits in einem Integer-
Datentyp manipuliert werden können (siehe Tabelle 18.1). Diese Operatoren tragen
zum Teil die gleichen Namen wie die logischen WAHR
/FALSCH
-Operatoren, die Sie
bereits früher kennen gelernt haben, unterscheiden sich aber in der Funktionsweise.
Dies alles sind binäre Operatoren, die die Bits im Ergebnis je nach den Bits in den
Operanden auf 1
oder 0
setzen:
AND
setzt das Bit im Ergebnis nur dann auf 1
, wenn die
entsprechenden Bits in beiden Operanden 1
sind. Andernfalls wird das Bit auf 0
gesetzt. Der AND
-Operator wird dazu verwendet, um ein oder mehrere Bits in
einem Wert auszuschalten oder zu löschen.
OR
setzt das Bit im Ergebnis nur dann auf 0
, wenn die
entsprechenden Bits in beiden Operanden 0
sind. Andernfalls wird das Bit auf 1
gesetzt. Der OR
-Operator wird dazu verwendet, um ein oder mehrere Bit in einem
Wert anzuschalten oder zu setzen.
OR
setzt das Bit in dem Ergebnis auf 1
, wenn die
entsprechenden Bits in den Operanden unterschiedlich sind (das heißt, wenn
eines 1
und das andere 0
ist). Andernfalls wird das Bit auf 0
gesetzt.
Im Folgenden sehen Sie einige Beispiele für die Arbeitsweise dieser Operatoren:
Was bedeutet es, wenn man mit Hilfe der Bit-Operatoren Bits in einem Integer-Wert
setzt beziehungsweise löscht? Angenommen Sie haben eine Variable vom Typ char
und wollen, dass die Bits in den Positionen 0 und 4 gelöscht werden (das heißt gleich
0
werden), während die anderen Bits ihre ursprünglichen Werte beibehalten sollen.
Wenn Sie die Variable mit Hilfe des AND
-Operators mit dem binären Wert 11101110
verrechnen, erhalten Sie das gewünschte Ergebnis. Der Ablauf ist folgender:
An jeder Position, an der im zweiten Wert eine 1
steht, wird das Ergebnis den gleichen
Wert (1
oder 0
) haben, der an dieser Position in der ursprünglichen Variablen steht:
0 & 1 == 0
1 & 1 == 1
An jeder Position, an der im zweiten Wert eine 0
steht, wird das Ergebnis auf 0
gesetzt, unabhängig von dem Wert, der an dieser Stelle in der ursprünglichen
Variablen gestanden hat:
0 & 0 == 0
1 & 0 == 0
Das Setzen von Bits mit OR
funktioniert ähnlich. An jeder Position, an der im zweiten
Wert eine 1
steht, wird das Ergebnis eine 1
sein, und an jeder Position, an der im
zweiten Wert eine 0
steht, bleibt das Ergebnis unverändert.
0 | 1 == 1
1 | 1 == 1
0 | 0 == 0
1 | 0 == 1
Der letzte Bit-Operator ist der Komplement-Operator ~
. Dabei handelt es sich um
einen unären Operator. Seine Aufgabe besteht darin, jedes Bit in seinem Operanden
umzukehren, das heißt alle 0-
en in 1-
en umzuwandeln und umgekehrt. So würde ~254
(binär 11111110
) zu 1
(binär 00000001
) ausgewertet.
Alle Beispiele in diesem Abschnitt beruhen auf Variablen vom Typ char
, die 8 Bit
enthalten. Das Gelernte lässt sich jedoch direkt auf größere Variablen, wie vom Typ
int
oder vom Typ long
, übertragen.
Das letzte Thema, das ich im Kontext der Bitprogrammierung ansprechen möchte, ist der Einsatz von Bitfeldern in Strukturen. Am Tag 10, »Strukturen«, haben Sie gelernt, wie Sie eigene Datenstrukturen definieren können, die den Daten Ihres Programms entsprechen. Durch den Einsatz von Bitfeldern können Sie noch eine genauere Anpassung vornehmen und darüber hinaus Speicherplatz sparen.
Ein Bitfeld ist ein Strukturelement, das eine bestimmte Anzahl an Bits enthält. Sie können ein Bitfeld mit einem, zwei oder einer beliebig großen Zahl an Bits deklarieren, je nachdem wie viele Sie benötigen, um die im Feld zu speichernden Daten aufzunehmen. Welchen Vorteil können Sie daraus ziehen?
Angenommen Sie programmieren eine Angestelltendatenbank, in der Datensätze
über die Angestellten der Firma verwaltet werden. Viele der Informationen, die in der
Datenbank gespeichert werden, haben Ja/Nein-Charakter, wie zum Beispiel »Hat der
Angestellte an den zahnärztlichen Untersuchungen teilgenommen?« oder »Hat der
Angestellte einen Universitätsabschluss?« Jede Ja/Nein-Information kann in einem
einzigen Bit gespeichert werden, wobei 1
für Ja steht und 0
für Nein.
Unter Zugrundelegung der Standarddatentypen ist der Typ char
der kleinste Typ von
C, der in einer Struktur verwendet werden kann. Natürlich können Sie ein
Strukturelement vom Typ char
für die Aufnahme von Ja/Nein-Daten verwenden,
doch wären dann sieben der acht Bit des char
-Typs verschwendeter Speicherplatz.
Wenn Sie Bitfelder verwenden, können Sie acht Ja/Nein-Werte in einem einzigen
char
-Typ speichern.
Bitfelder sind nicht nur beschränkt auf Ja/Nein-Werte. Bauen wir unser
Datenbankbeispiel etwas weiter aus und stellen wir uns vor, dass Ihre Firma drei
besondere Sozialversicherungspläne anbietet. Ihre Datenbank soll speichern, unter
welchen Plan jeder einzelne Angestellt fällt (falls überhaupt). Sie könnten 0
für die
normale gesetzliche Sozialversicherung wählen und die Werte 1
, 2
und 3
für die drei
verschiedenen Pläne. Ein Bitfeld mit zwei Bit reicht aus, da zwei binäre Bit die Werte 0
bis 3
darstellen können. Entsprechend kann ein Bitfeld mit drei Bit Werte im Bereich
von 0
bis 7
aufnehmen, vier Bit den Wertebereich 0
bis 15
und so weiter.
Bitfelder erhalten eigene Namen, so dass man auf die Bitfelder in der gleichen Weise
zugreifen kann wie auf die regulären Strukturelemente. Alle Bitfelder sind vom Typ
unsigned
int
, die Größe des Feldes (in Bit) geben Sie an, indem Sie an den
Elementnamen einen Doppelpunkt und die Anzahl der Bits anhängen. Die Definition
einer Struktur, die ein 1-Bit-Element namens zahn
, ein weiteres 1-Bit-Element namens
uni
und ein 2-Bit-Element namens gesund
enthält, lautet folgendermaßen:
struct ang_daten {
unsigned zahn : 1;
unsigned uni : 1;
unsigned gesund : 2;
...
};
Die Ellipse (...
) soll andeuten, dass noch Platz für weitere Strukturelemente
vorhanden ist. Die Elemente können Bitfelder sein oder Felder, die aus regulären
Datentypen bestehen. Um auf die Bitfelder zuzugreifen, verwenden Sie, wie bei den
anderen Strukturelementen, den Punktoperator. Sie könnten obige Strukturdefinition
beispielsweise wie folgt erweitern, so dass sie richtig nützlich ist:
struct ang_daten {
unsigned zahn : 1;
unsigned uni : 1;
unsigned gesund : 2;
char vname[20];
char nname[20];
char svnummer[10];
};
Anschließend können Sie ein Array von Strukturen deklarieren:
struct ang_daten arbeiter[100];
und dem ersten Array-Element wie folgt Werte zuweisen:
arbeiter[0].zahn = 1;
arbeiter[0].uni = 0;
arbeiter[0].gesund = 2;
strcpy(arbeiter[0].vname, "Mildred");
Ihr Code wäre natürlich verständlicher, wenn Sie für die Arbeit mit 1-Bit-Feldern die
Werte 1
und 0
durch die symbolischen Konstanten JA
und NEIN
ersetzt hätten. Sie
behandeln jedoch auf alle Fälle jedes Bitfeld als einen kleinen vorzeichenlosen Integer
mit der angegebenen Anzahl an Bits. Der Wertebereich, der einem Bitfeld mit n Bit
zugewiesen werden kann, reicht von 0 bis 2n-1. Wenn Sie versuchen, einem Bitfeld
einen Wert außerhalb des Gültigkeitsbereichs zuzuweisen, erhalten Sie keine
Fehlermeldung vom Compiler, jedoch unvorhersehbare Ergebnisse.
Heute haben wir eine Reihe von fortgeschrittenen Themen behandelt. Sie haben
gelernt, wie man Speicher zur Laufzeit reserviert, neu reserviert und freigibt und die
Speicherallokation flexibler gestalten kann. Es wurde Ihnen gezeigt, wie und wann
man Typumwandlungen für Variablen und Zeiger einsetzt. Übersehene oder falsch
verwendete Typumwandlungen sind eine häufige und schwer aufzuspürende
Fehlerquelle. Es lohnt sich deshalb, dieses Thema zu wiederholen! Ich haben Ihnen
darüber hinaus die Funktionen memset()
, memmove()
und memcpy()
vorgestellt, mit
denen Sie Speicherblöcke manipulieren können. Zum Schluss habe ich Ihnen gezeigt,
wie Sie einzelne Bits in Ihren Programmen einsetzen und manipulieren können.
Frage:
Worin liegen die Vorteile der dynamischen Speicherallokation? Warum kann ich
den Speicherplatz, den ich benötige, nicht einfach in meinem Quelltext
deklarieren?
Antwort:
Wenn Sie all Ihren Speicherbedarf in Ihrem Quellcode deklarieren, ist der
verfügbare Speicher in Ihrem Programm fest. Sie müssen bereits im Vorhinein
beim Schreiben des Programms wissen, wie viel Speicher Sie benötigen. Dank
der dynamischen Speicherallokation kann Ihr Programm auf der Basis der
aktuellen Bedingungen und etwaiger Benutzereingabe die Steuerung des
Speicherbedarfs übernehmen.
Frage:
Warum soll ich überhaupt Speicher freigeben?
Antwort:
Wenn Sie Neuling in der C-Programmierung sind, werden Ihre Programme
voraussichtlich nicht sehr groß sein. Doch mit zunehmender Programmgröße
steigt der Speicherbedarf. Sie sollten versuchen, Programme zu schreiben, die
möglichst effizient mit dem Speicher umgehen. Dazu gehört, dass man
Speicher, den man nicht mehr benötig, wieder freigibt. Wenn Sie in einer
Multitasking-Umgebung arbeiten, kann es andere Anwendungen geben, die
den Speicher benötigen, den Sie nicht mehr brauchen.
Frage:
Was passiert, wenn ich einen String wiederverwende, ohne realloc()
aufzurufen?
Antwort:
Sie müssen realloc()
nicht aufrufen, wenn für den von Ihnen verwendeten
String genug Speicher reserviert wurde. Rufen Sie realloc()
auf, wenn Ihr
aktueller String nicht groß genug ist. Denken Sie daran, dass der C-Compiler
Ihnen fast alles durchgehen lässt, auch Dinge, die Sie nicht machen sollten!
Sie können einen String mit einem größeren String überschreiben, solange
die Länge des neuen Strings gleich oder kleiner als der reservierte
Speicherplatz des Originalstrings ist. Wenn jedoch der neue String größer ist,
überschreiben Sie den Speicher, der auf den Bereich folgt, der für den
Originalstring reserviert wurde. Wenn Sie Glück haben, ist der Speicherplatz
nicht belegt, wenn Sie Pech haben, stehen dort wichtige Daten. Wenn Sie
einen größeren Speicherabschnitt reservieren möchten, rufen Sie realloc()
auf.
Frage:
Welche Vorzüge haben die Funktionen memset()
, memcpy()
und memmove()
?
Warum kann ich nicht einfach eine Schleife mit einer Zuweisung verwenden, um
Speicher zu initialisieren oder zu kopieren?
Antwort:
In einigen Fällen können Sie eine Schleife mit einer Zuweisung verwenden,
um Speicher zu initialisieren. Ehrlich gesagt, ist dies in manchen Fällen der
einzige Weg - zum Beispiel wenn Sie alle Elemente eines float
-Array auf den
Wert 1.23
setzen wollen. In anderen Situationen, wo der Speicher nicht für
ein Array oder eine Liste reserviert wurde, sind die mem...()
-Funktionen die
einzige Möglichkeit. Schließlich gibt es Fälle, in denen eine Schleife und eine
Zuweisung möglich, aber die mem...()
-Funktionen einfacher und schneller
sind.
Frage:
Wann kommen die Shift-Operatoren und die logischen Bit-Operatoren zum
Einsatz?
Antwort:
Am häufigsten werden diese Operatoren eingesetzt, wenn ein Programm
direkt mit der Computer-Hardware interagiert - eine Aufgabe, die oft die
Erzeugung und Interpretation besonderer Bitmuster erforderlich macht.
Leider kann dieses Thema im Rahmen dieses Buches nicht behandelt werden.
Aber auch wenn Sie nie in Situationen kommen, wo Sie die Hardware direkt
manipulieren müssen, können Sie die Shift-Operatoren nutzen -
beispielsweise um Integerwerte mit Vielfachen von 2 zu dividieren oder zu
multiplizieren.
Frage:
Gewinne ich wirklich so viel durch die Verwendung von Bitfeldern?
Antwort:
Ja, der Gewinn durch den Einsatz von Bitfeldern ist nicht unerheblich.
Betrachten wir einen Fall, der dem heutigen Beispiel sehr ähnlich ist und in
dem Daten aus einer Umfrage in einer Datei gespeichert werden. Die
Befragten sollen jede Frage mit Wahr
oder Falsch
beantworten. Wenn Sie
10.000 Leuten je 100 Fragen stellen und jede Antwort als W
oder F
vom Typ
char
abspeichern, benötigen Sie 10.000 x 100 Byte Speicher (da jedes Zeichen
1 Byte groß ist). Dies entspricht 1 Million Byte an Speicherbedarf. Wenn Sie
statt dessen Bitfelder verwenden und für jede Antwort ein Bit reservieren,
benötigen Sie 10.000 x 100 Bit. Da 1 Byte 8 Bit enthält, entspricht dies
einem Betrag von 130.000 Byte an Daten, was doch erheblich geringer ist als
1 Million Byte.
Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, sowie Übungen, die Sie anregen sollen, das Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Die Lösungen zu den Fragen und den Übungen finden Sie in Anhang C.
malloc()
und calloc()
?
c
eine
Variable vom Typ char
, i
eine Variable vom Typ int
, l
eine Variable vom Typ
long
und f
eine Variable vom Typ float
ist?
( c + i + l )
( i + 32 )
( c + 'A')
( i + 32.0 )
( 100 + 1.0 )
memcpy()
und memmove()
?
1
und 7
speichern muss.
Welcher Weg ist am speicherschonendsten ?
1000 << 4
ausgewertet?
8000 >> 4
ausgewertet?
(01010101 ^ 11111111 )
( ~01010101 )
malloc()
Speicher für 1000 long
-Variablen.
calloc()
Speicher für 1000 long
-Variablen.
float daten[1000];
0
zu initialisieren.
Verwenden Sie für den einen Weg eine Schleife und eine Zuweisung und für die
andere die memset()
-Funktion.
void funk()
{
int zahl1 = 100, zahl2 = 3;
float antwort;
antwort = zahl1 / zahl2;
printf("%d/%d = %lf", zahl1, zahl2, antwort)
}
void *p;
p = (float*) malloc(sizeof(float));
*p = 1.23;
struct quiz_antworten {
char student_name[15];
unsigned antwort1 : 1;
unsigned antwort2 : 1;
unsigned antwort3 : 1;
unsigned antwort4 : 1;
unsigned antwort5 : 1;
}
3
eingibt, sollte das Programm 00000011
ausgeben.
(Hinweis: Sie benötigen die Bit-Operatoren.)