This is an archived page. Back to the blog.
C++-arkivet
 

Minimera krascher och svårläst kod: Det enda tips du behöver för att bemästra referenser och pekare!

Vi börjar med tipset:

När du väljer mellan referens och pekare, använd referens så ofta du kan.
Använd pekare endast när:

  1. Du behöver en null-pekare
    (för att indikera ett ogiltigt objekt).
  2. Du behöver flytta ditt alias
    (för att peka ut ett annat objekt, eller null).
Tips: Algoritm för att välja mellan referens och pekare.



Varför finns referenser?

När man skapar en pekare eller en referens till ett objekt skapar man ett nytt sätt att komma åt objektet, dvs ett alias eller "smeknamn". Den kod som genereras när man använder en referens är identisk med den kod som du hade fått om du använt pekare. Så varför erbjuds man en valmöjlighet?

Man introducerade referenser i C++ för att hjälpa programmeraren. Jämfört med hur pekare fungerar så införde man två restriktioner:

pekare
  1. En referens pekar alltid ut ett giltigt objekt.
  2. En referens går inte att flytta.
Tabell 1: Egenskaper hos en referens.

Vad får detta för konsekvenser? På grund av (1) kan vi förutsätta att det refererade objektet är ok och går att använda. Det finns alltså ingen "null-referens" eller annan motsvarighet till null-pekare. Detta är en fördel eftersom vi dels slipper komma ihåg att jämföra med null när vi skriver vårt program, och dels slipper lägga ner tid på en sådan kontroll vid exekveringstillfället.

På grund av (2) måste referensen få sitt rätta värde när den skapas (dvs initieras vid definitionstillfället). Man riskerar därför aldrig att glömma bort att ge referensen ett värde. Kompilatorn hjälper dig med detta. Detta är en fördel eftersom oinitierade variabler ofta leder till fel i våra program som är svåra att hitta.

Referens eller pekare?

Man kan ge följande rekommendation när du vill skapa ett alias: Se referensen som ditt förstahandsval och ställ dig sedan frågan "Finns det någon bra anledning till att inte använda en referens här?"

Förutom "nej" så kan du få två svar på denna fråga:

  • "Ja, jag måste ha möjlighet att tala om att objektet är ogiltigt" (bryter mot punkt (1) i Tabell 1). Då måste jag kunna ha en pekare till null. Vi väljer alltså en pekare istället för referens.
  • "Ja, jag måste ha möjlighet att "flytta" mitt alias så att det pekar ut ett annat objekt" (bryter mot punkt (2) i Tabell 1). En referens går inte att flytta genom tilldelning, och därför måste jag ha en pekare. (Obs att tilldelning av referensen ändrar det refererade objektet. Se Kodexempel 1.) Pekararitmetik (såsom int *p; ++p;) är ett annat exempel på när man modifierar pekarens värde. Det finns alltså ingen motsvarande "referens-aritmetik", utan man måste välja en pekare istället för en referens.
int myint  = 7;
int & ref  = myint;  // vi skapar ett alias för myint
int eight  = 8;
ref = eight;     // myint får värdet av eight (dvs 8)
ref = 9;         // myint får värdet 9
                 // ref är fortfarande ett alias för myint
Kodexempel 1: Referenser går inte att flytta genom tilldelning. Däremot kan man tilldela det refererade objektet.
Här är en sammanfattning av dina valmöjligheter:

När du väljer mellan referens och pekare, använd referens så ofta du kan. Använd pekare endast när:

  1. Du behöver en null-pekare
    (för att indikera ett ogiltigt objekt).
  2. Du behöver flytta ditt alias
    (för att peka ut ett annat objekt, eller null).
Tabell 2: Algoritm för att välja mellan referens och pekare.

Argument till funktioner

Vi har pratat om att använda pekare och referenser som variabler. Man stöter även på pekare och referenser som argument eller returvärden i funktioner.

Vad tänker du när du ser en funktion som tar en pekare som argument? Jag tänker "aha, här kan jag skicka värdet null om jag behöver." En funktion som tar en pekare som argument måste alltid kontrollera så att pekaren inte är null innan den avrefereras! Alltid, alltid, alltid. Om pekaren inte får (eller kan) vara null så skulle funktionen deklarerats att ta en referens istället. Det förekommer att man med kommentarer eller dokumentation säger att "denna pekare får inte vara null", men en referens säger det så mycket tydligare. Man ser här hur viktigt det är med det gränssnitt man exponerar; pekaren kan locka användaren av funktionen att skicka värdet null.

Kodexempel 2 innehåller två varianter av funktionen marry. Ovanför marry1 finns en kommentar som säger att fiancee inte får vara null, eftersom man vill undvika giftermål med null-pekare. Men även om du och ditt program följer denna uppmaning idag så kan programkoden ändras av någon annan imorgon och därmed bryta mot förbudet. Då är marry2 mycket tydligare. Med denna version blir ditt program lättare att förstå och därmed att underhålla. Sensmoralen är att du bör skriva din programkod så att det är svårt att göra fel.

class Man
{
    // fiancee must not be null!
    void
marry1(const Woman * fiancee)
    {
        // här riskerar vi att avreferera null
    }

    void marry2(const Woman & fiancee);
};

