Gå til innhold

Verdier, referanser og pekere

C++ gir deg tre måter å referere til data på: ved verdi (du har din egen kopi), ved referanse (et alias for noen andres data) og ved peker (en adresse som kanskje, kanskje ikke, peker på noe).

Hver av dem oppfører seg forskjellig, og hver har sin plass. Å velge riktig avgjør om funksjonen din endrer kallerens data, om den lager en dyr kopi, og om programmet krasjer når noe går galt.

Dette kapittelet forklarer alle tre og gir et klart standardvalg for hver situasjon.


Verdityper

En verditype holder dataene selv. Å tilordne eller sende en verditype kopierer den.

int a = 25;
int b = a;    // b er en kopi
b = 30;       // a er fortsatt 25

Dette er den tryggeste standarden, og slik alle innebygde typer og de fleste klassetyper oppfører seg som standard. Hver variabel har sitt eget uavhengige lager.

Kostnaden er kopien: for en int er den i praksis gratis, for en std::vector på 10 MB er den en heap-allokering og en memcpy. (Kapittelet om flyttesemantikk forklarer hvordan moderne C++ unngår mange av disse kopiene automatisk.)


Referanser

En referanse er et alias for en eksisterende variabel. Lesing og skriving gjennom referansen går rett til originalen.

int age = 42;
int& refAge = age;    // refAge er et annet navn for age
refAge = 10;          // age er nå 10

Tre ting gjør referanser forskjellige fra pekere:

  • En referanse må initialiseres når den deklareres. Det finnes ingen "uinitialisert" referanse.
  • En referanse kan ikke bindes på nytt. Når den først refererer til age, refererer den til age for alltid.
  • Det finnes ingen null-referanse: en referanse starter alltid bundet til et virkelig objekt. Den kan derimot overleve objektet og bli dangling — se Den store levetidsfellen nedenfor.

Referanser er arbeidshesten for effektiv parameteroverføring i C++.

const-referanser

En const-referanse er skrivebeskyttet. Funksjonen kan se på dataene, men ikke endre dem.

void printVector(const std::vector<double>& v) {
    for (double x : v) {
        std::cout << x << "\n";
    }
    // v.push_back(0.0);   // kompileringsfeil, const
}

Dette er standardidiomet for å sende store objekter uten å kopiere dem:

std::vector<double> data = readSensorBatch();
printVector(data);   // ingen kopi, printVector ser originalen via const&

Uten const& ville printVector mottatt en 10 MB stor kopi ved hvert kall. Med den koster kallet bare arbeidet med å sende én peker.


Pekere

En peker er en variabel som holder en adresse. Operatoren * ser gjennom adressen til verdien som er lagret der:

int x = 7;
int* p = &x;     // p holder adressen til x
*p = 42;         // skriver gjennom p, x er nå 42
Symbol Betydning
int* "peker til int"; typen til p
&x "adressen til x"; produserer en peker
*p "det p peker på"; dereferering

En referanse og en peker kan begge referere til samme variabel x, men mekanikken er forskjellig — en referanse gir x et andre navn, mens en peker er en separat celle som lagrer adressen til x:

int& r = x; x · r 7 r er et annet navn for x int* p = &x; p &x x 7 p lagrer adressen til x

Pekere skiller seg fra referanser på tre viktige måter:

  • En peker kan være nullptr, som betyr at den peker på ingenting.
  • En peker kan tilordnes på nytt til å peke et annet sted.
  • En peker kan være farlig: å dereferere en null-peker eller en ugyldig peker er udefinert oppførsel.
int* p = nullptr;   // gyldig peker, peker på ingenting
if (p != nullptr) {
    *p = 5;         // trygt, sjekket først
}

Sjekk alltid før du derefererer, eller bruk språkkonstruksjoner som garanterer ikke-null (referanser, smartpekere).

Pekere til objekter

En peker kan peke på et klasseobjekt like lett som på en int. For å nå et medlem gjennom pekeren må du først dereferere den. Skrevet helt ut er det (*p).read() — parentesene trengs fordi . binder sterkere enn *. Fordi det er klønete, gir C++ deg pil-snarveien ->:

Sensor s;
Sensor* p = &s;

(*p).read();   // dereferer, kall så read()
p->read();     // nøyaktig det samme, skrevet på den lesbare måten

p->read() betyr presis (*p).read(). Pilen er det du kommer til å se i praksis — nesten ingen skriver (*p).member. Du vil møte -> hele tiden i kapittel 5, der objekter rutinemessig håndteres gjennom pekere og smartpekere.


Den store levetidsfellen

Regelen: en referanse eller peker er bare gyldig så lenge det den refererer til fortsatt lever. Den største enkeltkilden til krasj i C++ er å bruke en referanse eller peker til data som er blitt destruert.

Å returnere en referanse eller peker til en lokal variabel

int& createIntRef() {
    int value = 1;
    return value;     // ille — `value` destrueres når funksjonen returnerer
}

int* createIntPtr() {
    int value = 1;
    return &value;    // ille — samme problem
}

int main() {
    int& bad1 = createIntRef();    // dangling referanse — udefinert oppførsel
    int* bad2 = createIntPtr();    // dangling peker — udefinert oppførsel
}

Begge funksjonene returnerer et håndtak til minne som ikke lenger tilhører noen. Å lese fra bad1 eller bad2 er udefinert oppførsel. Moderne kompilatorer advarer om nøyaktig dette mønsteret; følg med på advarslene.

Løsningen: returner ved verdi (du får din egen kopi), eller send en referanse inn i funksjonen slik at kalleren styrer levetiden.

Pekere og referanser inn i klassers indre

Å returnere en referanse eller peker til en klasses private data bryter også innkapslingen du møtte i Klasser:

class Demo {
public:
    int  getValue() const   { return value_; }   // trygt — returnerer en kopi
    int& getValueRef()      { return value_; }   // deler ut skrivetilgang
    int* getValuePtr()      { return &value_; }  // deler ut skrivetilgang

private:
    int value_ = 0;
};

Demo obj;
int& ref = obj.getValueRef();
ref = 42;        // obj sine private data er nå 42, invariantene omgått

Hvis du må eksponere et medlem ved referanse, returner const T& for å holde det skrivebeskyttet. Ellers kan ekstern kode endre din private tilstand uten å gå gjennom metodene som håndhever invariantene dine.


Hvilken skal jeg bruke?

Bruk denne tabellen hver gang en funksjonsparameter eller returtype tvinger frem spørsmålet:

Situasjon Bruk
Liten type som er billig å kopiere (int, double, bool, en enum) Send ved verdi
Funksjonen skal ikke endre inndataene Send som const T&
Funksjonen endrer inndataene, og kalleren skal se endringen Send som T&
Funksjonen kan motta "ingen verdi" Send en peker (og sjekk for null), eller std::optional<T>
Funksjonen returnerer et nyberegnet resultat Returner ved verdi (RVO gjør dette billig)
Funksjonen returnerer en av inndataene sine uendret Returner ved referanse (vær forsiktig med levetider)

For datamedlemmer i en klasse er tommelfingerreglene lignende:

Situasjon Bruk
Klassen eier dataene Vanlig verdimedlem (f.eks. std::vector<int> data_)
Klassen observerer data som eies av noe annet En rå peker (ikke-eiende) — men tenk nøye over hvem som holder den i live
Klassen deler eierskap med andre std::shared_ptr<T> (se Minne)

Oppsummering

  • Verdityper kopierer. Trygt, noen ganger dyrt.
  • Referanser er alias. Kan ikke være null, kan ikke bindes på nytt, må initialiseres.
  • Pekere er adresser. Kan være null, kan tilordnes på nytt, må sjekkes.
  • En referanse eller peker som overlever det den peker på, er udefinert oppførsel — den vanligste enkeltårsaken til krasj.
  • For funksjonsparametere: små typer ved verdi, store typer som const T&, endre-inndataene-tilfeller som T&.
  • For eierskap på tvers av klassegrenser, foretrekk smartpekere fremfor rå pekere.