001
30.09.2003, 14:48 Uhr
0xdeadbeef
Gott (Operator)
|
Au weia, das ist ziemlich kompliziert, aber ebenso wichtig. Ich versuchs mal zu erklären. Also, man unterscheidet im wesentlichen zwischen drei logischen Speicherbereichen - dem statischen, dem Stack und dem heap. Statisch alloziierte Daten stehen schon zur Compilezeit fest; darunter fallen globale Variablen, und solche die explizit als static deklariert wurden. Das sind im Grunde ja auch globale Variablen, die halt nicht von überall ansprechbar sind. Beispiel:
C++: |
int globales_int_array[10]; /* <-- statisch */
int foo() { static int i = 0; /* <-- ebenfalls statisch. foo gibt zurück, wie oft foo bisher aufgerufen wurde */ return ++i; }
|
statische Daten sind einfach. Es steht schon zur Compilezeit fest, wieviel Speicher für sie benötigt wird, und sie sind immer vom Programmstart bis zum Programmende vorhanden. Deshalb werden sie idR gleich in die Executable reinkompiliert. Das zweite ist der Stack. Auf dem Stack liegen lokale Variablen, Parameter usw. Nachdem der Block, in dem sie deklariert wurden, verlassen wird, verlieren sie ihre Gültigkeit. Zum Beispiel:
C++: |
void bar(int i) { /* <-- i liegt auf dem Stack */ int j; /* <-- j liegt auf dem Stack */ for(j = 0; j < 10; ++j) { int k = 0; /* <-- k liegt auf dem Stack */ } /* <-- hier endet der Block, in dem k deklariert wurde. k verliert seine Gültigkeit */ /* tu ein bisschen mehr*/ } /* Hier verlieren i und j ihre Gültigkeit */
|
Dadurch, dass die Variablen ihre Gültigkeit verlieren, gibt es einige Pitfalls, die man kennen sollte. Zum Beispiel ist es nicht möglich (Naja, schon möglich, aber du kannst dich nicht drauf verlassen, dass es den gewünschten Effekt hat), Zeiger auf lokale Variablen zurückzugeben. Zum Beispiel:
C++: |
char *baz() { char ret[] = "Hello, World!" return ret; } /* <-- hier verliert ret seine Gültigkeit. */
/* ... */ puts(baz()); /* <-- Der Rückgabewert von baz ist nicht definiert */
|
sowas sind beliebte Heisenbugs. Das dritte ist der Heap. Auf dem Heap kann man dynamisch Speicher anfordern, der für alles andere so lange gesperrt ist, bis er explizit wieder freigegeben wird. Da kommen malloc und calloc usw. ins Spiel. Zum Beispiel:
C++: |
char *buf = malloc(sizeof(char) * 10); /* fordert 10 * sizeof(char) byte speicher an */ /*...*/ free(buf);
|
Damit kannst du dann auch Zeiger auf Speicherbereiche zurückgeben, z.B.:
C++: |
char *qux() { char *ret = malloc(sizeof("Hello, World!")); strcpy(ret, "Hello, World!"); return ret; }
/*...*/ char *s = qux(); puts(s); free(s); /* <-- hier fliegt der Speicher wieder vom Heap und kann neu vergeben werden*/
|
Das hat den Nachteil, dass du extrem vorsichtig sein musst, dass du deinen Speicher auch wieder freigibst. Wenn du in diesem Fall zum Beispiel schreibst:
...was ziemlich verlockend ist, hast du keine Möglichkeit mehr, den Speicher wieder vom Heap zu schmeißen. Der ist dann weg (sowas nennt sich auch "Speicherleck"). Aus diesem Grund empfiehlt es sich nicht, Speicher in einer Funktion anzufordern und in einer anderen wieder freizugeben. Auf die Art ist es nur eine Frage der Zeit, bis du Speicher leckst. Ein gängiger Weg, solche Speicherlecks zu umgehen, ist Buffer mitzugeben, etwa so:
C++: |
char *quux(char *buf) { strcpy(buf, "Hello, World!"); return buf; }
/*...*/ char buf[20]; puts(quux(buf)); /* keine Freigabe erforderlich - buf liegt auf dem Stack */
|
wenn man auch noch segfaults vermeiden will, gibt man die Länge des Buffers mit:
C++: |
char *xyzzy(char *buf, size_t len) { if(len < sizeof("Hello, World!") / sizeof(char)) { /* Fehlerbehandlung - buffer nicht lang genug */ } else { return quux(buf); /* quux siehe oben */ } }
|
Was die Syntax von malloc/calloc angeht - malloc fordert soviele byte speicher an, wie der Parameter es ihm sagt. (siehe oben). calloc könnte man schreiben als
C++: |
void *calloc(size_t anzahl, size_t groesse) { return malloc(anzahl * groesse); }
|
-- Einfachheit ist Voraussetzung für Zuverlässigkeit. -- Edsger Wybe Dijkstra Dieser Post wurde am 30.09.2003 um 14:54 Uhr von 0xdeadbeef editiert. |