Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: pekfos
Kurs C++

Wskaźniki

[lekcja] Rozdział 43. W rozdziale opisano, jak tworzyć wskaźniki i jak nimi się posługiwać.

Wstęp

Doświadczenie pokazuje, że wskaźniki należą do najtrudniejszych rzeczy, jakie początkujący programista powinien opanować. Wiele rzeczy byłoby niemożliwych do osiągnięcia, gdyby nie one, dlatego poprawne zrozumienie tej lekcji jest bardzo istotne, chociaż może nie być najłatwiejsze.

Co to jest wskaźnik

Wskaźnik, jak sama nazwa wskazuje, wskazuje na coś. Dokładniej, wskazuje na pewną lokalizację w pamięci. Zmienne, które dobrze znasz, są przechowywane w pamięci i mają tam swoje adresy. W końcu komputer musi jakoś odnosić się do różnych lokalizacji w pamięci i rozróżniać je między sobą. Wskaźnik jest zmienną, która przechowuje adres innej zmiennej.
Jeśli chcemy wskazywać na zmienną typu int, zmienna z jej adresem będzie miała typ int*. Dopisanie gwiazdki po typie X tworzy typ wskaźnika, wskazujący na element typu X.
C/C++
int * wskaznikInt1 = 0;
int * wskaznikInt2 = NULL;
int * wskaznikInt3 = nullptr; // C++11
Powyższy fragment tworzy 3 wskaźniki na int, wszystkie zainicjalizowane zerem. Zero oznacza, że wskaźnik jest pusty. Pusty wskaźnik nie wskazuje na żadną lokalizację, w przeciwieństwie do wskaźnika niezainicjalizowanego - ten może wskazywać na przypadkową lokalizację i tego należy unikać, bo nie da się rozróżnić takiego wskaźnika od poprawnego.
C/C++
int * wskaznik, zmienna;
W powyższym kodzie, tylko wskaznik jest typu int*, zmienna to już zwykły int. Dzieje się tak dlatego, że gwiazdka jest tak naprawdę związana z nazwą zmiennej, a nie nazwą typu. Jeśli chcesz utworzyć kilka wskaźników za jednym zamachem, musisz napisać gwiazdkę przed każdą nazwą, która ma być wskaźnikiem:
C/C++
int * wskaznik1, * wskaznik2;

Odczytywanie adresu zmiennej

Z pustego wskaźnika i Salomon nie naleje. Jak otrzymać adres jakiejś faktycznej, nazwanej zmiennej z programu? Służy do tego operator & postawiony przed nazwą zmiennej, której adres chcemy otrzymać.
C/C++
int zmienna;
int * wskaznik = & zmienna;
I co teraz z takim wskaźnikiem można zrobić? Na chwilę obecną musimy zadowolić się wyświetleniem go:
C/C++
int zmienna = 123;
int * wskaznik = & zmienna;
int tablica[ 2 ] = { 10, 20 };

std::cout << "Wartosc: " << zmienna << ", Adres: " << wskaznik << ", Adres wskaznika: " << & wskaznik << std::endl;
std::cout << & tablica[ 0 ] << '\n' << & tablica[ 1 ] << std::endl;
Wartosc: 123, Adres: 0x72fe3c, Adres wskaznika: 0x72fe30
0x72fe20
0x72fe24
Wskaźnik też jest zmienną, więc można dostać się też do jego adresu i nawet utworzyć wskaźnik na niego. Wszystko według omówionej już reguły: wskaźnik na int* jest typu int**.
Nie przejmuj się, jeśli u Ciebie ten kod wypisze inne adresy. Ich faktyczne wartości zależą od różnych rzeczy, więc nigdy nie próbuj ich zgadywać i wpisywać do programu ręcznie. Z tego wyjścia można wyczytać jeszcze kilka ciekawostek: int ma na tej maszynie 4 bajty, widać to po tym, że tyle właśnie miejsca mieści się między adresami pierwszego i drugiego elementu tablicy. Trzeci element tablicy byłby pod adresem 0x72fe28. Z tego wynika, że wskaznik i tablica[4], chociaż niepoprawne, mają ten sam adres. Jeśli kiedyś się zastanawiałeś, czemu wyjście poza zakres tablicy, ale tylko trochę, nie powoduje wysypania się całej aplikacji, to masz tu właśnie odpowiedź.

Używanie zmiennej przez wskaźnik

Co ostatecznie po tym adresie zmiennej, jeśli nie można nic z nim zrobić? Mając adres zmiennej, można się do niej dobrać operatorem dereferencji, czyli dość znajomo wyglądającą gwiazdką przed nazwą wskaźnika:
C/C++
int zmienna = 123;
int * wskaznik = & zmienna;

std::cout << zmienna << std::endl;
std::cout << wskaznik << std::endl;
std::cout << * wskaznik << std::endl;
123
0x72fe44
123
Z *wskaznik można zrobić wszystko to, co można zrobić ze zmienną zmienna i można ich używać zamiennie. Można powiedzieć, że jedno jest aliasem drugiego:
C/C++
std::cout << zmienna << " = " << * wskaznik << std::endl;
zmienna = 42;
std::cout << zmienna << " = " << * wskaznik << std::endl;
* wskaznik = 357;
std::cout << zmienna << " = " << * wskaznik << std::endl;
123 = 123
42 = 42
357 = 357

Arytmetyka na wskaźnikach

Wspomniałem przy wypisywaniu wskaźników, że elementy w tablicy zajmują ciągły obszar pamięci. Adres elementu 0 powiększony o rozmiar elementu, to adres elementu 1. Wtedy, patrząc na te wartości wskaźników, te obliczenia przeprowadzaliśmy w pamięci. W C++ w dokładnie ten sam sposób można używać tablic, a nawet prościej, bo nie musimy znać rozmiaru elementu:
C/C++
int tablica[ 2 ] = { 1, 2 };

std::cout << & tablica[ 0 ] << std::endl;
std::cout << & tablica[ 0 ] + 1 << std::endl;
0x72fe40
0x72fe44
Pomimo tego, że w kodzie napisane jest "powiększ o 1", do adresu został dodany rozmiar zmiennej int. "Powiększ o 1 element", nie "Powiększ o 1 bajt". Dodatkowo tablica, chociaż nie jest wskaźnikiem na swój pierwszy element, jest konwertowalna na taki wskaźnik, więc powyższy program możemy zapisać krócej:
C/C++
int tablica[ 2 ] = { 1, 2 };

std::cout << tablica << std::endl;
std::cout << tablica + 1 << std::endl;
Operator indeksowania [] w praktyce też jest dereferencją. Poniższe 2 zapisy są równoważne:
C/C++
std::cout << tablica[ 1 ] << std::endl;
std::cout << *( tablica + 1 ) << std::endl;
Powyższą równoważność można pociągnąć również w drugą stronę i operator [] stosować na wskaźniku:
C/C++
int tablica[ 2 ] = { 1, 2 };
int * wskaznikTab = tablica;

std::cout << wskaznikTab[ 1 ] << std::endl;

Odejmowanie wskaźników

Dwóch wskaźników nie można do siebie dodać, ale można je od siebie odjąć:
C/C++
int tablica[ 2 ] = { 1, 2 };
int * wskBegin = tablica;
int * wskEnd = tablica + 2;

std::cout << wskBegin << '\n' << wskEnd << std::endl;
std::cout << wskEnd - wskBegin << std::endl;
0x72fe30
0x72fe38
2
Program obliczył rozmiar tablicy, mając do dyspozycji parę wskaźników: wskaźnik na początek i wskaźnik na element za końcem. Tak określone 2 wskaźniki zobaczysz jeszcze wiele razy ;)
Odejmowanie wskaźników ma sens tylko wtedy, gdy oba wskazują na elementy jednej tablicy. W przeciwnym razie, wynik nie musi mieć sensu nawet matematycznie!

Wskaźniki char*, const char*

Wskaźniki na typy znakowe mają specjalne znaczenie w C i C++. Przyjmuje się, że te wskaźniki wskazują na tablicę znaków przechowującą tekst i zakończoną zerem. Tzw łańcuch znaków w stylu C:
C/C++
char tab[ 10 ] = { 'a', 's', 'd', 0 };

