12.5 — Przekaż referencję lwartości

W poprzednich lekcjach wprowadziliśmy odniesienia do wartości (12.3 -- Odniesienia do wartości) i odniesienia do wartości do const (12.4 -- Odniesienia do wartości do const). Same w sobie mogły one nie wydawać się zbyt przydatne — po co tworzyć alias do zmiennej, skoro można po prostu użyć samej zmiennej?

W tej lekcji w końcu przedstawimy pewien wgląd w to, co sprawia, że ​​odniesienia są przydatne. A w dalszej części tego rozdziału zobaczysz regularnie używane odniesienia.

Najpierw trochę kontekstu. Wracając do lekcji >2.4 -- Wprowadzenie do parametrów i argumentów funkcji omawialiśmy pass by value, gdzie argument przekazywany do funkcji jest kopiowany do parametru funkcji:

#include <iostream>

void printValue(int y)
{
    std::cout << y << '\n';
} // y is destroyed here

int main()
{
    int x { 2 };

    printValue(x); // x is passed by value (copied) into parameter y (inexpensive)

    return 0;
}

W powyższym programie, gdy printValue(x) jest wywoływane, wartość x (2) jest kopiowany do parametru y. Następnie na końcu funkcji obiekt y jest zniszczony.

Oznacza to, że wywołując funkcję, zrobiliśmy kopię wartości naszego argumentu, tylko po to, by ją krótko wykorzystać, a następnie zniszczyć! Na szczęście, ponieważ kopiowanie typów podstawowych jest tanie, nie stanowi to problemu.

Kopiowanie niektórych obiektów jest kosztowne

Większość typów udostępnianych przez bibliotekę standardową (takich jak std::string) to typy klasowe. Kopiowanie typów klas jest zwykle kosztowne. O ile to możliwe, chcemy unikać tworzenia niepotrzebnych kopii obiektów, których kopiowanie jest kosztowne, szczególnie gdy zniszczymy te kopie niemal natychmiast.

Rozważmy następujący program ilustrujący ten punkt:

#include <iostream>
#include <string>

void printValue(std::string y)
{
    std::cout << y << '\n';
} // y is destroyed here

int main()
{
    std::string x { "Hello, world!" }; // x is a std::string

    printValue(x); // x is passed by value (copied) into parameter y (expensive)

    return 0;
}

To wypisuje

Hello, world!

Chociaż program ten zachowuje się zgodnie z naszymi oczekiwaniami, jest również nieefektywny. Identycznie jak w poprzednim przykładzie, przy wywołaniu printValue() argument x jest kopiowany do printValue() parametr y. Jednak w tym przykładzie argumentem jest std::string zamiast int, I std::string jest to typ klasy, którego kopiowanie jest kosztowne. I ta kosztowna kopia jest wykonywana za każdym razem, gdy printValue() jest wywoływane!

Możemy zrobić to lepiej.

Przekazywanie przez odniesienie

Jednym ze sposobów uniknięcia kosztownej kopii argumentu podczas wywoływania funkcji jest użycie pass by reference zamiast pass by value. Używając przejdź obok referencja, deklarujemy parametr funkcji jako typ referencyjny (lub stały typ referencyjny), a nie jako typ normalny. Kiedy funkcja jest wywoływana, każdy parametr referencyjny jest powiązany z odpowiednim argumentem. Ponieważ odwołanie działa jak alias argumentu, nie jest tworzona żadna jego kopia.

Oto ten sam przykład co powyżej, w którym zastosowano przekazywanie przez odwołanie zamiast przekazywania przez wartość:

#include <iostream>
#include <string>

void printValue(std::string& y) // type changed to std::string&
{
    std::cout << y << '\n';
} // y is destroyed here

int main()
{
    std::string x { "Hello, world!" };

    printValue(x); // x is now passed by reference into reference parameter y (inexpensive)

    return 0;
}

Ten program jest identyczny z poprzednim, z tą różnicą, że typ parametru y został zmieniony z std::string Do std::string& (odniesienie do lwartości). Teraz, gdy zostanie wywołane printValue(x) , parametr referencyjny wartości y jest powiązany z argumentem x. Oprawienie referencji jest zawsze niedrogie i nie ma potrzeby wykonywania kopii x . Ponieważ referencja działa jak alias dla obiektu, do którego się odwołujemy, gdy printValue() używa referencji y, uzyskuje dostęp do rzeczywistego argumentu x (a nie do kopii x).

Kluczowa informacja

Przekazanie przez referencję pozwala nam przekazywać argumenty do funkcji bez tworzenia kopii tych argumentów przy każdym wywołaniu funkcji.

Poniższy program demonstruje, że wartość parametr jest obiektem odrębnym od argumentu, podczas gdy parametr referencyjny jest traktowany tak, jakby był argumentem:

#include <iostream>

