Pointers naar pointers begrijpen in C

0
12

Het klinkt als een recursie-nachtmerrie, maar verwijzingen naar verwijzingen vormen een hoofdbestanddeel van systeemprogrammering. U wijst in wezen naar een geheugenadres dat het adres van uw feitelijke gegevens bevat. Deze dubbellaagse indirectheid is wat ontwikkelaars een handle noemen. Het is niet alleen een codeertruc. Het is een noodzakelijk mechanisme voor besturingssystemen om heapgeheugen efficiënt te beheren.

Beschouw de hoop als een volle ruimte. Soms moet het besturingssysteem mensen door elkaar heen schuiven om ruimte te maken. Als u een directe verwijzing naar iemand vasthoudt, kan deze niet bewegen zonder uw referentie te verbreken. Maar als u een verwijzing naar een lijst met namen vasthoudt, kan het besturingssysteem de locatie van elke persoon in die lijst wijzigen zonder dat u uw oorspronkelijke referentie hoeft bij te werken. Dat is de kracht van een handvat.

Hier ziet u hoe die logica zich vertaalt in onbewerkte C-code. Je declareert p als een pointer naar een pointer. Dan wordt q een standaardaanwijzer. Je wijst geheugen toe voor p en wijst vervolgens geheugen toe voor datgene waar p naar verwijst. Tenslotte derefereer je tweemaal om de waarde 12 toe te kennen.

Windows en macOS vertrouwen op deze structuur voor geheugencompressie. Het onderscheid is hier essentieel. Jij, de programmeur, beheert de buitenste aanwijzer p. Het besturingssysteem beheert de interne aanwijzer *p. Omdat het besturingssysteem *p bestuurt, kan het het daadwerkelijke datablok (**p) overal in de heap verplaatsen. Het werkt simpelweg *p bij om het nieuwe adres weer te geven. Je code blijft p probleemloos gebruiken.

Naast het geheugenbeheer van het besturingssysteem is dit patroon essentieel voor het doorgeven van verwijzingen naar functies. Als je een functie nodig hebt om een ​​pointer zelf te wijzigen, geef je een pointer door aan die pointer. Het is de enige manier om de referentie opnieuw toe te wijzen vanuit een ander bereik.

Verwijzingen naar structuren beheren

De complexiteit stopt niet bij eenvoudige gehele getallen. Je kunt deze logica in structuren nesten. Dit is gebruikelijk bij het verwerken van gegevens met variabele lengte, zoals tekenreeksen.

Neem de Addr -structuur. Het bevat arrays met een vaste grootte voor namen, steden en telefoons. Maar de opmerkingen variëren in lengte. Dus ‘commentaar’ wordt gedefinieerd als een verwijzing naar char. Wanneer u geheugen toewijst aan de structuur zelf, reserveert u ruimte voor de pointers en de vaste arrays. U reserveert nog geen ruimte voor de daadwerkelijke commentaartekst.

U wijst eerst s toe. Vervolgens leest u gebruikersinvoer in de vaste buffers. Daarna leest u de opmerking in een tijdelijk bericht

Je verliest geheugen als je negeert hoe wijzers zich nestelen.

Neem een ​​pointer s die naar een structuur wijst. Die structuur bevat nog een andere aanwijzer. Die tweede pointer verwijst naar een daadwerkelijke string in het geheugen. Twee lagen van indirectheid. Eén toewijzing voor de struct. Eén toewijzing voor de tekenreeks.

Eenvoudig genoeg totdat je probeert op te ruimen.

Dit is waar de meeste ontwikkelaars struikelen. Je ziet ‘gratis(en)’. Je denkt dat je klaar bent. Je hebt het mis.

Kijk eens naar dit codefragment:

De variabele s verwijst naar de Addr -structuur. Binnen die structuur bevindt zich een veld ‘commentaar’. comment verwijst naar een blok heap-geheugen dat is toegewezen aan de stringgegevens.

Wanneer u free(s) aanroept, maakt u het geheugen voor de structuur zelf vrij. De Addr -structuur is verdwenen. Het geheugen wordt teruggestuurd naar het systeem.

Maar wat gebeurt er met s->commentaar?

Het verdwijnt in de ether.

De verwijzing naar de stringgegevens is opgeslagen in de structuur die u zojuist hebt vrijgemaakt. Je hebt er geen toegang meer toe. Je hebt free() niet aangeroepen voor de string. Het geheugen blijft toegewezen maar onbereikbaar.

Dit is een geheugenlek.

Het is geen crash. Het is geen foutmelding. Het programma draait prima. Het verbruikt gewoon langzaam meer RAM totdat het systeem verwisselt of crasht.

Hoe dubbele pointer-lekken op te lossen

Je moet eerst de innerlijke aanwijzer vrijmaken. Of sla het op in een tijdelijke variabele voordat u de buitenste structuur vrijmaakt.