std::cout << tab << std::endl;
Nie jest to wymuszone, możesz mieć tablicę znaków bez zera na końcu i operować wskaźnikami na taką tablicę, ale musisz uważać, czy nie przekazujesz wskaźnika gdzieś, gdzie taki format danych jest oczekiwany. Przede wszystkim uważaj, gdy przekazujesz gdzieś wskaźnik, ale nie podajesz rozmiaru tablicy. Być może ta funkcja/itp sama będzie chciała ten rozmiar określić szukając zera na końcu danych i wyjdzie poza zakres tablicy, jeśli tego zera tam nie będzie.
C/C++
std::cout << "Hello world" << std::endl;
Jak ten typowy pierwszy program działa, w świetle tego wszystkiego, co przeczytałeś? "Hello world" to tablica znaków, dokładniej, jest to wartość typu const char[12], stała tablica 11 znaków tekstu i zera na końcu. Z tablicy można niejawnie wyciągnąć wskaźnik na pierwszy element, typu const char*, a o specjalnym znaczeniu takiego wskaźnika właśnie się dowiedziałeś.
C/C++
const char * hello = "Hello world";
std::cout << hello << std::endl;

Wskaźniki, a const

const w przypadku zwykłej zmiennej jest proste - tej zmiennej nie można zmieniać. Na taką zmienną można utworzyć wskaźnik, wskaźnik na stałą:
C/C++
const int stala = 123;
const int * wskaznikNaStala = & stala;
Wskaźnik to też zmienna i też może być stały. Jak utworzyć stały wskaźnik? Reguła do tego jest bardzo prosta, identyczna do tej z samego początku lekcji: Przed gwiazdką typ wskazywany, po gwiazdce nazwa wskaźnika. Znaczenie const jest różne w zależności od tego, po której stronie gwiazdki się znajduje:
C/C++
const int * p1; // Wskaźnik na stałą, bo const jest po stronie typu wskazywanego (const int)
int * const p2; // Stały wskaźnik, bo const jest po stronie wskaźnika
const int * const p3; // Stały wskaźnik na stałą, bo const jest wszędzie :)
Zanim postawimy gwiazdkę, mówimy o typie wskazywanym, po gwiazdce, mówimy już o wskaźniku.
Czasem można spotkać const zamienione z typem. Te zapisy są parami równoważne:
C/C++
const int s1 = 1;
int const s2 = 1;

const int * p1;
int const * p2;
Co w przypadku większej ilości gwiazdek?
C/C++
int * const ** const * potwor;
Przez to że T* to "wskaźnik na T" dla dowolnego T, gwiazdki zdejmujemy od prawej i od prawej całość trzeba czytać:
Wskaźnik na (int*const**const) stały wskaźnik na (int*const*) wskaźnik na (int*const) stały wskaźnik na int. Uf..

Przykład

C/C++
#include <iostream>

void generuj( int * begin, int * end, int start )
{
    for( int * p = begin; p != end; ++p )
    {
        * p = start;
        ++start;
    }
}

void wypisz( int * begin, int size )
{
    while( size > 0 )
    {
        std::cout << * begin << ' ';
        ++begin;
        --size;
    }
}

int main()
{
    int tab[ 10 ];
    generuj( tab, tab + 10, 100 );
    wypisz( tab, 10 );
}

Zadanie domowe

Napisz funkcję void sortuj(int* begin, int* end), która dowolną metodą sortuje liczby w tablicy, podanej przez wskaźnik na pierwszy element i element za końcem tablicy. Weź funkcję wypisz() z przykładu i użyj tego main():
C/C++
int main()
{
    int tab[ 10 ] = { 0, 9, 1, 3, 8, 2, 6, 7, 5, 4 };
    sortuj( tab, tab + 10 );
    wypisz( tab, 10 );
   
    std::cout << '\n';
   
    int tab2[ 16 ] = { 9, 7, 8, 6, 5, 4, 4, 0, 9, 6, 7, 1, 6, 3, 1, - 100 };
    sortuj( tab2, tab2 + 15 );
    wypisz( tab2, 15 );
}
Wewnątrz sortuj() postaraj się uniknąć przechodzenia na wskaźnik na początek i ilość elementów. (Wzoruj się na generuj(), nie na wypisz()). Poprawny wynik programu:
0 1 2 3 4 5 6 7 8 9
0 1 1 3 4 4 5 6 6 6 7 7 8 9 9
Podpowiedź
Poprzedni dokument Następny dokument
Poziom 5 Zarządzanie pamięcią new, delete