C ++ Kort gezegd pointers, referenties en const-correctheid

Inleiding tot Pointers

Een pointer is niets meer dan een variabele met een geheugenadres. Bij correct gebruik houdt een aanwijzer een geldig geheugenadres bij dat een object bevat dat compatibel is met het type aanwijzer. Net als verwijzingen in C # hebben alle aanwijzers in een bepaalde uitvoeringsomgeving dezelfde grootte, ongeacht het type gegevens waarnaar de aanwijzer verwijst. Wanneer een programma bijvoorbeeld is gecompileerd voor en wordt uitgevoerd op een 32-bits besturingssysteem, is een aanwijzer meestal 4 bytes (32 bits).

Pointers kunnen naar elk geheugenadres wijzen. U kunt en krijgt vaak verwijzingen naar objecten die zich op de stapel bevinden. U kunt ook verwijzingen naar statische objecten maken, om lokale objecten in te voegen en, natuurlijk, naar dynamische (d.w.z. met heap toegewezen) objecten. Wanneer programmeurs met slechts een voorbijgaande vertrouwdheid met pointers denken aan hen, is het meestal in de context van dynamische objecten.

Vanwege mogelijke lekken mag u nooit dynamisch geheugen toewijzen buiten een slimme aanwijzer. De C ++ Standard Library biedt twee slimme aanwijzers die u zou moeten overwegen: std :: shared_ptr en std :: unique_ptr.

Door dynamische durationobjecten in een van deze te plaatsen, garandeert u dat wanneer de std :: unique_ptr, of de laatste std :: shared_ptr die een aanwijzer naar dat geheugen bevat, valt buiten het bereik, het geheugen zal naar behoren worden vrijgegeven met de juiste versie van verwijderen (verwijderen of verwijderen []) zodat het niet zal lekken. Dat is het RAII-patroon uit het vorige hoofdstuk in actie.

Er kunnen slechts twee dingen gebeuren wanneer u RAII rechts doet met slimme aanwijzers: de toewijzing slaagt en daarom wordt het geheugen correct vrijgegeven wanneer de slimme aanwijzer buiten bereik raakt of de toewijzing mislukt, in welk geval er geen toegewezen geheugen was en dus geen lekken. In de praktijk zou de laatste situatie op moderne pc's en servers vrij zeldzaam moeten zijn vanwege hun grote geheugen en hun beschikbaarstelling van virtueel geheugen.

Als u geen slimme aanwijzers gebruikt, vraagt ​​u gewoon om een ​​geheugenlek. Elke uitzondering tussen het toewijzen van het geheugen met nieuwe of nieuwe [] en het vrijmaken van het geheugen met verwijderen of verwijderen [] zal waarschijnlijk resulteren in een geheugenlek. Als u niet voorzichtig bent, kunt u per ongeluk een pointer gebruiken die al is verwijderd, maar niet gelijk is aan nullptr. U zou vervolgens een willekeurige locatie in het geheugen openen en deze behandelen alsof het een geldige aanwijzer is.

Het beste dat in dat geval kan gebeuren, is dat je programma vastloopt. Als dit niet het geval is, dan corrumpeer je gegevens op vreemde, onbekende manieren en bewaar je deze mogelijk in een database of duw ze over het web. Je zou ook de deur kunnen openen voor veiligheidsproblemen. Gebruik dus slimme aanwijzers en laat de taal geheugenbeheerproblemen voor u afhandelen.


Const Pointer

Een const pointer heeft de vorm SomeClass * const someClass2 = & someClass1;. Met andere woorden, de * komt vóór const. Het resultaat is dat de aanwijzer zelf niet naar iets anders kan verwijzen, maar de gegevens waarnaar de aanwijzer wijst, blijven veranderlijk. Dit is waarschijnlijk niet erg handig in de meeste situaties.

Wijzer naar Const

Een aanwijzer naar const neemt de vorm aan const SomeClass * someClass2 = & someClass1;. In dit geval komt de * na const. Het resultaat is dat de aanwijzer naar andere dingen kan verwijzen, maar u kunt de gegevens waarnaar deze verwijst niet wijzigen. Dit is een veelgebruikte manier om parameters te declareren die u eenvoudig wilt inspecteren zonder hun gegevens aan te passen.

Const Pointer naar Const

Een const-pointer naar const neemt de vorm aan const SomeClass * const someClass2 = & someClass1;. Hier is de * ingeklemd tussen twee const-sleutelwoorden. Het resultaat is dat de aanwijzer niet naar iets anders kan verwijzen en dat u de gegevens waarnaar deze verwijst niet kunt wijzigen.

Const-Correctness en Const Member-functies

Const-correctheid verwijst naar het gebruik van het const-sleutelwoord om zowel parameters als functies te versieren, zodat de aanwezigheid of afwezigheid van het const-sleutelwoord alle mogelijke bijwerkingen correct weergeeft. U kunt een member function const markeren door het const-sleutelwoord na de declaratie van de parameters van de functie te plaatsen.

Bijvoorbeeld, int GetSomeInt (void) const; verklaart een const-lidfunctie - een ledfunctie die de gegevens van het object waartoe het behoort niet wijzigt. De compiler dwingt deze garantie af. Het zal ook de garantie afdwingen dat wanneer u een object doorgeeft aan een functie die het als const neemt, die functie geen niet-const-lidfuncties van dat object kan aanroepen.

Het ontwerpen van je programma om vast te houden aan const-correctheid is gemakkelijker wanneer je het vanaf het begin begint te doen. Wanneer u zich aan const-correctheid houdt, wordt het gemakkelijker om multithreading te gebruiken, omdat u precies weet welke lidfuncties bijwerkingen hebben. Het is ook eenvoudiger om fouten op te sporen die betrekking hebben op ongeldige gegevenstoestanden. Anderen die met u samenwerken aan een project, zullen zich ook bewust zijn van mogelijke wijzigingen in de gegevens van de klas wanneer zij bepaalde ledenfuncties oproepen.


De *, &, en -> operators

Bij het werken met pointers, inclusief slimme aanwijzers, zijn drie operators van belang: *, & en ->.

De indirecte operator * verwijdert de verwijzing naar een aanwijzer, wat betekent dat u werkt met de gegevens waarnaar wordt verwezen in plaats van de aanwijzer zelf. Laten we voor de volgende paar alinea's aannemen dat p_someInt een geldige verwijzing is naar een geheel getal zonder const kwalificaties.

De verklaring p_someInt = 5000000; zou de waarde 5000000 niet toewijzen aan het gehele getal waarnaar wordt verwezen. In plaats daarvan zou de aanwijzer zo worden ingesteld dat deze verwijst naar het geheugenadres 5000000, 0X004C4B40 op een 32-bits systeem. Wat zit er op geheugenadres 0X004C4B40? Wie weet? Het kan uw gehele getal zijn, maar de kans is groot dat het iets anders is. Als je geluk hebt, is het een ongeldig adres. De volgende keer dat u probeert te gebruiken p_someInt goed, je programma zal crashen. Als het een geldig gegevensadres is, dan zult u waarschijnlijk gegevens beschadigen.

De verklaring * p_someInt = 5000000; zal de waarde 5000000 toewijzen aan het gehele getal waarnaar wordt verwezen door p_someInt. Dit is de indirecte operator in actie; het kost p_someInt en vervangt het met een L-waarde die de gegevens vertegenwoordigt op het adres waarnaar wordt verwezen (we zullen binnenkort L-waarden bespreken).

Het adres-van-operator, &, haalt het adres van een variabele of een functie op. Hiermee kunt u een aanwijzer naar een lokaal object maken, dat u kunt doorgeven aan een functie die een aanwijzer wil. U hoeft niet eens een lokale aanwijzer te maken om dat te doen; je kunt gewoon je lokale variabele gebruiken met de address-of-operator ervoor als het argument, en alles zal prima werken.

