Un pointer nu este altceva decât o variabilă care deține o adresă de memorie. Atunci când este utilizat corect, un pointer deține o adresă de memorie valabilă care conține un obiect compatibil cu tipul indicatorului. Ca referințe în C #, toți indicatorii dintr-un anumit mediu de execuție au aceeași dimensiune, indiferent de tipul de date pe care indicatorul indică. De exemplu, atunci când un program este compilat și executat pe un sistem de operare pe 32 de biți, un indicator va fi de obicei 4 octeți (32 biți).
Pointerii pot indica orice adresă de memorie. Poți, și frecvent vei avea, pointeri la obiecte care se află pe stivă. De asemenea, puteți avea indicatori pentru obiecte statice, pentru a atașa obiecte locale și, bineînțeles, pentru obiecte dinamice (adică, alocate cu heap). Atunci când programatorii care au doar o cunoaștere trecătoare cu indicii gândesc la ele, de obicei, acestea sunt în contextul obiectelor dinamice.
Din cauza unor scurgeri potențiale, nu trebuie să alocați niciodată o memorie dinamică în afara unui indicator inteligent. Biblioteca standard C ++ oferă două indicatoare inteligente pe care ar trebui să le luați în considerare: std :: shared_ptr
și std :: unique_ptr
.
Prin plasarea obiectelor de durată dinamică în interiorul uneia dintre acestea, vă garantați că atunci când std :: unique_ptr
, sau ultima std :: shared_ptr
care conține un indicator pentru acea memorie care iese din domeniul de aplicare, memoria va fi eliberată corespunzător cu versiunea corectă de ștergere (ștergeți sau ștergeți []), astfel încât să nu se scurgă. Acesta este modelul RAII din capitolul anterior în acțiune.
Doar două lucruri se pot întâmpla atunci când faceți RAII cu indicii inteligenți: alocarea este reușită și, prin urmare, memoria va fi eliberată în mod corespunzător când indicatorul inteligent va ieși din domeniul de aplicare sau alocarea nu reușește, caz în care nu a fost alocată nici o memorie și deci nu scurgeri. În practică, ultima situație ar trebui să fie destul de rară pe PC-urile și serverele moderne datorită memoriei lor mari și furnizării de memorie virtuală.
Dacă nu folosiți pointeri inteligenți, doar cereți o scurgere de memorie. Orice excepție între alocarea memoriei cu noul sau noul [] și eliberarea memoriei cu ștergerea sau ștergerea [] va duce probabil la o scurgere de memorie. Dacă nu sunteți atent, ați putea folosi accidental un indicator care a fost deja șters, dar nu a fost setat egal cu nullptr. Apoi, veți accesa o locație aleatoare în memorie și o veți trata ca pe un pointer valabil.
Cel mai bun lucru care s-ar putea întâmpla în acest caz este ca programul tău să se prăbușească. Dacă nu, atunci veți corupe datele în moduri necunoscute și necunoscute și eventual salvați aceste corupții într-o bază de date sau împingându-le pe web. Ai putea deschide ușa și la problemele de securitate. Deci, utilizați pointeri inteligenți și lăsați limba să gestioneze problemele de gestionare a memoriei pentru dvs..
Un pointer const are forma Câteva clase * const someClass2 = & someClass1;
. Cu alte cuvinte, * vine înainte de const. Rezultatul este că pointerul în sine nu poate să indice nimic altceva, dar datele pe care indicatorul indică, rămân neschimbate. Acest lucru nu este probabil să fie foarte util în majoritatea situațiilor.
Un pointer pentru const are forma const SomeClass * someClass2 = & someClass1;
. În acest caz, * vine după const. Rezultatul este că pointerul poate indica alte lucruri, dar nu puteți modifica datele pe care le indică. Acesta este un mod obișnuit de a declara parametrii pe care pur și simplu doriți să le inspectați fără a le modifica datele.
Un pointer const pentru const are forma const SomeClass * const someClass2 = & someClass1;
. Aici, * este introdus între două cuvinte cheie const. Rezultatul este că pointerul nu poate indica nimic altceva și nu puteți modifica datele pe care le indică.
Const-corectitudinea se referă la utilizarea cuvântului cheie const pentru a decora atât parametrii, cât și funcțiile, astfel încât prezența sau absența cuvântului cheie const convinge în mod corespunzător orice efecte secundare potențiale. Puteți marca o funcție const constând prin plasarea cuvântului cheie const după declararea parametrilor funcției.
De exemplu, int GetSomeInt (void) const;
declară o funcție a unui membru const - o funcție membru care nu modifică datele obiectului de care aparține. Compilatorul va aplica această garanție. De asemenea, se va aplica garanția că atunci când treci un obiect într-o funcție care o ia ca const, această funcție nu poate apela funcții ale membrilor non-const ai acelui obiect.
Proiectarea programului dvs. de a adera la const-corectitudine este mai ușoară atunci când începeți să o faceți de la început. Când aderă la const-corectitudine, devine mai ușor de utilizat multithreading, deoarece știți exact care dintre funcțiile membre au efecte secundare. De asemenea, este mai ușor să urmăriți erorile legate de stările de date nevalide. Cei care colaborează cu dvs. pe un proiect vor fi, de asemenea, conștienți de posibilele schimbări ale datelor din clasă atunci când numesc anumite funcții ale membrilor.
*
, &
, și ->
operatoriiCând lucrați cu indicatori, inclusiv indicatori inteligenți, trei operatori interesează: *, &, și ->.
Operatorul de direcție, *, de-trimite un pointer, ceea ce înseamnă că lucrați cu datele la care se referă, în loc de indicatorul propriu-zis. Pentru următoarele câteva paragrafe, să presupunem că p_someInt este un pointer valabil pentru un număr întreg fără calificări const.
Declaratia p_someInt = 5000000;
nu ar atribui valoarea 5000000 la întregul indicator. În schimb, ar stabili pointerul să indice adresa de memorie 5000000, 0X004C4B40 pe un sistem pe 32 de biți. Ce este la adresa de memorie 0X004C4B40? Cine știe? Ar putea fi intregul tău, dar șansele sunt altceva. Dacă aveți noroc, este o adresă nevalidă. Data viitoare când încercați să utilizați p_someInt
în mod corespunzător, programul tău se va prăbuși. Dacă este o adresă de date validă, atunci veți fi probabil date corupte.
Declaratia * p_someInt = 5000000;
va atribui valoarea 5000000 la întregul indicat de p_someInt. Acesta este operatorul indirecției în acțiune; este nevoie de p_someInt și o înlocuiește cu o valoare L care reprezintă datele de la adresa la care se referă (vom discuta în curând valori L).
Adresa operatorului, &, preia adresa unei variabile sau a unei funcții. Acest lucru vă permite să creați un pointer la un obiect local, pe care îl puteți trece la o funcție care dorește un pointer. Nici măcar nu trebuie să creați un pointer local pentru a face acest lucru; puteți utiliza pur și simplu variabila locală cu adresa operatorului în fața sa ca argument și totul va funcționa foarte bine.
Indicatorii pentru funcții sunt similare cu delegarea instanțelor din C #. Având în vedere această declarație a funcției: dublu GetValue (int idx);
acesta ar fi pointerul funcției corecte: dublu (* SomeFunctionPtr) (int);
.
Dacă funcția dvs. a returnat un pointer, spuneți astfel: int * GetIntPtr (vid);
atunci ar fi indicatorul funcției corecte: int * (* SomeIntPtrDelegate) (void);
. Nu lăsa asteriscurile duble să te deranjeze; țineți minte primul set de paranteze din jurul denumirii * și a funcției pointerului, astfel încât compilatorul interpretează corect acest lucru ca un indicator al funcției, mai degrabă decât o declarație a funcției.
Operatorul de acces -> membru este ceea ce utilizați pentru a accesa membrii clasei atunci când aveți un pointer la o instanță de clasă. Funcționează ca o combinație între operatorul de indirecție și. operatorul de acces. Asa de p_someClassInstance-> SetValue (10);
și (* P_someClassInstance) .SetValue (10);
ambele fac același lucru.
Nu ar fi C ++ dacă nu am vorbi despre valori L și valori R cel puțin pentru scurt timp. Valorile L sunt așa numite deoarece apar în mod tradițional pe partea stângă a unui semn egal. Cu alte cuvinte, ele sunt valori care pot fi atribuite - celor care vor supraviețui evaluării expresiei curente. Tipul cel mai familiar de valoare L este o variabilă, dar include și rezultatul apelării unei funcții care returnează o valoare de referință L.
Valorile R apar în mod tradițional în partea dreaptă a ecuației sau, probabil, mai precis, ele sunt valori care nu pot apărea pe partea stângă. Ele sunt lucruri precum constantele sau rezultatul evaluării unei ecuații. De exemplu, a + b unde a și b pot fi valori L, dar rezultatul adăugării acestora este o valoare R sau valoarea returnată a unei funcții care returnează orice altceva decât void sau o valoare de referință L.
Referințele acționează exact ca variabilele non-pointer. Odată ce o referință este inițializată, ea nu se poate referi la un alt obiect. De asemenea, trebuie să inițializați o referință în cazul în care o declarați. Dacă funcțiile dvs. iau referințe mai degrabă decât obiecte, nu veți suporta costul unei construcții de copii. Deoarece referința se referă la obiect, modificările la acesta reprezintă modificări ale obiectului în sine.
La fel ca indicii, puteți avea și o referință const. Cu excepția cazului în care trebuie să modificați obiectul, trebuie să utilizați referințele const, deoarece acestea oferă verificări compilatoare pentru a vă asigura că nu mutați obiectul când credeți că nu sunteți.
Există două tipuri de referințe: referințele pentru valoarea L și referințele pentru valoarea R. O referință pentru valoarea L este marcată de un & appended to the type name (de exemplu, SomeClass &), în timp ce o referință pentru valoarea R este marcată de && atașat la numele de tip (de exemplu, SomeClass &&). În cea mai mare parte, ei acționează la fel; diferența principală este că referința valorii R este extrem de importantă pentru a muta semantica.
Următorul exemplu prezintă utilizarea pointerială și de referință cu explicații în comentarii.
Mostră: PointerSample \ PointerSample.cpp
#include//// A se vedea comentariul la prima utilizare a lui assert () în _pmain de mai jos. // # definește NDEBUG 1 #include #include "... /pchar.h" folosind namespace std; void SetValueToZero (int și valoare) value = 0; void SetValueToZero (int * valoare) * valoare = 0; int _pmain (int / * argc * /, _pchar * / * argv * / []) valoare int = 0; const int intArrCount = 20; // Creați un pointer la int. int * p_intArr = nou int [intArrCount]; // Creați un pointer const la int. const * const cp_intArr = p_intArr; // Aceste două instrucțiuni sunt bine deoarece putem modifica datele pe care un // const pointer indică. // Setați toate elementele la 5. uninitialized_fill_n (cp_intArr, intArrCount, 5); // Setează primul element la zero. * cp_intArr = 0; //// Această declarație este ilegală, deoarece nu putem modifica ceea ce indică un const // // indicatorul. // cp_intArr = nullptr; // Creați un pointer pentru const int. const int * pc_intArr = nullptr; // Acest lucru este bine pentru că putem modifica ceea ce un pointer constă în punctele // // la. pc_intArr = p_intArr; // Asigurați-vă că "folosim" pc_intArr. valoare = * pc_intArr; //// Această declarație este ilegală, deoarece nu putem modifica datele pe care un // // indicatorul la const indică. // * pc_intArr = 10; const int * const cpc_intArr = p_intArr; //// Aceste două instrucțiuni sunt ilegale, deoarece nu putem modifica // // ceea ce indică un const pointer la care const point sau la care datele // // indică. // cpc_intArr = p_intArr; // * cpc_intArr = 20; // Asigurați-vă că "folosim" cpc_intArr. valoare = * cpc_intArr; * p_intArr = 6; SetValueToZero (* p_intArr); // Din , această macrocomandă va afișa un mesaj de diagnostic dacă expresia // din paranteze evaluează altceva decât zero. // Spre deosebire de macro-ul _ASSERTE, acesta va fi rulat în timpul versiunilor Build. Pentru a // dezactiva aceasta, definiți NDEBUG înainte de a include antet. afirmați (* p_intArr == 0); * p_intArr = 9; int & r_first = * p_intArr; SetValueToZero (r_first); afirmați (* p_intArr == 0); const int & cr_first = * p_intArr; //// Această declarație este ilegală deoarece cr_first este o referință const, //// dar SetValueToZero nu ia o referință const, doar o // // non-const referință, care are sens considerând că vrea să //// modificați valoarea. // SetValueToZero (cr_first); value = cr_first; // Putem inițializa un pointer folosind adresa operatorului. // Amintește-te doar pentru că variabilele locale non-statice devin invalide atunci când ieși din domeniul lor de aplicare, astfel încât orice indicii pentru a le deveni invalizi. int * p_firstElement = &r_first; * p_firstElement = 10; SetValueToZero (* p_firstElement); afirmă (* p_firstElement == 0); // Aceasta va apela suprasarcina SetValueToZero (int *) deoarece // folosim operatorul de adresa pentru a transforma referinta in // un pointer. SetValueToZero (& r_first); * p_intArr = 3; SetValueToZero (& (* p_intArr)); afirmă (* p_firstElement == 0); // Creați un pointer al funcției. Observați cum trebuie să puneți variabila // în paranteze cu * înainte de ea. void (* FunctionPtrToSVTZ) (int &) = nullptr; // Setați indicatorul funcției pentru a indica SetValueToZero. Se alege // suprasarcina corecta automat. FunctionPtrToSVTZ = & SetValueToZero; * p_intArr = 20; // Apelați funcția indicată de FunctionPtrToSVTZ, adică // SetValueToZero (int &). FunctionPtrToSVTZ (* p_intArr); afirmați (* p_intArr == 0); * p_intArr = 50; // De asemenea, putem apela un indicator al funcției ca acesta. Acesta este // mai aproape de ceea ce se întâmplă de fapt în spatele scenei; // Funcția FunctionPtrToSVTZ este descrisă cu rezultatul // fiind funcția la care se indică, pe care apoi sunăm // folosind valoarea (valorile) specificată în al doilea set de // paranteze, adică * p_intArr aici. (* FunctionPtrToSVTZ) (* p_intArr); afirmați (* p_intArr == 0); // Asigurați-vă că vom obține valoarea setată la 0, astfel încât să o putem "folosi". * p_intArr = 0; valoare = * p_intArr; // Ștergeți p_intArray utilizând operatorul de ștergere [], deoarece este // // p_intArray dinamic. ștergeți [] p_intArr; p_intArr = nullptr; valoare returnată;
Menționez volatile doar pentru a prudența împotriva folosirii. Ca const, o variabilă poate fi declarată volatilă. Puteți avea chiar și o volatilă const; cele două nu se exclud reciproc.
Iată lucrurile despre volatilă: Probabil că nu înseamnă ceea ce credeți că înseamnă. De exemplu, nu este bine pentru programarea cu mai multe fire. Cazul real de utilizare pentru volatil este extrem de îngust. Sunt șanse, dacă puneți calificativul volatil pe o variabilă, faceți ceva greșit greșit.
Eric Lippert, membru al echipei de limbi C # de la Microsoft, a descris folosirea volatilă ca fiind: "Un semn că faceți ceva cu adevărat nebun: Încercați să citiți și să scrieți aceeași valoare pe două fire diferite, fără a pune o blocare la locul lui ". Are dreptate, iar argumentul lui trece perfect în C++.
Utilizarea volatilelor ar trebui să fie întâmpinată cu mai multă scepticism decât folosirea lui. Spun acest lucru pentru că mă pot gândi la cel puțin o utilizare valabilă a scopului general: trebuie să izbucnească dintr-o construcție de buclă adânc imbricată la finalizarea unei condiții non-excepționale. volatile, în schimb, este într-adevăr utilă numai dacă scrieți un driver de dispozitiv sau scriind un cod pentru un tip de cip ROM. În acest moment, ar trebui să cunoașteți bine standardul standard de limbaj de programare ISO / IEC C ++, specificațiile hardware pentru mediul de execuție codul dvs. va fi rulat și, probabil, ISO / IEC C Language Standard prea.
Notă: De asemenea, trebuie să cunoașteți limba de asamblare a hardware-ului țintă, astfel încât să puteți vedea codul care este generat și să vă asigurați că compilatorul generează codul corect (PDF) pentru utilizarea de către dvs. a volatilității.
Am ignorat existența cuvântului cheie volatil și va continua să o facă pentru restul acestei cărți. Acest lucru este perfect sigur, deoarece:
O ultimă notă despre volatilă: Unul efect care este foarte probabil să producă este un cod mai lent. Odată, oamenii credeau că volatile au produs același rezultat ca și atomicitatea. Nu este. Atunci când este implementat corect, atomicitatea garantează că firele multiple și procesoarele multiple nu pot citi și scrie o bucată de memorie accesată atomic în același timp. Mecanismele pentru aceasta sunt încuietori, mutexuri, semaphone, garduri, instrucțiuni speciale de procesor și altele asemenea. Singurul lucru volatil nu este de a forța CPU-ul să preia o variabilă volatilă din memorie, mai degrabă decât să utilizeze orice valoare pe care ar fi putut să o stocheze într-un registru sau într-un teanc. Este memoria de preluare care încetinește totul jos.
Indicatorii și referințele nu doar confundă o mulțime de dezvoltatori, ci sunt foarte importanți într-o limbă precum C ++. Prin urmare, este important să vă faceți timp pentru a înțelege conceptul, astfel încât să nu vă confruntați cu probleme pe drum. Următorul articol este despre turnarea în C++.
Această lecție reprezintă un capitol din C ++ Succinctly, o carte electronică gratuită de la echipa de la Syncfusion.