Nu worden de stringgegevens vrijgegeven. Vervolgens wordt de structuur vrijgegeven. Geen verloren blokken.

Waarom dit zo vaak gebeurt

Mensen beschouwen aanwijzingen als waarden. Ze vergeten dat verwijzingen verwijzingen naar bronnen zijn.

Wanneer een structuur een aanwijzer bevat, staat die structuur niet op zichzelf. Het is afhankelijk van extern geheugen. Door de container vrij te maken, komt de inhoud niet vrij.

Dit wordt erger bij dieper nestelen. Een pointer naar een pointer naar een pointer. Drie niveaus. Je hebt drie free() -aanroepen nodig. In de juiste volgorde.

Als je er één vergeet, lek je.

Het get()-probleem

In het voorbeeld wordt gets() gebruikt. Die functie is gevaarlijk. Het is verwijderd uit de C11-standaard omdat het bufferoverflows toestaat. Maar de geheugenlogica blijft hetzelfde.

Of u nu gets(), fgets() of scanf() gebruikt, de toewijzingsstrategie is hier het knelpunt voor lekken.

Je wijst toe voor s.
Je wijst toe voor s->commentaar.

Als u alleen s vrijgeeft, laat u s->commentaar achter.

Belangrijkste afhaalrestaurants

  • Dubbele pointers vereisen dubbel vrij.
  • Controleer elke ‘malloc’ op een overeenkomstige ‘gratis’.
  • Als een structuur een verwijzing naar dynamisch geheugen bevat, moet dat geheugen vóór de structuur worden vrijgegeven.
  • free() bevrijdt leden niet recursief.

Het is gemakkelijk te missen. De code compileert. Het loopt. Het lek is stil.

Tot het niet meer zo is.

Gekoppelde lijsten bouwen

U kunt een geheugenramp krijgen als u de container weggooit vóór de gegevens waarnaar deze verwijst. De structuur die de aanwijzer vasthoudt, wordt opgeruimd. Het stringblok blijft. Het wordt een verloren blok. Dit gebeurt wanneer de volgorde van verwijdering verkeerd is.

Koppelen

Structuren kunnen naar zichzelf verwijzen. Hierdoor kunt u identieke records aan elkaar koppelen. Het resultaat is een gekoppelde lijst. Het is een standaardmanier om gegevens in C te ordenen.

Hier is hoe je het definieert:

typedef struct {charnaam[21]; char stad[21]; char-status [21]; Adres volgende; } Adres;
Adres
eerste;

Het veld ‘volgende’ bevat het adres van het volgende record. U gebruikt een enkele pointervariabele om de keten te starten.

De kosten van flexibiliteit

Met de compiler kunt u regels ombuigen die op het eerste gezicht misschien contra-intuïtief lijken. Met voldoende ervaring kun je structuren ontwerpen die er uitzien als hierboven weergegeven. Het is een machtsbeweging.

Maar het is niet zonder risico. Je bevindt je op een dunne lijn tussen slimme code en onhoudbare spaghetti.

Waarom het ertoe doet

Dit gaat niet alleen over syntaxis. Het gaat erom wat de taal je toestaat te doen als je tegen de beperkingen ervan ingaat.

  • Controle : u krijgt gedetailleerde controle over de geheugenindeling.
  • Interoperabiliteit : u kunt gemakkelijker met C-bibliotheken praten.
  • Prestaties : soms bespaart het omzeilen van veiligheidscontroles cycli.

De vangst? De compiler zal je niet van jezelf redden. Hiermee kunt u code compileren die tijdens runtime crasht.

Hoe u het veilig kunt gebruiken

Als je dit gaat doen, doe het dan met de bedoeling.

  1. Isoleer het : Verspreid dit patroon niet door uw codebase. Bewaar het in een kleine, goed geteste module.
  2. Documenteer alles : Je zult de toekomst dankbaar zijn. Of haat je. Ik haat je waarschijnlijk.
  3. Gebruik abstracties : Verpak de onbewerkte verwijzingen of onveilige blokken in een schone interface. Verberg de rommel.

De realitycheck

De meeste ontwikkelaars hebben dit niet nodig. Ze zullen standaardstructuren gebruiken. Ze zullen er gelukkiger door zijn.

Maar als je tegen een muur aanloopt waar de standaardbibliotheek niet past, zul je blij zijn dat de compiler je niet heeft tegengehouden.

De vraag is of u bereid bent de onderhoudskosten te betalen.

Het is een afweging. Snelheid voor veiligheid. Kracht voor duidelijkheid.

Jij kiest.

попередня статтяUnix versus Linux: waarom het onderscheid belangrijk is voor ontwikkelaars
наступна статтяHoe synthesizers muziek veranderden door de vormregels te overtreden