Verwijzingen naar functies zijn vergelijkbaar met instanties voor delegeren in C #. Gezien deze functieverklaring: dubbele GetValue (int idx); dit zou de juiste functie aanwijzer zijn: dubbel (* SomeFunctionPtr) (int);.

Als uw functie een aanwijzer retourneert, zegt u dit als volgt: int * GetIntPtr (ongeldig); dan is dit de juiste functie-aanwijzer: int * (* SomeIntPtrDelegate) (void);. Laat je niet lastigvallen door de dubbele sterretjes; onthoud gewoon de eerste reeks haakjes rond de * en de functie pointer naam, zodat de compiler dit correct interpreteert als een functie pointer in plaats van een functieverklaring.

De -> -lidtoegangsoperator is wat u gebruikt om toegang te krijgen tot klassenleden wanneer u een aanwijzer naar een klasse-instantie hebt. Het functioneert als een combinatie van de indirecte operator en de. lid toegang operator. Zo p_someClassInstance-> SetValue (10); en (* P_someClassInstance) .SetValue (10); beide doen hetzelfde.

L-waarden en R-waarden

Het zou geen C ++ zijn als we niet tenminste kort over L-waarden en R-waarden zouden praten. L-waarden worden zo genoemd omdat ze traditioneel aan de linkerkant van een gelijkteken worden weergegeven. Met andere woorden, het zijn waarden waaraan kan worden toegewezen - die waarden die de evaluatie van de huidige uitdrukking overleven. Het meest bekende type L-waarde is een variabele, maar deze bevat ook het resultaat van het aanroepen van een functie die een L-waarde-verwijzing retourneert.

R-waarden verschijnen traditioneel aan de rechterkant van de vergelijking of, misschien juister gezegd, het zijn waarden die links niet kunnen verschijnen. Het zijn dingen zoals constanten of het resultaat van het evalueren van een vergelijking. Bijvoorbeeld a + b waarbij a en b L-waarden kunnen zijn, maar het resultaat van het optellen van deze waarden is een R-waarde of de retourwaarde van een functie die iets anders retourneert dan een lege waarde of een L-waarde.

Referenties

Referenties werken net als variabelen die geen pointer zijn. Nadat een referentie is geïnitialiseerd, kan deze niet naar een ander object verwijzen. U moet ook een referentie initialiseren waar u het declareert. Als uw functies referenties gebruiken in plaats van objecten, maakt u geen kosten voor een kopieerconstructie. Aangezien de verwijzing naar het object verwijst, zijn wijzigingen erin het wijzigen van het object zelf.

Net als pointers kunt u ook een const-referentie hebben. Tenzij u het object moet wijzigen, moet u const-verwijzingen gebruiken, omdat deze compileercontroles bevatten om ervoor te zorgen dat u het object niet muteert als u denkt dat u het niet bent.

Er zijn twee soorten referenties: L-waardeverwijzingen en R-waardeverwijzingen. Een L-waardeverwijzing wordt gemarkeerd door een & toegevoegd aan de typenaam (bijv. SomeClass &), terwijl een R-waardeverkenning wordt gemarkeerd door een && toegevoegd aan de typenaam (bijv. SomeClass &&). Grotendeels handelen ze hetzelfde; het belangrijkste verschil is dat de R-waarde-verwijzing uiterst belangrijk is om de semantiek te verplaatsen.


Wijzer en referentiesteekproef

In het volgende voorbeeld worden aanwijzer- en referentiegebruik weergegeven met uitleg in de opmerkingen.

Voorbeeld: PointerSample \ PointerSample.cpp

