C ++ Succinctly Pointers, References og Const-Correctness

Introduksjon til pekere

En peker er ikke noe mer enn en variabel som har en minneadresse. Når det brukes riktig, har en peker en gyldig minneadresse som inneholder et objekt som er kompatibelt med pekeren. Som referanser i C #, har alle poengene i et bestemt eksekveringsmiljø samme størrelse, uansett hvilken type data pekeren peker på. For eksempel, når et program er kompilert for og kjøres på et 32-biters operativsystem, vil en peker typisk være 4 byte (32 bits).

Pekere kan peke på en hvilken som helst minneadresse. Du kan, og ofte vil, ha pekere til objekter som står på stakken. Du kan også ha poeng til statiske objekter, å tråkke lokale objekter, og selvfølgelig til dynamiske (dvs. heap allokert) objekter. Når programmerere med bare en passerende kjennskap til pekere tenker på dem, er det vanligvis i sammenheng med dynamiske objekter.

På grunn av potensielle lekkasjer, bør du aldri tildele dynamisk minne utenfor en smart peker. C ++ Standard Library gir to smarte pekere som du bør vurdere: std :: shared_ptr og std :: unique_ptr.

Ved å sette dynamiske varighetsobjekter inne i en av disse, garanterer du at når std :: unique_ptr, eller den siste std :: shared_ptr som inneholder en peker til at minnet går utenfor omfanget, vil minnet bli riktig frigjort med den riktige versjonen av slett (slett eller slett []) slik at det ikke vil lekke. Det er RAII-mønsteret fra forrige kapittel i aksjon.

Bare to ting kan skje når du gjør RAII riktig med smarte pekere: Tildelingen lykkes, og derfor vil minnet bli riktig frigjort når smart pekeren går ut av omfanget eller tildelingen mislykkes, i hvilket tilfelle det ikke var noe minne allokert og dermed ikke lekke. I praksis bør den siste situasjonen være ganske sjelden på moderne PCer og servere på grunn av deres store minne og deres levering av virtuelt minne.

Hvis du ikke bruker smarte pekere, ber du bare om hukommelseskap. Ethvert unntak mellom å tildele minnet med nytt eller nytt [] og frigjøre minnet ved å slette eller slette [] vil trolig føre til hukommelseskap. Hvis du ikke er forsiktig, kan du ved et uhell bruke en peker som allerede er slettet, men ikke satt til nullptr. Du vil da få tilgang til noe tilfeldig sted i minnet og behandle det som om det er en gyldig peker.

Det beste som kan skje i dette tilfellet er at programmet ditt skal krasje. Hvis det ikke gjør det, ødelegger du data på rare, ukjente måter og muligens lagrer disse korrupsjonene i en database eller skyver dem over nettet. Du kan også åpne døren for sikkerhetsproblemer. Så bruk smarte pekere og la språket håndtere problemer med minnehåndtering for deg.


Const Pointer

En konstpeker tar formen SomeClass * const someClass2 = & someClass1;. Med andre ord, * kommer før const. Resultatet er at pekeren selv ikke kan peke på noe annet, men dataene pekeren peker på forblir mutable. Dette er ikke sannsynlig å være svært nyttig i de fleste situasjoner.

Pekeren til Const

En peker til const tar skjemaet const SomeClass * someClass2 = & someClass1;. I dette tilfellet kommer * etter const. Resultatet er at pekeren kan peke på andre ting, men du kan ikke endre dataene den peker på. Dette er en vanlig måte å deklarere parametere som du bare vil inspisere uten å endre deres data.

Const Pointer til Const

En const peker til const tar form const SomeClass * const someClass2 = & someClass1;. Her er * sandwichet mellom to const søkeord. Resultatet er at pekeren ikke kan peke på noe annet, og du kan ikke endre dataene den peker på.

Const-Correctness og Const Member Funksjoner

Konstkorrektitet refererer til bruk av const-nøkkelordet for å dekorere både parametere og funksjoner, slik at tilstedeværelsen eller fraværet av const-nøkkelordet overfører eventuelle potensielle bivirkninger. Du kan merke en medlemsfunksjon const ved å sette const-nøkkelordet etter deklarasjonen av funksjonens parametere.

