Hoe verwijzingen naar structuren werken in C

7

Je kunt naar bijna alles verwijzen in C. Met de taal kun je verwijzingen maken naar primitieve typen, arrays, functies en door de gebruiker gedefinieerde typen. Maar verwijzingen naar structuren zijn waar dingen interessant worden. Ze komen zeer vaak voor. Als u C-code schrijft, zult u deze waarschijnlijk vaak tegenkomen.

Hier is een eenvoudig voorbeeld. Het laat zien hoe u een structuur definieert en er vervolgens een verwijzing naar maakt.

Dit fragment doet meer dan alleen een geheugenadres toewijzen. Het legt een relatie vast tussen de variabele ptr en de struct p1. De aanwijzer bevat het adres van de structuur. Hiermee kunt u indirect toegang krijgen tot de leden x en y. Dit is een fundamenteel patroon. Je ziet het in gekoppelde lijsten, dynamische geheugentoewijzing en functieargumenten.

Waarom verwijzingen naar structuren gebruiken?

Het kopiëren van hele structuren is duur. Als uw structuur veel gegevens bevat, betekent het doorgeven ervan aan een functie dat de compiler een volledige kopie maakt. Dit verspilt tijd en geheugen. Het gebruik van een aanwijzer vermijdt de kopie. U geeft in plaats daarvan het adres door. De functie kan de originele gegevens zonder overhead wijzigen.

Er is ook de kwestie van dynamische dimensionering. Arrays hebben tijdens het compileren een vaste grootte. Structuren lossen het dynamische probleem niet noodzakelijkerwijs op zichzelf op. Maar als je een struct combineert met malloc, krijg je flexibele gegevensopslag. U kunt een structuur op de heap toewijzen. Met een verwijzing naar die structuur kunt u de levensduur ervan handmatig beheren.

Toegang tot leden via pointers

Zodra u een aanwijzer heeft, moet u toegang krijgen tot de gegevens daarin. U kunt de puntoperator niet gebruiken. De expressie ptr.x kan niet worden gecompileerd. ptr is een adres, niet de structuur zelf. Je moet het eerst derefereren.

Er zijn twee manieren om dit te doen. De eerste is uitgebreid.

De haakjes zijn hier verplicht. De puntoperator heeft een hogere prioriteit dan de dereferentieoperator. Zonder deze probeert de compiler toegang te krijgen tot ptr.x, wat niet bestaat.

De tweede manier is schoner. C levert de pijloperator ->. Het combineert dereferentie en ledentoegang in één stap.

Deze syntaxis is standaard. Het is wat je zult zien in echte codebases. Het luidt uiteraard als “ga naar het object waarnaar ptr verwijst en haal het x lid op.” Het is beknopt. Het vermindert visuele rommel.

Veelvoorkomende valkuilen

Nulaanwijzers vormen een risico. Als u een pointer declareert maar deze niet initialiseert, verwijst deze naar garbage. Toegang tot ptr->x op een null pointer veroorzaakt een segmentatiefout. U moet op nul controleren voordat u de aanwijzer gebruikt.

Deze controle is niet optioneel in robuuste code. Het voorkomt crashes.

Een ander probleem zijn geheugenlekken. Als u geheugen toewijst met malloc en de pointer verliest, blijft dat geheugen gereserveerd totdat het programma eindigt. U moet elke toewijzing bijhouden. free() is uw verantwoordelijkheid. Vergeet het

Verwijzingen naar structuren in C laten ontwikkelaars vaak struikelen die vertrouwd zijn met basisverwijzingen, maar verstrikt raken in de syntaxis. Overweeg een eenvoudige recorddefinitie voor een gebruikersprofiel.

typedef-structuur {
tekennaam [21];
char stad[21];
char-status [3];
} Opname;

typedef Rec *RecPointer;

Hier is RecPointer slechts een alias voor een verwijzing naar een Rec -structuur. Wanneer u een variabele van dit type declareert, heeft u te maken met een geheugenadres, niet met de gegevens zelf.

RecPointer r;

De variabele r neemt vier bytes in beslag op een 32-bits systeem (of acht op 64-bits). Het is slechts een aanwijzer. Het bevat niet de naam, stad of staat. Om daadwerkelijke gegevens op te slaan, moet u geheugen op de heap toewijzen.

r = (RecPointer)malloc(groottevan(Rec));

Deze ‘malloc’-aanroep reserveert 45 bytes. Dat zijn 21 bytes voor de naam, 21 voor de stad en 3 voor de staat, plus één byte voor opvulling of uitlijning om aan de geheugengrenzen te voldoen. Nu verwijst r naar een geldig geheugenblok dat zich precies als een Rec -structuur gedraagt.

Toegang tot leden via dereferentie

Om met de gegevens te kunnen communiceren, moet u de aanwijzer derefereren. Dit is waar prioriteitsfouten binnensluipen. U kunt toegang krijgen tot leden met behulp van de standaard puntnotatie, maar u moet de dereferentie tussen haakjes plaatsen.

strcpy((r).naam, “Leigh”);
strcpy((
r).stad, “Raleigh”);
strcpy((*r).state, “NC”);

Let op de syntaxis. (*r).naam is correct. Als u *r.name schrijft, mislukt het compileren. Waarom? Omdat de puntoperator een hogere prioriteit heeft dan de dereferentieoperator. De compiler interpreteert *r.name als *(r.name). Omdat r een pointer is, is r.name een ongeldige syntaxis en valt de expressie samen. De haakjes zorgen ervoor dat de dereferentie *r eerst plaatsvindt, waardoor de structuur ontstaat, waarna de puntoperator toegang krijgt tot het naam -veld.

Het is vervelend om te typen. Het ziet er rommelig uit. Het nodigt uit tot fouten.

De pijlnotatie

C biedt een schonere manier om hiermee om te gaan. De pijloperator -> is syntactische suiker voor (*pointer).member. Het is geen ander mechanisme. Het is geen nieuwe operator die de manier verandert waarop toegang tot het geheugen wordt verkregen. Het is eenvoudigweg een kortere manier om de dereferentie en ledentoegang in één stap te schrijven.

strcpy(r->naam, “Leigh”);

Dit is identiek aan strcpy((*r).name, "Leigh"). Het bespaart twee karakters. Het elimineert de noodzaak voor geneste haakjes. Het is de standaardmanier waarop de meeste C-ontwikkelaars omgaan met structuuraanwijzers.

Implicaties voor geheugenbeheer

De free(r) oproep is verplicht. Het geheugen is toegewezen vanuit de heap. Als je het niet loslaat, lekt het. De pointer r zelf, de variabele van vier bytes die het adres bevat, is lokaal voor het stapelframe of de globale scope, maar de gegevens waarnaar deze verwijst, bevinden zich elders.

Wanneer u r->naam gebruikt, wijzigt u de gegevens op het adres dat is opgeslagen in r. De wijzer r blijft ongewijzigd. Het adres wel

Het direct toewijzen van geheugen voor arrays is een hoofdbestanddeel van C-programmeren, maar vereist wel inzicht in de manier waarop pointers omgaan met onbewerkte geheugenblokken. Als je een array met een vaste grootte nodig hebt die tijdens het compileren niet bekend is, volstaat standaard stacktoewijzing niet. Je moet naar de hoop reiken.

Het onderstaande codefragment laat één gemeenschappelijk patroon zien:

Hier reserveert malloc ruimte voor tien gehele getallen. De cast naar (int *) zorgt ervoor dat de pointer overeenkomt met het verwachte type, ook al waarschuwen moderne C-compilers vaak voor expliciete casts voor void*. De lus initialiseert vervolgens elk element naar nul met behulp van subscriptnotatie. Ten slotte geeft ‘free’ het geheugen weer vrij aan het systeem.

Maar dit is niet de enige manier om het te schrijven.

U kunt exact hetzelfde resultaat bereiken door p[i] uit te wisselen voor pointer-berekeningen:

Waarom doet dit er toe? Omdat p[i] slechts syntactische suiker is voor *(p+i). De compiler behandelt ze op dezelfde manier. Als u met embedded systemen werkt of strakke loops schrijft waarbij elke cyclus telt, kunt u in de wetenschap dat deze uitwisselbaar zijn, oudere code lezen en uw eigen code schrijven zonder verwarring.

Toch is er een subtiele valkuil.

Als je een pointer rechtstreeks naar een arraytype declareert, zoals int (*p)[10], heb je te maken met een heel ander beest. Die aanwijzer verwijst naar de volledige array, niet alleen naar het eerste element. Als u dit verhoogt, wordt de aanwijzer verplaatst met de grootte van de hele array, niet met een enkel geheel getal. De meeste ontwikkelaars blijven bij int * omdat het eenvoudiger en flexibeler is.

Wanneer welke aanpak gebruiken?

Kiezen tussen subscriptnotatie en expliciete pointer-berekeningen komt vaak neer op leesbaarheid en intentie.

  • Gebruik p[i] als u de nadruk wilt leggen op indexgebaseerde toegang. Voor de meeste lezers is het duidelijker.
  • Gebruik *(p+i) wanneer u geheugenmanipulatie op laag niveau uitvoert of de eigenaardigheden van array-verval in complexe expressies wilt vermijden.

Beide benaderingen vereisen zorgvuldig geheugenbeheer. Vergeet ‘gratis’ en je lekt. Noem het te vroeg en je zult ongedefinieerd gedrag tegenkomen. En hoewel ‘malloc’ hier eenvoudig is, moet u altijd controleren of de geretourneerde waarde niet ‘NULL’ is voordat u de verwijzingen verwijdert.

“Verwijzingen naar arrays zijn krachtig, maar ze vereisen discipline. Eén misstap en je leest onzin, of erger nog: het bederft het geheugen van een andere variabele.”

