Articles

C++ pekare: varför vi behöver dem, när vi använder dem, hur de skiljer sig från att komma åt själva objektet

Även om de flesta programmerare förstår vad som är skillnaden mellan objekt och pekare till dem, så är det ibland inte helt klart vilket av sätten att komma åt objektet man ska välja. Vi har försökt besvara denna fråga nedan.

Fråga

Jag kommer från en Java-bakgrund och har börjat arbeta med objekt i C++. Men en sak som slog mig är att man ofta använder pekare till objekt istället för själva objekten, till exempel den här deklarationen:

Object *myObject = new Object;

i stället för:

Object myObject;

Och istället för att använda en funktion, låt oss säga testFunc(), så här:

myObject.testFunc();

måste vi skriva:

myObject->testFunc();

Men jag kommer inte på varför vi ska göra på det här sättet. Jag antar att det har att göra med effektivitet och snabbhet eftersom vi får direkt tillgång till minnesadressen. Har jag rätt?

Svar

Förresten bör vi notera att pekare i Java inte används explicit, dvs. en programmerare kan inte komma åt ett objekt i koden genom en pekare till det. I Java är dock alla typer, utom base, refererade: åtkomst till dem sker via länken, även om man inte uttryckligen kan skicka parametern via länken. Dessutom är new i C++ och Java eller C# olika saker.

För att ge en liten uppfattning om pekare i C++ ger vi två liknande kodfragment:

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

Den närmaste motsvarigheten till detta, är:

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.

Vi ska se det alternativa C++-sättet:

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

Får vi en hastighetsvinst genom att komma åt direkt till minnet?

Inte alls. Pekare används vanligen för åtkomst till heap medan objekten finns i stacken – detta är en enklare och snabbare struktur. Om du är nybörjare har vi för dig ett material där vi i detalj berättar vad en stack och en heap är.

Strikt taget kombinerar denna fråga två olika frågor. För det första: När använder vi dynamisk minnesallokering? För det andra: när är det bättre att använda pekare? Visst, vi kommer inte att klara oss utan vanliga ord om att man alltid måste välja det lämpligaste verktyget för jobbet. Nästan alltid finns det bättre förverkligande än att använda manuell dynamisk allokering (dynamisk allokering) och/eller råpekare.

Det är mycket olyckligt att man ser dynamisk allokering så ofta. Det visar bara hur många dåliga C++-programmerare det finns.

På sätt och vis har du två frågor samlade i en. Den första är när ska vi använda dynamisk allokering (med hjälp av new)? Den andra är när ska vi använda pekare?

Det viktiga budskapet är att du alltid ska använda rätt verktyg för jobbet. I nästan alla situationer finns det något lämpligare och säkrare än att utföra manuell dynamisk allokering och/eller använda råpekare.

Dynamisk allokering

I din fråga har du visat två sätt att skapa ett objekt. Den största skillnaden är objektets lagringstid. När du gör Object myObject; inom ett block skapas objektet med automatisk lagringstid, vilket innebär att det förstörs automatiskt när det går utanför räckvidden. När du gör new Object() har objektet en dynamisk lagringstid, vilket innebär att det lever tills du uttryckligen raderar det. Du bör endast använda dynamisk lagringstid när du behöver det. Det vill säga, du bör alltid föredra att skapa objekt med automatisk lagringstid när du kan.

De två viktigaste situationerna där du kan behöva dynamisk allokering:

  1. Du behöver objektet för att överleva den aktuella räckvidden – det specifika objektet på den specifika minnesplatsen, inte en kopia av det. Om du är okej med att kopiera/flytta objektet (för det mesta bör du vara det) bör du föredra ett automatiskt objekt.
  2. Du behöver allokera mycket minne, vilket lätt kan fylla upp stapeln. Det vore trevligt om vi inte behövde bry oss om detta (för det mesta borde du inte behöva göra det), eftersom det egentligen ligger utanför C++, men tyvärr måste vi hantera verkligheten i de system som vi utvecklar för.
  3. Du vet inte exakt vilken array-storlek, som du kommer att behöva använda. Som du vet, i C++ har arrays storlek är fast. Det kan orsaka problem, till exempel när man läser användarinmatning. Pekaren definierar endast den del av minnet, där början av en array kommer att skrivas, inte begränsa dess storlek.

Om en användning av dynamisk allokering är nödvändig, bör du kapsla in den med hjälp av smart pekare eller av en annan typ som stödjer idiomet ”Resursförvärv är initialisering” (standardcontainrar stödjer det – det är ett idiom, enligt vilket resursen: ett block av minne, en fil, en nätverksanslutning etc. – initialiseras samtidigt som du får i konstruktören, och sedan försiktigt förstörs av destruktorn). Till exempel är smarta pekare std::unique_ptr och std::shared_ptr

Pointers

Det finns dock andra mer allmänna användningsområden för råpekare utöver dynamisk allokering, men de flesta har alternativ som du bör föredra. Som tidigare ska du alltid föredra alternativen om du inte verkligen behöver pekare.

  1. Du behöver referenssemantik. Ibland vill du överlämna ett objekt med hjälp av en pekare (oavsett hur det allokerades) eftersom du vill att den funktion som du överlämnar det till ska ha tillgång till det specifika objektet (inte en kopia av det). I de flesta situationer bör du dock föredra referenstyper framför pekare, eftersom detta är specifikt vad de är utformade för. Observera att detta inte nödvändigtvis handlar om att förlänga objektets livstid bortom det aktuella tillämpningsområdet, som i situation 1 ovan. Som tidigare, om det är okej att överlämna en kopia av objektet behöver du inte referenssemantik.
  2. Du behöver polymorfism. Du kan endast anropa funktioner polymorft (dvs. enligt ett objekts dynamiska typ) genom en pekare eller referens till objektet. Om det är det beteendet du behöver måste du använda pekare eller referenser. Återigen är referenser att föredra.
  3. Du vill representera att ett objekt är valfritt genom att tillåta att en nullptr lämnas över när objektet utelämnas. Om det är ett argument bör du föredra att använda standardargument eller funktionsöverladdningar. Annars bör du föredra att använda en typ som kapslar in detta beteende, t.ex. std::optional (införd i C++17 – med tidigare C++-standarder, använd boost::optional).
  4. Du vill frikoppla kompileringsenheter för att förbättra kompileringstiden. Den användbara egenskapen hos en pekare är att du bara behöver en framåtriktad deklaration av den typ som pekas på (för att faktiskt använda objektet behöver du en definition). Detta gör att du kan frikoppla delar av din kompileringsprocess, vilket kan förbättra kompileringstiden avsevärt. Se Pimpl-idiomet.
  5. Du behöver ett gränssnitt mot ett C-bibliotek eller ett bibliotek i C-stil. I det här läget är du tvungen att använda råpekare. Det bästa du kan göra är att se till att du bara släpper loss dina råpekare i sista möjliga stund. Du kan till exempel hämta en råpekare från en smart pekare genom att använda dess get-medlemsfunktion. Om ett bibliotek utför någon allokering åt dig som det förväntar sig att du ska avallokera via ett handtag, kan du ofta linda in handtaget i en smart pekare med en anpassad deleter som kommer att avallokera objektet på lämpligt sätt.