Kodexempel 2: marry1() är ett exempel på dåligt gränssnitt eftersom det tillåter folk att gifta sig med en null-pekare. Kommentaren i funktionshuvudet bör få varningsklockorna att ringa. marry2() är tydligare.

God programmeringshygien undviker smittsamma pekare

Många objekt uppstår som pekare, t.ex. vid dynamiskt allokerat minne med new. Bara för att objektet gömmer sig bakom en pekare från början måste du inte skicka runt en pekare. Kolla istället, så fort som möjligt, att pekaren inte är null och paketera sedan om till en referens. Då slipper du jämföra pekaren med null på alla ställen du avrefererar den, vilket skulle påverka ("smitta") resten av ditt program!

Ett annat exempel där man ofta ser att pekare "smittar av sig" är i nedanstående kodsnutt (Kodexempel 3):

class House
{
    // ...
public:
    const Person * getOwner1() const;   // kolla ägare
    const
Person & getOwner2() const;

private
    const Person * owner; // owner kan aldrig vara null!
};

Kodexempel 3: getOwner2() är tydligare än getOwner1() eftersom ägaren inte kan vara null.

Klassen House representerar ett hus. Vi antar att ett hus alltid har en ägare, och att den kan byta ägare. Medlemsvariabeln owner är en pekare till ägaren. Vi har valt en pekare eftersom vi måste kunna flytta vårt alias för att representera ett ägarbyte.

Vi har två varianter på funktionen getOwner. Den första varianten (getOwner1) har en pekare som returvärde. En pekare kan kännas behändig eftersom owner är en pekare, men vi har då låtit vår interna datarepresentation påverka gränssnittet. Den observante programmeraren som använder funktionen kommer att kolla denna pekare mot null. Därmed spenderar hans program sin tid på meningslösa saker. Vi vet ju att ägaren aldrig kan vara null. Då är getOwner2 tydligare och mer effektiv.

Pekarens många ansvar

Låt oss diskutera ett exempel med en bil. Antag att bilen kan sakna ägare, t.ex. när den omhändertagits av myndigheter. Nu kan owner vara null. Men man skulle ändå kunna argumentera för att getOwner2 är att föredra! (Det är i och för sig en fråga om tycke och smak, men väl värt att nämna.)

Varför referens? Jo, en pekare kan representera andra saker än bara ett alias till ett objekt. En pekare kan representera det första elementet i en array. Den kan även peka till nyallokerat minne med new. Funktioner som returnerar nyallokerat minne är vanligt när man programmerar C (se t.ex. biblioteksfunktionen strdup). Det är dock inget jag skulle rekommendera i C++ där vi har andra valmöjligheter.

Så om du exponerar ett gränssnitt där din funktion returnerar en pekare, bli inte förvånad om någon frågar dig "Här får jag en pekare av dig. Varför? Är det möjligen ett nyallokerat objekt? Isåfall, vem äger detta minne?" (Dvs vem ansvarar för att göra delete?) Då är en referens mer okomplicerad: den är alltid bara ett alias för ett giltigt objekt.

Om bilen kan sakna ägare måste vi kunna tala om detta. Kom ihåg att referensen inte kan hjälpa oss med detta eftersom det inte finns några "null-referenser". Vi inför en funktion hasOwner. Se Kodexempel 4.

class Car
{
    // ...
public:
    bool hasOwner() const;             // har ägare?
    const
Person & getOwner2() const;  // hämta ägare

private
    const Person * owner;  // owner kan vara null
};

Kodexempel 4: Om owner kan vara null behöver vi en funktion hasOwner() för att se om någon ägare finns.

Testning och kontrakt

Att använda referenser som i hasOwner/getOwner2 i Kodexempel 4 har även andra fördelar. Oavsett om vi returnerar en pekare (getOwner1) eller om vi returnerar en referens (getOwner2), så måste vi vid något tillfälle avreferera pekaren owner. Vi riskerar då att krascha programmet om det är en null-pekare. Då du testar din kod kan du ha nytta av att ha en assert precis innan du avrefererar pekaren. Då får du så tidigt som möjligt veta om en null-pekare slinker förbi dina kontroller med if. Problemet med getOwner1 är att det kan bli väldigt många ställen med assert. Då är getOwner2 bättre, där behöver vi bara en assert. Se Kodexempel 5.

const Person & getOwner2() const
{
   
assert(owner);  // kolla att pekaren inte är null
   
return *owner;  // avreferera pekare
}

Kodexempel 5: Vi använder en assert för att under testning se till att ingen använder getOwner2() utan att först ha använt hasOwner().
Användaren anropar hasOwner för att kolla om det finns en ägare. Användaren lovar att inte anropa getOwner2 när ingen ägare finns. Vi kallar detta löfte för ett kontrakt (som i "design by contract"). Som en extra kontroll ser din assert till att kontraktet efterlevs. Under testning stoppar programmet om man anropar getOwner2 och pekaren är null. Observera att alla assert försvinner när du kompilerar din release-version av programmet.

Du väljer själv!

I STL (standard template library, en del av standardbiblioteket i C++) har man valt att returnera referenser. Man har bl.a. typen std::vector där man måste kolla om vektorn är tom med empty innan man får ut en referens med t.ex. back. Om man tycker bäst om pekare eller referens bestämmer man själv. Men man bör ha en bra motivering för det val man gör.

Lycka till med referenserna!



Relaterade artiklar: