12.10 — Przekazywanie adresu

W poprzednich lekcjach omówiliśmy dwa różne sposoby przekazywania argumentu do funkcji: przekazywanie przez wartość (>2.4 -- Wprowadzenie do parametrów i argumentów funkcji) i przekazywanie przez referencję (12.5 -- Przekazywanie przez lvalue reference).

Oto przykładowy program, który pokazuje std::string obiekt przekazywany przez wartość i referencję:

#include <iostream>
#include <string>

void printByValue(std::string val) // The function parameter is a copy of str
{
    std::cout << val << '\n'; // print the value via the copy
}

void printByReference(const std::string& ref) // The function parameter is a reference that binds to str
{
    std::cout << ref << '\n'; // print the value via the reference
}

int main()
{
    std::string str{ "Hello, world!" };
    
    printByValue(str); // pass str by value, makes a copy of str
    printByReference(str); // pass str by reference, does not make a copy of str

    return 0;
}

Kiedy przekazujemy argument str przez wartość, parametr funkcji val otrzymuje kopię argumentu. Ponieważ parametr jest kopią argumentu, wszelkie zmiany w val dokonują kopii, a nie oryginału argument.

Kiedy przekazujemy argument str przez referencję, parametr referencyjny ref jest powiązany z rzeczywistym argumentem. Pozwala to uniknąć tworzenia kopii argumentu. Ponieważ nasz parametr referencyjny jest stały, nie wolno nam zmieniać ref. Gdyby jednak ref nie był stały, wszelkie zmiany, które wprowadziliśmy w ref , zostałyby wykonane. zmiana str.

W obu przypadkach wywołujący podaje rzeczywisty obiekt (str), który ma być przekazany jako argument do wywołania funkcji.

Przekazywanie przez adres

C++ zapewnia trzeci sposób przekazywania wartości do funkcji, zwany przekazywaniem przez adres Za pomocą przekazaniu adresu, zamiast podawać obiekt jako argument, osoba wywołująca udostępnia obiektu adres (poprzez wskaźnik). Ten wskaźnik (przetrzymujący adres obiektu) jest kopiowany do parametru wskaźnika wywoływanej funkcji (który teraz przechowuje także adres obiektu). Funkcja może następnie wyłuskać ten wskaźnik, aby uzyskać dostęp do obiektu, którego adres został przekazany.

Oto wersja powyższego programu, która dodaje wariant adresu przejścia:

#include <iostream>
#include <string>

void printByValue(std::string val) // The function parameter is a copy of str
{
    std::cout << val << '\n'; // print the value via the copy
}

void printByReference(const std::string& ref) // The function parameter is a reference that binds to str
{
    std::cout << ref << '\n'; // print the value via the reference
}

void printByAddress(const std::string* ptr) // The function parameter is a pointer that holds the address of str
{
    std::cout << *ptr << '\n'; // print the value via the dereferenced pointer
}

int main()
{
    std::string str{ "Hello, world!" };
    
    printByValue(str); // pass str by value, makes a copy of str
    printByReference(str); // pass str by reference, does not make a copy of str
    printByAddress(&str); // pass str by address, does not make a copy of str

    return 0;
}

Zauważ, jak podobne są wszystkie trzy wersje. Przyjrzyjmy się bardziej szczegółowo wersji z przekazywaniem adresu.

Po pierwsze, ponieważ chcemy, aby nasza printByAddress() funkcja korzystała z przekazywania przez adres, stworzyliśmy parametr naszej funkcji jako wskaźnik o nazwie ptr. Ponieważ printByAddress() , który będzie używany ptr w trybie tylko do odczytu, ptr jest wskaźnikiem do wartości stałej.

void printByAddress(const std::string* ptr)
{
    std::cout << *ptr << '\n'; // print the value via the dereferenced pointer
}

Wewnątrz projektu printByAddress() funkcją, my dereference ptr aby uzyskać dostęp do wartości wskazywanego obiektu.

Po drugie, gdy funkcja jest wywoływana, nie możemy po prostu przekazać str obiektu - musimy przekazać adres str. Najłatwiej to zrobić, używając operatora adresu (&), aby uzyskać wskaźnik przechowujący adres str:

printByAddress(&str); // use address-of operator (&) to get pointer holding address of str

When. to wywołanie zostanie wykonane, &str utworzy wskaźnik przechowujący adres str. Adres ten jest następnie kopiowany do parametru funkcji ptr w ramach wywołania funkcji. Ponieważ ptr przechowuje teraz adres str, gdy funkcja wyłuska ptr, otrzyma wartość str, którą funkcja wypisuje na konsoli.

To wszystko.

Chociaż w powyższym przykładzie używamy operatora adresu w celu uzyskania adresu str, jeśli mamy już zmienną wskaźnikową zawierającą adres str, moglibyśmy go zamiast tego użyć:

int main()
{
    std::string str{ "Hello, world!" };
    
    printByValue(str); // pass str by value, makes a copy of str
    printByReference(str); // pass str by reference, does not make a copy of str
    printByAddress(&str); // pass str by address, does not make a copy of str

    std::string* ptr { &str }; // define a pointer variable holding the address of str
    printByAddress(ptr); // pass str by address, does not make a copy of str    

    return 0;
}

