vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 18

Vom Umgang mit dem Speicher

Die heutige Lektion behandelt einige der fortgeschritteneren Aspekte der Speicherverwaltung in C-Programmen. Heute lernen Sie:

Typumwandlungen

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.

Automatische Typumwandlungen

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.

Typumwandlungen in Ausdrücken

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:

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.

Umwandlung mittels Zuweisung

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.

Explizite Typumwandlungen

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.

Typumwandlung bei arithmetischen Ausdrücken

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.

Zeiger umwandeln

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.

Was Sie tun sollten

Was nicht

Arbeiten Sie mit Typumwandlungen, wenn Sie die Werte von Variablen befördern oder herabstufen wollen.

Verwenden Sie keine Typumwandlungen, um Compiler-Warnungen zu unterdrücken. Falls doch, prüfen Sie, ob Sie den Grund für die Warnung verstanden haben und die Warnung ignorieren können.

Speicherallokation

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.

Die Funktion malloc()

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 Funktion calloc()

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.

Die Funktion realloc()

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:

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.

Die Funktion free()

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.

Was Sie tun sollten

Was nicht

Geben Sie reservierten Speicher frei, wenn Sie ihn nicht mehr benötigen.

Gehen Sie nie davon aus, dass ein Aufruf von malloc(), calloc() oder realloc() erfolgreich war. Mit anderen Worten, prüfen Sie immer, ob der Speicher auch wirklich reserviert wurde.

Speicherblöcke manipulieren

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.

Die Funktion memset()

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.

Die Funktion memcpy()

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.

Die Funktion memmove()

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.

Was Sie tun sollten

Was nicht

Verwenden Sie memmove() anstelle von memcpy(), es könnte ja sein, dass sich die Speicherbereiche überlappen.

Versuchen Sie nicht, mit memset() Arrays vom Typ int, float oder double mit einem anderen Wert als 0 zu initialisieren.

Mit Bits arbeiten

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.

Die Shift-Operatoren

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

und

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

Die logischen Bit-Operatoren

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.

Operator

Aktion

&

AND

|

OR

^

XOR (exklusives OR)

Tabelle 18.1: Die logischen Bit-Operatoren.

Dies alles sind binäre Operatoren, die die Bits im Ergebnis je nach den Bits in den Operanden auf 1 oder 0 setzen:

Im Folgenden sehen Sie einige Beispiele für die Arbeitsweise dieser Operatoren:

Operation

Beispiel

AND

11110000

& 01010101

-----------

01010000

OR

11110000

| 01010101

-----------

11110101

XOR

11110000

^ 01010101

-----------

10100101

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 Komplement-Operator

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.

Bitfelder in Strukturen

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.

Was Sie tun sollten

Was nicht

Verwenden Sie vordefinierte Konstanten wie JA und NEIN oder WAHR und FALSCH, wenn Sie mit Bits arbeiten. Damit lässt sich der Quelltext wesentlich leichter lesen als bei Verwendung von 1 und 0.

Definieren Sie keine Bitfelder, die 8 oder 32 Bit belegen. Diese entsprechen Variablen vom Typ char oder int.

Zusammenfassung

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.

Fragen und Antworten

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.

Workshop

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.

Quiz

  1. Worin besteht der Unterschied zwischen den Speicherallokationsfunktionen malloc() und calloc()?
  2. Nennen Sie den häufigsten Grund für Typumwandlung von numerischen Variablen?
  3. Zu welchem Datentyp werden die folgende Ausdrücke ausgewertet, wenn 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?
  4. a. ( c + i + l )
  5. b. ( i + 32 )
  6. c. ( c + 'A')
  7. d. ( i + 32.0 )
  8. e. ( 100 + 1.0 )
  9. Was versteht man unter dynamischer Reservierung von Speicher?
  10. Worin besteht der Unterschied zwischen den Funktionen memcpy() und memmove()?
  11. Stellen Sie sich vor, Ihr Programm verwendet eine Struktur, die (als eines ihrer Elemente) den Tag der Woche als einen Wert zwischen 1 und 7 speichern muss. Welcher Weg ist am speicherschonendsten ?
  12. Was ist der kleinste Speicherbereich, in dem das aktuelle Datum gespeichert werden kann? (Hinweis: Tag/Monat/Jahr )
  13. Als was wird 1000 << 4 ausgewertet?
  14. Als was wird 8000 >> 4 ausgewertet?
  15. Beschreiben Sie den Unterschied zwischen den Ergebnissen der folgenden zwei Ausdrücke (vorausgesetzt alle Zahlen sind binär):
    (01010101 ^ 11111111 )
    ( ~01010101 )

Übungen

  1. Reservieren Sie mit malloc() Speicher für 1000 long-Variablen.
  2. Reservieren Sie mit calloc() Speicher für 1000 long-Variablen.
  3. Angenommen Sie haben folgendes Array deklariert:
    float daten[1000];
  4. Zeigen Sie zwei Möglichkeiten, alle Elemente des Arrays mit 0 zu initialisieren. Verwenden Sie für den einen Weg eine Schleife und eine Zuweisung und für die andere die memset()-Funktion.
  5. FEHLERSUCHE: Ist an dem folgenden Code irgendetwas falsch?
    void funk()
    {
    int zahl1 = 100, zahl2 = 3;
    float antwort;
    antwort = zahl1 / zahl2;
    printf("%d/%d = %lf", zahl1, zahl2, antwort)
    }
  6. FEHLERSUCHE: Ist der folgende Code korrekt? Wenn nein, was ist falsch?
    void *p;
    p = (float*) malloc(sizeof(float));
    *p = 1.23;
  7. FEHLERSUCHE: Ist die folgende Strukturdefinition korrekt?
    struct quiz_antworten {
    char student_name[15];
    unsigned antwort1 : 1;
    unsigned antwort2 : 1;
    unsigned antwort3 : 1;
    unsigned antwort4 : 1;
    unsigned antwort5 : 1;
    }
  8. Zu den folgenden Übungen gibt es keine Antworten.
  9. Schreiben Sie ein Programm, das sämtliche logischen Bit-Operatoren verwendet. Das Programm sollte die Bit-Operatoren auf eine Zahl anwenden und die Ergebnisse ausgeben. Schauen Sie sich die Ausgabe an und versuchen Sie, die Ergebnisse der Operationen zu erklären.
  10. Schreiben Sie ein Programm, das den binären Wert einer Zahl ausgibt. Wenn der Anwender beispielsweise 3 eingibt, sollte das Programm 00000011 ausgeben. (Hinweis: Sie benötigen die Bit-Operatoren.)


vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbackKapitelanfangnächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH