12.3 — Referencje Lvalue

W C++ instrukcja odniesieniem jest aliasem dla istniejącego obiektu. Po zdefiniowaniu odniesienia, wszelkie operacje na nim zostaną zastosowane do obiektu, do którego się odwołujemy. Oznacza to, że możemy użyć referencji do odczytania lub zmodyfikowania obiektu, do którego się odwołujemy.

Chociaż referencje mogą początkowo wydawać się głupie, bezużyteczne lub zbędne, referencje są używane wszędzie w C++ (zobaczymy przykłady tego w kilku lekcjach).

Kluczowa informacja

Referencja jest zasadniczo identyczna z obiektem, do którego się odwołuje.

Możesz także tworzyć referencje do funkcji, chociaż robi się to rzadziej często.

Współczesny C++ zawiera dwa typy referencji: referencje do lwartości i referencje do wartości. W tym rozdziale omówimy odniesienia do lwartości.

Powiązana treść

Ponieważ w tej lekcji będziemy mówić o lwartościach i rwartościach, przed kontynuowaniem sprawdź 12.2 — Kategorie wartości (lwartości i rwartości) czy potrzebujesz odświeżenia tych pojęć. Referencje do wartości są omówione w rozdziale poświęconym semantyce przenoszenia (rozdziale 22).

Typy referencji do wartości

An Odniesienie do wartości (powszechnie nazywane „odniesieniami”, ponieważ przed wersją C++11 istniał tylko jeden typ referencji) działają jako alias dla istniejącej wartości (takiej jak zmienna).

Podobnie jak typ obiektu określa, jaki rodzaj wartości może on przechowywać, typ referencji określa jaki typ obiektu, do którego może się odwoływać. Typy referencyjne wartości można zidentyfikować za pomocą pojedynczego znaku ampersand (&) w specyfikatorze typu:

// regular types
int        // a normal int type (not an reference)
int&       // an lvalue reference to an int object
double&    // an lvalue reference to a double object
const int& // an lvalue reference to a const int object

Dla przykład int& jest typem odniesienia do wartości do int obiektu i const int& jest typem odniesienia do wartości do const int obiektem.

Typ, który określa odwołanie (np. int&) nazywany jest odniesienie typ. Typ, do którego można się odwoływać (np. int) nazywany jest typem, do którego się odwołuje.

Nomenklatura

Istnieją dwa rodzaje odniesień do wartości:

  • Odwołanie do wartości niebędącej stałą jest powszechnie nazywane „odniesieniem do wartości”, ale można je również określić jako odniesienie do wartości non-const lub a non-const odniesienie do wartości (ponieważ nie jest zdefiniowane przy użyciu słowa kluczowego const ).
  • odniesienie do wartości stałej jest powszechnie nazywane odniesieniem wartości do const lub a const lwartość odniesienie.

skoncentrujemy się na w tej lekcji odniesienia do lwartości niebędące stałą, a w następnej lekcji odniesienia do wartości stałej (12.4 -- Odniesienia do wartości do const).

Zmienne referencyjne lwartości

Jedną z rzeczy, które możemy zrobić z typem odniesienia lwartość jest utworzenie zmiennej referencyjnej lwartość. An Zmienna referencyjna wartości jest zmienną, która działa jako odniesienie do lwartości (zwykle innej zmiennej).

Aby utworzyć zmienną referencyjną lwartość, po prostu definiujemy zmienną o typie referencyjnym lwartość:

#include <iostream>

int main()
{
    int x { 5 };    // x is a normal integer variable
    int& ref { x }; // ref is an lvalue reference variable that can now be used as an alias for variable x

    std::cout << x << '\n';  // print the value of x (5)
    std::cout << ref << '\n'; // print the value of x via ref (5)

    return 0;
}

W powyższym przykładzie type int& definiuje ref jako odniesienie do wartości typu int, które następnie inicjujemy wyrażeniem lvalue x. Następnie ref i x można używać zamiennie. Ten program wypisuje:

5
5

Z punktu widzenia kompilatora nie ma znaczenia, czy ampersand jest „dołączony” do nazwy typu. (int& ref) lub nazwę zmiennej (int &ref), a to, którą wybierzesz, jest kwestią stylu. Współcześni programiści C++ wolą dołączać znak ampersand do typu, ponieważ wyraźniej widać, że odwołanie jest częścią informacji o typie, a nie identyfikatorem.

Najlepsza praktyka

Podczas definiowania odniesienia umieść ampersand obok typu (a nie nazwy zmiennej referencyjnej).

Dla zaawansowanych czytelników

W przypadku tych z Was, którzy już znają wskaźniki, znak ampersand w tym kontekście nie oznacza „adresu”, lecz „odniesienia do wartości”.

Modyfikowanie wartości poprzez odwołanie do lwartości niebędącej stałą

W powyższym przykładzie pokazaliśmy, że możemy użyć referencji do odczytania wartości obiektu, do którego się odwołujemy. Możemy również użyć referencji innej niż stała, aby zmodyfikować wartość obiektu, do którego się odwołujemy:

#include <iostream>

int main()
{
    int x { 5 }; // normal integer variable
    int& ref { x }; // ref is now an alias for variable x

    std::cout << x << ref << '\n'; // print 55

    x = 6; // x now has value 6

    std::cout << x << ref << '\n'; // prints 66

    ref = 7; // the object being referenced (x) now has value 7

    std::cout << x << ref << '\n'; // prints 77

    return 0;
}

Ten kod wypisuje:

55
66
77

W powyższym przykładzie ref jest pseudonimem dla x, więc możemy to zrobić. mogą zmienić wartość x poprzez x lub ref.

Inicjalizacja odniesienia

Podobnie jak stałe, wszystkie odniesienia muszą zostać zainicjowane. Odniesienia są inicjowane przy użyciu formy inicjalizacji zwanej inicjowaniem odniesienia.

int main()
{
    int& invalidRef;   // error: references must be initialized

    int x { 5 };
    int& ref { x }; // okay: reference to int is bound to int variable

    return 0;
}

Gdy odwołanie jest inicjowane obiektem (lub funkcją), mówimy, że jest powiązane do tego obiektu (lub funkcji). Proces wiązania takiego odniesienia nazywany jest wiązaniem odniesienia. Obiekt (lub funkcja), do którego się odwołuje, jest czasami nazywany referentem.

Odniesienia do wartości niebędącej stałą, mogą być powiązane tylko z modyfikowalną wartością.

int main()
{
    int x { 5 };
    int& ref { x };         // okay: non-const lvalue reference bound to a modifiable lvalue

    const int y { 5 };
    int& invalidRef { y };  // invalid: non-const lvalue reference can't bind to a non-modifiable lvalue 
    int& invalidRef2 { 0 }; // invalid: non-const lvalue reference can't bind to an rvalue

    return 0;
}

Kluczowa informacja

Jeśli odniesienia do wartości niebędące stałymi mogłyby być powiązane z niemodyfikowalnymi (stałymi) wartościami lub rwartościami, wówczas można byłoby zmienić te wartości poprzez odniesienie, co byłoby naruszeniem ich stałość.

Lwartości odniesienia do void są niedozwolone (o co by to miałoby sens?).

Nawet jeśli typ odniesienia (np. int&) nie odpowiada dokładnie typowi powiązanego obiektu (np. int), nie jest tu stosowana żadna konwersja (nawet trywialna konwersja) - różnica typów jest obsługiwana jako część procesu inicjalizacji referencji.

Odniesienie będzie (zwykle) wiązać się tylko z obiektem pasującym do jego typu, do którego się odnosi.

W większości przypadków odwołanie będzie wiązać się tylko z obiektem, którego typ pasuje do typu odniesienia (istnieją pewne wyjątki od tej reguły, które omówimy, kiedy przejdziemy do dziedziczenia).

Jeśli spróbujesz powiązać referencję z obiektem, który nie pasuje do jego odniesienia typu, kompilator spróbuje niejawnie przekonwertować obiekt na typ, do którego się odwołuje, a następnie powiązać z nim odwołanie.

Kluczowa informacja

Ponieważ wynikiem konwersji jest wartość, a odwołanie do wartości innej niż stała nie może zostać powiązane z wartością, próba powiązania odwołania do wartości innej niż stała z obiektem, który nie pasuje do jego typu, do którego się odwołuje, spowoduje błąd kompilacji.

int main()
{
    int x { 5 };
    int& ref { x };            // okay: referenced type (int) matches type of initializer

    double d { 6.0 };
    int& invalidRef { d };     // invalid: conversion of double to int is narrowing conversion, disallowed by list initialization
    double& invalidRef2 { x }; // invalid: non-const lvalue reference can't bind to rvalue (result of converting x to double)

    return 0;
}

Nie można ponownie podłączyć odwołań (zmieniono, aby odnosiło się do innego obiektu)

Po zainicjowaniu odwołanie w C++ nie może być ponownie umieszczone, co oznacza, że nie można go zmienić tak, aby odwoływał się do innego obiektu.

Nowi programiści C++ często próbują ponownie umieścić referencję za pomocą przypisania, aby zapewnić referencję inną zmienną do odniesienia. Spowoduje to skompilowanie i uruchomienie - ale nie będzie działać zgodnie z oczekiwaniami. Rozważmy następujący program:

#include <iostream>

int main()
{
    int x { 5 };
    int y { 6 };

    int& ref { x }; // ref is now an alias for x
    
    ref = y; // assigns 6 (the value of y) to x (the object being referenced by ref)
    // The above line does NOT change ref into a reference to variable y!

    std::cout << x << '\n'; // user is expecting this to print 5

    return 0;
}

Być może zaskakujące jest to, że wypisuje:

6

Gdy odwołanie jest oceniane w wyrażeniu, jest ono rozpoznawane jako obiekt, do którego się odnosi. Zatem ref = y nie zmienia się ref i teraz odnosi się do y. Raczej dlatego, że ref jest pseudonimem dla x, wyrażenie jest oceniane tak, jakby zostało zapisane x = y -- i ponieważ y wylicza wartość 6, x , ma przypisaną wartość 6.