void printAddresses(int val, int& ref)
{
    std::cout << "The address of the value parameter is: " << &val << '\n';
    std::cout << "The address of the reference parameter is: " << &ref << '\n';   
}

int main()
{
    int x { 5 };
    std::cout << "The address of x is: " << &x << '\n';
    printAddresses(x, x);

    return 0;
}

Jedno uruchomienie tego programu dało następujący wynik:

The address of x is: 0x7ffd16574de0
The address of the value parameter is: 0x7ffd16574de4
The address of the reference parameter is: 0x7ffd16574de0

Widzimy, że argument ma inny adres niż parametr value, co oznacza, że parametr value jest innym obiektem. Ponieważ mają one oddzielne adresy pamięci, aby parametr value miał taką samą wartość jak argument, wartość argumentu musi zostać skopiowana do pamięci przechowywanej przez wartość. parametr.

Z drugiej strony widzimy, że wzięcie adresu parametru referencyjnego daje adres identyczny z adresem argumentu. Oznacza to, że parametr referencyjny jest traktowany tak, jakby był tym samym obiektem co argument.

Przekazywanie przez referencję pozwala nam zmienić wartość argumentu

Gdy obiekt jest przekazywany przez wartość, parametr funkcji otrzymuje kopię argumentu. Oznacza to, że wszelkie zmiany wartości parametru dokonywane są w kopii argumentu, a nie w samym argumencie:

#include <iostream>

void addOne(int y) // y is a copy of x
{
    ++y; // this modifies the copy of x, not the actual object x
}

int main()
{
    int x { 5 };

    std::cout << "value = " << x << '\n';

    addOne(x);

    std::cout << "value = " << x << '\n'; // x has not been modified

    return 0;
}

W powyższym programie, ponieważ wartość parametru y jest kopią x, gdy zwiększamy y, dotyczy to tylko y. Ten program wyprowadza:

value = 5
value = 5

Ponieważ jednak odniesienie działa identycznie jak obiekt, do którego się odwołuje, podczas korzystania z przekazywania przez odniesienie wszelkie zmiany dokonane w parametrze odniesienia będziesz wpływają na argument:

#include <iostream>

void addOne(int& y) // y is bound to the actual object x
{
    ++y; // this modifies the actual object x
}

int main()
{
    int x { 5 };

    std::cout << "value = " << x << '\n';

    addOne(x);

    std::cout << "value = " << x << '\n'; // x has been modified

    return 0;
}

Ten program wyprowadza:

value = 5
value = 6

W powyższym przykładzie x początkowo ma wartość 5. Podczas oceny addOne(x) jest wywoływany, parametr odniesienia y jest powiązany z argumentem x. Kiedy addOne() funkcja zwiększa odniesienie y, w rzeczywistości jest to argument zwiększający x z 5 Do 6 (nie kopia x). Ta zmieniona wartość utrzymuje się nawet po addOne() zakończeniu wykonywania.

Kluczowa informacja

Przekazaniu wartości przez referencję do wartości innej niż stała pozwala nam pisać funkcje modyfikujące wartość przekazywanych argumentów.

Przydatna może być możliwość modyfikowania przez funkcje wartości przekazywanych argumentów. Wyobraź sobie, że napisałeś funkcję sprawdzającą, czy potwór skutecznie zaatakował gracza. Jeśli tak, potwór powinien wyrządzić pewne obrażenia zdrowiu gracza. Jeśli przekażesz obiekt gracza przez referencję, funkcja może bezpośrednio zmodyfikować stan faktycznego obiektu gracza, który został przekazany. Jeśli przekażesz obiekt gracza według wartości, możesz zmodyfikować jedynie stan kopii obiektu gracza, co nie jest tak przydatne.

Przekazywanie przez odwołanie może akceptować tylko modyfikowalne argumenty lvalue

Ponieważ odwołanie do wartości innej niż stała może wiązać się tylko z modyfikowalną wartością (zasadniczo a) zmienna inna niż stała), oznacza to, że przekazywanie przez referencję działa tylko z argumentami, które są modyfikowalnymi wartościami. W praktyce znacznie ogranicza to użyteczność przekazywania przez referencje do obiektów innych niż const, ponieważ oznacza to, że nie możemy przekazywać zmiennych ani literałów const. Na przykład:

#include <iostream>

void printValue(int& y) // y only accepts modifiable lvalues
{
    std::cout << y << '\n';
}

int main()
{
    int x { 5 };
    printValue(x); // ok: x is a modifiable lvalue

    const int z { 5 };
    printValue(z); // error: z is a non-modifiable lvalue

    printValue(5); // error: 5 is an rvalue

    return 0;
}

Na szczęście istnieje prosty sposób na obejście tego problemu, który omówimy w następnej lekcji. Przyjrzymy się także, kiedy przekazywać wartość, a kiedy przekazywać przez referencję.

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:  
371 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze