Articles

C++ pointers: waarom we ze nodig hebben, wanneer we ze gebruiken, hoe ze verschillen van toegang tot het object zelf

Ondanks dat de meeste programmeurs begrijpen wat het verschil is tussen objecten en pointers naar objecten, is het soms niet helemaal duidelijk welke van de manieren om toegang te krijgen tot het object ze moeten kiezen. We hebben geprobeerd deze vraag hieronder te beantwoorden.

Vraag

Ik kom uit een Java-achtergrond en ben begonnen met het werken met objecten in C++. Maar een ding dat me opviel is dat mensen vaak pointers naar objecten gebruiken in plaats van de objecten zelf, bijvoorbeeld deze declaratie:

Object *myObject = new Object;

in plaats van:

Object myObject;

Of in plaats van een functie te gebruiken, laten we zeggen testFunc(), zoals dit:

myObject.testFunc();

moeten we schrijven:

myObject->testFunc();

Maar ik kan er niet achter komen waarom we het op deze manier moeten doen. Ik neem aan dat het te maken heeft met efficiëntie en snelheid, omdat we direct toegang krijgen tot het geheugenadres. Heb ik gelijk?

Antwoord

Opgemerkt zij, dat pointers in Java niet expliciet worden gebruikt, d.w.z. een programmeur kan een object in code niet benaderen door er een pointer naar te maken. Echter, in Java wordt naar alle types, behalve base, verwezen: toegang tot hen gaat via de link, hoewel je de parameter niet expliciet via de link kunt doorgeven. Bovendien, nieuw in C++ en Java of C# zijn verschillende dingen.

Om een idee te geven over de pointers in C++, geven we twee vergelijkbare code fragmenten:

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

Het dichtstbijzijnde equivalent hiervan, is:

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.

Laten we eens kijken naar de alternatieve C++ manier:

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...

Krijgen we een snelheidswinst, door direct toegang tot het geheugen te krijgen?

Eigenlijk, helemaal niet. Pointers worden meestal gebruikt voor de toegang tot de heap, terwijl de objecten zich in de stack bevinden – dit is een eenvoudigere en snellere structuur. Als je een beginner bent, hebben we voor jou wat materiaal waarin we in detail vertellen wat een stack en een heap zijn.

Strikt genomen, combineert deze vraag twee verschillende zaken. Ten eerste: wanneer gebruiken we dynamische geheugenallocatie? Ten tweede: wanneer is het beter om pointers te gebruiken? Zeker, we zullen het niet zonder gemeenplaatsen stellen dat je altijd het meest geschikte gereedschap voor de klus moet kiezen. Bijna altijd is er een betere realisatie dan het gebruik van handmatige dynamische toewijzing (dynamic allocation) en/of raw pointers.

Het is erg jammer dat je dynamische toewijzing zo vaak ziet. Dat laat alleen maar zien hoeveel slechte C++ programmeurs er zijn.

In zekere zin heb je twee vragen gebundeld in één. De eerste is wanneer moeten we dynamische allocatie gebruiken (met behulp van new)? De tweede vraag is: wanneer moeten we pointers gebruiken?

De belangrijkste boodschap is dat je altijd het juiste gereedschap moet gebruiken voor de klus. In bijna alle situaties is er iets geschikter en veiliger dan het uitvoeren van handmatige dynamische toewijzing en/of het gebruik van raw pointers.

Dynamische toewijzing

In je vraag heb je twee manieren gedemonstreerd om een object te maken. Het belangrijkste verschil is de opslagduur van het object. Wanneer je Object myObject; doet binnen een blok, wordt het object gemaakt met automatische opslagduur, wat betekent dat het automatisch wordt vernietigd wanneer het buiten het bereik gaat. Wanneer je new Object() doet, heeft het object een dynamische opslagduur, wat betekent dat het in leven blijft totdat je het expliciet verwijdert. Je zou dynamische opslagduur alleen moeten gebruiken als je het nodig hebt. Dat wil zeggen, je zou altijd de voorkeur moeten geven aan het maken van objecten met automatische opslagduur wanneer je kunt.

De belangrijkste twee situaties waarin je dynamische toewijzing nodig zou kunnen hebben:

  1. Je hebt het object nodig om de huidige reikwijdte te overleven – dat specifieke object op die specifieke geheugenlocatie, niet een kopie ervan. Als je het goed vindt om het object te kopiëren/verplaatsen (meestal zou je dat moeten doen), zou je de voorkeur moeten geven aan een automatisch object.
  2. Je moet veel geheugen toewijzen, wat gemakkelijk de stack kan vullen. Het zou mooi zijn als we ons hier niet mee bezig hoefden te houden (meestal hoeft dat ook niet), omdat het eigenlijk buiten het bereik van C++ ligt, maar helaas hebben we te maken met de realiteit van de systemen waarvoor we ontwikkelen.
  3. U weet niet precies de array-grootte, die u zult moeten gebruiken. Zoals u weet, in C++ is de grootte van de arrays vast. Dat kan problemen veroorzaken, bijvoorbeeld bij het lezen van gebruikersinvoer. De pointer definieert alleen dat deel van het geheugen, waar het begin van een array zal worden geschreven, niet het beperken van de grootte.

Als een gebruik van dynamische allocatie nodig is, moet je inkapselen met behulp van slimme pointer of van een ander type dat het idioom “Resource acquisition is initialization” (standaard containers ondersteunen het – het is een idioom, in overeenstemming met die de bron: een blok geheugen, bestand, netwerkverbinding, enz. – worden geïnitialiseerd tijdens het krijgen in de constructor, en dan zorgvuldig worden vernietigd door destructor) ondersteunt. Slimme pointers zijn bijvoorbeeld std::unique_ptr en std::shared_ptr

Pointers

Echter, er zijn andere meer algemene toepassingen voor raw pointers buiten dynamische toewijzing, maar de meeste hebben alternatieven die je zou moeten verkiezen. Zoals voorheen, geef altijd de voorkeur aan de alternatieven, tenzij je pointers echt nodig hebt.

  1. Je hebt reference semantics nodig. Soms wil je een object doorgeven met behulp van een pointer (ongeacht hoe het is toegewezen), omdat je wilt dat de functie waaraan je het doorgeeft toegang heeft tot dat specifieke object (niet een kopie ervan). In de meeste situaties zou je echter referentietypes moeten verkiezen boven pointers, omdat dit specifiek is waarvoor ze ontworpen zijn. Merk op dat dit niet noodzakelijk gaat over het verlengen van de levensduur van het object buiten het huidige bereik, zoals in situatie 1 hierboven. Net als voorheen, als je het goed vindt om een kopie van het object door te geven, heb je geen referentie semantiek nodig.
  2. Je hebt polymorfisme nodig. Je kunt alleen functies polymorf aanroepen (dat wil zeggen, volgens het dynamische type van een object) via een pointer of referentie naar het object. Als dat het gedrag is dat je nodig hebt, dan moet je pointers of referenties gebruiken. Nogmaals, referenties verdienen de voorkeur.
  3. Je wilt weergeven dat een object optioneel is door een nullptr te laten passeren als het object wordt weggelaten. Als het een argument is, zou je de voorkeur moeten geven aan standaard argumenten of functie overloads. Anders zou u de voorkeur moeten geven aan een type dat dit gedrag inkapselt, zoals std::optional (geïntroduceerd in C++17 – met eerdere C++ standaarden, gebruik boost::optional).
  4. U wilt compilatie-eenheden ontkoppelen om de compilatietijd te verbeteren. De handige eigenschap van een pointer is dat je alleen een forward declaratie nodig hebt van het type waarnaar wordt verwezen (om het object daadwerkelijk te gebruiken, heb je een definitie nodig). Dit staat u toe om delen van uw compilatieproces te ontkoppelen, wat de compilatietijd aanzienlijk kan verbeteren. Zie het Pimpl idioom.
  5. Je moet een interface maken met een C bibliotheek of een C-stijl bibliotheek. Op dit punt, ben je gedwongen om raw pointers te gebruiken. Het beste wat je kunt doen is ervoor zorgen dat je je raw pointers pas op het laatst mogelijke moment loslaat. Je kunt bijvoorbeeld een raw pointer uit een smart pointer halen door zijn get member functie te gebruiken. Als een library een allocatie voor je uitvoert waarvan het verwacht dat je die dealloceert via een handle, kun je vaak de handle verpakken in een smart pointer met een aangepaste deleter die het object op de juiste manier dealloceert.