#include  //// Zie de opmerking bij het eerste gebruik van assert () in onderstaand _pmain. // # define NDEBUG 1 # include  #include "... /pchar.h" met behulp van naamruimte std; void SetValueToZero (int & value) value = 0;  void SetValueToZero (int * value) * waarde = 0;  int _pmain (int / * argc * /, _pchar * / * argv * / []) int value = 0; const int intArrCount = 20; // Maak een aanwijzer naar int. int * p_intArr = new int [intArrCount]; // Maak een const-pointer naar int. int * const cp_intArr = p_intArr; // Deze twee uitspraken zijn prima, omdat we de gegevens kunnen aanpassen waarnaar een // const-aanwijzer verwijst. // Stel alle elementen in op 5. uninitialized_fill_n (cp_intArr, intArrCount, 5); // Stelt het eerste element in op nul. * cp_intArr = 0; //// Deze verklaring is illegaal omdat we niet kunnen wijzigen waarnaar een const //// pointer verwijst. // cp_intArr = nullptr; // Maak een aanwijzer naar const int. const int * pc_intArr = nullptr; // Dit is prima omdat we kunnen wijzigen wat een aanwijzer naar const punten // to is. pc_intArr = p_intArr; // Zorg ervoor dat we "pc_intArr" gebruiken. waarde = * pc_intArr; //// Deze verklaring is illegaal omdat we de gegevens waarnaar een //// pointer to const verwijst, niet kunnen wijzigen. // * pc_intArr = 10; const int * const cpc_intArr = p_intArr; //// Deze twee uitspraken zijn illegaal omdat we //// niet kunnen aanpassen naar wat een const pointer naar const verwijst of naar de gegevens waarnaar //// verwijst. // cpc_intArr = p_intArr; // * cpc_intArr = 20; // Zorg ervoor dat we cpc_intArr "gebruiken" waarde = * cpc_intArr; * p_intArr = 6; SetValueToZero (* p_intArr); // Van , deze macro zal een diagnostisch bericht weergeven als de // uitdrukking tussen haakjes evalueert naar iets anders dan nul. // Anders dan de _ASSERTE-macro, wordt deze uitgevoerd tijdens Release-builds. Om het te deactiveren, definieert u NDEBUG voordat u de  header. beweren (* p_intArr == 0); * p_intArr = 9; int & r_first = * p_intArr; SetValueToZero (r_first); beweren (* p_intArr == 0); const int & cr_first = * p_intArr; //// Deze verklaring is illegaal omdat cr_first een const-referentie is, //// maar SetValueToZero neemt geen const-referentie, alleen een //// niet-const-verwijzing, wat logisch is, gezien het wil /// wijzig de waarde. // SetValueToZero (cr_first); waarde = cr_first; // We kunnen een aanwijzer initialiseren met behulp van de operator address-of. // Wees voorzichtig, omdat lokale niet-statische variabelen // ongeldig worden wanneer u het bereik ervan sluit, zodat alle verwijzingen ernaar // ongeldig worden. int * p_firstElement = &r_first; * p_firstElement = 10; SetValueToZero (* p_firstElement); beweren (* p_firstElement == 0); // Dit roept de SetValueToZero (int *) overbelasting aan omdat we // de operator address of gebruiken om van de verwijzing een // / pointer te maken. SetValueToZero (& r_first); * p_intArr = 3; SetValueToZero (& (* p_intArr)); beweren (* p_firstElement == 0); // Maak een functiewijzer. Merk op hoe we de // variabelenaam tussen haakjes moeten plaatsen met een * ervoor. void (* FunctionPtrToSVTZ) (int &) = nullptr; // Stel de functie pointer in om naar SetValueToZero te wijzen. Het pakt // de juiste overbelasting automatisch in. FunctionPtrToSVTZ = & SetValueToZero; * p_intArr = 20; // Roep de functie waarnaar wordt verwezen door FunctionPtrToSVTZ, d.w.z. // SetValueToZero (int &). FunctionPtrToSVTZ (* p_intArr); beweren (* p_intArr == 0); * p_intArr = 50; // We kunnen ook een functie-aanwijzer als deze noemen. Dit is // dichter bij wat er achter de schermen gebeurt; // FunctionPtrToSVTZ wordt vergeleken met het resultaat // zijnde de functie waarnaar wordt verwezen, die we dan // aanroepen met de waarde (n) die is opgegeven in de tweede set van // parentheses, d.w.z. * p_intArr hier. (* FunctionPtrToSVTZ) (* p_intArr); beweren (* p_intArr == 0); // Zorg ervoor dat we value op 0 zetten, zodat we het kunnen "gebruiken". * p_intArr = 0; waarde = * p_intArr; // Verwijder de p_intArray met de operator delete [] omdat het een // dynamic p_intArray is. verwijder [] p_intArr; p_intArr = nullptr; winstwaarde; 

vluchtig

Ik noem vluchtigheid alleen om voorzichtigheid te betrachten tegen het gebruik ervan. Net als const kan een variabele vluchtig worden verklaard. Je kunt zelfs een const vluchtig hebben; de twee sluiten elkaar niet uit.

Hier is het ding over vluchtig: het betekent waarschijnlijk niet wat je denkt dat het betekent. Het is bijvoorbeeld niet goed voor multithreaded programmeren. De feitelijke use case voor volatile is extreem smal. De kans is groot dat als je de volatile qualifier op een variabele plaatst, je iets vreselijk verkeerd doet.

Eric Lippert, lid van het C # -team bij Microsoft, beschreef het gebruik van volatile als: "Een teken dat je iets ronduit gek doet: je probeert dezelfde waarde op twee verschillende threads te lezen en te schrijven zonder een slot te plaatsen op zijn plaats. "Hij heeft gelijk, en zijn argument draagt ​​perfect over in C++.

Het gebruik van vluchtige moet worden begroet met meer scepticisme dan het gebruik van Goto. Ik zeg dit omdat ik tenminste één geldig algemeen gebruik van goto kan bedenken: het uitbreken van een diep genest lusconstructie bij het voltooien van een niet-uitzonderlijke conditie. vluchtig, daarentegen, is eigenlijk alleen nuttig als je een apparaatstuurprogramma schrijft of code schrijft voor een type ROM-chip. Op dat punt moet u echt goed bekend zijn met de ISO / IEC C ++ Programming Language Standard zelf, de hardware-specificaties voor de uitvoeringsomgeving waarin uw code wordt uitgevoerd en waarschijnlijk ook de ISO / IEC C-taalstandaard.

Notitie: U moet ook bekend zijn met de assemblagetaal voor de doelhardware, zodat u de gegenereerde code kunt bekijken en ervoor kunt zorgen dat de compiler de juiste code (PDF) genereert voor uw gebruik van vluchtige.

Ik heb het bestaan ​​van het vluchtige zoekwoord genegeerd en zal dit blijven doen voor de rest van dit boek. Dit is volkomen veilig, omdat:

  • Het is een taalfunctie die niet in het spel komt, tenzij je hem echt gebruikt.
  • Het gebruik ervan kan veilig worden vermeden door vrijwel iedereen.

Een laatste opmerking over vluchtig: Het enige effect dat het waarschijnlijk zal produceren, is langzamere code. Ooit dachten mensen dat vluchtige stoffen hetzelfde resultaat hadden als atomiciteit. Dat doet het niet. Wanneer goed uitgevoerd, garandeert atomiciteit dat meerdere threads en meerdere processoren tegelijkertijd een atomisch ontgrendeld geheugengeheugen niet kunnen lezen en schrijven. De mechanismen hiervoor zijn vergrendelingen, mutexen, semafonen, hekken, speciale processorinstructies en dergelijke. Het enige wat vluchtige doet, is de CPU dwingen om een ​​vluchtige variabele uit het geheugen op te halen in plaats van een waarde te gebruiken die hij in een register of op een stapel heeft opgeslagen. Het is het geheugen ophalen dat alles vertraagt.

Conclusie

Aanwijzingen en verwijzingen verwarren niet alleen veel ontwikkelaars, ze zijn ook erg belangrijk in een taal als C ++. Het is daarom belangrijk om de tijd te nemen om het concept te begrijpen, zodat u onderweg geen problemen tegenkomt. Het volgende artikel gaat helemaal over casten in C++.

Deze les staat voor een hoofdstuk uit C ++ Kort gezegd, een gratis eBoek van het team van Syncfusion.