Wskaźniki

Informacja przechowywana jest w pamięci w postaci bitów umieszczanych w komórkach (ang memory cell), których mogą być miliardy. Aby komputer mógł uzyskiwać w prosty sposób dostęp do każdej komórki pamięci, zostały one ponumerowane. Numery komórek nazywamy adresami komórek pamięci (ang. memory cell address). Poniżej przedstawiamy fragment logicznej struktury pamięci (czyli tak, jak widzi swoją pamięć komputer):

 

Pamięć
Adres Zawartość komórki
0 11000110
1 00001111
2 11000011
3 11111110
4 00000001
5 11100111
... ...

 

W pamięci komputer przechowuje swój program (binarne kody instrukcji procesora) oraz dane, które są przez ten program przetwarzane. Jedna komórka pamięci może przechowywać 8 bitów informacji. W takiej komórce mieści się np. zmienna typu char lub bool. Zmienne innych typów wymagają zwykle więcej komórek pamięci (poniższe dane dotyczą Ming32):

 

Typ liczba bitów Liczba komórek
char 8 1
bool 8 1
short 16 2
int 32 4
long long 64 8
float 32 4
double 64 8
long double 80 10

 

Każda zmienna znajdująca się w pamięci komputera posiada unikalny adres, czyli numer pierwszej komórki, od której rozpoczyna się obszar pamięci zajęty przez zmienną.

 

// Adresy zmiennych
// (C)2011 ILO w Tarnowie
// KOŁO INFORMATYCZNE
//-----------------------

#include <iostream>

using namespace std;


int main()
{
    int a,b;

    cout << "Adres a = " << & a << endl
         << "Adres b = " << & b << endl;

    return 0;
}

 

Powyższy program tworzy dwie zmienne a i b. Następnie przesyła do strumienia cout adresy tych zmiennych. Dostęp do adresu zmiennej uzyskujemy w C++ za pomocą operatora &:

 

& zmienna

 

Nie myl operatora adresu & z operatorem bitowej koniunkcji &. Wyglądają tak samo, lecz pierwszy jest zawsze jednoargumentowy, a drugi dwuargumentowy. Dodatkowo argumentem operatora adresu może być tylko obiekt pamięciowy, czyli taki, który posiada adres (zmienna, element tablicy). Argumentami operatora koniunkcji bitowej mogą być dowolne wyrażenia.

 

Operator adresu zmiennej Operator koniunkcji bitowej
& zmienna wyrażenie1 & wyrażenie2

 

Strumień cout wyświetla adresy zawsze w postaci liczb szesnastkowych. Zwróć uwagę, iż w pamięci komputera zmienne a i b znajdują się w porządku odwrotnym: najpierw b, później a. To taka ciekawostka. Nie licz na tą cechę na innych platformach sprzętowych.

Zmienna przechowująca adres innej zmiennej nazywa się w języku C++ wskaźnikiem (ang. pointer). Wskaźnik tworzymy następująco:

 

typ * wskaźnik;

 

typ - określa rodzaj wskazywanego obiektu

* - oznacza, że jest to wskaźnik
wskaźnik - będzie przechowywał adres obiektu o danym typie

 

Wskaźnik jest zmienną. Możemy jej przypisać wartość - np. adres innej zmiennej:

 

wskaźnik = & zmienna;

 

Odwołanie do zawartości wskazywanego obiektu udostępnia nam operator * (nie myl go z mnożeniem - patrz powyżej, operator adresu &):

 

* wskaźnik

 

Zawartość wskazywanego obiektu możemy zmieniać następującą instrukcją przypisania:

 

* wskaźnik = wyrażenie;

 

// Wskaźniki
// (C)2011 ILO w Tarnowie
// KOŁO INFORMATYCZNE
//-----------------------

#include <iostream>

using namespace std;


int main()
{
    int a, b; // to są zwykłe zmienne
    int * p;  // to jest wskaźnik do danych typu int

    // ustawiamy w p adres zmiennej a

    p = & a;

    // pod tym adresem umieszczamy liczbę 10

    * p = 10;

    // teraz zmieniamy adres w p na adres zmiennej b

    p = & b;

    // umieszczamy pod tym adresem liczbę 25

    * p = 25;

    cout << "a = " << a << " b = " <<  b << endl;

    return 0;
}

 

Zwróć uwagę, iż dane wstawiliśmy do zmiennych a i b za pomocą wskaźnika p. Następny przykład demonstruje dostęp do danych za pomocą wskaźnika.

 

// Wskaźniki
// (C)2011 ILO w Tarnowie
// KOŁO INFORMATYCZNE
//-----------------------

#include <iostream>

using namespace std;


int main()
{
    int a, b, * p1, * p2;

    // w zmiennych a i b umieszczamy jakieś liczby

    a = 10; b = 25;

    // wskaźniki ustawiamy na a i b

    p1 = & a;  p2 = & b;

    cout << "a = " << * p1 << " b = " << * p2 << endl;

    return 0;
}

 

Język C++ jest bardzo konsekwentny przy operacji na wskaźnikach. Przypisanie:

 

wskaźnik1 = wskaźnik2;

 

jest możliwe tylko wtedy, gdy oba wskaźniki wskazują obiekty tego samego typu. Jeśli wskaźniki są różnego typu, to zwykle takie przypisanie nie ma większego sensu (jeśli ma sens dla programisty, to stosuje on rzutowanie, ale o takich technikach dowiemy się później). Poniższy program tworzy dwa wskaźniki do tego samego obiektu:

 

// Wskaźniki
// (C)2011 ILO w Tarnowie
// KOŁO INFORMATYCZNE
//-----------------------

#include <iostream>

using namespace std;


int main()
{
    int a, * p1, * p2;

    // wskaźnik p1 ustawiamy na adres zmiennej a

    p1 = & a;

    // adres ten kopiujemy do wskaźnika p2

    p2 = p1;

    // w zmiennej a umieszczamy jakąś liczbę poprzez wskaźnik p1

    * p1 = 25;

    // wyświetlamy zawartość zmiennej a poprzez wskaźnik p2

    cout << "a = " << * p2 << endl;

    return 0;
}

 

Wskaźniki do tablic

Jeśli wskaźnik p wskazuje element tablicy, to mają sens następujące operacje:

 

p ++;  // wskaźnik będzie wskazywał element następny w tablicy
p --;  // wskaźnik będzie wskazywał element poprzedni w tablicy

 

// Wskaźniki i tablice
// (C)2011 ILO w Tarnowie
// KOŁO INFORMATYCZNE
//-----------------------

#include <iostream>

using namespace std;


int main()
{
    int T[] = {5, 7, 9, 11, 2, 3, 6, 0};
    int * p;

    // ustawiamy p na pierwszy element tablicy

    p = & T[0];

    // przechodzimy przez kolejne elementy, aż do elementu 0

    while(* p) cout << * (p++) << " ";

    cout << endl;

    // cofamy się aż do elementu 5

    do cout << * (--p) << " "; while(* p != 5);

    cout << endl;

    return 0;
}

 

wskaźnik + n
wskaźnik - n

 

W obu przypadkach otrzymujemy adres leżący o n obiektów wskazywanego typu (n może być dowolnym wyrażeniem arytmetycznym):

 

wskaźnik + n : n obiektów dalej za adresem wskazywanym przez wskaźnik
wskaźnik - n : n obiektów wskazywanego typu przed adresem wskazywanym przez wskaźnik

 

// Wskaźniki i tablice
// (C)2011 ILO w Tarnowie
// KOŁO INFORMATYCZNE
//-----------------------

#include <iostream>

using namespace std;


int main()
{
    int T[] = {5, 7, 9, 11, 2, 3, 6, 0};
    int * p;

    // ustawiamy p na pierwszy element tablicy

    p = & T[0];

    // wyświetlamy 7 komórek tablicy w przód

    for(int i = 0; i < 7; i++) cout << * (p + i) << " ";

    cout << endl;

    // ustawiamy p na przedostatni element tablicy

    p = & T[6];

    // wyświetlamy 7 komórek tablicy wstecz

    for(int i = 0; i < 7; i++) cout << * (p - i) << " ";

    cout << endl;    

    return 0;
}

 

Notacja równoważna:

 

* (wskaźnik + n)  -->  wskaźnik[n]
* (wskaźnik - n)  -->  wskaźnik[-n]
// Wskaźniki i tablice
// (C)2011 ILO w Tarnowie
// KOŁO INFORMATYCZNE
//-----------------------

#include <iostream>

using namespace std;


int main()
{
    int T[] = {5, 7, 9, 11, 2, 3, 6, 0};
    int * p;

    // ustawiamy p na pierwszy element tablicy

    p = & T[0];

    // wyświetlamy 7 komórek tablicy w przód

    for(int i = 0; i < 7; i++) cout << p[i] << " ";

    cout << endl;

    // ustawiamy p na przedostatni element tablicy

    p = & T[6];

    // wyświetlamy 7 komórek tablicy wstecz

    for(int i = 0; i < 7; i++) cout << p[-i] << " ";

    cout << endl;    

    return 0;
}

 

Z ostatniego przykładu widzimy wyraźnie, iż pomiędzy tablicami i wskaźnikami istnieje duże podobieństwo. W rzeczywistości nazwa tablicy jest wskaźnikiem jej pierwszego elementu, co obrazuje poniższy program:

 

// Wskaźniki i tablice
// (C)2011 ILO w Tarnowie
// KOŁO INFORMATYCZNE
//-----------------------

#include <iostream>

using namespace std;


int main()
{
    int T[] = {5, 7, 9, 11, 2, 3, 6, 0};

    // wyświetlamy tablicę "normalnie"

    for(int i = 0; i < 7; i++) cout << T[i] << " ";

    cout << endl;

    // wyświetlamy tablicę "wskaźnikowo"

    for(int i = 0; i < 7; i++) cout << * (T + i) << " ";

    cout << endl;

    return 0;
}

 

Jedyna różnica jest taka, iż nazwa tablicy jest stałą i nie możemy jej zmieniać w przeciwieństwie do zmiennej będącej wskaźnikiem. Zatem w programach możemy stosować wymiennie zapis:

 

Tablica[indeks]   -->  * (Tablica + indeks)

* (wskaźnik + n)  -->     wskaźnik[n]

 

Tablice dynamiczne

Tablica statyczna jest definiowana w kodzie programu. Określamy wtedy jej typ oraz liczbę elementów. Jednakże zdarza się, iż w trakcie pisania programu nie wiemy dokładnie jaki rozmiar tablicy będzie potrzebny - znamy jedynie rozmiar maksymalny. W takich przypadkach tworzenie tablicy o maksymalnym rozmiarze jest nieefektywnym wykorzystywaniem pamięci komputera. Np. tworzymy tablicę zawierającą 100.000 komórek, z których wykorzystujemy zaledwie 3. Pozostałe 99.997 leży sobie odłogiem i nie może być wykorzystane przez inne procesy.

Z tych powodów powstały tablice dynamiczne - tworzone w trakcie wykonywania programu. Aby utworzyć taką tablicę, należy:

 

Stworzyć wskaźnik do typu, który chcemy użyć na elementy tablicy:

 

typ * T;

 

Przypisać wskaźnikowi adres obszaru pamięci, w którym będą przechowywane elementy tablicy:

 

T = new typ[n];

 

Operator new przydziela pamięć zwracając adres zarezerwowanego obszaru. Wymaga on podania typu elementów, które będą w tym obszarze przechowywane oraz ich liczby. Adres obszaru trafia do przygotowanego wcześniej wskaźnika. Tablica jest gotowa. Możemy odwoływać się do jej elementów za pomocą notacji:

 

T[indeks]
* (T + indeks)

 

Z tak utworzoną tablicą możemy zrobić wszystko to, co ze zwykłą tablicą statyczną. Gdy tablica dynamiczna przestanie nam być potrzebna, to po prostu zwalniamy przydzielony jej obszar pamięci:

 

delete [] T;

 

Zwolniony obszar można wykorzystać do innych celów, np. dla nowej tablicy dynamicznej.

 

Poniższy program prosi użytkownika o określenie rozmiaru tablicy dynamicznej, następnie tworzy taką tablicę i wypełnia kolejnymi wielokrotnościami liczb 2 i 3.

 

// Tablica dynamiczna
// (C)2011 ILO w Tarnowie
// KOŁO INFORMATYCZNE
//-----------------------

#include <iostream>

using namespace std;


int main()
{
    int * T, n, i, w;

    // odczytujemy rozmiar tablicy

    cin >> n;   

    // tworzymy obszar na elementy tablicy i zapamiętujemy
    // jego adres we wskaźniku T

    T = new int[n];

    // tablicę dynamiczną wypełniamy kolejnymi wielokrotnościami  2 i 3

    for(w = 1, i = 0; i < n; i++)
    {
        while((w % 2) && (w % 3)) w++;

        T[i] = w++;
    }

    // wyświetlamy zawartość tablicy

    for(i = 0; i < n; i++) cout << T[i] << " ";

    cout << endl;

    // tablicę usuwamy z pamięci

    delete [] T;

    return 0;
}

 



List do administratora Serwisu Edukacyjnego Nauczycieli I LO

Twój email: (jeśli chcesz otrzymać odpowiedź)
Temat:
Uwaga: ← tutaj wpisz wyraz  ilo , inaczej list zostanie zignorowany

Poniżej wpisz swoje uwagi lub pytania dotyczące tego rozdziału (max. 2048 znaków).

Liczba znaków do wykorzystania: 2048

 

W związku z dużą liczbą listów do naszego serwisu edukacyjnego nie będziemy udzielać odpowiedzi na prośby rozwiązywania zadań, pisania programów zaliczeniowych, przesyłania materiałów czy też tłumaczenia zagadnień szeroko opisywanych w podręcznikach.



   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2017 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.