Zakres i czas trwania odniesienia

Zmienne referencyjne podlegają tym samym regułom zakresu i czasu trwania, co zwykłe zmienne:

#include <iostream>

int main()
{
    int x { 5 }; // normal integer
    int& ref { x }; // reference to variable value

     return 0;
} // x and ref die here

Odniesienia i desygnaty mają niezależny czas życia

Z jednym wyjątkiem (który omówimy w następnej lekcji), czas życia odniesienia i czas życia jego desygnatu są niezależne. Innymi słowy, oba poniższe stwierdzenia są prawdziwe:

  • Odniesienie może zostać zniszczone przed obiektem, do którego się odnosi.
  • Obiekt, do którego się odnosi, może zostać zniszczony przed odniesieniem.

Kiedy odniesienie zostaje zniszczone przed odniesieniem, nie ma to wpływu na odniesienie. Poniższy program demonstruje to:

#include <iostream>

int main()
{
    int x { 5 };

    {
        int& ref { x };   // ref is a reference to x
        std::cout << ref << '\n'; // prints value of ref (5)
    } // ref is destroyed here -- x is unaware of this

    std::cout << x << '\n'; // prints value of x (5)

    return 0;
} // x destroyed here

Powyższe wypisuje:

5
5

Po wywołaniu ref umiera, zmienna x działa normalnie, błogo nieświadoma, że odniesienie do niego zostało zniszczone.

Wiszące odniesienia

Gdy obiekt, do którego odnosi się odwołanie, zostaje zniszczony przed powołaniem się do niego, odniesienie pozostaje odniesieniem do obiektu, który już nie istnieje. Takie odniesienie nazywa się odniesieniem wiszącym. Dostęp do odniesienia wiszącego prowadzi do niezdefiniowanego zachowania.

Wiszące odniesienia są dość łatwe do uniknięcia, ale pokażemy przypadek, w którym może się to zdarzyć w praktyce na lekcji 12.12 -- Powrót przez referencję i powrót przez adres.

Odniesienia nie są obiekty

Być może, co zaskakujące, referencje nie są obiektami w C++. Odniesienie nie musi istnieć ani zajmować miejsca w pamięci. Jeśli to możliwe, kompilator zoptymalizuje referencje, zastępując wszystkie wystąpienia referencji referencjami. Jednak nie zawsze jest to możliwe i w takich przypadkach referencje mogą wymagać przechowywania.

Oznacza to również, że termin „zmienna referencyjna” jest nieco myląca, ponieważ zmienne są obiektami z nazwą, a referencje nie są obiektami.

Ponieważ referencje nie są obiektami, nie można ich używać wszędzie tam, gdzie wymagany jest obiekt (np. nie można mieć referencji do referencji, ponieważ referencja do lwartości musi odwoływać się do identyfikowalny przedmiot). W przypadkach, gdy potrzebne jest odniesienie w postaci obiektu lub odniesienie, które można ponownie osadzić, std::reference_wrapper (które omówimy w lekcji 23.3 - Agregacja) zapewnia rozwiązanie.

Na marginesie…

Rozważ następujące zmienne:

int var{};
int& ref1{ var };  // an lvalue reference bound to var
int& ref2{ ref1 }; // an lvalue reference bound to var

Ponieważ ref2 (odniesienie) jest inicjowane za pomocą ref1 (odniesienie), możesz pokusić się o wyciągnięcie wniosku to ref2 jest odniesieniem do odniesienia. Tak nie jest. Ponieważ ref1 jest odniesieniem do var, gdy jest używany w wyrażeniu (takim jak inicjator), ref1 wyniesie var. Zatem ref2 jest zwykłym odwołaniem do wartości (jak wskazuje jej typ int&), powiązanym z var.

Odwołanie do referencji (do int) miałoby składnię int&& --- ale ponieważ C++ nie obsługuje odniesień do referencji, składnia ta została zmieniona w C++11, aby wskazać odwołanie do wartości (które omówimy w lekcja 22.2 -- Odniesienia do wartości R).

Nota autora

Jeśli odniesienia wydają się w tym momencie nieco bezużyteczne, nie martw się, że odniesienia są często używane, a jeden z głównych powodów tego omówimy wkrótce w lekcjach 12.5 -- Przekazywanie przez lvalue reference i 12.6 -- Przekaż stałą lwartość odniesienia.

Czas quizu

Pytanie nr 1

Określ, jakie wartości wyświetla następujący program samodzielnie (nie kompiluj programu).

#include <iostream>

int main()
{
    int x{ 1 };
    int& ref{ x };

    std::cout << x << ref << '\n';

    int y{ 2 };
    ref = y;
    y = 3;

    std::cout << x << ref << '\n';

    x = 4;

    std::cout << x << ref << '\n';

    return 0;
}

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
370 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze