C++ pointeri: de ce avem nevoie de ei, când îi folosim, cum diferă de accesarea obiectului însuși
Chiar dacă majoritatea programatorilor înțeleg care este diferența dintre obiecte și pointeri la acestea, uneori nu este complet clar care dintre modalitățile de accesare a obiectului trebuie aleasă. Am încercat să răspundem la această întrebare mai jos.
Întrebare
Vin dintr-un mediu Java și am început să lucrez cu obiecte în C++. Dar un lucru care mi-a atras atenția este că oamenii folosesc adesea pointeri la obiecte în loc de obiectele în sine, de exemplu această declarație:
Object *myObject = new Object;
în loc de:
Object myObject;
Orice în loc să folosim o funcție, să zicem testFunc(), așa:
myObject.testFunc();
trebuie să scriem:
myObject->testFunc();
Dar nu-mi dau seama de ce ar trebui să o facem în acest fel. Presupun că are de-a face cu eficiența și viteza, deoarece avem acces direct la adresa de memorie. Am dreptate?
Răspuns
Apropoi, ar trebui să observăm că, în Java, pointerii nu sunt utilizați în mod explicit, de exemplu, un programator nu poate accesa un obiect în cod prin intermediul unui pointer la acesta. Cu toate acestea, în Java toate tipurile, cu excepția celor de bază, sunt referențiate: accesul la ele se face prin legătură, deși nu se poate trece explicit parametrul prin legătură. În afară de asta, noi în C++ și Java sau C# sunt lucruri diferite.
Pentru a da o mică idee despre pointeri în C++ , vom da două fragmente de cod similare:
Java:
Object object1 = new Object(); //A new object is allocated by JavaObject object2 = new Object(); //Another new object is allocated by Javaobject1 = object2; //object1 now points to the object originally allocated for object2//The object originally allocated for object1 is now "dead" – //nothing points to it, so it//will be reclaimed by the Garbage Collector.//If either object1 or object2 is changed, //the change will be reflected to the other
Cel mai apropiat echivalent de acesta, este:
C++:
Object * object1 = new Object(); //A new object is allocated on the heapObject * object2 = new Object(); //Another new object is allocated on the heapdelete object1;//Since C++ does not have a garbage collector, //if we don't do that, the next line would //cause a "memory leak", i.e. a piece of claimed memory that //the app cannot use //and that we have no way to reclaim...object1 = object2; //Same as Java, object1 points to object2.
Să vedem modalitatea alternativă din C++:
Object object1; //A new object is allocated on the STACKObject object2; //Another new object is allocated on the STACKobject1 = object2;//!!!! This is different! //The CONTENTS of object2 are COPIED onto object1,//using the "copy assignment operator", the definition of operator =.//But, the two objects are still different. //Change one, the other remains unchanged.//Also, the objects get automatically destroyed //once the function returns...
Avem un câștig de viteză, accesând direct în memorie?
De fapt, deloc. De obicei, pointerii sunt utilizați pentru accesul la heap, în timp ce obiectele sunt localizate în stivă – aceasta este o structură mai simplă și mai rapidă. Dacă sunteți începător, avem pentru dumneavoastră un material în care vă spunem în detaliu ce este o stivă și un heap.
Strict vorbind, această întrebare combină două aspecte diferite. Prima: când folosim alocarea dinamică a memoriei? A doua: când este mai bine să folosim pointeri? Sigur, nu vom face abstracție de cuvintele comune că trebuie să alegeți întotdeauna cel mai potrivit instrument pentru treaba respectivă. Aproape întotdeauna există o realizare mai bună decât utilizarea alocării dinamice manuale (alocare dinamică) și/sau a pointerelor brute.
Este foarte nefericit că vedeți alocarea dinamică atât de des. Asta arată doar cât de mulți programatori proști de C++ există.
Într-un fel, aveți două întrebări adunate într-una singură. Prima este când ar trebui să folosim alocarea dinamică (folosind new)? A doua este când ar trebui să folosim pointeri?
Mesajul important de reținut este că ar trebui să folosiți întotdeauna instrumentul potrivit pentru treaba respectivă. În aproape toate situațiile, există ceva mai adecvat și mai sigur decât efectuarea manuală a alocării dinamice și/sau utilizarea de pointeri brute.
Alocarea dinamică
În întrebarea dumneavoastră, ați demonstrat două moduri de a crea un obiect. Principala diferență este durata de stocare a obiectului. Atunci când se face Object myObject; în cadrul unui bloc, obiectul este creat cu o durată de stocare automată, ceea ce înseamnă că va fi distrus automat atunci când iese din domeniu. Atunci când faceți new Object(), obiectul are o durată de stocare dinamică, ceea ce înseamnă că rămâne în viață până când îl ștergeți în mod explicit. Ar trebui să utilizați durata de stocare dinamică numai atunci când aveți nevoie de ea. Adică, ar trebui să preferați întotdeauna să creați obiecte cu durată de stocare automată atunci când puteți.
Principalele două situații în care ați putea avea nevoie de alocare dinamică:
- Aveți nevoie ca obiectul să supraviețuiască domeniului de aplicare curent – acel obiect specific la acea locație de memorie specifică, nu o copie a acestuia. Dacă nu vă deranjează copierea/ mutarea obiectului (de cele mai multe ori ar trebui să vă deranjeze), ar trebui să preferați un obiect automat.
- Aveți nevoie să alocați o mulțime de memorie, care poate umple cu ușurință stiva. Ar fi frumos dacă nu ar trebui să ne preocupăm de acest lucru (de cele mai multe ori nu ar trebui să o faceți), deoarece este într-adevăr în afara domeniului de competență al C++, dar, din păcate, trebuie să ne confruntăm cu realitatea sistemelor pentru care dezvoltăm.
- Nu știți exact dimensiunea array-ului, pe care va trebui să o folosiți. După cum știți, în C++ au dimensiunea array-urilor este fixă. Aceasta poate cauza probleme, de exemplu, la citirea datelor introduse de utilizator. Pointerul definește doar acea secțiune de memorie, în care va fi scris începutul unui array, fără a limita dimensiunea acestuia.
Dacă este necesară o utilizare a alocării dinamice, ar trebui să o încapsulați folosind un pointer inteligent sau de alt tip care suportă idiomul „Achiziționarea resursei este inițializare” (containerele standard îl suportă – este un idiom, conform căruia resursa: un bloc de memorie, un fișier, o conexiune de rețea, etc. – sunt inițializate în timp ce se obțin în constructor, iar apoi cu atenție sunt distruse de destructor). De exemplu, pointerii inteligenți sunt std::unique_ptr și std::shared_ptr
Pointers
Cu toate acestea, există și alte utilizări mai generale pentru pointeri brute dincolo de alocarea dinamică, dar majoritatea au alternative pe care ar trebui să le preferați. Ca și înainte, preferați întotdeauna alternativele, cu excepția cazului în care aveți cu adevărat nevoie de pointeri.
- Aveți nevoie de semantica referințelor. Uneori doriți să transmiteți un obiect folosind un pointer (indiferent de modul în care a fost alocat) deoarece doriți ca funcția căreia îi transmiteți obiectul să aibă acces la acel obiect specific (nu la o copie a acestuia). Cu toate acestea, în majoritatea situațiilor, ar trebui să preferați tipurile de referință în detrimentul pointerilor, deoarece acestea sunt concepute special pentru acest lucru. Rețineți că nu este vorba neapărat de extinderea duratei de viață a obiectului dincolo de domeniul de aplicare curent, ca în situația 1 de mai sus. Ca și înainte, dacă sunteți de acord cu transmiterea unei copii a obiectului, nu aveți nevoie de semantica referințelor.
- Aveți nevoie de polimorfism. Puteți apela funcții în mod polimorf (adică în funcție de tipul dinamic al unui obiect) numai prin intermediul unui pointer sau al unei referințe la obiect. Dacă acesta este comportamentul de care aveți nevoie, atunci trebuie să folosiți pointeri sau referințe. Din nou, referințele ar trebui să fie preferate.
- Doriți să reprezentați faptul că un obiect este opțional, permițând trecerea unui nullptr atunci când obiectul este omis. Dacă este un argument, ar trebui să preferați să folosiți argumente implicite sau supraîncărcări de funcții. În caz contrar, ar trebui să preferați să utilizați un tip care încapsulează acest comportament, cum ar fi std::optional (introdus în C++17 – cu standardele C++ anterioare, utilizați boost::optional).
- Doriți să decuplați unitățile de compilare pentru a îmbunătăți timpul de compilare. Proprietatea utilă a unui pointer este că aveți nevoie doar de o declarație directă a tipului indicat (pentru a utiliza efectiv obiectul, veți avea nevoie de o definiție). Acest lucru vă permite să decuplați părți ale procesului de compilare, ceea ce poate îmbunătăți semnificativ timpul de compilare. A se vedea idiomul Pimpl.
- Trebuie să realizați o interfață cu o bibliotecă C sau o bibliotecă în stil C. În acest moment, sunteți forțat să folosiți pointeri brute. Cel mai bun lucru pe care îl puteți face este să vă asigurați că dați drumul la pointeri raw doar în ultimul moment posibil. Puteți obține un pointer brut de la un pointer inteligent, de exemplu, utilizând funcția membră get a acestuia. Dacă o bibliotecă efectuează pentru dvs. o alocare pe care se așteaptă ca dvs. să o dezalocați prin intermediul unui handle, puteți adesea să înfășurați handle-ul într-un pointer inteligent cu un ștergător personalizat care va dezaloca obiectul în mod corespunzător.