Menu

Parametry funkcji w C++: przez wartość, przez referencję i const

Jak argumenty trafiają do funkcji w C++: przekazywanie przez wartość i przez referencję, referencje const do taniego odczytu, argumenty domyślne, wskaźniki i pułapki kosztownego kopiowania, które po cichu spowalniają programy.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Parametry a argumenty

Parametry funkcji to nazwane zmienne w jej definicji; argumenty to rzeczywiste wartości, które przekazujesz przy wywołaniu. Poprzednia strona pokazała, jak definiować i wywoływać funkcje. Ta strona dotyczy tego, jak te wartości właściwie trafiają do środka, bo C++ daje kilka sposobów, a wybór wpływa zarówno na poprawność, jak i na szybkość.

Domyślnie C++ stosuje przekazywanie przez wartość: funkcja dostaje kopię.

Wewnątrz addTen n to osobna zmienna zainicjalizowana wartością score. Ponowne przypisanie do n dotyka tylko tej kopii, więc score w main pozostaje nietknięte. To bezpieczne i przewidywalne (funkcja nie może przypadkiem nadpisać twoich danych) i właśnie dlatego jest to zachowanie domyślne.

Przekazywanie przez referencję: funkcja zmienia dane wywołującego

Czasem chcesz, aby funkcja zmieniła zmienną wywołującego. Dodaj & do typu parametru, a stanie się on referencją, czyli aliasem oryginału, a nie kopią:

Jedyna różnica względem pierwszego przykładu to &, ale teraz n i score to ten sam obiekt. To standardowy sposób, by "zwrócić" więcej niż jedną wartość albo zaktualizować coś w miejscu. Klasyczne zastosowanie to zamiana dwóch zmiennych:

Bez & funkcja swapValues zamieniłaby dwie kopie, a main nie zobaczyłby żadnej zmiany. To bardzo częsty błąd początkujących.

Referencje const: tani dostęp tylko do odczytu

Przekazywanie przez wartość kopiuje argument. Dla int to nic nie kosztuje, ale kopiowanie dużego string albo vector przy każdym wywołaniu to realna, zmarnowana praca. Rozwiązaniem jest referencja const (const T&): dostajesz szybkość referencji (bez kopii) plus pilnowaną przez kompilator obietnicę, że argument nie zostanie zmieniony.

Przydatna zasada: małe typy wbudowane (int, double, char, bool, wskaźniki) przekazuj przez wartość, a duże obiekty, które tylko odczytujesz, przez referencję const. Zwykłe T& bez const zostaw na sytuacje, gdy naprawdę zamierzasz zmienić obiekt wywołującego.

Subtelna pułapka: zwykłe int& n nie może się związać z wartością tymczasową ani literałem. addTen(5) z pierwszego przykładu nie skompilowałoby się, gdyby parametr był typu int&, bo 5 to nie zmienna, której można nadać alias. const int& może związać się z 5 i to kolejny powód, dla którego referencje const są tak powszechne.

Argumenty domyślne

Parametrowi możesz nadać wartość zastępczą, dzięki czemu wywołujący może go pominąć. Gdy argumentu brakuje, używana jest wartość domyślna:

Dwie reguły często zaskakują. Po pierwsze, wartości domyślne muszą być na końcu: gdy jeden parametr ma wartość domyślną, każdy kolejny też musi ją mieć. Nie napiszesz void f(int a = 1, int b), bo nie byłoby sposobu, by podać b, pomijając a. Po drugie, gdy funkcja jest zadeklarowana w nagłówku, a zdefiniowana gdzie indziej, wartość domyślną podaj tylko w deklaracji i nigdy nie powtarzaj jej w definicji: powtórzenie to błąd kompilacji.

Przekazywanie tablic i wektorów

Surowa tablica przy przekazaniu rozpada się do wskaźnika, więc funkcja traci informację o jej rozmiarze. Prawie zawsze przekazujesz więc obok niej długość:

Ponieważ tablica stała się wskaźnikiem, sizeof(arr) wewnątrz sum dałoby rozmiar wskaźnika, a nie tablicy. To osławiony błąd. We współczesnym C++ wybieraj std::vector (albo std::span w C++20) przekazywany przez referencję const, który zna własny rozmiar:

Zwróć uwagę na const&: usuń je, a każde wywołanie skopiuje cały wektor. Przy czterech elementach to nieszkodliwe, ale przy milionie elementów to cichy pożeracz wydajności.

Parametry wskaźnikowe

Możesz też przekazać wskaźnik (T*). Podobnie jak referencja pozwala on funkcji sięgnąć do danych wywołującego, ale wskaźnik można przestawić albo może być pusty, więc to właściwe narzędzie, gdy "brak wartości" jest dopuszczalną opcją:

Wywołujący przekazuje &value, aby udostępnić jej adres, a funkcja zapisuje przez *out. Kluczowa różnica względem referencji: wskaźnik może być nullptr, więc funkcja, która go przyjmuje, powinna to sprawdzić przed wyłuskaniem. Pominięcie tego zabezpieczenia i wyłuskanie pustego wskaźnika to niezdefiniowane zachowanie, zwykle kończące się awarią. Jeśli "brak wartości" nigdy nie ma sensu, referencja jest czystsza, bo w ogóle nie może być pusta.

Dalej: referencje

To przy parametrach referencje najbardziej się przydają, ale są też samodzielną funkcją języka: aliasami, które możesz utworzyć dla dowolnej zmiennej, nie tylko w sygnaturze funkcji. Następna strona pokazuje, jak referencje działają same w sobie: jak je deklarować, dlaczego trzeba je od razu zainicjalizować, czym różni się referencja do l-wartości od referencji const i w jaki subtelny sposób referencja może stać się wisząca.

Najczęściej zadawane pytania

Jaka jest różnica między przekazywaniem przez wartość a przez referencję w C++?

Przekazywanie przez wartość kopiuje argument do parametru, więc zmiany wewnątrz funkcji nie wpływają na wywołującego. Przekazywanie przez referencję (int&) sprawia, że parametr jest aliasem zmiennej wywołującego, więc zmiany są widoczne na zewnątrz. Użyj void f(int x), aby skopiować, i void f(int& x), aby zmienić oryginał.

Kiedy używać parametru będącego referencją const w C++?

Używaj const T&, gdy chcesz odczytać duży obiekt bez kopiowania i bez pozwalania funkcji na jego modyfikację, na przykład void print(const string& s). Dostajesz szybkość przekazywania przez referencję z bezpieczeństwem przekazywania przez wartość. Dla małych typów, takich jak int czy char, zwykłe przekazywanie przez wartość jest równie szybkie.

Czym są argumenty domyślne w C++?

Argumenty domyślne pozwalają parametrowi przyjąć wartość zastępczą, gdy wywołujący ją pominie, np. void greet(string name = "there"). Wartości domyślne muszą mieć parametry końcowe (ostatnie na liście), a podajesz je tylko w deklaracji, a nie w definicji, jeśli są one rozdzielone.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