In de praktijk hoeven de meeste ontwikkelaars zelden ruwe pointer-berekeningen te schrijven voor eenvoudige arrays. Bibliotheken zoals std::vector in C++ of abstracties op een hoger niveau in andere talen verwerken dit automatisch. Maar in C? Je staat er alleen voor.

En daarom is het belangrijk om de mechanismen te begrijpen. Niet alleen voor het afleggen van interviews of het schrijven van schoolboeken, maar ook voor als de code om twee uur ‘s nachts breekt en je precies moet weten wat er in het geheugen gebeurt.

Wanneer u een pointer naar een integer-array declareert, creëert u niets exotisch. Het is slechts een standaard verwijzing naar een int. De magie gebeurt met malloc. U wijst een geheugenblok toe dat groot genoeg is voor het aantal gehele getallen dat u nodig heeft. De aanwijzer richt zich vervolgens op het eerste element van dat blok.

Het maakt C niet uit hoe je er toegang toe krijgt. U kunt vierkante haakjes gebruiken, zoals p[5], of pointer-berekeningen gebruiken, zoals *(p + 5). De compiler beschouwt ze als identiek. Deze flexibiliteit is de reden waarom dynamische arrays zo handig zijn voor strings. Je raadt de maat niet. U wijst precies genoeg opslagruimte toe voor de tekenreekslengte plus de null-terminator.

Arrays van aanwijzers versus arrays van structuren

Waarom een array van pointers gebruiken als je gewoon een array van structs zou kunnen gebruiken? Ruimte. Of beter gezegd: het gebrek daaraan.

Beschouw een structuur ‘Rec’ met drie karakterarrays van elk 81 bytes. Dat is 243 bytes per record. Als u Rec records[10] declareert, reserveert u onmiddellijk 2.430 bytes in het geheugen. Alles. Zelfs als u maar één record gebruikt.

Een reeks pointers verandert de wiskunde.

De array a zelf bevat slechts 10 pointers. Op een 64-bits systeem is dat 80 bytes. Dat is een fractie van het geheugen dat nodig is voor de volledige structuren. Het geheugen voor de daadwerkelijke records blijft ongebruikt totdat u het nodig heeft.

U kunt op aanvraag één record toewijzen.

Dit patroon lost geheugenintensieve problemen op door de toewijzing uit te stellen. U betaalt alleen voor wat u gebruikt. Als je klaar bent met de plaat, bel je ‘gratis’. De aanwijzer wordt een bungelende referentie, maar het geheugen wordt teruggestuurd naar het systeem.

Structuren die aanwijzingen bevatten

Structuren kunnen aanwijzingen bevatten. Hierdoor kunt u gegevens met een vaste grootte combineren met gegevens met een variabele grootte in hetzelfde object.

Neem een ​​adresboekvermelding op. De naam, stad en telefoonnummer kunnen een redelijke maximale lengte hebben. Een opmerking kan echter van alles zijn, van een enkel woord tot een roman. U wilt geen ruimte verspillen door voor elke invoer een enorme buffer toe te wijzen, voor het geval één persoon een lang commentaar schrijft.

De Addr -structuur zelf is klein

Hoe commentaarvelden omgaan met lege versus ingevulde records

Niet elk record in de database bevat een opmerking. Wanneer een veld leeg wordt gelaten, blijft het niet leeg. Er zit een wijzer in. Concreet een pointer van 4 bytes die naar niets wezenlijks verwijst. Het systeem beschouwt deze afwezigheid als een geldige status. Het record is nog steeds compleet. De metagegevens zijn intact.

Maar wat gebeurt er als een gebruiker daadwerkelijk iets typt?

De toewijzing verandert. De database reserveert geen vaste buffer voor deze opmerkingen. Het raadt niet naar een maximale lengte en vult de rest op met null-bytes. Dat zou ruimteverspilling zijn. In plaats daarvan berekent het systeem de exacte snaarlengte. Vervolgens wijst het precies zoveel bytes toe.

Deze dynamische allocatie is efficiënt. Het voorkomt fragmentatie door een opgeblazen gevoel. Een korte notitie neemt een paar bytes in beslag. Een lang essay kost meer. De aanwijzer in het lege record wijst naar een nulreferentie, waardoor de voetafdruk minimaal blijft. De records met inhoud worden uitgerekt zodat ze bij de gegevens passen. Er wordt niets verspild. Niets wordt geforceerd.

Is dit de enige manier om tekst op te slaan? Nee. Maar het is een slimme manier om snelheid en ruimte in evenwicht te brengen. U krijgt de responsiviteit van headers met een vaste grootte en de flexibiliteit van gegevens met een variabele lengte. Het is een klein detail. Een monteur van laag niveau. Maar het komt erop neer als u miljoenen records beheert. De database ademt gemakkelijker. Het schijfgebruik blijft laag.

En de gebruiker? Ze zien de wijzer nooit. Ze zien alleen hun commentaar. Of het ontbreken daarvan. De complexiteit is verborgen. De opslag is geoptimaliseerd. Het resultaat is een systeem dat licht aanvoelt, zelfs als de data groeit.