Przekazywanie danych przez wskaźniki i referencje

Kurs: Wstęp do programowania
Lekcja 7: Wskaźniki i pamięć dynamiczna
Temat 2: Przekazywanie danych przez wskaźniki i referencje

⇓ spis treści ⇓


Przekazywanie danych do funkcji jest kluczową częścią programowania. W językach takich jak C i C++ można to robić na różne sposoby: przez wartość, przez wskaźnik lub przez referencję. Wybór metody ma ogromny wpływ na wydajność oraz sposób, w jaki funkcje modyfikują dane. W tej lekcji skupimy się na szczegółowym omówieniu przekazywania danych przez wskaźniki i referencje, aby zrozumieć, kiedy i dlaczego należy używać każdej z tych technik.

Przekazywanie danych przez wartość

Zanim przejdziemy do omawiania wskaźników i referencji, warto przypomnieć, czym jest przekazywanie danych przez wartość. Gdy dane są przekazywane przez wartość, kopia zmiennej jest tworzona i przekazywana do funkcji. Wszelkie zmiany dokonywane na kopii wewnątrz funkcji nie mają wpływu na oryginalną zmienną. Przykład:

#include <iostream>

void zmienWartosc(int liczba) {
    liczba = 100; // Modyfikacja kopii zmiennej
}

int main() {
    int x = 5;
    zmienWartosc(x);
    std::cout << "Wartość x: " << x << std::endl; // x nadal wynosi 5
    return 0;
}

W tym przykładzie zmienna x nie zostaje zmieniona, ponieważ funkcja zmienWartosc operuje na kopii zmiennej. Teraz zobaczmy, jak różni się to od przekazywania przez wskaźniki i referencje.

Przekazywanie danych przez wskaźniki

Przekazywanie danych przez wskaźniki polega na przekazaniu adresu zmiennej do funkcji, co umożliwia bezpośrednią modyfikację wartości zmiennej w funkcji wywołującej. Przykład:

#include <iostream>

void zmienWartosc(int *liczba) {
    *liczba = 100; // Modyfikacja wartości poprzez dereferencję wskaźnika
}

int main() {
    int x = 5;
    zmienWartosc(&x); // Przekazujemy adres zmiennej x
    std::cout << "Wartość x: " << x << std::endl; // x wynosi teraz 100
    return 0;
}

W tym przypadku zmienna x zostaje zmieniona, ponieważ przekazujemy jej adres do funkcji zmienWartosc. Funkcja modyfikuje oryginalną wartość zmiennej za pomocą dereferencji wskaźnika.

Zastosowanie przekazywania danych przez wskaźniki

Przekazywanie danych przez wskaźniki jest szczególnie przydatne, gdy chcemy:

  • Modyfikować oryginalne zmienne w funkcji wywołującej.
  • Unikać kopiowania dużych struktur danych, co może być kosztowne pod względem wydajności.
  • Przekazywać tablice do funkcji, ponieważ nazwa tablicy to wskaźnik na jej pierwszy element.

Przykład przekazywania tablicy do funkcji:

#include <iostream>

void wypiszTablice(int *tablica, int rozmiar) {
    for (int i = 0; i < rozmiar; ++i) {
        std::cout << "Element " << i << ": " << tablica[i] << std::endl;
    }
}

int main() {
    int tablica[] = {1, 2, 3, 4, 5};
    wypiszTablice(tablica, 5); // Przekazujemy wskaźnik na pierwszy element tablicy
    return 0;
}

W funkcji wypiszTablice przekazujemy wskaźnik na tablicę i rozmiar tablicy, co pozwala na iterację po jej elementach.

Przekazywanie danych przez referencje

Przekazywanie danych przez referencje jest bardziej intuicyjne i bezpieczniejsze niż przekazywanie przez wskaźniki. Referencja to alias (inna nazwa) dla istniejącej zmiennej. Wszelkie zmiany dokonane na referencji wpływają bezpośrednio na oryginalną zmienną. Przykład:

#include <iostream>

void zmienWartosc(int &liczba) {
    liczba = 100; // Modyfikacja zmiennej poprzez referencję
}

int main() {
    int x = 5;
    zmienWartosc(x); // Przekazujemy zmienną przez referencję
    std::cout << "Wartość x: " << x << std::endl; // x wynosi teraz 100
    return 0;
}

W tym przypadku zmienna x zostaje zmieniona, ponieważ przekazujemy ją przez referencję. Referencja jest często preferowana w C++ ze względu na prostszą składnię i mniejsze ryzyko błędów w porównaniu z wskaźnikami.

Zastosowanie przekazywania danych przez referencje

Przekazywanie danych przez referencje jest użyteczne w sytuacjach, gdy chcemy:

  • Modyfikować oryginalne zmienne w funkcji wywołującej bez użycia wskaźników.
  • Zwiększyć czytelność i bezpieczeństwo kodu, ponieważ referencje nie mogą być nullptr.
  • Przekazywać duże obiekty lub struktury, aby uniknąć kosztownego kopiowania danych.

Przykład przekazywania dużego obiektu przez referencję:

#include <iostream>
#include <string>

class Osoba {
public:
    std::string imie;
    int wiek;
    
    Osoba(const std::string &imie, int wiek) : imie(imie), wiek(wiek) {}
};

void wypiszOsobe(const Osoba &osoba) {
    std::cout << "Imię: " << osoba.imie << ", Wiek: " << osoba.wiek << std::endl;
}

int main() {
    Osoba jan("Jan", 30);
    wypiszOsobe(jan); // Przekazujemy obiekt przez referencję
    return 0;
}

W tym przykładzie przekazujemy obiekt Osoba przez referencję, aby uniknąć kosztownego kopiowania danych. Użycie const zapewnia, że obiekt nie zostanie zmodyfikowany wewnątrz funkcji.

Przekazywanie danych przez wskaźniki vs. referencje

Przekazywanie danych przez wskaźniki i referencje ma swoje wady i zalety. Oto niektóre z nich:

  • Wskaźniki: Dają większą kontrolę nad pamięcią, ale mogą prowadzić do błędów, takich jak wskaźniki zerowe czy wiszące.
  • Referencje: Są prostsze i bezpieczniejsze w użyciu, ale nie można ich zmieniać, aby wskazywały na inną zmienną po ich inicjalizacji.

Wskaźniki są bardziej elastyczne, ponieważ można zmieniać, na co wskazują, oraz mogą wskazywać na nullptr, co jest przydatne w niektórych sytuacjach, na przykład przy implementacji struktur danych. Referencje są natomiast preferowane w C++ do przekazywania danych do funkcji, gdy nie potrzebujemy zmieniać wskaźnika na inny adres.

Przykład zaawansowanego użycia wskaźników i referencji

Oto bardziej złożony przykład, w którym pokazujemy użycie wskaźników i referencji w kontekście dynamicznej alokacji pamięci oraz funkcji modyfikujących dane:

#include <iostream>
#include <memory>

void zmienWartosc(std::unique_ptr<int> &ptr) {
    *ptr = 200; // Modyfikacja wartości poprzez referencję do smart pointera
}

int main() {
    std::unique_ptr<int> ptr = std::make_unique<int>(50);
    std::cout << "Wartość przed: " << *ptr << std::endl;
    zmienWartosc(ptr);
    std::cout << "Wartość po: " << *ptr << std::endl;
    return 0;
}

W tym przykładzie używamy inteligentnego wskaźnika std::unique_ptr i przekazujemy go do funkcji przez referencję, aby zmodyfikować wartość, na którą wskazuje. To pokazuje, jak wskaźniki i referencje mogą być łączone w nowoczesnym C++.

Podsumowanie

Przekazywanie danych przez wskaźniki i referencje to podstawowe techniki, które mają ogromne znaczenie w programowaniu. Wskaźniki dają dużą kontrolę nad pamięcią i umożliwiają tworzenie dynamicznych struktur danych, ale ich nieprawidłowe użycie może prowadzić do trudnych do wykrycia błędów. Referencje są łatwiejsze w użyciu i bezpieczniejsze, ale nie oferują tej samej elastyczności co wskaźniki.

Zrozumienie, kiedy używać wskaźników, a kiedy referencji, jest kluczowe dla pisania wydajnego i bezpiecznego kodu. W przypadku prostych operacji preferowane są referencje, podczas gdy wskaźniki są niezbędne przy dynamicznej alokacji pamięci i bardziej zaawansowanych strukturach danych. Opanowanie tych koncepcji pozwala na efektywne zarządzanie pamięcią oraz optymalizację kodu, co jest szczególnie ważne w aplikacjach o wysokiej wydajności.

Następny temat ==> Zmienne globalne kontra lokalne



Spis Treści - Wstęp do programowania

Lekcja 3: Rozwiązywanie problemów i poprawność programów Lekcja 4: Praca z różnymi typami danych Lekcja 5: Obsługa plików i pamięci Lekcja 6: Zaawansowane techniki programistyczne Lekcja 7: Wskaźniki i pamięć dynamiczna Lekcja 8: Struktura kodu i abstrakcja Lekcja 9: Rekurencja i jej zastosowania Lekcja 10: Analiza wydajności algorytmów Lekcja 11: Technika "dziel i zwyciężaj" Lekcja 12: Struktury danych o dynamicznej budowie Lekcja 13: Struktury hierarchiczne: Drzewa Lekcja 14: Struktury danych z bibliotek Lekcja 15: Algorytmy z nawrotami Lekcja 16: Programowanie dynamiczne Lekcja 17: Programowanie zachłanne Lekcja 18: Praca z grafami

Jeśli chciałbyś być poinformowany o następnych kursach to zapisz się do naszego newslettera: