- Leren
- Informatica & ICT
- Programmeren
- Programmeren in C: pointers, geheugenbeheer en debuggen
- Dynamisch geheugenbeheer in C
Dynamisch geheugenbeheer in C
BeginnerLes 5 van 8 in Cursus Programmeren in C: pointers, geheugenbeheer en debuggen
Wat je leert
- Beschrijf de stapel, de heap en het statische geheugen, hoelang gegevens op elk van die plekken bestaan en wanneer een programma heapgeheugen nodig heeft.
- Vergelijk stapelgeheugen, heapgeheugen en statische opslagBeschrijf hoe automatische, statische en toegewezen opslag verschillen in levensduur en wie ze beheert, en wat er bij functieaanroepen met elk ervan gebeurt.
- Bepaal wanneer gegevens op de heap thuishorenKies tussen lokale variabelen en geheugenallocatie op de heap op basis van de grootte, levensduur en groei van de gegevens, en voorkom dat je het adres van een lokale variabele teruggeeft.
- Vergelijk stapelgeheugen, heapgeheugen en statische opslag
- Reserveer blokken van de juiste grootte met malloc en calloc, controleer of elke aanvraag is geslaagd en geef elk blok precies één keer vrij onder verantwoordelijkheid van een duidelijke eigenaar.
- Vraag geheugen aan met malloc en sizeofBereken het aantal aan te vragen bytes, roep malloc aan, controleer op fouten en gebruik het blok als array.
- Vraag met nullen gevuld geheugen aan met callocGebruik calloc om geheugen te reserveren voor arrays waarvan de elementen bij nul beginnen, en weet wanneer dit een betere keuze is dan malloc.
- Geef geheugen vrij met free en houd het eigenaarschap bijGeef elk blok precies één keer op het juiste moment vrij, bepaal welk deel van het programma eigenaar is van elk blok en ruim correct op als een geheugenallocatie mislukt.
- Vraag geheugen aan met malloc en sizeof
- Vergroot en verklein heapblokken zonder gegevens of geheugen te verliezen en maak arrays die meegroeien als er gegevens binnenkomen.
- Vergroot en verklein een geheugenblok veilig met reallocRoep realloc aan via een tijdelijke pointer, behoud de gegevens en het oorspronkelijke blok als de aanroep mislukt, en werk alle pointers bij nadat het blok is verplaatst.
- Maak een uitbreidbare arrayHoud naast de pointer een lengte en een capaciteit bij, laat de capaciteit meetkundig groeien en lees invoer van onbekende lengte in een buffer die meegroeit.
- Vergroot en verklein een geheugenblok veilig met realloc
- Herken geheugenlekken, pointers naar niet meer bestaande objecten, dubbele vrijgave, toegang buiten de grenzen en het lezen van niet-geïnitialiseerde waarden in C-code, leg uit waarom ze gevaarlijk zijn en herleid een segmentatiefout tot de oorzaak.
- Spoor geheugenlekken op en voorkom zeLeg uit wat een geheugenlek is, herken codepatronen die geheugenlekken veroorzaken en pas werkwijzen toe die ze voorkomen.
- Voorkom pointers naar niet meer bestaande objecten, gebruik na vrijgave en dubbele vrijgaveHerken pointers naar geheugen waarvan de levensduur is geëindigd, leg uit waarom het gebruik of opnieuw vrijgeven ervan ongedefinieerd gedrag is en pas werkwijzen toe die dit voorkomen.
- Blijf binnen het geheugenblok: grensoverschrijdingen en het lezen van niet-geïnitialiseerde waardenBlijf bij elke toegang binnen de grenzen van het blok, bepaal de juiste grootte voor stringbuffers en stel geheugen in voordat je het leest.
- Verklaar segmentatiefouten en spoor de oorzaak opLeg uit wat een segmentatiefout is, noem veelvoorkomende oorzaken en werk vanaf de crash terug naar de regel met de fout.
- Spoor geheugenlekken op en voorkom ze
- Voer programma’s uit onder Valgrind Memcheck en met AddressSanitizer, lees hun rapporten en gebruik die om geheugenlekken en ongeldige geheugentoegang te herstellen totdat een programma zonder fouten draait.
- Controleer een programma met Valgrind MemcheckBouw een programma voor Valgrind, voer het uit met geheugenlekcontrole en lees de foutmeldingen, het heapoverzicht en de geheugenlekcategorieën.
- Gebruik AddressSanitizer en LeakSanitizerBouw een programma met AddressSanitizer, lees de rapporten en kies tussen sanitizers en Valgrind.
- Herstel geheugenfouten aan de hand van een hulpmiddelrapportVolg een herhaalbare werkwijze van een hulpmiddelrapport naar de regel met de fout, herstel de oorzaak in plaats van het symptoom en bevestig de oplossing met een uitvoering zonder fouten.
- Controleer een programma met Valgrind Memcheck
Dynamisch geheugenbeheer in C: uitleg
1Leg uit waar een C-programma zijn gegevens opslaat
Elk object heeft een levensduur en een pointer is alleen bruikbaar zolang het object waarnaar die wijst bestaat. Lokale variabelen, statische gegevens en toegewezen blokken volgen verschillende regels voor hun levensduur. Door die regels te verbinden met de stapel en de heap begrijp je waarom een functie een toegewezen array kan teruggeven, maar geen bruikbaar adres van haar eigen lokale array. De grootte, levensduur en latere groei bepalen welke opslag geschikt is voor een taak.
1.1 Opslag en levensduur
Het adres van een variabele vertelt je waar de gegevens staan. De opslagduur vertelt je hoelang die gegevens bestaan. Een adres bewaren houdt het object niet in leven.
Drie levensduren
| Opslagduur | Typische objecten | Levensduur eindigt |
|---|---|---|
| Automatisch | Gewone lokale variabelen en parameters | Als hun blok eindigt |
| Statisch | Globale variabelen en lokale variabelen gedeclareerd met static | Als het programma eindigt |
| Toegewezen | Blokken verkregen via geheugenallocatiefuncties | Als het blok wordt vrijgegeven |
Op gangbare systemen gebruiken automatische objecten de aanroepstapel en toegewezen blokken de heap. C bepaalt de levensduren, maar vereist deze fysieke gebieden niet. Machinecode en statische gegevens nemen andere gebieden in, waarvan de indeling afhangt van het systeem.
Een functieaanroep maakt normaal gesproken een stackframe met de parameters, lokale variabelen en terugkeerinformatie. Aanroepen worden genest: het meest recente frame wordt als eerste verwijderd. Bij terugkeer wordt de automatische opslag van die aanroep vrijgegeven, zodat latere aanroepen de ruimte opnieuw kunnen gebruiken. De allocator van de heap beheert een afzonderlijke geheugenvoorraad. Die reserveert op aanvraag een aaneengesloten blok en geeft het beginadres. Terugkeren uit een functie geeft dat blok niet vrij.
De pointer en zijn blok zijn afzonderlijke objecten
Bekijk deze functie, gedeclareerd met <stdlib.h> beschikbaar:
int *make_array(size_t n) {
if (n == 0 || n > SIZE_MAX / sizeof(int)) return NULL;
int *p = malloc(n * sizeof *p);
if (p == NULL) return NULL;
return p;
}
SIZE_MAX komt uit <stdint.h>. Als main int *nums = make_array(5); aanroept, horen de parameter n en de pointervariabele p bij de aanroep. De toegewezen array hoort daar niet bij. Als een int 4 bytes is, bevat het blok 20 bytes. Bij het terugkeren wordt het adres naar nums gekopieerd; de array wordt niet gekopieerd. De aanroepende functie controleert nums, gebruikt de elementen nadat ze zijn ingesteld en geeft uiteindelijk het blok vrij.
De lokale variabele p houdt op te bestaan als de functie terugkeert. Het blok blijft bestaan omdat het toegewezen opslagduur heeft, geen automatische. Dit onderscheid werkt ook andersom: een lokale pointer kan naar een globaal object wijzen en een globale pointer kan naar een heapblok wijzen.
Statische gegevens blijven bestaan tussen aanroepen
int next_id(void) {
static int id = 0;
return ++id;
}
Opeenvolgende aanroepen geven 1, 2 en 3 terug. De initialisator wordt één keer uitgevoerd en id bestaat gedurende de hele uitvoering van het programma. Als je static verwijdert, wordt bij elke aanroep een nieuwe automatische id gemaakt, geïnitialiseerd op nul; elke aanroep geeft dan 1 terug.
De stapelruimte is beperkt
Een grote lokale array en diep geneste aanroepen gebruiken allebei stapelruimte. Overschrijding van de limiet heet stapeloverloop en beëindigt meestal het programma. Veel Linux-configuraties staan een stapel van 8 MiB toe; de standaard voor de hoofdthread gereserveerde stapel van Microsoft is gewoonlijk 1 MiB. Dit zijn systeeminstellingen, geen C-regels. Een lokale array van 4.000.000 ints heeft 16.000.000 bytes nodig als een int 4 bytes is, ongeveer 15,3 MiB.
Geheugenallocatie op de heap brengt administratiekosten mee bij het reserveren en vrijgeven van blokken. Het lezen van een element is niet vanzelf langzamer alleen omdat de array op de heap staat. De heap als geheugenvoorraad staat ook los van de heapdatastructuur die in een prioriteitswachtrij wordt gebruikt.
Belangrijkste punten
- De opslagduur van een object bepaalt zijn levensduur.
- Automatische lokale variabelen houden op te bestaan als hun blok eindigt; statische objecten bestaan zolang het programma draait.
- Een lokale pointer en het heapblok waarnaar die wijst, hebben afzonderlijke levensduren.
- Grote lokale arrays en diepe aanroepen kunnen de stapel uitputten.
- C specificeert levensduren, terwijl stapel en heap gangbare implementaties beschrijven.
1.2 De juiste opslag kiezen
Kies opslag door je af te vragen hoeveel ruimte nodig is, wanneer die grootte bekend wordt en hoelang de gegevens moeten bestaan. Het eenvoudigste geschikte ontwerp is vaak een gewone lokale array.
Stem de opslag af op de gegevens
| Behoefte | Geschikte eerste keuze |
|---|---|
| Een kleine vaste array die tijdens één aanroep wordt gebruikt | Lokale array |
| Een aantal dat uit invoer wordt gelezen | Gecontroleerde geheugenallocatie op de heap |
| Een resultaat dat langer bestaat dan de functie die het maakt | Geheugenallocatie op de heap of een buffer van de aanroepende functie |
| Gegevens die groeien naarmate invoer binnenkomt | Buffer op de heap waarvan de grootte kan worden gewijzigd |
| Een zeer grote array | Geheugenallocatie op de heap met een foutcontrole |
Voor de dagelijkse temperaturen van een week zijn zeven waarden nodig, dus double temps[7]; is een logische lokale array. Voor een cijfer per student is een grootte nodig die tijdens de uitvoering wordt bepaald. Gebruik bij n studenten double *grades = malloc(n * sizeof *grades); nadat je de grootte hebt gecontroleerd. Voor 250 studenten en doubles van 8 bytes vraagt dat 2.000 bytes aan. Controleer daarna het resultaat voordat je de array vult.
Waarom een lokale buffer teruggeven misgaat
char *greeting(void) {
char buf[16] = "hello";
return buf; /* error: buf ends when this call returns */
}
Het teruggegeven adres kan de levensduur van buf niet verlengen. De aanroepende functie krijgt een ongeldige pointer naar een niet meer bestaand object, ook als de oude bytes ongewijzigd lijken. GCC en Clang kunnen voor dit patroon waarschuwen.
Een oplossing is de aanroepende functie opslag te laten aanleveren:
#include <string.h>
void greeting(char out[16]) {
strcpy(out, "hello");
}
/* In the caller: */
char message[16];
greeting(message);
Hier blijft de array van de aanroepende functie bestaan tijdens de hele aanroep en daarna. De parameter gedraagt zich nog steeds als een pointer; de aanroepende functie moet voldoende ruimte aanleveren. Een tweede ontwerp reserveert een resultaat op de heap en maakt de aanroepende functie verantwoordelijk voor het vrijgeven. Een derde geeft een statische buffer terug. Die buffer blijft bestaan, maar alle aanroepende functies delen die en een latere aanroep kan de inhoud overschrijven. Kies op basis van de vraag of aanroepende functies onafhankelijke resultaten nodig hebben.
Een grootte tijdens de uitvoering betekent niet altijd de heap
C99 introduceerde arrays met variabele lengte, zoals int a[n];. Ze hebben automatische opslagduur, meestal op de stapel. C11 maakte ondersteuning optioneel en de compiler van Microsoft ondersteunt ze niet. Grote invoer kan de stapel uitputten en de declaratie van de array geeft geen nulresultaat dat je kunt controleren. Geheugenallocatie op de heap biedt een controleerbaar foutpad en ondersteunt het later wijzigen van de grootte.
Een pointer geeft geen eigenaarschap aan
int value = 3;
int *p = &value;
p wijst naar een automatisch object. Die pointer aan free doorgeven is ongeldig. Dezelfde regel sluit het vrijgeven van een statische array of een stringliteral uit. Het adres moet horen bij een nog bestaand toegewezen blok waarvan het programma eigenaar is.
Ook wijst char *s = "hello"; naar een stringliteral; het maakt geen beschrijfbare kopie. Het wijzigen van de literal is ongedefinieerd gedrag. Gebruik char s[] = "hello"; voor een beschrijfbare lokale kopie, of reserveer ruimte voor een afzonderlijke kopie op de heap.
Belangrijkste punten
- Kies opslag op basis van de grootte, de vereiste levensduur en de noodzaak om te groeien.
- Een door een functie teruggegeven adres is alleen bruikbaar als het object waarnaar het wijst nog bestaat.
- Bij een buffer die de aanroepende functie beschikbaar stelt, blijft het eigenaarschap bij die functie.
- Een statische resultaatbuffer wordt gedeeld tussen aanroepen.
- Pointers kunnen naar lokale, statische of toegewezen objecten wijzen; alleen nog bestaande toegewezen blokken kunnen worden vrijgegeven.
- Stringliterals zijn geen beschrijfbare kopieën.
2Reserveer heapgeheugen en geef het vrij
Een geheugenaanvraag heeft een byteaantal, een foutpad en een eigenaar nodig. Ga uit van het aantal elementen om de benodigde opslag te bepalen, beslis of hun beginwaarden nul moeten zijn en gebruik elk blok pas na een geslaagde aanvraag. Eigenaarschap verbindt het laatste gebruik vervolgens met het opruimen, ook bij geneste geheugenallocaties en functies die hun werk maar gedeeltelijk kunnen uitvoeren.
2.1 Het juiste aantal bytes aanvragen
malloc reserveert een aaneengesloten blok bytes en geeft het beginadres terug. Neem <stdlib.h> op voor de declaratie. Een geslaagde geheugenallocatie levert opslag op, maar vult die opslag niet met bruikbare waarden.
Tel de elementen en bepaal hun grootte
int *nums = malloc(n * sizeof *nums);
n is een aantal elementen; sizeof *nums is het aantal bytes per element. Vermenigvuldigen levert het aantal bytes op. Als een int 4 bytes is, hebben tien ints 40 bytes nodig. Tien doubles van 8 bytes hebben 80 bytes nodig. Tien pointers hebben 80 bytes nodig als elke pointer 8 bytes is. Een char is altijd één C-byte.
De vorm sizeof *nums volgt het type waarnaar wordt gewezen als de declaratie verandert. Voor dit gewone type zonder variabele lengte evalueert sizeof de expressie *nums niet en derefereert dus geen niet-geïnitialiseerde pointer. In C wordt de teruggegeven void * zonder expliciete typeconversie geconverteerd naar het objectpointertype.
malloc(n) reserveert n bytes, geen ruimte voor n ints. sizeof nums meet de pointervariabele, niet een element of het hele blok. Op een systeem met pointers van 8 bytes en ints van 4 bytes vraagt malloc(n * sizeof nums) tweemaal de benodigde ruimte aan, waardoor de verkeerde expressie verborgen blijft. Bij andere typen kan die te weinig reserveren.
Controleer de grootte en fouten vóór gebruik
Dit volledige programma wijst een lege aanvraag af, beveiligt de vermenigvuldiging tegen overloop, controleert de geheugenallocatie en initialiseert elk element:
#include <stdint.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *nums;
if (n == 0 || n > SIZE_MAX / sizeof *nums) {
return EXIT_FAILURE;
}
nums = malloc(n * sizeof *nums);
if (nums == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < n; ++i) {
nums[i] = (int)(i * i);
}
free(nums);
return EXIT_SUCCESS;
}
SIZE_MAX, uit <stdint.h>, is de grootste size_t-waarde. Controleer met een deling vóór de vermenigvuldiging. Als het product eerst modulair terugloopt, kun je de bedoelde grootte niet herstellen door het kleinere teruggelopen resultaat te controleren. Een aanvraag met een grootte ongelijk aan nul kan mislukken, ook als de berekening binnen het bereik past. Daarom is de controle op een nulpointer een afzonderlijke stap.
De geïnitialiseerde waarden zijn hier 0, 1, 4, 9 en 16. nums[i] bereikt hetzelfde element als *(nums + i). Pointerrekenen gaat in stappen van elementen, dus de code vermenigvuldigt i niet met de grootte in bytes.
Houd het aantal bij de pointer
Er is geen standaardbewerking in C die het aantal elementen van een geheugenallocatie uit de pointer kan afleiden. Bewaar n afzonderlijk en geef zowel de pointer als het aantal door aan functies. Het aantal bepaalt de lussen en indexcontroles.
Een gekopieerde string heeft ruimte nodig voor de tekens en het afsluitende '\0'. De string “Amsterdam” heeft negen tekens en heeft tien bytes nodig, dus vraag strlen(s) + 1 aan voor de kopie. <string.h> levert strlen.
Een malloc-aanvraag van grootte nul kan een nulpointer teruggeven of een pointer die kan worden vrijgegeven maar niet mag worden gederefereerd. Behandel een aantal van nul als een expliciet leeg geval in plaats van zo’n pointer als bruikbare arrayopslag te behandelen.
Belangrijkste punten
- Vermenigvuldig voor het aantal bytes van een array het aantal elementen met
sizeof *pointer. - Controleer op overloop bij de vermenigvuldiging voordat je
mallocaanroept. - Controleer op
NULLvoordat je het resultaat gebruikt. - Initialiseer elk
malloc-element voordat je het leest. - Bewaar het aantal elementen afzonderlijk;
sizeof pointerkan het niet achterhalen. - Tel het afsluitende nulteken mee wanneer je geheugen reserveert voor een kopie van een string.
- Behandel een aantal van nul expliciet.
2.2 Een array bij nul laten beginnen
Een teller heeft een bepaalde beginwaarde nodig vóór de eerste verhoging. calloc combineert geheugenallocatie met het op nul zetten, zodat een array van gehele tellers na de foutcontrole klaar is om te tellen.
Twee argumenten beschrijven de array
Neem <stdlib.h> op en schrijf:
int *tally = calloc(26, sizeof *tally);
if (tally == NULL) return EXIT_FAILURE;
Het eerste argument is het aantal elementen en het tweede is de grootte van één element. Als een int 4 bytes is, reserveert dit 104 bytes. Elke byte begint op nul, dus elke gehele teller begint op nul. Een aanroep van calloc met één gecombineerd argument compileert niet; de interface vereist twee argumenten.
Verhoog voor het woord “banana” de teller die bij elke letter hoort. Na verwerking bevatten de tellers voor a, b en n de waarden 3, 1 en 2. Alle andere blijven nul. Bij een tekencodering met opeenvolgende kleine letters is de indexexpressie c - 'a'. De invoer moet een kleine letter zijn voordat die expressie als array-index wordt gebruikt.
#include <stdlib.h>
int main(void) {
int *counts = calloc(3, sizeof *counts);
if (counts == NULL) return EXIT_FAILURE;
counts[1]++;
counts[1]++;
/* The counters now hold 0, 2, 0. */
free(counts);
return EXIT_SUCCESS;
}
Alleen de middelste teller is door de telcode beschreven. De twee tellers ernaast hebben nog steeds bruikbare waarden, omdat ze bij de geheugenallocatie op nul zijn gezet.
Wat met nullen gevuld betekent
calloc zet elke bit op nul. Dit levert nulwaarden op voor elementen van gehele typen en van char. Een met nullen gevulde tekenbuffer met een grootte groter dan nul begint daarom als een lege string: het eerste teken is '\0'.
Op gangbare platforms stellen uitsluitend nulbits ook drijvendekommanul en nulpointers voor. Overdraagbare C-code kan voor drijvendekomma- en pointertypen niet op die representaties rekenen. Als code zulke waarden in alle C-implementaties nodig heeft, wijs dan expliciet 0.0 of NULL toe aan de elementen.
Kiezen tussen de geheugenallocatiefuncties
| Begin van het gebruik | Logische keuze |
|---|---|
| Gehele tellers verhogen | calloc |
| Gehele vlaggen geleidelijk instellen vanuit een toestand met alleen nullen | calloc |
| Elk element direct overschrijven | malloc |
Een geslaagde malloc gevolgd door memset kan een blok op nul zetten, maar de vermenigvuldiging voor de grootte in die code moet nog steeds tegen overloop worden beveiligd. Gangbare implementaties van calloc wijzen een product af dat te groot is voor size_t; C23 vereist dit expliciet. Het resultaat moet nog steeds op een nulpointer worden gecontroleerd, omdat een aanvraag met een correcte grootte kan mislukken.
Het op nul zetten geldt voor deze geheugenallocatie. Als realloc het blok later vergroot, zijn de toegevoegde bytes niet geïnitialiseerd. Stel die elementen in voordat je ze verhoogt of leest. Geef het blok ten slotte vrij met free, net als een blok dat met malloc is verkregen.
Belangrijkste punten
- Geef
calloceen aantal elementen en een elementgrootte door. - Gehele tellers die met nullen zijn gevuld, kun je direct na de controle van de geheugenallocatie verhogen.
- Bytes met alleen nullen zijn geen overdraagbare vervanging voor het toewijzen van drijvendekommanul of nulpointers.
- Bij groei met
reallocworden de toegevoegde bytes niet op nul gezet.
2.3 Eén eigenaar en één laatste vrijgave
Voor een toegewezen blok moet duidelijk zijn: “Wie geeft dit vrij?” Die verantwoordelijkheid heet eigenaarschap. Andere code mag het adres lenen zonder toestemming te krijgen om het blok vrij te geven.
Geef vrij na het laatste gebruik
free, gedeclareerd in <stdlib.h>, maakt een nog bestaand toegewezen blok beschikbaar voor hergebruik. Geef het beginadres precies één keer door. free(NULL) doet niets. Een lokaal adres, een stringliteral, een adres binnen een blok zoals p + 1, of een al vrijgegeven blok doorgeven is ongedefinieerd gedrag.
free(p) wijst niets toe aan de variabele p en garandeert niet dat de bytes worden gewist. Na vrijgave is de oude pointerwaarde ongeldig. Als de variabele in gebruik blijft, wijs je p = NULL; toe zonder de oude waarde te bekijken. Dit verandert alleen p; aliassen mogen het vrijgegeven blok nog steeds niet meer gebruiken.
Een functie die nieuw toegewezen geheugen teruggeeft, draagt het eigenaarschap over aan de aanroepende functie:
#include <stdlib.h>
#include <string.h>
char *copy_string(const char *s) {
char *copy = malloc(strlen(s) + 1);
if (copy == NULL) return NULL;
strcpy(copy, s);
return copy;
}
De aanroepende functie controleert het resultaat, gebruikt de kopie en geeft die vrij. De invoerstring is geleend; deze functie geeft die niet vrij. POSIX en C23 bieden strdup met dezelfde verdeling van het eigenaarschap; de functie hierboven werkt ook in C17.
Geef geneste geheugenallocaties van binnen naar buiten vrij
Een raster dat is opgebouwd uit een array van rijpointers heeft afzonderlijke blokken. Drie rijpointers en drie rijen van vier ints vereisen vier geheugenallocaties. Met pointers van 8 bytes en ints van 4 bytes gebruikt het buitenste blok 24 bytes en gebruiken de rijen 48 bytes, in totaal 72 bytes.
Geef elke rij vrij zolang het adres nog beschikbaar is en geef daarna de pointerarray vrij. Het vrijgeven van het buitenste blok geeft de rijen niet recursief vrij. Als je grid[i] benadert nadat je grid hebt vrijgegeven, gebruik je vrijgegeven opslag.
Ruim op bij gedeeltelijk succes
Dit volledige voorbeeld houdt bij hoeveel rijen er daadwerkelijk bestaan:
#include <stdlib.h>
int main(void) {
size_t rows = 3, made = 0;
int **grid = malloc(rows * sizeof *grid);
if (grid == NULL) return EXIT_FAILURE;
for (; made < rows; ++made) {
grid[made] = malloc(4 * sizeof *grid[made]);
if (grid[made] == NULL) break;
}
int status = made == rows ? EXIT_SUCCESS : EXIT_FAILURE;
/* Use the grid here only if every row was allocated. */
for (size_t i = 0; i < made; ++i) free(grid[i]);
free(grid);
return status;
}
Als de geheugenallocatie voor de tweede rij mislukt, is made 1, dus wordt alleen rij nul vrijgegeven. Er wordt geen niet-geïnitialiseerde rijpointer gelezen. Een ander bruikbaar ontwerp zet onafhankelijke pointers aanvankelijk op NULL en stuurt elke fout naar één opruimsectie; free(NULL) maakt die sectie veilig na gedeeltelijk succes.
Tel levensduren van eigenaarschap, geen geheugenallocatieaanroepen. Een buffer die met malloc is gemaakt en waarvan de grootte twee keer met succes is gewijzigd, heeft één laatste free op het nieuwste resultaat nodig; een geslaagde realloc beëindigt de levensduur van het vorige blok al. De allocator kan vrijgegeven ruimte bewaren voor latere geheugenallocaties, dus een systeemmonitor hoeft geen onmiddellijke daling van het procesgeheugen te tonen.
Belangrijkste punten
- De eigenaar geeft elk blok na het laatste gebruik vrij.
- Alleen het beginadres van een nog bestaand toegewezen blok, of
NULL, is een geldig argument voorfree. - Eén pointer terugzetten herstelt geen aliassen.
- Geef binnenste blokken vrij vóór het buitenste blok dat hun adressen bevat.
- Foutpaden moeten alle eerder verkregen blokken vrijgeven.
- Na geslaagde wijzigingen van de grootte is uiteindelijk één
freevan het nieuwste blok nodig.
3Wijzig de grootte van buffers met realloc
Invoer komt niet altijd binnen met een bekende uiteindelijke grootte. Met een buffer waarvan de grootte kan worden gewijzigd, kan een programma meer ruimte reserveren naarmate duidelijk wordt hoeveel nodig is. Veilig vergroten behoudt zowel bestaande waarden als de mogelijkheid om van een fout te herstellen. Door een pointer, een lengte en een capaciteit bij elkaar te houden, maak je die regels concreet, terwijl meetkundige groei de hoeveelheid herhaald kopieerwerk beperkt.
3.1 De grootte wijzigen zonder het oude blok te verliezen
realloc wijzigt de grootte van een toegewezen blok. De functie kan de opslag vergroten of verkleinen en het teruggegeven adres kan in beide gevallen veranderen. Neem <stdlib.h> op en behandel succes en falen als verschillende gebeurtenissen in de levensduur.
Wat blijft en wat verandert
Als een blok groeit, blijft de bestaande inhoud behouden tot aan de oude grootte. Toegevoegde bytes zijn niet geïnitialiseerd. Als het blok krimpt, blijft alleen het begindeel behouden dat in de nieuwe grootte past. Een array met 3, 1, 4 en 1 behoudt die vier waarden bij vergroting naar zes ints; de laatste twee elementen moeten vóór gebruik een waarde krijgen. Bij ints van 4 bytes verandert de aanvraag daarmee van 16 naar 24 bytes.
De allocator kan het blok op dezelfde plek uitbreiden, of elders geheugen reserveren, de behouden bytes kopiëren en het oude blok vrijgeven. Je code moet met beide resultaten werken.
Vang het resultaat op in een tijdelijke pointer
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
/* nums still owns its original block. */
} else {
nums = tmp;
nums[4] = 5;
nums[5] = 9;
}
Bij deze aanvraag met een grootte groter dan nul blijft het oorspronkelijke blok bij een fout gereserveerd en ongewijzigd. Het programma kan het blijven gebruiken of vrijgeven voordat het de fout meldt. Bij succes gaat het eigenaarschap over op het teruggegeven blok. Wijs het nieuwe resultaat toe voordat je de array met gewijzigde grootte benadert.
De directe toewijzing nums = realloc(nums, bytes); verliest deze bescherming. Als de aanroep mislukt, overschrijft die het enige opgeslagen adres met NULL. Het oude blok blijft gereserveerd, maar het programma kan het niet meer vrijgeven.
Opgeslagen pointers naar elementen moeten opnieuw worden berekend
Stel dat code int *selected = &nums[2]; opslaat. Een geslaagde wijziging van de grootte beëindigt de levensduur van het oude object, ook als het teruggegeven adres toevallig gelijk is. selected is na dat succes ongeldig. Bewaar index 2 tijdens de wijziging en bereken daarna selected = &nums[2]; vanuit de nieuwe pointer, als die index nog bestaat.
Vergelijk de oude pointer niet met het resultaat om te bepalen of aliassen moeten worden bijgewerkt. Werk bij elk succes vanuit het nieuwe resultaat. Als het geselecteerde element bij het verkleinen is verwijderd, is er geen elementpointer om opnieuw te berekenen.
Een volledig voorbeeld van vergroten en vrijgeven
#include <stdlib.h>
int main(void) {
int *nums = malloc(4 * sizeof *nums);
if (nums == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < 4; ++i) nums[i] = (int)i;
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
free(nums);
return EXIT_FAILURE;
}
nums = tmp;
nums[4] = 4;
nums[5] = 5;
free(nums);
return EXIT_SUCCESS;
}
Controleer bij een variabel aantal vóór deze aanroep de vermenigvuldiging naar bytes, net als bij malloc. Vraag bytes aan, geen elementen: realloc(nums, 6) vraagt zes bytes aan.
realloc(NULL, positive_size) gedraagt zich als malloc, zodat een lege buffer met een nulpointer kan beginnen. Roep free expliciet aan om een buffer vrij te geven. Vermijd het wijzigen van de grootte naar nul: bibliotheken gedroegen zich in het verleden verschillend en het wijzigen van een bestaand blok naar grootte nul is ongedefinieerd gedrag in C23. Lokale arrays en eerder vrijgegeven blokken zijn beide geen geldige objecten om op deze manier van grootte te veranderen.
Belangrijkste punten
- Gebruik een tijdelijke pointer zodat bij een mislukte wijziging naar een grootte groter dan nul het adres van de eigenaar behouden blijft.
- Bij vergroten blijft de oude inhoud behouden, maar zijn de toegevoegde bytes niet geïnitialiseerd.
- Bij verkleinen blijft alleen het deel behouden dat nog past.
- Gebruik na succes de teruggegeven pointer en bereken opgeslagen pointers naar elementen opnieuw.
- Gebruik
freeom opslag vrij te geven; behandel een grootte van nul expliciet.
3.2 Een array die met de invoer meegroeit
Een uitbreidbare array heeft 3 gegevens nodig om de toestand bij te houden: de pointer, de lengte en de capaciteit. De lengte telt de geïnitialiseerde elementen die in gebruik zijn. De capaciteit telt de gereserveerde plaatsen voor elementen. Houd de lengte kleiner dan of gelijk aan de capaciteit.
Voeg in een veilige volgorde toe
Begin met int *data = NULL; size_t len = 0, cap = 0;. Controleer vóór een toevoeging of len == cap. Een volle array moet groeien voordat je naar data[len] schrijft. Pas als het wijzigen van de grootte slaagt, mogen de pointer en capaciteit veranderen. Sla de nieuwe waarde op en verhoog daarna de lengte.
Het volgende volledige voorbeeld voegt tien waarden toe met meetkundige groei:
#include <stdint.h>
#include <stdlib.h>
int main(void) {
int *data = NULL;
size_t len = 0, cap = 0;
for (int value = 0; value < 10; ++value) {
if (len == cap) {
size_t next;
if (cap == 0) {
next = 4;
} else {
if (cap > SIZE_MAX / 2 / sizeof *data) {
free(data);
return EXIT_FAILURE;
}
next = cap * 2;
}
int *tmp = realloc(data, next * sizeof *data);
if (tmp == NULL) {
free(data);
return EXIT_FAILURE;
}
data = tmp;
cap = next;
}
data[len++] = value;
}
free(data);
return EXIT_SUCCESS;
}
De overloopcontrole controleert zowel de verdubbeling als de omzetting naar bytes voordat een van beide producten modulair kan teruglopen. Ook de begincapaciteit van vier past bij dit elementtype. Bij een fout geeft het programma het oude blok, waarvan het nog eigenaar is, vrij en stopt het; het werkt de capaciteit niet ten onrechte bij.
Volg lengte en capaciteit afzonderlijk
Bij een begincapaciteit van 4 gebruiken de eerste vier toevoegingen bestaande ruimte. Vóór de vijfde verdubbelt de capaciteit naar 8. Vóór de negende verdubbelt die naar 16. Tien toevoegingen eindigen met lengte 10 en capaciteit 16. Als beide vergrotingen tot een verplaatsing leiden, kopiëren ze hoogstens vier en acht elementen, in totaal twaalf.
Overcapaciteit is bewust gekozen. Bij elke toevoeging met één element groeien veroorzaakt herhaald kopiëren. Vanaf begincapaciteit 1 kan het bereiken van 1.000 elementen 999 wijzigingen van de grootte en 499.500 gekopieerde elementen vereisen. Verdubbelen bereikt capaciteit 1.024 na tien wijzigingen van de grootte en hoogstens 1.023 kopieën. Een afzonderlijke wijziging kan duur zijn, maar bij meetkundige groei blijft het gemiddelde werk per toevoeging constant.
Een factor zoals 1,5 laat minder ongebruikte ruimte over, maar vergroot vaker. Als de invoer is geëindigd, kan het programma verkleinen tot de gebruikte lengte en een fout afhandelen met hetzelfde patroon van een tijdelijke pointer. Een lege array wordt vrijgegeven met free, niet door de grootte naar nul te wijzigen.
Tekenbuffers hebben één extra plaats nodig
Reserveer voor een groeiende string ruimte voor '\0'. Vergroot vóór het toevoegen van een teken als len + 1 >= cap. Bij capaciteit 8 passen zeven tekens terwijl de plaats voor het afsluitende nulteken vrij blijft; “hello world” leidt tot groei naar 16 en heeft twaalf bytes nodig inclusief het afsluitende nulteken.
Een opgeslagen pointer naar een element wordt ongeldig na een geslaagde vergroting. Bewaar een index en zoek het element opnieuw op met het nieuwe beginadres. Als je een array terugzet voor verder gebruik, geef je het blok vrij en stel je data = NULL; len = cap = 0; in.
Belangrijkste punten
- De lengte telt gebruikte elementen; de capaciteit telt beschikbare plaatsen.
- Vergroot vóór een toevoeging als de lengte gelijk is aan de capaciteit.
- Werk de pointer en capaciteit pas bij nadat de wijziging van de grootte is geslaagd.
- Meetkundige groei beperkt herhaald kopiëren ten koste van ongebruikte ruimte.
- Beveilig berekeningen voor groei vóór de vermenigvuldiging tegen overloop.
- Reserveer een extra byte voor het afsluitende nulteken van een string.
- Bewaar indexen tijdens het vergroten en bereken pointers naar elementen daarna opnieuw.
4Herken en voorkom geheugenfouten
Geheugenfouten ontstaan wanneer regels over eigenaarschap, levensduur, grenzen of initialisatie worden geschonden. Elke fout laat een ander spoor achter: een verloren adres, toegang na vrijgave, een index buiten het blok of een waarde die te vroeg wordt gelezen. Sommige fouten laten het programma direct crashen, terwijl andere gegevens beschadigen en later aan het licht komen. Door de code te lezen in termen van blokken en hun geschiedenis kun je de oorzaak achter het symptoom herkennen.
4.1 Het pad opsporen waar geheugen verloren gaat
Een geheugenlek is toegewezen opslag die een programma niet meer nodig heeft, maar nooit vrijgeeft. Vaak verdwijnt de laatste pointer, waardoor een latere free onmogelijk wordt. Een geheugenlek kan onopgemerkt blijven terwijl het geheugengebruik groeit.
Volg adressen, geen variabelenamen
char *p = malloc(100);
if (p == NULL) return EXIT_FAILURE;
p = malloc(200); /* loses the only address of the first block */
if (p == NULL) return EXIT_FAILURE;
free(p);
De laatste free geeft alleen het tweede blok vrij. De naam p is hergebruikt; de eerste toegewezen opslag niet. Als de tweede aanvraag mislukt, is het blok van 100 bytes nog steeds verloren. Geef het eerste blok vrij voordat je het vervangt, wanneer het niet meer nodig is, of bewaar het adres in een andere variabele die het blok beheert.
Als je een pointer die eigenaar is op NULL zet voordat je het blok vrijgeeft, ontstaat hetzelfde verlies. De toewijzing verandert een pointervariabele; die geeft de allocator geen opdracht om iets vrij te geven. Automatische lokale variabelen verdwijnen aan het einde van een aanroep, maar de toegewezen blokken waarnaar ze wezen blijven bestaan.
Bekijk elke manier waarop een functie kan eindigen
Een vroege terugkeer kan het opruimen overslaan:
char *line = malloc(100);
if (line == NULL) return EXIT_FAILURE;
if (input_is_bad) {
free(line);
return EXIT_FAILURE;
}
/* Use line on the successful path. */
free(line);
Beide paden geven het blok vrij. In een functie met meer hulpbronnen vermindert één opruimsectie het aantal te controleren paden. Een functie die een toegewezen resultaat teruggeeft, draagt de verantwoordelijkheid van de eigenaar over; een functie die slechts een lokale pointer die eigenaar is achterlaat, veroorzaakt een geheugenlek.
Directe toewijzing van een mislukt realloc-resultaat is een andere manier om geheugen te verliezen. Geneste blokken lekken wanneer hun buitenste pointerarray wordt vrijgegeven voordat de binnenste adressen zijn gebruikt om op te ruimen.
Herhaling maakt kleine geheugenlekken kostbaar
Stel dat een lus per doorgang 100 bytes reserveert en die nooit vrijgeeft. Na 1.000 doorgangen heeft de lus 100.000 bytes achtergelaten. Geef elke buffer aan het einde van de doorgang vrij, of reserveer één keer vóór de lus geheugen, gebruik het blok opnieuw en geef het daarna vrij. Hergebruik voorkomt ook herhaald werk voor de allocator wanneer dezelfde capaciteit volstaat.
Een dienst die 64 bytes per verzoek lekt, verliest na 10.000 verzoeken 640.000 bytes, precies 625 KiB. Na een miljoen verzoeken is 64.000.000 bytes verloren, ongeveer 61 MiB. Programma’s die lang draaien kunnen uiteindelijk geen geheugen meer reserveren of het systeem vertragen. Het besturingssysteem vordert procesgeheugen normaal gesproken terug bij het afsluiten, maar dat helpt de dienst niet zolang die draait.
Verloren en nog bereikbaar zijn verschillende meldingen
Als een globale pointer bij het afsluiten nog het beginadres van een toegewezen blok bevat, noemt Valgrind het blok “still reachable”. Het adres is niet verloren; het programma heeft het blok alleen niet vrijgegeven. “Definitely lost” betekent dat er geen bruikbare pointer meer bestaat. Ruim bewust op, zodat deze rapporten onverklaarde geheugenallocaties duidelijk tonen.
Belangrijkste punten
- Als je de laatste pointer die eigenaar is overschrijft of wist, kan een blok lekken.
- Zowel normale paden als paden met een vroege terugkeer of fout moeten opruimen.
- Herhaalde kleine geheugenlekken stapelen zich op terwijl het programma draait.
- Door een geschikte buffer opnieuw te gebruiken, kun je zowel herhaald allocatiewerk als geheugenlekken per iteratie voorkomen.
4.2 Wanneer een pointer langer bestaat dan zijn doel
Een pointer wordt ongeldig wanneer de levensduur eindigt van het object waarnaar die wijst. De variabele kan nog bestaan, maar via die pointer het oude object lezen of beschrijven is ongeldig. Zowel het teruggeven van een lokaal adres als het vrijgeven van een toegewezen blok kan dit probleem veroorzaken.
Zoek het laatste gebruik vóór de vrijgave
int *scores = malloc(3 * sizeof *scores);
if (scores == NULL) return EXIT_FAILURE;
scores[0] = 90;
scores[1] = 75;
scores[2] = 82;
free(scores);
printf("%d\n", scores[0]); /* use-after-free */
De toewijzing aan scores[0] gebeurde tijdens de levensduur van het blok. De laatste leesbewerking niet. Verplaats free(scores) naar na het afdrukken, zodra elk gebruik is afgerond. Alleen scores = NULL toewijzen vóór het afdrukken verandert de ongeldige toegang in een dereferentie van een nulpointer; het levert de ontbrekende gegevens niet op.
Dit is ongedefinieerd gedrag: C stelt geen eisen aan het resultaat. Het programma kan een oude waarde tonen, crashen of ongemerkt gegevens beschadigen. Vrijgegeven bytes blijven vaak op hun plek totdat de allocator ze opnieuw gebruikt. Een aannemelijk resultaat kan een leesbewerking na vrijgave daarom niet geldig maken.
Aliassen delen het levensduurprobleem
int *a = malloc(3 * sizeof *a);
if (a == NULL) return EXIT_FAILURE;
int *b = a;
free(a);
a = NULL;
b[0] = 1; /* b also lost its live target */
Er was één blok, met twee pointervariabelen die het adres bevatten. Als je het blok via a vrijgeeft, eindigt de levensduur voor beide pointers. a op nul zetten wijst alleen aan a een waarde toe; het kan b niet bijwerken. Code die geleende aliassen gebruikt, moet klaar zijn voordat de eigenaar het blok vrijgeeft. Daarna mogen die aliassen niet meer worden gebruikt.
Dubbele vrijgave gebruikt ook een beëindigde levensduur
free twee keer voor één blok aanroepen is dubbele vrijgave, ook als de tweede aanroep via een alias gebeurt. Dit kan de administratie van de allocator beschadigen of een latere geheugenallocatie laten mislukken. Sommige bibliotheken detecteren dit direct en stoppen het programma, maar directe detectie is niet gegarandeerd.
Een variabele die eigenaar is na vrijgave op NULL zetten helpt bij een herhaalde opruimaanroep, omdat free(NULL) geldig is. Het maakt free(b) niet veilig als b nog naar het voormalige blok verwijst. Precies één eigenaar moet bepalen wanneer het blok wordt vrijgegeven.
Ook een wijziging van de grootte beëindigt de oude levensduur
Een geslaagde realloc vervangt het oude object, ook als het teruggegeven numerieke adres onveranderd is. Opgeslagen pointers naar posities in het oude blok moeten opnieuw worden berekend vanuit de teruggegeven pointer. Bewaar posities als indexen zolang groei mogelijk is. Een mislukte realloc met een grootte groter dan nul laat het oude toegewezen blok daarentegen bestaan.
Een functie die het adres van een automatische lokale variabele teruggeeft, veroorzaakt een vergelijkbare levensduurfout zonder een aanroep van free: het object houdt op te bestaan wanneer de functie terugkeert. Gebruik opslag van de aanroepende functie of een resultaat op de heap met duidelijk eigenaarschap. Gebruik na vrijgave en dubbele vrijgave kunnen ook beveiligingsfouten worden wanneer een aanvaller de hergebruikte opslag kan beïnvloeden.
Belangrijkste punten
- Een ongeldige pointer naar een niet meer bestaand object wijst naar een object waarvan de levensduur is geëindigd.
- Rond elke lees- en schrijfbewerking af voordat je het blok vrijgeeft.
- Eén vrijgave maakt elke alias naar het oude object ongeldig.
- Een pointer die eigenaar is op nul zetten helpt bij herhaald opruimen, maar herstelt geleende aliassen niet.
- Dubbele vrijgave kan de administratie van de allocator beschadigen.
- Een geslaagde
reallocmaakt opgeslagen pointers naar posities in het oude blok ongeldig.
4.3 Geldige grenzen en geïnitialiseerde waarden
Voor geldige toegang tot een element is zowel voldoende opslag nodig als een waarde die is ingesteld voordat die wordt gelezen. Een blok kan de juiste grootte hebben maar niet-geïnitialiseerde gegevens bevatten, of bruikbare waarden bevatten maar buiten zijn grenzen worden benaderd.
Controleer het bereik van de elementen
Voor n elementen lopen de geldige indexen van nul tot en met n - 1. Een lus met i <= n voert één extra toegang uit. Bij tien ints van 4 bytes is het toegewezen blok 40 bytes; p[10] begint direct na die bytes. Gebruik i < n om de tien elementen te doorlopen.
Ook een index uit invoer moet vóór gebruik op zijn bereik worden gecontroleerd. Invoer met teken moet niet-negatief en kleiner dan het aantal elementen zijn. Houd het aantal bij de pointer en geef beide door aan functies. Een pointer alleen kan het toegestane bereik niet beschrijven.
Houd bij strings rekening met het afsluitende nulteken
char *copy = malloc(strlen(s));
strcpy(copy, s); /* writes the terminator beyond the allocation */
Dit onjuiste fragment heeft ook een foutcontrole nodig. De grootte herstel je met malloc(strlen(s) + 1), gevolgd door de controle en daarna de kopieerbewerking. “Amsterdam” heeft negen tekens, maar heeft tien bytes nodig met '\0'. Het afsluitende nulteken is een daadwerkelijk opgeslagen byte, ook al wordt het niet als onderdeel van de tekst afgedrukt.
Een pointerarray is geen opslag voor strings
char **names = malloc(n * sizeof *names);
Dit reserveert n plaatsen voor pointers. Het maakt geen tekenbuffers waarnaar die pointers kunnen wijzen. Wijs aan names[i] een geldig stringadres toe voordat je de pointer als string gebruikt. De pointer kan een bestaande array of een literal met een voldoende lange levensduur lenen, of eigenaar zijn van een afzonderlijk toegewezen stringkopie. Bij het opruimen van de geneste blokken worden alleen de strings vrijgegeven waarvan het programma eigenaar is.
Dezelfde redenering over grootte geldt voor numerieke arrays: malloc(n) telt bytes en sizeof p meet een pointer. Gebruik n * sizeof *p, met de overloopcontrole en de controle op een nulpointer, om een aantal elementen correct uit te drukken.
Schrijf vóór het lezen
int *t = malloc(3 * sizeof *t);
if (t == NULL) return EXIT_FAILURE;
t[0] = 5;
t[1] = 7;
printf("%d\n", t[0] + t[1] + t[2]); /* t[2] was never set */
free(t);
t[2] ligt binnen het toegewezen blok, maar de waarde is niet geïnitialiseerd. Wijs vóór de som een waarde toe, of gebruik calloc als nul de bedoelde beginwaarde is. De bytes die bij groei met realloc worden toegevoegd, vereisen dezelfde aandacht. Binnen de grenzen blijven initialiseert het geheugen niet.
Waarom de crash later kan optreden
Een schrijfbewerking buiten de grenzen kan nabijgelegen gegevens of de administratie van de allocator beschadigen. Een leesbewerking buiten de grenzen kan niet-gerelateerde gegevens blootleggen. Beide zijn ongedefinieerd gedrag. Een kleine grensoverschrijding kan toegewezen geheugen raken en aan directe detectie ontsnappen; een latere free kan de eerste zichtbare fout zijn.
Controleer de grootte van de geheugenallocatie, het indexbereik en de initialisatiegeschiedenis afzonderlijk. Een groter toegewezen blok kan geen betekenisvolle waarde leveren aan een niet-geïnitialiseerd element en op nul zetten kan een index buiten het bereik niet geldig maken.
Belangrijkste punten
- Geldige indexen voor
nelementen lopen van nul tot en metn - 1. - Opslag voor een string bevat één byte voor het afsluitende nulteken.
- Een array van stringpointers reserveert geen geheugen voor de strings zelf.
- Initialiseer alle waarden die een berekening leest, inclusief nieuw toegevoegde elementen.
- Een ongeldige schrijfbewerking kan gegevens al lang vóór een zichtbare crash beschadigen.
4.4 Een crash tot de oorzaak herleiden
Een segmentatiefout is een reactie van het besturingssysteem op verboden geheugentoegang. Veelvoorkomende oorzaken zijn het derefereren van een nulpointer of niet-geïnitialiseerde pointer, schrijven naar een stringliteral, toegang ver buiten een array, gebruik van vrijgegeven opslag en het uitputten van de stapel.
Wat de melding je vertelt
Op Linux stopt SIGSEGV meestal het proces en kan een shell “Segmentation fault (core dumped)” afdrukken. Een veelvoorkomende afsluitstatus van de shell is 139, volgens de signaalconventie van 128 plus signaal 11. macOS kan bij ongeldige toegang SIGSEGV of SIGBUS melden. Windows meldt de vergelijkbare fout als een toegangsschending, vaak met uitzonderingscode 0xC0000005.
Deze meldingen wijzen op een mislukte toegang, niet op de volledige voorgeschiedenis. Een kleine grensoverschrijding kan al vroeg een pointer beschadigen en veel later een crash veroorzaken. Veel ongeldige geheugentoegangen blijven binnen geheugen dat het proces kan adresseren, zodat ze ongemerkt gegevens beschadigen in plaats van een foutmelding te veroorzaken.
Begin met regelinformatie
Bouw een uitvoerbaar debugbestand met waarschuwingen:
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
Voer het daarna uit onder een debugger of hulpmiddel voor geheugencontrole. Een aanroepoverzicht vermeldt de functieaanroepen die actief waren bij de crash. Zoek het bovenste frame dat bij je eigen code hoort. Bibliotheekfuncties zoals free kunnen de plek zijn waar een ongeldige toestand zichtbaar wordt, terwijl een eerdere aanroep in je programma die toestand veroorzaakte.
Zoek op de gemelde regel elke pointer die wordt gederefereerd. Volg voor elke pointer de waarde terug naar de oorsprong. Is de geheugenallocatie gecontroleerd? Bestaat het object waarnaar wordt gewezen nog? Past de index binnen het blok? Heeft de pointer überhaupt een geldig adres gekregen?
Onderzoek een nulresultaat
int *p = NULL;
printf("%d\n", *p); /* invalid dereference */
Het afdrukken vereist een leesbewerking via p, maar op dat adres bestaat geen object met een geheel getal. Als p van een geheugenallocatiefunctie kwam, controleer dan het resultaat vóór deze regel en volg bij een fout een foutpad. Als de pointer na vrijgave is gewist, verplaats het benodigde gebruik dan naar vóór de vrijgave of verander waar de gegevens worden bewaard.
Aan een niet-geïnitialiseerde pointer, zoals in int *p; *p = 5;, is geen geldig doel toegewezen. Een geheugenallocatie, het adres van een nog bestaand object of een gecontroleerde geleende pointer moet dat doel vastleggen vóór het derefereren.
Volg de oorzaak wanneer symptomen verschuiven
Een eindeloos recursieve functie kan in een niet-geoptimaliseerde bouw de stackframes uitputten. Een stopvoorwaarde herstelt de logica. Een optimaliserende compiler kan sommige recursieve aanroepen echter omzetten in een lus, zodat het uitblijven van een stapelcrash de onbegrensde berekening niet nuttig maakt.
Afgedrukte voortgangsmeldingen zijn ook onvolledig bewijs. stdout wordt gebufferd: uitvoer kan ongeschreven blijven als een proces crasht, vooral bij omgeleide uitvoer. Gebruik voor een diagnostische melding stderr of roep fflush(stdout) aan na een voortgangsmelding. Als je een afdrukopdracht toevoegt, kan de geheugenindeling veranderen en kan een fout verborgen blijven; de ongeldige toegang is daarmee niet hersteld. Voer het programma opnieuw uit met een controlehulpmiddel om de onjuiste bewerking zelf te vinden.
Belangrijkste punten
- Een segmentatiefout meldt een verboden toegang; de oorspronkelijke beschadiging kan eerder zijn ontstaan.
- Bouw met debuginformatie en zoek het eerste relevante frame in je eigen code.
- Volg de oorsprong van elke pointer, de levensduur van het object waarnaar die wijst en het indexbereik.
- Gebufferde uitvoer kan een afdrukopdracht verbergen die vóór een crash is uitgevoerd.
- Een veranderd symptoom na het toevoegen van een afdrukopdracht vereist nog steeds onderzoek.
5Spoor geheugenfouten op met Valgrind en sanitizers
Hulpmiddelen voor geheugencontrole maken verborgen fouten zichtbaar door de toegang en de geschiedenis van het blok te melden. Valgrind observeert een uitvoerbaar debugbestand; AddressSanitizer voegt controles toe wanneer het programma wordt gebouwd. Leer hun meldingen te verbinden met elementgrenzen, allocatieplaatsen en vrijgaveplaatsen, en herstel de fout met behoud van correct eigenaarschap. Na een uitvoering zonder fouten volgen nieuwe invoerwaarden die andere paden doorlopen.
5.1 Een Valgrind-rapport lezen
Valgrinds standaardhulpmiddel Memcheck observeert een gecompileerd programma terwijl het draait. Het houdt heapgrenzen, levensduren van toegewezen blokken en de initialisatie van waarden bij. Het heeft geen bouw met een sanitizer nodig, maar debuginformatie maakt de rapporten veel gemakkelijker te gebruiken.
Bouwen en uitvoeren
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
valgrind --leak-check=full ./program
Plaats de eigen argumenten van het programma na ./program. Voeg --show-leak-kinds=all toe om alle geheugenlekcategorieën te tonen en --track-origins=yes wanneer de herkomst van een niet-geïnitialiseerde waarde onduidelijk is.
Valgrind draait op Linux en enkele andere Unix-achtige systemen, maar niet rechtstreeks op Windows of Macs met Apple-siliconchips. Memcheck vertraagt de uitvoering vaak ongeveer 20 tot 30 keer. Gebruik eerst een kleine invoer die de fout reproduceert en voer daarna verdere tests uit. Een test zonder fouten dekt alleen de paden die daadwerkelijk zijn uitgevoerd.
Lees toegangsgrootte en blokgrootte afzonderlijk
Een rapport met de kop “Invalid write of size 4” beschrijft een schrijfbewerking van vier bytes. Een adres “0 bytes after a block of size 40 alloc'd” begint direct na een toegewezen blok van 40 bytes. Als dat blok tien ints van 4 bytes bevat, begint de ongeldige toegang bij element 10; het laatste geldige element is 9.
Het rapport noemt ook de regel van de toegang en de aanroepstapel van de geheugenallocatie. Gebruik die samen: de geheugenallocatie vertelt je wat de bedoelde grenzen zijn en de toegang laat zien welke index die grenzen heeft overschreden. “Size 4” betekent niet dat het programma vier elementen voorbij de grens ging.
“Invalid read of size N” beschrijft op dezelfde manier een ongeldige leesbewerking. Een adres binnen een blok dat als “free'd” wordt beschreven, wijst op een levensduurprobleem; vergelijk de locaties van de geheugenallocatie, vrijgave en toegang. “Invalid free() / delete / delete[] / realloc()” kan wijzen op een dubbele vrijgave of een adres dat nooit het begin van een geldig toegewezen blok was.
Bij een voorwaarde die niet-ingestelde gegevens gebruikt, kan Memcheck “Conditional jump or move depends on uninitialised value(s)” melden. Die spelling hoort bij de melding van het hulpmiddel. De gemelde voorwaarde gebruikte de waarde; --track-origins=yes helpt achterhalen waar de niet-geïnitialiseerde opslag vandaan kwam.
Lees de geheugenlekcategorie
| Rapportcategorie | Betekenis |
|---|---|
| Definitely lost | Er bestaat geen pointer naar het blok meer |
| Indirectly lost | Het blok is alleen via een ander verloren blok bereikbaar |
| Possibly lost | Er is alleen een pointer naar een positie binnen het blok gevonden |
| Still reachable | Bij afsluiten bestaat nog een pointer, maar het blok is niet vrijgegeven |
Stel dat een hulpfunctie geheugen voor tien ints van 4 bytes reserveert in een lokale pointer en terugkeert zonder het geheugen vrij te geven of het adres terug te geven. Het blok van 40 bytes is zeker verloren. Als een globale pointer het beginadres bewaart, zou een niet-vrijgegeven blok daarentegen nog bereikbaar zijn. De totalen voor geheugenallocaties en vrijgaven in het heapoverzicht beschrijven gebeurtenissen, inclusief wijzigingen van de grootte; ze vervangen het redeneren over eigenaarschap niet.
Een afsluiting zonder fouten kan “All heap blocks were freed -- no leaks are possible” en “ERROR SUMMARY: 0 errors from 0 contexts” melden. Memcheck vindt doorgaans niet elke overschrijding van een lokale of globale array die binnen verder geldig geheugen blijft. Herstel de eerste gemelde fout, voer opnieuw uit en test meer invoerwaarden, waaronder lege invoer en paden die vroeg terugkeren.
Belangrijkste punten
- Gebruik debuginformatie zodat Memcheck je bronregels kan aanwijzen.
- Lees de toegangsbreedte, blokgrenzen en allocatiegeschiedenis in samenhang.
- Gebruik
--track-origins=yesom de herkomst van niet-geïnitialiseerde waarden te achterhalen. - Onderscheid verloren blokken van blokken die bij afsluiten nog bereikbaar zijn.
- Herstel de eerste fout voordat je latere gevolgen interpreteert.
- Voer meerdere invoerwaarden uit, omdat een controlehulpmiddel alleen uitgevoerde paden observeert.
5.2 Controles in het programma inbouwen
AddressSanitizer, of ASan, voegt tijdens het compileren controles op geheugentoegang toe. Het resulterende uitvoerbare bestand controleert zichzelf tijdens de uitvoering. Dit is nuttig voor gewone ontwikkelingstests, omdat de vertraging doorgaans ongeveer een factor twee is, veel minder dan bij Memcheck.
Compileer en koppel met de sanitizer
cc -std=c17 -g -fsanitize=address -fno-omit-frame-pointer \
program.c -o program-asan
./program-asan
Gebruik de sanitizervlag zowel bij het compileren als bij het definitieve koppelen als je in afzonderlijke stappen bouwt. -g levert regelinformatie en -fno-omit-frame-pointer helpt bruikbare aanroepstapels te behouden. De compiler van Microsoft gebruikt /fsanitize=address.
ASan detecteert grensoverschrijdingen op de heap, op de stapel en in globale arrays, gebruik na vrijgave en ongeldige en dubbele vrijgaven. Controles op de levensduur binnen het geldigheidsgebied en optionele controles op gebruik na terugkeer kunnen ook pointers naar voormalige lokale variabelen vinden. ASan vereist een nieuwe bouw en extra geheugen. Houd deze instrumentatie in ontwikkel- en testversies.
Lees de drie geschiedenissen
Een rapport over gebruik na vrijgave kan beginnen met “ERROR: AddressSanitizer: heap-use-after-free”. Een daaropvolgende “READ of size 4” beschrijft de ongeldige bewerking. Het rapport toont daarna waar het geheugen is “freed by thread T0 here:” en “previously allocated by thread T0 here:”.
Begin in elke aanroepstapel bij de eerste relevante regel in je code. De geheugenallocatie maakt het blok aan; de vrijgave beëindigt de levensduur; de gemelde leesbewerking vindt na dat einde plaats. Bepaal of je de vrijgave naar na de leesbewerking verplaatst of stopt met toegang tot het blok na vrijgave. Het rapport levert de geschiedenis, terwijl het bedoelde eigenaarschap de oplossing bepaalt.
Bij “heap-buffer-overflow” begint een adres “located 0 bytes after 20-byte region” direct voorbij het gebied. Reken bytes pas om naar elementen nadat je de door het programma gebruikte typegrootte hebt gecontroleerd. ASan stopt normaal bij de eerste fout, dus herstel kan bij de volgende uitvoering een andere fout aan het licht brengen.
Geheugenlekcontrole en niet-geïnitialiseerde waarden zijn verschillend
LeakSanitizer controleert bij afsluiten op niet-vrijgegeven toegewezen blokken. Op Linux is dit normaal ingeschakeld met ASan. Bij ondersteunde macOS-toolchains kun je dit inschakelen met ASAN_OPTIONS=detect_leaks=1; sommige Mac-toolchains bieden het niet. Apples afzonderlijke hulpmiddel leaks is daar een andere mogelijkheid om geheugenlekken te controleren.
ASan houdt niet bij of een waarde binnen de grenzen is geïnitialiseerd. Gebruik voor dat probleem Memcheck of Clangs MemorySanitizer op een ondersteunde Linux-configuratie. UndefinedBehaviorSanitizer controleert andere soorten ongedefinieerd gedrag, zoals overloop bij gehele getallen met teken, en kan met ASan worden gecombineerd met -fsanitize=address,undefined.
Kies voor elk controlehulpmiddel een afzonderlijke uitvoering
| Behoefte | Bruikbaar hulpmiddel |
|---|---|
| Snelle controles tijdens ontwikkeling, inclusief grenzen van lokale arrays | Bouw met ASan |
| Herkomst van niet-geïnitialiseerde waarden volgen zonder sanitizerinstrumentatie | Memcheck op een ondersteund systeem |
| Geheugenlekken bij afsluiten | LeakSanitizer of Memcheck |
Voer een uitvoerbaar ASan-bestand niet uit onder Valgrind. Bouw voor Memcheck een afzonderlijk gewoon uitvoerbaar debugbestand en combineer ASan en MemorySanitizer niet in één bouw. Varieer na een uitvoering zonder fouten de invoer, zodat andere paden voor geheugenallocatie, groei en opruimen worden uitgevoerd.
Belangrijkste punten
- ASan vereist compilatie en koppeling met zijn instrumentatievlag.
- Lees de aanroepstapels van de ongeldige toegang, vrijgave en geheugenallocatie om een levensduurfout te reconstrueren.
- LeakSanitizer controleert bij afsluiten op geheugenlekken waar het platform dit ondersteunt.
- ASan detecteert het gewone lezen van niet-geïnitialiseerde waarden niet.
- Gebruik afzonderlijke uitvoeringen en geschikte bouwversies voor ASan en Memcheck.
5.3 Van rapport naar gecontroleerde oplossing
Een geheugenrapport beschrijft een waargenomen fout en delen van de voorgeschiedenis. Een bruikbare oplossing herstelt de juiste grootte, grenzen, initialisatie en het juiste eigenaarschap en controleert daarna het gecorrigeerde programma opnieuw.
Beperk en reproduceer
Zoek de kleinste invoer die de fout nog veroorzaakt. Bouw met -g, waarschuwingen en weinig optimalisatie en gebruik daarna Memcheck of een bouw met ASan. Lees het eerste rapport voordat je latere rapporten onderzoekt; een beschadigde toestand kan een reeks misleidende fouten veroorzaken.
Stel dat een programma het volgende bevat:
size_t n = 5;
int *v = malloc(n * sizeof *v);
for (size_t i = 0; i <= n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
return 0;
Op een systeem met ints van 4 bytes meldt ASan een schrijfbewerking van vier bytes op de eerste byte na een toegewezen blok van 20 bytes. De schrijfbewerking staat in de lus. Memcheck kan ook de grensoverschrijding melden en later een niet-vrijgegeven blok; of het blok verloren of nog bereikbaar is, hangt af van welke pointers bij afsluiten nog bestaan.
Koppel het rapport aan de code
Vijf ints bezetten de indexen nul tot en met vier. De voorwaarde i <= n bereikt index vijf. De grootte van het toegewezen blok is correct; de lusgrens niet. Het blok groter maken zou het symptoom verbergen terwijl de doorloop verkeerd blijft.
Ook ontbreekt een controle van de geheugenallocatie en wordt het blok waarvan het programma eigenaar is niet vrijgegeven. Die problemen vereisen hun eigen oplossingen, ook als het eerste rapport het programma stopte voordat het programma die punten bereikte.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *v = malloc(n * sizeof *v);
if (v == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
free(v);
return EXIT_SUCCESS;
}
Het herstelde programma initialiseert vijf geldige elementen, drukt 4 af terwijl het blok bestaat en geeft het vrij. Het vaste kleine aantal past; een aantal uit invoer vereist vóór de geheugenallocatie ook een controle op overloop bij de berekening van de grootte.
Gebruik de geschiedenis in het rapport voor het juiste herstel
Vergelijk bij gebruik na vrijgave de aanroepstapels van de vrijgave en de toegang om het laatste geldige gebruik te vinden. Verplaats de vrijgave naar daarna of verwijder de toegang via de verouderde pointer. Een pointer op NULL zetten kan de vroegere gegevens niet opnieuw beschikbaar maken.
Bij een geheugenlek vertelt de aanroepstapel van de geheugenallocatie waar het blok ontstond. Die wijst niet de juiste regel voor vrijgave aan. Volg het eigenaarschap langs geslaagde terugkeer en fouten om te vinden waar het laatste gebruik eindigt. Verwijder bij dubbele vrijgave de dubbele vrijgave of verbeter tegenstrijdig eigenaarschap. De enige geldige vrijgave verwijderen veroorzaakt een geheugenlek.
Voeg bij niet-geïnitialiseerde gegevens --track-origins=yes van Memcheck toe. De regel die de waarde gebruikt kan ver verwijderd zijn van de geheugenallocatie die niet-ingestelde bytes leverde. Initialiseer elke waarde die de berekening leest, of gebruik calloc als nul de juiste begintoestand is.
Controleer het herstel op meer dan één pad
Bouw opnieuw en voer de oorspronkelijke invoer die de fout reproduceert opnieuw uit. Ga door totdat het rapport geen fouten meer bevat en probeer daarna lege invoer, groeigrenzen, gewone invoer en terugkeer bij fouten. Compilerwaarschuwingen vinden sommige problemen vóór de uitvoering; hulpmiddelen voor uitvoeringstijd controleren de paden die deze tests bereiken.
Belangrijkste punten
- Reproduceer met een kleine invoer en lees de eerste fout.
- Gebruik de blokgrootte en toegangspositie om de werkelijke grensfout te vinden.
- Controleer of geheugenallocatie is mislukt en of er wordt opgeruimd, ook als de eerste toegangsfout de uitvoering stopt.
- Geheugenlekrapporten tonen allocatieplaatsen; eigenaarschap bepaalt de vrijgaveplaatsen.
- Bouw na herstel opnieuw, voer de invoer die de fout reproduceerde opnieuw uit en doorloop extra paden.
Dynamisch geheugenbeheer in C: voorbeelden
1Volg een object door functieaanroepen
9 stappen
- 1IntroductieEen adres heeft een bestaand object nodig
Met een pointer kan een deel van een programma gegevens vinden die elders zijn gemaakt. Of de pointer bruikbaar is, hangt ervan af of die gegevens nog bestaan wanneer de pointer wordt gebruikt.
Volg een hulpfunctie die een array van gehele getallen maakt, het adres teruggeeft en de aanroepende functie het resultaat laat gebruiken. Houd de lokale pointer en de array bij het volgen van de aanroep uit elkaar: ze hoeven niet dezelfde levensduur te hebben.
- 2UitlegBenoem de drie levensduren
Een gewone lokale variabele heeft automatische opslagduur en houdt op te bestaan met haar blok. Een globale variabele of lokale variabele met
staticbestaat zolang het programma draait. Een toegewezen blok bestaat tot de vrijgave.Op gangbare systemen staan automatische objecten op de aanroepstapel en gebruiken toegewezen blokken de heap. Deze namen beschrijven gebruikelijke implementaties; de regels van C bepalen de levensduren ongeacht de geheugenindeling.
- 3VoorbeeldMaak de lokale pointer en het blok
Binnen
make_arraymaaktint *p = malloc(5 * sizeof *p);twee afzonderlijke objecten: de lokale pointer en, bij succes, een toegewezen blok voor vijf ints.Als een
int4 bytes is, bevat het blok 20 bytes. Controleerpvóór gebruik. De pointervariabele hoort bij de functieaanroep, maar het toegewezen blok maakt geen deel uit van de automatische opslag van die aanroep. - 4VoorbeeldGeef het adres terug
De hulpfunctie geeft
pterug en de aanroepende functie slaat het resultaat op inint *nums. Het adres wordt gekopieerd; de array niet. Daarna houden de lokale variabelen van de hulpfunctie op te bestaan.Bij succes verwijst
numsnog steeds naar de toegewezen array. De aanroepende functie controleert het resultaat, initialiseert en gebruikt de elementen en geeft het blok uiteindelijk vrij. De terugkeer heeft het blok niet vrijgegeven, omdat het toegewezen opslagduur heeft. - 5UitdagingWat blijft bestaan na de terugkeer?
Een hulpfunctie maakt een automatische
int counten een afzonderlijke array op de heap. De functie geeft het adres van de array terug. Welk van deze twee objecten kan de aanroepende functie nog gebruiken?---reveal---
De array op de heap blijft bestaan totdat die wordt vrijgegeven. De automatische
countvan de hulpfunctie hield op te bestaan bij de terugkeer. Het gekopieerde adres verwijst alleen naar de array; een adres bewaren zoucountniet behouden als de hulpfunctie in plaats daarvan&counthad teruggegeven. - 6VoorbeeldProbeer een lokale array terug te geven
Verander de hulpfunctie nu zodat die
int values[5];gebruikt envaluesteruggeeft. Die array heeft automatische opslagduur. De array houdt op te bestaan als de hulpfunctie terugkeert, waardoor de aanroepende functie geen bestaand object heeft om te gebruiken.De grootte is nog steeds vijf ints, maar de levensduur is anders. Een bruikbaar resultaat kan in plaats daarvan worden geplaatst in een buffer die de aanroepende functie beschikbaar stelt, of worden toegewezen en aan die functie worden overgedragen met een duidelijke verantwoordelijkheid voor het opruimen.
- 7VerbandKies een afspraak voor de buffer
Een statische resultaatbuffer blijft na terugkeer bestaan, maar elke aanroep deelt die. Een latere aanroep kan een eerder resultaat overschrijven. Een resultaat op de heap kan elke aanroep een eigen onafhankelijke opslag geven, waarbij de aanroepende functie verantwoordelijk is voor de vrijgave.
Voor tijdelijk werk is een kleine vaste lokale array eenvoudiger. Arrays waarvan de grootte afhangt van invoer, zeer grote arrays of uitbreidbare arrays vragen om gecontroleerde geheugenallocatie, zodat het programma fouten kan afhandelen en de levensduur kan bepalen.
- 8UitdagingTwee resultaten moeten onafhankelijk blijven
Een aanroepende functie heeft twee beschrijfbare resultaten van opeenvolgende aanroepen nodig en moet beide bewaren. Ze levert geen buffer aan. Zou één statische resultaatarray voldoen? Welk ontwerp past?
---reveal---
Eén statische array zou worden gedeeld en overschreven. Geef bij elke aanroep een afzonderlijk toegewezen resultaat terug, controleer elk resultaat en laat de aanroepende functie beide na hun laatste gebruik vrijgeven. De levensduur op de heap zorgt dat ze blijven bestaan; afzonderlijke blokken zorgen voor onafhankelijkheid.
- 9SamenvattingHoud het adres en de levensduur uit elkaar
Volg het object waarnaar een pointer wijst: automatische lokale variabelen houden op te bestaan met hun blok, statische objecten bestaan tijdens de uitvoering en toegewezen blokken moeten bewust worden vrijgegeven.
Vraag je bij een teruggegeven resultaat af of de opslag blijft bestaan en of aanroepende functies onafhankelijke kopieën nodig hebben. Vraag je bij tijdelijk werk af of een kleine vaste lokale array al voldoet. De vereiste grootte en levensduur bepalen het ontwerp voordat je een geheugenallocatieaanroep schrijft.
2Reserveer geheugen voor een raster en ruim op elk pad op
13 stappen
Sla deze les op om elke Walkthrough stap voor stap te volgen.
- 1IntroductieHoud rekening met succes en falen
- 2UitlegZet aantallen om naar bytes
- 3TipControleer vóór de vermenigvuldiging
- 4UitdagingAantal of pointergrootte?
- 5UitlegGeef tellers een beginwaarde
- 6VoorbeeldGebruik de rij die op nul is gezet
- 7UitdagingHoe zit het met latere groei?
- 8UitlegTel blokken, geen pointernamen
- 9VoorbeeldLaat een gedeeltelijke fout de opbouw stoppen
- 10UitdagingKies de volgorde van opruimen
- 11KernpuntEen eigenaar kan uitlenen
- 12UitdagingEén pad stopt vroeg
- 13SamenvattingHoud opruimen en opbouwen bij elkaar
3Voeg waarden toe zonder gegevens te verliezen
12 stappen
Sla deze les op om elke Walkthrough stap voor stap te volgen.
- 1IntroductieVergroot vóór de volgende opslag
- 2UitlegEen wijziging van de grootte kan het object vervangen
- 3VoorbeeldHoud falen en succes uit elkaar
- 4UitdagingWat blijft behouden na verkleinen?
- 5UitlegLengte en capaciteit beantwoorden verschillende vragen
- 6VoorbeeldPasseer de eerste groeigrens
- 7VoorbeeldPasseer de volgende grens
- 8KernpuntExtra ruimte zorgt voor minder kopieën
- 9UitdagingWelke toestand blijft na een mislukte toevoeging?
- 10VerbandEen string reserveert een plaats voor het afsluitende nulteken
- 11UitdagingEen geleende selectie behouden bij groei
- 12SamenvattingWerk de toestand pas na succes bij
4Onderzoek de hele geschiedenis van een blok
15 stappen
Sla deze les op om elke Walkthrough stap voor stap te volgen.
- 1IntroductieVier vragen bij elke toegang
- 2VoorbeeldVerlies een toegewezen blok door het adres te vervangen
- 3UitdagingRuimt het wissen van de pointer op?
- 4TipOnderzoek zowel uitgangen als lussen
- 5VoorbeeldGeef één keer vrij en maak alle aliassen ongeldig
- 6UitdagingHerstel het laatste gebruik
- 7KernpuntVerwachte uitvoer kan uit ongeldige code komen
- 8VoorbeeldTel het afsluitende nulteken mee
- 9VoorbeeldToegewezen betekent niet geïnitialiseerd
- 10UitdagingOnderscheid twee grenzen
- 11UitlegEen fout is een symptoom
- 12VoorbeeldLees de toegang die crasht
- 13UitdagingEen afdrukopdracht laat de crash verdwijnen
- 14UitdagingCombineer eigenaarschap en grenzen
- 15SamenvattingBepaal welke regel is geschonden
5Lees een rapport en controleer de oplossing
14 stappen
Sla deze les op om elke Walkthrough stap voor stap te volgen.
- 1IntroductieMaak van een melding een blokgeschiedenis
- 2UitlegGebruik een gewone debugversie voor Memcheck
- 3VoorbeeldInterpreteer de adresbeschrijving
- 4UitdagingLees bytes als bytes
- 5TipKies een rapportoptie voor het probleem
- 6UitlegBouw afzonderlijk een uitvoerbaar ASan-bestand
- 7VoorbeeldVolg gebruik na vrijgave door drie aanroepstapels
- 8UitdagingEen ASan-uitvoering zonder fouten mist een waardefout
- 9VoorbeeldKoppel het rapport aan de lus
- 10VoorbeeldZorg dat de gegevens tijdens het werk bestaan
- 11UitdagingWaar moet een geheugenlek worden hersteld?
- 12VerbandControleer afsluiting en platformondersteuning
- 13UitdagingRond de herstelcyclus af
- 14SamenvattingGebruik het bewijs, herstel en voer opnieuw uit
Dynamisch geheugenbeheer in C: oefenvragen
Set 1: Levensduren, byteaantallen en eerste controles10 Oefenvragen
Vraag 1 van 10
Toon antwoordVerberg antwoord
Vraag 2 van 10
Toon antwoordVerberg antwoord
Vraag 3 van 10
sizeof(int) gelijk aan 4. Hoeveel bytes moet de geheugenallocatie aanvragen?Toon antwoordVerberg antwoord
Vraag 4 van 10
hits[4] na een geslaagde int *hits = calloc(6, sizeof *hits);, vóór latere toewijzingen?Toon antwoordVerberg antwoord
calloc zet elke byte op nul, waardoor elementen van een geheel type de waarde nul krijgen. Index 4 ligt binnen de array van zes elementen en kan dus na de controle van de geheugenallocatie als nul worden gelezen. Nieuwe malloc-opslag zou niet zijn geïnitialiseerd; die regel geldt niet voor deze geslaagde calloc-allocatie.Vraag 5 van 10
Toon antwoordVerberg antwoord
Vraag 6 van 10
int *tmp = realloc(data, bytes); en geeft NULL terug. Het oorspronkelijke blok was geldig. Wat kan de eigenaar nu doen?Toon antwoordVerberg antwoord
realloc met een grootte groter dan nul laat het oorspronkelijke toegewezen blok en de inhoud ongewijzigd. Omdat het resultaat in tmp is opgevangen, bevat data nog steeds het geldige adres. Het vrijgeven van de tijdelijke nulpointer doet niets; daarmee kun je het nog behouden blok waarnaar data verwijst niet vrijgeven.Vraag 7 van 10
Toon antwoordVerberg antwoord
Vraag 8 van 10
int *view = owner; free(owner); owner = NULL; uit. Wat gebeurt er als de code daarna view[0] leest?Toon antwoordVerberg antwoord
view was een alias naar hetzelfde toegewezen blok, waarvan de levensduur eindigde bij free(owner). De toewijzing van NULL aan owner werkt alleen die pointervariabele bij. Ze verandert view niet en herstelt het oude blok niet, dus de latere leesbewerking is gebruik na vrijgave met ongedefinieerd gedrag.Vraag 9 van 10
strcpy naar een nieuwe heapbuffer. Wat is de kleinste benodigde geheugenallocatie in C-bytes?Toon antwoordVerberg antwoord
strcpy slaat ook het afsluitende nulteken op. De kleinste buffer heeft daarom acht bytes. Zeven bytes bieden ruimte voor de zichtbare letters, maar plaatsen het afsluitende nulteken buiten het toegewezen blok. Dat veroorzaakt een schrijfbewerking buiten de grenzen, ook als afdrukken lijkt te werken.Vraag 10 van 10
Een globale int *saved is aanvankelijk gelijk aan NULL. Een hulpfunctie voert deze code uit en keert terug. Ints zijn 4 bytes. Memcheck meldt een ongeldige schrijfbewerking direct na het toegewezen blok. Welke doorloopfout en geheugenlekcategorie bij afsluiten volgen hieruit?
int *p = malloc(7 * sizeof *p);
if (p == NULL) return;
for (size_t i = 0; i <= 7; ++i) p[i] = (int)i;
saved = p;
Toon antwoordVerberg antwoord
Set 2: Volg geheugenallocatie- en opruimpaden10 Oefenvragen
Sla deze les op om elk antwoord te zien en elke set te oefenen.
Vraag 1 van 10
p. De aanroepende functie slaat het resultaat op in values. Wat blijft direct na terugkeer van de hulpfunctie bestaan?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 2 van 10
n dat niet nul is vóór de geheugenallocatie dat n * sizeof *p niet in size_t past?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 3 van 10
a, controleert het resultaat en probeert daarna geheugen voor b te reserveren. De tweede geheugenallocatie mislukt. De functie is eigenaar van a en zal direct met een fout terugkeren. Wat moet worden opgeruimd?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 4 van 10
static int visits = 0; en voert return ++visits; uit. Welke waarde geeft de derde aanroep terug?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 5 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 6 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 7 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 8 van 10
free te crashen na een lus die een heaparray vult. Wat moet het onderzoek controleren voordat de vrijgaveaanroep wordt gewijzigd?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 9 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 10 van 10
Een programma begint met dit gecontroleerde blok en kiest later een tak op basis van p[5]. Een wijziging naar een grootte groter dan nul is geslaagd. ASan meldt niets, terwijl Memcheck een niet-ingestelde waarde bij de tak meldt. Welke oplossing past bij de volledige geschiedenis?
int *p = calloc(4, sizeof *p);
if (p == NULL) return EXIT_FAILURE;
int *tmp = realloc(p, 6 * sizeof *p);
if (tmp == NULL) { free(p); return EXIT_FAILURE; }
p = tmp;
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Set 3: Groei, levensduren en rapportgegevens10 Oefenvragen
Sla deze les op om elk antwoord te zien en elke set te oefenen.
Vraag 1 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 2 van 10
int *p = malloc(11 * sizeof p);. Hoeveel bytes vraagt de code aan en wat is er mis met de expressie?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 3 van 10
calloc met een grootte groter dan nul?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 4 van 10
p. Code bewaart index 4 en een pointer naar dat element. Een poging om via een tijdelijke pointer naar negen ints te groeien mislukt. Een latere verkleining naar drie slaagt en de pointer van de eigenaar wordt bijgewerkt. Kan de selectie daarna worden gebruikt?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 5 van 10
old, stelt cap = 16 in en wijst data = realloc(data, cap * sizeof *data) toe. Die aanvraag met een grootte groter dan nul mislukt voordat nog een waarde is opgeslagen. Welke toestand herstelt de ongewijzigde array?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 6 van 10
buffer. Na het laatste gebruik voert ze buffer = NULL; free(buffer); uit. Wat is het resultaat voor het oorspronkelijke blok?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 7 van 10
p en roept later free(alias) aan, waarbij alias vóór de vrijgave uit p is gekopieerd. Welke fout is dit?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 8 van 10
stdout bevat geen nieuweregelteken, waarna het programma crasht. De tekst verschijnt nooit. Waarom kun je hiermee niet betrouwbaar bepalen hoever de uitvoering is gekomen?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 9 van 10
program-asan gecompileerd met AddressSanitizer en wil een onafhankelijke Memcheck-uitvoering. Welk uitvoerbaar bestand moet Memcheck krijgen?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 10 van 10
items[n] = 0; direct na een geheugenallocatie voor n ints. De lus gebruikt i <= n, het resultaat is niet gecontroleerd en opruimen ontbreekt. Welke oplossing herstelt de oorzaak en de resterende problemen rond eigenaarschap?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Set 4: Herstel eigenaarschap onder veranderende omstandigheden10 Oefenvragen
Sla deze les op om elk antwoord te zien en elke set te oefenen.
Vraag 1 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 2 van 10
calloc geheugen voor vier gehele tellers gereserveerd en het blok later met succes vergroot naar zeven tellers. Wat moet gebeuren voordat teller 6 voor het eerst wordt verhoogd?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 3 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 4 van 10
p. Een poging om via tmp de grootte te wijzigen naar zeven ints mislukt, waardoor p ongewijzigd blijft. Daarna voert code p = realloc(p, 9 * sizeof *p) uit en ook die aanvraag mislukt. Er zijn geen aliassen. Welk toegewezen blok is nu verloren?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 5 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 6 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 7 van 10
char **labels = malloc(4 * sizeof *labels);, controleert het resultaat en voert daarna direct strcpy(labels[0], "east"); uit. Welk ontwerp herstelt het eerste item en maakt het eigenaarschap duidelijk?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 8 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 9 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 10 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Set 5: Volg volledige geheugengeschiedenissen10 Oefenvragen
Sla deze les op om elk antwoord te zien en elke set te oefenen.
Vraag 1 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 2 van 10
sizeof samples voor een gewone double *samples?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 3 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 4 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 5 van 10
X met succes toe zonder te groeien en werkt het afsluitende nulteken bij. Het volgende teken Y veroorzaakt een aanvraag voor groei via een tijdelijke pointer, maar die mislukt. De API belooft de string bij een fout te behouden. Welke toestand blijft over?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 6 van 10
a en 40 bytes in de lokale b. Op het foutpad geeft de taak a vrij, stelt b = NULL in en keert terug. Er blijven geen aliassen over. Welke totale grootte aan toegewezen geheugen gaat verloren en welke wijziging voorkomt dit?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 7 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 8 van 10
int *p = malloc(6 * sizeof p);, controleert of dit slaagt, initialiseert indexen 0 tot en met 5 en telt daarna indexen 0 tot en met 11 op. Welke diagnose past bij de hele som?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 9 van 10
p[index] leest. Wat moet je aan die toegang controleren?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 10 van 10
display, een eerdere vrijgave in reset en een geheugenallocatie in load. De aanroepende functie roept load, reset en daarna display aan op hetzelfde resultaat. Ze heeft die weergave nodig. Welke oplossing volgt uit de geschiedenis van het blok?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Kom je niet uit je eigen opgave? Huiswerkhulp
Kernbegrippen
A
Aanroepoverzicht
Aanroepstapel
AddressSanitizer
Afsluitend nulteken
Alias
NULL zetten laat opgeslagen aliassen onbruikbaar voor latere leesbewerkingen.Allocatieplaats
Array met variabele lengte
int a[n] biedt niet de controle op een nulresultaat bij fouten die geheugenallocatie op de heap wel biedt.Automatische opslag
B
Buffer die de aanroepende functie beschikbaar stelt
Bufferhergebruik
C
calloc
calloc reserveert een array met een aantal en elementgrootte, zet de bytes op nul en geeft een nulpointer terug als de geheugenallocatie mislukt.calloc-aanvraag beginnen op nul vóór de eerste verhoging.Capaciteit
Controle-uitvoering
D
Dubbele vrijgave
free aanroepen op wat één gedeeld bestaand toegewezen blok was.Dynamische array
E
Eén-ernaastfout
i <= n te veranderen in i < n bezoekt een doorloop vanaf nul precies de n bedoelde elementen.Eerste gemelde fout
Eigenaarschap
F
free
free geeft een nog bestaand toegewezen blok vrij voor hergebruik wanneer het beginadres wordt doorgegeven en doet niets wanneer NULL wordt doorgegeven.G
Gebruik na vrijgave
Gedeelde statische buffer
Geheugenallocatie
Geheugenlek
Geleende pointer
Geneste geheugenallocatie
H
Heap
Heapoverzicht
Herkomsttracering
--track-origins=yes kan Memcheck verklaren waarom een latere tak afhangt van niet-ingestelde heapgegevens.I
Indirect verloren
Initialisatie
Instrumentatie
-fsanitize=address toe te voegen tijdens het compileren en koppelen, maak je een uitvoerbaar bestand met ASan-instrumentatie.Invoer die de fout reproduceert
L
LeakSanitizer
Lengte
Levensduur van een object
realloc beëindigt de levensduur van het oude object, ook als het teruggegeven adres onveranderd lijkt.Lezen van een niet-geïnitialiseerde waarde
M
malloc
malloc reserveert een aangevraagd aantal bytes en geeft een blokadres of een nulpointer terug, zonder de opgeslagen waarden te initialiseren.malloc is verkregen, heeft toewijzingen nodig voordat de elementen worden opgeteld of vergeleken.Meetkundige groei
Memcheck
MemorySanitizer
Met nullen gevuld geheugen
calloc begint met een nulteken en bevat een lege string.Mogelijk verloren
N
Nog bereikbaar
Nulpointer
O
Ongedefinieerd gedrag
Ongeldige pointer naar een niet meer bestaand object
Opruimpad
Opslagduur
Overloop bij de berekening van de geheugengrootte
size_t overschrijdt en modulair terugloopt naar een kleinere weergeefbare waarde.SIZE_MAX / sizeof *p voordat je het met de elementgrootte vermenigvuldigt.Overschrijding van heapbuffergrenzen
P
Pointer naar een positie binnen een object
R
realloc
realloc vervangt een toegewezen blok door een object met de aangevraagde grootte, behoudt het passende begindeel en kan het adres veranderen.S
Segmentatiefout
size_t
size_t is het geheelgetaltype zonder teken voor objectgroottes en byteaantallen bij geheugenallocatie, inclusief het resultaat van sizeof.size_t-variabelen voor de lengte en capaciteit zodat die objectgroottes kunnen beschrijven.sizeof
sizeof geeft de grootte van een type of object in C-bytes; op een pointer meet die de pointer in plaats van het toegewezen blok waarnaar die wijst.n * sizeof *p volgt het elementtype, terwijl sizeof p de lengte van de array niet kan achterhalen.Stapeloverloop
Statische opslag
static.Stringliteral
char word[] = "hello"; wanneer een lokale beschrijfbare kopie nodig is in plaats van een pointer naar de literal.T
Tijdelijke pointer
tmp = realloc(p, bytes) blijft p bij een nulresultaat beschikbaar voor verder gebruik of opruiming.U
Uitvoerbuffering
fflush(stdout) na een voortgangsmelding aan te roepen, maak je de gebufferde tekst beschikbaar vóór de volgende bewerking.UndefinedBehaviorSanitizer
-fsanitize=address,undefined gebruiken om de twee verenigbare verzamelingen controles in te schakelen.V
Verloren geheugenallocatie
NULL zetten zonder eerst free aan te roepen, laat het adres van het toegewezen blok verloren gaan.Z
Zeker verloren
Over deze les
Door StudeliFout melden
Maak je eigen Studiemateriaal
Maak van je aantekeningen, slides en documenten Leermodules, Oefenvragen, Flashcards en 6 andere tools.