For eksempel, int GetSomeInt (void) const; erklærer en const-medlem-funksjon - en medlemsfunksjon som ikke endrer dataene til objektet den tilhører. Kompilatoren vil håndheve denne garantien. Det vil også håndheve garantien for at når du overfører et objekt til en funksjon som tar det som const, kan denne funksjonen ikke kalle noen ikke-const medlemsfunksjoner av objektet.

Å designe programmet for å overholde constkority er lettere når du begynner å gjøre det fra begynnelsen. Når du holder fast i korrekthet, blir det lettere å bruke multithreading, siden du vet nøyaktig hvilke medlemsfunksjoner som har bivirkninger. Det er også lettere å spore bugs relatert til ugyldige datastatus. Andre som samarbeider med deg på et prosjekt vil også være oppmerksomme på potensielle endringer i klassens data når de kaller enkelte medlemsfunksjoner.


De *, &, og -> operatører

Når du arbeider med pekere, inkludert smarte pekere, er tre operatører av interesse: *, &, og ->.

Indirection-operatøren, *, de-refererer til en peker, noe som betyr at du arbeider med dataene som er peket på, i stedet for pekeren selv. For de neste avsnittene, må vi anta at p_someInt er en gyldig peker til et heltall uten konstkvalifikasjoner.

Uttalelsen p_someInt = 5000000; ville ikke tildele verdien 5000000 til heltallet som er pekt på. I stedet ville det sette pekeren til å peke til minnesadressen 5000000, 0X004C4B40 på et 32-biters system. Hva er i minnesadressen 0X004C4B40? Hvem vet? Det kan være ditt heltall, men sjansen er at det er noe annet. Hvis du er heldig, er det en ugyldig adresse. Neste gang du prøver å bruke p_someInt riktig, programmet ditt vil krasje. Hvis det er en gyldig dataadresse skjønt, vil du sannsynligvis korrupte data.

Uttalelsen * p_someInt = 5000000; vil tildele verdien 5000000 til heltallet peket til av p_someInt. Dette er indirection operatøren i aksjon; det tar p_someInt og erstatter det med en L-verdi som representerer dataene på adressen pekt på (vi diskuterer L-verdier snart).

Adressen til operatøren, &, henter adressen til en variabel eller en funksjon. Dette lar deg lage en peker til et lokalt objekt, som du kan overføre til en funksjon som vil ha en peker. Du trenger ikke engang å opprette en lokal peker for å gjøre det; Du kan ganske enkelt bruke din lokale variabel med adressen til operatøren foran det som argumentet, og alt fungerer bra.

Pekere til funksjoner ligner på delegerte forekomster i C #. Gitt denne funksjonen erklæring: dobbelt GetValue (int idx); dette ville være den rette funksjonspekeren: dobbelt (* SomeFunctionPtr) (int);.

Hvis din funksjon returnerte en peker, si slik: int * GetIntPtr (void); da ville dette være riktig funksjonspeker: int * (* SomeIntPtrDelegate) (void);. Ikke la de doble stjernene forstyrre deg; bare husk det første settet av parenteser rundt * og funksjonspekerenavnet slik at kompilatoren tolker dette som en funksjonspeker snarere enn en funklighetserklæring.

Medlemmet -> medlemstilgang er det du bruker for å få tilgang til klassemedlemmer når du har en peker til en klasseeksempel. Den fungerer som en kombinasjon av indirection operatøren og. medlem tilgang operatør. Så p_someClassInstance-> SetValue (10); og (* P_someClassInstance) .SetValue (10); begge gjør det samme.

L-verdier og R-verdier

Det ville ikke være C ++ hvis vi ikke snakket om L-verdier og R-verdier minst kort. L-verdier er såkalte fordi de tradisjonelt vises på venstre side av et likestilling. Med andre ord, de er verdier som kan tildeles til-de som vil overleve evalueringen av dagens uttrykk. Den mest kjente typen L-verdi er en variabel, men det inkluderer også resultatet av å ringe en funksjon som returnerer en L-verdi referanse.

R-verdier vises tradisjonelt på høyre side av ligningen, eller kanskje mer nøyaktig, er de verdier som ikke kunne vises til venstre. De er ting som konstanter, eller resultatet av å vurdere en ligning. For eksempel kan a + b hvor a og b er L-verdier, men resultatet av å legge dem sammen er en R-verdi, eller returverdien til en funksjon som returnerer noe annet enn tomrom eller en L-verdi referanse.

referanser

Referanser fungerer som ikke-pekervariabler. Når en referanse er initialisert, kan den ikke referere til et annet objekt. Du må også initialisere en referanse der du erklærer det. Hvis dine funksjoner tar referanser snarere enn objekter, vil du ikke pådra seg prisen på kopiering. Siden referansen refererer til objektet, er endringer i det endringer i selve objektet.

Akkurat som pekere, kan du også ha en const referanse. Med mindre du trenger å endre objektet, bør du bruke const referanser siden de gir kompilator sjekker for å sikre at du ikke muterer objektet når du tror du ikke er.

Det finnes to typer referanser: L-verdi referanser og R-verdi referanser. En L-verdi referanse er merket med en og vedlagt typenavnet (for eksempel SomeClass &), mens en R-verdi referanse er merket med en && vedlagt typenavnet (for eksempel SomeClass &&). For det meste handler de det samme; Hovedforskjellen er at R-verdi referansen er ekstremt viktig å flytte semantikk.


Pekeren og referanseprøven

Følgende eksempel viser pekeren og referanseforbruket med forklaringer i kommentarene.

Eksempel: PointerSample \ PointerSample.cpp

#inkludere  //// Se kommentaren til den første bruken av påstanden () i _pmain nedenfor. // # definere NDEBUG 1 #include  #include "... /pchar.h" ved hjelp av navneområde std; void SetValueToZero (int & verdi) value = 0;  void SetValueToZero (int * verdi) * verdi = 0;  int _pmain (int / * argc * /, _pchar * / * argv * / []) int verdi = 0; const int intArrCount = 20; // Lag en pointer til int. int * p_intArr = new int [intArrCount]; // Lag en const pointer til int. int * const cp_intArr = p_intArr; // Disse to setningene er fine siden vi kan endre dataene som en // const pointer peker på. // Sett alle elementene til 5. uninitialized_fill_n (cp_intArr, intArrCount, 5); // Setter det første elementet til null. * cp_intArr = 0; //// Denne setningen er ulovlig fordi vi ikke kan endre hva en const //// peker peker på. // cp_intArr = nullptr; // Lag en pointer til const int. const int * pc_intArr = nullptr; // Dette er bra fordi vi kan endre hva en peker til const poeng // til. pc_intArr = p_intArr; // Pass på at vi "bruker" pc_intArr. verdi = * pc_intArr; //// Denne setningen er ulovlig siden vi ikke kan endre dataene som en //// peker til const peker på. // * pc_intArr = 10; const int * const cpc_intArr = p_intArr; //// Disse to erklæringene er ulovlige fordi vi ikke kan endre //// hva en const pointer til const peker på eller dataene den //// peker på. // cpc_intArr = p_intArr; // * cpc_intArr = 20; // Pass på at vi bruker "cpc_intArr". verdi = * cpc_intArr; * p_intArr = 6; SetValueToZero (* p_intArr); // Fra , Denne makroen vil vise en diagnostisk melding hvis // uttrykket i parenteser evaluerer til noe annet enn null. // I motsetning til _ASSERTE-makroen, kjører dette under Utgivelsesbygg. Å // deaktivere den, definer NDEBUG før du inkluderer  Overskrift. assert (* p_intArr == 0); * p_intArr = 9; int & r_first = * p_intArr; SetValueToZero (r_first); assert (* p_intArr == 0); const int & cr_first = * p_intArr; //// Denne setningen er ulovlig fordi cr_first er en const referanse, //// men SetValueToZero tar ikke en const referanse, bare en //// non-const referanse, som er fornuftig vurderer det vil //// endre verdien. // SetValueToZero (cr_first); verdi = cr_first; // Vi kan initialisere en peker ved hjelp av adressen til operatøren. // Bare vær forsiktig fordi lokale ikke-statiske variabler blir // ugyldige når du avslutter deres omfang, så noen poeng til dem // blir ugyldige. int * p_firstElement = &r_first; * p_firstElement = 10; SetValueToZero (* p_firstElement); påstå (* p_firstElement == 0); // Dette vil kalle SetValueToZero (int *) overbelastning fordi vi // bruker adressen til operatøren for å slå referansen til // en peker. SetValueToZero (& r_first); * p_intArr = 3; SetValueToZero (& (* p_intArr)); påstå (* p_firstElement == 0); // Opprett en funksjonspeker. Legg merke til hvordan vi må sette // variabelt navn i parentes med en * før det. void (* FunctionPtrToSVTZ) (int &) = nullptr; // Sett funksjonspekeren for å peke til SetValueToZero. Den plukker // riktig overbelastning automatisk. FunctionPtrToSVTZ = & SetValueToZero; * p_intArr = 20; // Ring funksjonen peket til av FunctionPtrToSVTZ, dvs. // SetValueToZero (int &). FunctionPtrToSVTZ (* p_intArr); assert (* p_intArr == 0); * p_intArr = 50; // Vi kan også ringe en funksjonspeker som denne. Dette er // nærmere hva som egentlig skjer bak kulissene; // FunctionPtrToSVTZ blir de-referenced med resultatet // er den funksjonen som peker på, som vi deretter // ringer ved hjelp av verdien (e) angitt i det andre settet // parenteser, det vil si * p_intArr her. (* FunctionPtrToSVTZ) (* p_intArr); assert (* p_intArr == 0); // Sørg for at vi får verdien satt til 0 slik at vi kan "bruke" den. * p_intArr = 0; verdi = * p_intArr; // Slett p_intArray ved hjelp av delete [] operatøren siden det er en // dynamisk p_intArray. slett [] p_intArr; p_intArr = nullptr; returverdi; 

Flyktige

Jeg nevner bare flyktig for å være forsiktig mot å bruke den. Som const kan en variabel deklareres flyktig. Du kan til og med ha en konstflyktig; de to er ikke gjensidig utelukkende.

Her er tingen om flyktig: Det betyr sannsynligvis ikke hva du mener det betyr. For eksempel er det ikke bra for multithreaded programmering. Den faktiske bruk saken for flyktig er ekstremt smal. Sjansen er at hvis du setter den flyktige kvalifiseringen på en variabel, gjør du noe grusomt feil.

Eric Lippert, medlem av C # språkteamet hos Microsoft, beskrev bruken av flyktige som "Et tegn på at du gjør noe helt gal. Du prøver å lese og skrive samme verdi på to forskjellige tråder uten å sette en lås på plass. "Han har rett, og hans argument overfører helt til C++.

Bruken av flyktige bør hilses med mer skepsis enn bruken av goto. Jeg sier dette fordi jeg kan tenke på minst en gyldig generell bruk av goto: bryte ut av en dypt nestet sløyfekonstruksjon når en ikke-eksepsjonell tilstand er fullført. flyktig, derimot, er egentlig bare nyttig hvis du skriver en enhetsdriver eller skriverkode for en hvilken som helst type ROM-chip. På det tidspunktet bør du virkelig være grundig kjent med ISO / IEC C ++ Programmering Språk Standard selv, maskinvare spesifikasjonene for utførelsesmiljøet koden din kjører i, og sannsynligvis også ISO / IEC C Language Standard også.

Merk: Du bør også være kjent med monteringsspråket for målhardwaren, slik at du kan se på kode som genereres og sørge for at kompilatoren genererer riktig kode (PDF) for bruk av flyktige.

Jeg har ignorert eksistensen av det flyktige søkeordet og skal fortsette å gjøre det for resten av denne boken. Dette er helt trygt siden:

  • Det er en språkfunksjon som ikke kommer inn i spill med mindre du faktisk bruker den.
  • Dens bruk kan trygt unngås av nesten alle.

Et siste notat om flyktig: Den ene effekten det er sannsynlig å produsere, er langsommere kode. En gang trodde folk flyktige samme resultat som atomicitet. Det gjør det ikke. Når det er riktig implementert, garanterer atomicitet at flere tråder og flere prosessorer ikke kan lese og skrive en atomlig tilgang til bit av minne samtidig. Mekanismene for dette er låser, mutexes, semaphones, gjerder, spesielle prosessorinstruksjoner og lignende. Det eneste flyktige gjør, er å tvinge CPUen til å hente en flyktig variabel fra minnet i stedet for å bruke en hvilken som helst verdi den kan ha cached i et register eller på en stabel. Det er minnet henting som bremser alt ned.

Konklusjon

Pekere og referanser forvirrer ikke bare mange utviklere, de er svært viktige på et språk som C ++. Det er derfor viktig å ta deg tid til å forstå konseptet slik at du ikke får problemer i veien. Neste artikkel handler om casting i C++.

Denne leksjonen representerer et kapittel fra C ++ Succinctly, en gratis eBok fra teamet på Syncfusion.