« Wskaźniki, lekcja »
Dokument opisuje co to są wskaźniki oraz pokazuje jak się z nich korzysta. (lekcja)
Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Zarejestruj się!
Autor: Piotr Szawdyński
Późniejsze modyfikacje: 'Dante'
Kurs C++

Wskaźniki

[lekcja] Dokument opisuje co to są wskaźniki oraz pokazuje jak się z nich korzysta.

Odczytywanie adresu pamięci istniejących zmiennych

Język C++ w bardzo łatwy sposób umożliwia nam pobieranie adresu pamięci wybranych zmiennych. Wskaźnik zajmuje zazwyczaj 4 bajty bez względu na jaki typ danych wskazuje. Rozmiar wskaźnika może być jednak różny w zależności od użytego kompilatora (np. gdy użyjemy 64 bitowego kompilatora). Wskaźnik zwraca adres pierwszego bajta danych wybranej zmiennej. Aby pobrać adres dowolnej zmiennej wystarczy napisać: &nazwa_zmiennej.
C/C++
#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
    int zmienna1 = 213;
    int tablica[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    struct
    {
        int liczba;
        long long duzaLiczba;
    } struktura;
    cout << "Adres zmienna1=" <<& zmienna1 << endl << endl;
    cout << "Adres tablica=" <<& tablica << endl;
    cout << "Adres tablica[0]=" <<& tablica[ 0 ] << endl;
    cout << "Adres tablica[1]=" <<& tablica[ 1 ] << endl << endl;
    cout << "Adres struktura=" <<& struktura << endl;
    cout << "Adres struktura.liczba=" <<&( struktura.liczba ) << endl;
    cout << "Adres struktura.duzaLiczba=" <<&( struktura.duzaLiczba ) << endl;
    getch();
    return( 0 );
}
Zauważmy, że wskaźnik ze zmiennej tablica i ze zmiennej tablica[0] jest taki sam. Dzieje się tak dlatego, że wskaźnik ze zmiennej tablica wskazuje na początek wszystkich danych w tablicy, a pierwszym elementem jest tablica[0]. To samo dotyczy adresu zmiennej struktura i struktura.liczba. Adresy zmiennych są wyświetlane w postaci szesnastkowej.

Wskaźniki pierwsze spojrzenie.

C/C++
//Wskaźniki pierwsze spojrzenie----------
#include <iostream>
#include <conio.h>
int main()
{
    using namespace std;
   
    int liczba = 9;
    int * wsk_liczba; /*--deklaracja wskaźnika
    na int--*/
    wsk_liczba = & liczba; /* przypisanie wskaźnikowi
    adresu int */
    //dwa sposoby wyświetlenia wartości liczba
    cout << "Zmienna liczba = " << liczba
    << "\ni *wsk_liczba jako zmienna liczba = "
    << * wsk_liczba
    << endl;
    //dwa sposoby wyświetlenia adresu zmiennej
    cout << "Adres liczby = " << & liczba
    << "\ni wsk_liczba jako adres liczby = "
    << wsk_liczba
    << endl;
    //zmiana wartości za pomocą wskaźnika
    * wsk_liczba = * wsk_liczba + 1;
    cout << "Liczba = " << liczba;
   
    getch();
    return 0;
}
//---------------------------------------
Deklaracja zmiennej wskaźnikowej jest również prosta. Aby utworzyć zmienną wskaźnikową, to po typie zmiennej dopisujemy *(gwiazdkę). Tak więc, jeśli chcemy utworzyć wskaźnik, który ma wskazywać na liczbę typu int, zapis ten będzie wyglądał tak:
C/C++
Stary zapis C
// int *wsk_liczba;
Nowy zapis C++
//int* liczba, liczba1
gdzie dokładnie znajduje się *(gwiazdka) dla kompilatora nie ma znaczenia można nawet zapisać w ten sposób:
C/C++
//int * liczba
Jakiego Ty będziesz używał zapisu zależy od Ciebie, uważaj jednak na taki zapis:
C/C++
//int* liczba, liczba1
w ten sposób deklarujesz jeden wskaźnik liczba i jeden zmiennej typu int liczba1. Ważne !!! zmienna, która ma być wskaźnikiem musi zawierać gwiazdkę.

Wyświetlanie adresu wskaźnika

Jeśli wypiszemy teraz wartość zmiennej wskaźnik, otrzymamy liczbę wyświetloną szesnastkowo.
C/C++
#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
    long long zmienna = 213;
    long long * wskaznik =& zmienna;
    cout << "&zmienna=" <<& zmienna << endl;
    cout << "wskaznik=" << wskaznik << endl;
    getch();
    return( 0 );
}
Jak pokazuje przykład i jak można było się tego spodziewać, wartość adresu wskaźnika jest taka, jaką do niego przypisaliśmy(long long* wskaznik=&zmienna;).

Wyświetlanie danych, na które wskazuje adres wskaźnika

Aby wyświetlić dane jakie znajdują się pod adresem jaki mamy zapisany we wskaźniku, musimy przed nazwą zmiennej dopisać *. Tak więc, modyfikując poprzedni program, będzie to wyglądało tak:
C/C++
#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
    long long zmienna = 213;
    long long * wskaznik =& zmienna;
    cout << "zmienna=" << zmienna << endl;
    cout << "*wskaznik=" <<* wskaznik << endl;
    getch();
    return( 0 );
}

Modyfikacja danych, na które wskazuje wskaźnik

Mając zapisany adres do zmiennej we wskaźniku, mamy możliwość zmiany wartości zmiennej nie używając nazwy zmiennej, z której pobraliśmy adres. Przykład:
C/C++
#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
    long long zmienna = 213;
    long long * wskaznik =& zmienna;
    cout << "zmienna=" << zmienna << endl;
    * wskaznik = 50;
    cout << "zmienna=" << zmienna << endl;
    getch();
    return( 0 );
}

Bezpieczeństwo a użycie wskaźników.

Podczas używania wskaźników można popełnić błąd przy tworzeniu i używaniu wskaźnika. Jeżeli nie zadbamy o przypisanie wskaźnikowi adresu.
C/C++
int * wsk_liczba;
* wsk_liczba = 373;
Jest to błąd, ponieważ nie wiemy gdzie(pod jakim adresem) umieszczona jest wartość 373. Kompilator będzie starał się umieścić wartość 373 bo takim właśnie adresem. Co jeżeli jednak adres ten będzie już zajęty przez program? Taka sytuacja może spowoduje, iż nie będzie można było zapisać nic w miejsce wskazane przez wsk_liczba. Błąd ten jest nie dopuszczalny i do tego trudno go wykryć. Pamiętaj gdy używasz wskaźnika zadbaj o to by zawsze miał przypisaną zmienną(miał prawidłowy adres).

Arytmetyka wskaźników.

Wskaźniki posiadają pewne podobieństwo z nazwami tablic. Wywodzi się to z arytmetyki wskaźników i sposobie przedstawienia tablic w C++. Przykład pokazuje wspomnianą analogię:
C/C++
//Wskaźniki drugie starcie---------------
#include <iostream>
#include <conio.h>
int main()
{
    using namespace std;
    //tablice deklaracia inicjalizacja
    double waga[ 5 ] = { 55.3, 747.8, 1001.2, 5.2, 6.4 };
    short odliczanie[ 4 ] = { 3, 2, 1, 0 };
    //wskaźniki
    double * wsk_waga = waga; //nazwa tabeli = adres
    short * wsk_odliczanie = & odliczanie[ 0 ];
   
    //Wyświetlanie adresu i wartości wskaźnika wsk_waga
    cout << "wsk_waga = " << wsk_waga
    << ", *wsk_waga = " << * wsk_waga
    << endl
    << "Dodawanie wsk_waga + 1 ";
    wsk_waga += 1;
    cout << "\nTeraz wsk_waga = " << wsk_waga
    << ", *wsk_waga = " << * wsk_waga << endl
    << endl;
    //Wyświetlanie adresu i wartości wskaźnika wsk_odliczanie
    cout << "wsk_odliczanie = " << wsk_odliczanie
    << ", *wsk_odliczanie = " << * wsk_odliczanie
    << endl
    << "Dodawanie wsk_odliczanie + 1 ";
    wsk_odliczanie += 1;
    cout << "\nTeraz wsk_odliczanie = " << wsk_odliczanie
    << ", *wsk_odliczanie = " << * wsk_odliczanie << endl
    << endl;
   
    //Wyświetlanie zapisu tablicowego
    cout << "\nPodobienstwa tablic i wskaznikow\n"
    << "Pierwszy element tab waga[0] = "
    << waga[ 0 ] << endl
    << "Drugi element tab odliczanie[1] = "
    << odliczanie[ 1 ] << endl << endl;
   
    //Wyświetlanie zapisu wskaźnikowego
    cout << "Pierwszy element tab waga "
    "z uzyciem wskaznika *waga = "
    << * waga << endl
    << "Drugi element tab odliczanie "
    "z uzyciem wskaznika *(odliczanie + 1) = "
    << *( odliczanie + 1 ) << endl << endl << endl;
   
    //porównanie wielkości tablic i wskaźników
    cout << "Tablica waga wazy " << sizeof( waga )
    << " bajtow!" << endl
    << "Jednak wskaznik na ta tablice *wsk_waga "
    << "wazy tylko " << sizeof( wsk_waga )
    << " bajty!" << endl;
   
    getch();
    return 0;
}
//---------------------------------------
Pierwsza część programu wyświetlenia adresy wskaźników *wsk_waga i *wsk_odliczanie, oraz wartości zapisane pod adresami na które wskazują. Następnie dodajemy 1 do obu wskaźników, co powoduje przesunięcie ich adresów o 8 bajtów dla wsk_waga(ponieważ typ double to 8 bajtów) i 2 bajty dla wsk_odliczanie(typ short to 2 bajty). Przesunięcie powoduje, iż oba wskaźniki wskazują na adres drugiej wartość w obu tablicach. Wniosek dodanie do wskaźnika 1 powoduje jego przesunięcie o tyle bajtów ile ma wskazany typ danych.

Kolejna część programu pokazuje zależności między tablicą a wskaźnikiem. Dlaczego zapisy waga[0] = *waga, oraz odliczanie[1] = *(odliczanie + 1) dają ten sam rezultat? Ponieważ tak działa kompilator C++, zamienia on zapis tab[10] dla bardziej czytelny sobie zapis *(tab + 10). Stąd właśnie wskaźniki i nazwy tablic można używać zamiennie. Różnice między tab., a wsk. to:
C/C++
NazwaWska ź nik = NazwaWska ź nika + 1; //prawidłowo
NazwaTabel = NazwaTabeli + 1; // błąd !!! 
oraz różnią się wielkością, którą możesz sprawdzić sam stosując operator sizeof.

Dostęp do danych struktury za pośrednictwem wskaźnika

Jeśli chcemy odczytać lub zapisać dane do struktury za pomocą wskaźnika wskazującego na nią, postępujemy prawie tak samo jak w przypadku zwykłej zmiennej - poprzedzamy wskaźnik znakiem *. Wskaźnik ten musimy jednak umieścić w okrągłe nawiasy, żeby kompilator wiedział czego się tyczy symbol *. Kolejny przykład:
C/C++
#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
    struct daneST
    {
        int liczba;
        char znak;
    };
    daneST dane;
    dane.liczba = 55;
    dane.znak = 'a';
    daneST * wskaznik =& dane;
    cout << "(*wskaznik).liczba=" <<( * wskaznik ).liczba << endl;
    ( * wskaznik ).liczba = 99;
    cout << "dane.liczba=" << dane.liczba << endl;
    getch();
    return( 0 );
}

Wskaźniki i struktury po raz drugi

Oprócz przedstawionej wyżej metody uzyskiwania dostępu do danych istnieje również drugi, który jest równoważny pierwszemu. Jest on moim zdaniem wygodniejszy w użyciu, jednak chciałem pokazać Ci różne zapisy ponieważ starsi programiści, którzy 'przesiedli' się z C na C++ korzystają zazwyczaj z pierwszego zapisu. Zamiast poprzedzać zmienną wskaźnikową gwiazdką i wstawiać ją w nawiasy, wystarczy kropkę zastąpić takim zapisem: ->. Przykład z poprzedniego podrozdziału ze zmodyfikowanym zapisem przedstawiam poniżej.
C/C++
#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
    struct daneST
    {
        int liczba;
        char znak;
    };
    daneST dane;
    dane.liczba = 344;
    dane.znak = 'a';
    daneST * wskaznik =& dane;
    cout << "wskaznik->liczba=" << wskaznik->liczba << endl;
    wskaznik->liczba = 221;
    cout << "dane.liczba=" << dane.liczba << endl;
    getch();
    return( 0 );
}

Podsumowanie

Przeanalizuj dokładnie cały materiał, jaki znalazł się w tym rozdziale. Dobra znajomość całej teorii o wskaźnikach będzie niezbędna, gdy dojdziesz do rozdziału poświęconemu dynamicznemu zarządzaniu pamięcią.

Ćwiczenia


1. Popraw błędy w następującym kodzie:
C/C++
//Wskaźniki pierwsze zadanie----------
#include <iostream>
#include <conio.h>
int main()
{
    using namespace std;
   
    short zmienna = 213;
    short long * wskaznik = zmienna;
   
    //Wyświetlanie adresu wskaźnika
    cout << "&zmienna=" << zmienna << endl
    cout << "wskaznik=" << wskaznik << endl;
    //Wyświetlanie danych, na które wskazuje adres wskaźnika
    cout "Adres zmienna=" << * zmienna << endl;
    cout << "*wskaznik=" << wskaznik
    //Modyfikacja danych, na które wskazuje wskaźnik
    cout << "zmienna=" << zmiena << endl;
    * wskaznik = & 50;
    cout << "zmienna=" << zienna << endl;
   
    getch();
    return 0;
}
//---------------------------------------
Poprzedni dokumentNastępny dokument
Struktury danychObsługa plików