Nomenklatura

Gdy przekazujemy adres zmiennej jako argument za pomocą operator&, powiedzmy, że zmienna jest przekazywana przez adres.

Gdy mamy zmienną wskaźnikową przechowującą adres obiektu i przekazujemy wskaźnik jako argument do parametru tego samego typu, mówimy, że obiekt jest przekazywany przez adres, a wskaźnik przez wartość.

Przekazywanie adresu nie tworzy kopii wskazywanego obiektu

Rozważ następujące stwierdzenia:

std::string str{ "Hello, world!" };
printByAddress(&str); // use address-of operator (&) to get pointer holding address of str

As zauważyliśmy w 12.5 -- Przekazywanie przez lvalue reference, kopiowanie std::string jest kosztowne, więc tego chcemy uniknąć. Kiedy przekazujemy std::string przez adres, nie kopiujemy rzeczywistego std::string obiektu - kopiujemy jedynie wskaźnik (przechowujący adres obiektu) od obiektu wywołującego do wywoływanej funkcji. Ponieważ adres ma zazwyczaj tylko 4 lub 8 bajtów, jest to wskaźnik ma tylko 4 lub 8 bajtów, więc kopiowanie wskaźnika jest zawsze szybkie.

Tak więc, podobnie jak przekazywanie przez referencję, przekazywanie przez adres jest szybkie i pozwala uniknąć tworzenia kopii obiektu argumentu.

Przekazywanie adresu pozwala funkcji modyfikować wartość argumentu.

Kiedy przekazujemy obiekt przez adres, funkcja otrzymuje adres przekazanego obiektu, do którego może uzyskać dostęp poprzez dereferencję. Ponieważ jest to adres aktualnie przekazywanego obiektu argumentu (a nie kopia obiektu), jeśli parametr funkcji jest wskaźnikiem do wartości innej niż stała, funkcja może zmodyfikować argument za pomocą parametru wskaźnika:

#include <iostream>

void changeValue(int* ptr) // note: ptr is a pointer to non-const in this example
{
    *ptr = 6; // change the value to 6
}

int main()
{
    int x{ 5 };

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

    changeValue(&x); // we're passing the address of x to the function

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

    return 0;
}

Wypisuje:

x = 5
x = 6

Jak widać, argument jest modyfikowany i modyfikacja ta utrzymuje się nawet po changeValue() zakończeniu działania.

Jeśli funkcja nie ma modyfikować przekazywanego obiektu, parametr funkcji powinien zostać zmieniony pointer-to-const:

void changeValue(const int* ptr) // note: ptr is now a pointer to a const
{
    *ptr = 6; // error: can not change const value
}

Z wielu tych samych powodów zazwyczaj nie const zwykłych (niewskaźnikowych, nieodnoszących się) parametrów funkcji (omówionych w >5.1 -- Zmienne stałe (zwane stałymi)), zazwyczaj nie const parametrów funkcji wskaźnikowych. Zróbmy dwa stwierdzenia:

  • A const słowo kluczowe użyte do utworzenia parametru funkcji wskaźnika wskaźnik const zapewnia niewielką wartość (ponieważ nie ma wpływu na osobę wywołującą i służy głównie jako dokumentacja, że wskaźnik się nie zmieni).
  • A const słowo kluczowe użyte do odróżnienia wskaźnika do stałej od wskaźnika do innej niż stała, który może modyfikować przekazywany obiekt, jest istotne (ponieważ wywołujący musi wiedzieć, czy funkcja może zmień wartość argumentu).

Jeśli użyjemy tylko parametrów funkcji wskaźnikowej innych niż stałe, wówczas wszystkie użycia const są istotne. Gdy tylko zaczniemy używać const dla parametrów funkcji wskaźnika stałego, trudniej będzie określić, czy dane użycie const jest znaczące czy nie. Co ważniejsze, utrudnia to także dostrzeżenie parametrów wskaźnika do wartości innych niż stałe. Na przykład:

void foo(const char* source, char* dest, int count);             // Using non-const pointers, all consts are significant.
void foo(const char* const source, char* const dest, int count); // Using const pointers, `dest` being a pointer-to-non-const may go unnoticed amongst the sea of spurious consts.

W pierwszym przypadku łatwo zauważyć, że source jest wskaźnikiem do stałej i dest jest wskaźnikiem do nie-stałej. W tym drugim przypadku znacznie trudniej jest zauważyć, że dest jest wskaźnikiem const-to-non-const, którego wskazany obiekt może być modyfikowany przez funkcję!

Najlepsza praktyka

Preferuj parametry funkcji wskaźnik do const zamiast parametrów funkcji wskaźnik do non-const, chyba że funkcja musi zmodyfikować przekazywany obiekt.
Nie twórz parametrów funkcji const wskaźników, chyba że istnieje jakiś konkretny powód aby to zrobić.

Sprawdzanie wartości zerowej

Rozważmy teraz całkiem niewinnie wyglądający program:

#include <iostream>

void print(int* ptr)
{
	std::cout << *ptr << '\n';
}

int main()
{
	int x{ 5 };
	print(&x);

	int* myPtr {};
	print(myPtr);

	return 0;
}

Gdy ten program zostanie uruchomiony, wydrukuje wartość 5 , a następnie najprawdopodobniej ulegnie awarii.

W wywołaniu print(myPtr), myPtr jest wskaźnikiem zerowym, więc parametr funkcji ptr będzie również wskaźnikiem zerowym. Kiedy w treści funkcji następuje wyłuskiwanie tego wskaźnika zerowego, następuje niezdefiniowane zachowanie.

Przekazując parametr przez adres, przed wyłuskaniem wartości należy upewnić się, że wskaźnik nie jest wskaźnikiem zerowym. Jednym ze sposobów osiągnięcia tego jest użycie instrukcji warunkowej:

#include <iostream>

void print(int* ptr)
{
    if (ptr) // if ptr is not a null pointer
    {
        std::cout << *ptr << '\n';
    }
}

int main()
{
	int x{ 5 };
	
	print(&x);
	print(nullptr);

	return 0;
}

W powyższym programie testujemy ptr aby upewnić się, że nie ma ona wartości null, zanim ją usuniemy. Chociaż jest to w porządku w przypadku tak prostej funkcji, w bardziej skomplikowanych funkcjach może to skutkować nadmiarową logiką (wielokrotne sprawdzanie, czy ptr ma wartość null) lub zagnieżdżeniem podstawowej logiki funkcji (jeśli jest zawarta w bloku).

W większości przypadków skuteczniejsze jest postępowanie odwrotne: sprawdź, czy parametr funkcji ma wartość null jako warunek wstępny (9.6 — Assert i static_assert) i natychmiast zajmij się przypadkiem ujemnym:

#include <iostream>

void print(int* ptr)
{
    if (!ptr) // if ptr is a null pointer, early return back to the caller
        return;

    // if we reached this point, we can assume ptr is valid
    // so no more testing or nesting required

    std::cout << *ptr << '\n';
}

int main()
{
	int x{ 5 };
	
	print(&x);
	print(nullptr);

	return 0;
}

Jeśli a Do funkcji nigdy nie należy przekazywać wskaźnika zerowego, zamiast tego można użyć assert (o którym mówiliśmy w lekcji 9.6 — Assert i static_assert) (ponieważ asercje mają na celu dokumentowanie rzeczy, które nigdy nie powinny się wydarzyć):

#include <iostream>
#include <cassert>

void print(const int* ptr) // now a pointer to a const int
{
	assert(ptr); // fail the program in debug mode if a null pointer is passed (since this should never happen)

	// (optionally) handle this as an error case in production mode so we don't crash if it does happen
	if (!ptr)
		return;

	std::cout << *ptr << '\n';
}

int main()
{
	int x{ 5 };
	
	print(&x);
	print(nullptr);

	return 0;
}

Preferuj przekazywanie (const) referencji

Zauważ, że funkcja print() w powyższym przykładzie nie radzi sobie zbyt dobrze z wartościami null - w rzeczywistości po prostu przerywa funkcję. Biorąc to pod uwagę, po co w ogóle pozwalać użytkownikowi na przekazywanie wartości null? Przekazywanie przez referencję ma te same zalety, co przekazywanie przez adres, bez ryzyka przypadkowego wyłuskania wskaźnika zerowego.

Przekazywanie przez referencję stałą ma kilka innych zalet w porównaniu z przekazywaniem przez adres.

>Po pierwsze, ponieważ obiekt przekazywany przez adres musi mieć adres, przez adres można przekazywać tylko lwartości (ponieważ wartości nie mają adresów). Przekazywanie przez odwołanie do stałej jest bardziej elastyczne, ponieważ może akceptować lwartości i rwartości:

#include <iostream>

void printByValue(int val) // The function parameter is a copy of the argument
{
    std::cout << val << '\n'; // print the value via the copy
}

void printByReference(const int& ref) // The function parameter is a reference that binds to the argument
{
    std::cout << ref << '\n'; // print the value via the reference
}

void printByAddress(const int* ptr) // The function parameter is a pointer that holds the address of the argument
{
    std::cout << *ptr << '\n'; // print the value via the dereferenced pointer
}

int main()
{
    printByValue(5);     // valid (but makes a copy)
    printByReference(5); // valid (because the parameter is a const reference)
    printByAddress(&5);  // error: can't take address of r-value

    return 0;
}

Po drugie, składnia przekazywania przez referencję jest naturalna, ponieważ możemy po prostu przekazywać literały lub obiekty. W przypadku przekazywania adresu nasz kod jest zaśmiecony ampersandami (&) i gwiazdkami (*).

W nowoczesnym C++ większość rzeczy, które można wykonać za pomocą przekazywania adresu, można lepiej wykonać innymi metodami. Postępuj zgodnie z tą powszechną maksymą: „Przekazuj przez referencję, kiedy możesz, podawaj przez adres, kiedy musisz”.

Najlepsza praktyka

Preferuj podawanie przez referencję zamiast adresu, chyba że masz konkretny powód, aby użyć przekazywania przez adres.

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