1. WSKAŹNIKI - PODSTAWY
Wskaźniki, jak sama nazwa wskazuje, mają za zadanie na coś pokazywać. W wypadku języków programowania tym czymś jest fragment pamięci RAM, w którym zapisana jest wartość zmiennej. Co więcej, wskaźnik musi (prawie) zawsze wskazywać na wartość określonego typu (np. int).
Wskaźniki definiujemy poprzez dodanie gwiazdki przed nazwą, np:
int *wskaznik;
Zobaczmy:
char znak = 'M'; // definicja "zwyklej" zmiennej typu char
char *wskaznik; // definicja wskaznika typu char
wskaznik = &znak; // do wskaznika zapisujemy adres w pamieci do zmiennej znak
cout << *wskaznik << endl; // wyswietlenie wartosci zmiennej, na ktora pokazuje wskaznik
cout << wskaznik; // wyswietlenie adresu zmiennej, na ktora pokazuje wskaznik
Wyjaśnijmy sobie, o co chodzi. Aby wpisać do wskaźnika adres do zmiennej znak, skorzystaliśmy z referencji (zapis z & - przypomnij sobie). Jeżeli chcemy w programie odnieść się do wartości zmiennej, na którą pokazuje wskaźnik, musimy dodać przed nim *. Jeśli zaś chcemy zobaczyć jedynie adres zapisany w zmiennej wskaźnikowej, podajemy tylko jego nazwę.
Przykładowo:
int zmienna = 25, *wskaznik; // definicje zmiennych
wskaznik = &zmienna; // wpisanie adresu do wskaznika
*wskaznik = 28; /* wpisanie wartosci do zmiennej, na ktora pokazuje wskaznik (występuje *) */
cout << zmienna;
Do zapamiętania z rozdziału:
- wskaźnik zawiera w sobie adres do komórki pamięci, w której zapisana jest wskazywana zmienna
- aby odnieść się do wskazywanej zmiennej dodajemy przed nazwą wskaźnika *
- zapis &zmienna oznacza, że odnosimy się do adresu zmiennej, a nie do jej wartości
Zadanie:
Napisz program, który iteracyjnie (przy pomocy pętli) wyświetli wszystkie adresy do poszczególnych elementów tablicy liczb całkowitych (tablicę zdeklarujmy jako int tab[100]).
2. OPERATOR NEW I DELETE, TABLICE DYNAMICZNE
Dzięki wskaźnikom możliwe jest w C++ korzystanie z dynamicznych struktur danych. Już wyjaśniam, o co chodzi. Wyobraźmy sobie taką sytuację - mamy stworzyć tablicę n-elementową, przy czym ilość elementów nie jest z góry znana, ale zostanie wprowadzona przez użytkownika. Niektóre kompilatory w takich sytuacjach statyczną (taką, z jakiej korzystaliśmy do tej pory) definicję (int tablica[n]) zamienią automatycznie na dynamiczną (o której zaraz sobie powiemy). Najpierw musimy zdefiniować wskaźnik do danych odpowiedniego typu. W tym celu piszemy:
int *tab;
Aby stworzyć nową tablicę skorzystamy z operatora new:
tab = new int[n];
Zwróćmy uwagę, że nie podajemy ponownie nazwy tablicy (po operatorze new), a jedynie jej typ (który, nawiasem mówiąc, musi być zgodny z typem wskaźnika) oraz rozmiar (n).
Stworzony obiekt możemy oczywiście wykasować i zwolnić miejsce dla nowych zmiennych. Służy do tego operator delete. Używamy go bardzo prosto:
delete [] tab;
Zobaczmy przykład wykorzystujący new i delete:
#include <iostream.h>
main()
{
int *tab, n;
cout << "Podaj ilosc elementow tablicy: ";
cin >> n;
tab = new int[n]; // tworzymy tablice o rozmiarze n
cout << "Powstala tablica " << n << "-elementowa << endl";
delete [] tab; // usuwamy tablice
cout << "Tablicy juz nie ma";
}
Oczywiście operatorów new i delete można także używać w wypadku standardowych zmiennych - wtedy jednak nie piszemy nawiasów kwadratowych [].
Tym właśnie różnią się dynamiczne struktury danych od statycznych - w wypadku dynamicznych program w momencie uruchomienia nie rezerwuje w pamięci RAM obszaru pamięci na zmienne - robi to dopiero w trakcie działania. Dynamiczne zmienne są szczególnie często używane w sytuacjach, gdy mamy do czynienia z danymi o dużych rozmiarach.
Po co nam wskaźniki? Po pierwsze (o czym już powiedzieliśmy): możemy dzięki nim znacznie oszczędzać RAM, rezerwując jej odpowiedni obszar - tworzymy np. tablice o nieznanym początkowo rozmiarze i zwalniamy później pamięć komputera. Po drugie (ale tym się na razie nie będziemy zajmować): możemy odnosić się do konkretnych komórek pamięci operacyjnej. Po trzecie: zużywamy krótszy czas na dostęp do konkretnych elementów tablicy.
Wyjaśnijmy sobie punkt 3 (w ramach dygresji - nie musisz tego pamiętać). Jeśli mamy zdeklarowaną tablicę (np. int tab[n][m]), to gdy odnosimy się do któregoś elementu (przykładowo tab[k][l]), program za każdym razem musi obliczyć jej połozenie względem pierwszego elementu (w naszym przykładzie wyglądałoby to następująco: (k * n) + l). Jest to czynność dosyć czasochłonna (przy dużych rozmiarach danych). Wskaźnik natomiast cały czas pamięta adres do konkretnej komórki pamięci.
Co więcej, aby "przestawić" wskaźnik na kolejny element tablicy wystarczy go inkrementować, np. wskaznik++;
Zobaczmy jak wygląda to w praktyce:
#include <iostream.h>
main()
{
int *wskaznik, tab[100], k;
wskaznik = &tab[0]; // ustawiamy wskaznik na pierwszym elemencie
for(k=0; k<=99; k++)
{
cout << *wskaznik;
wskaznik++; // inkrementujemy wskaźnik
}
Można oczywiście tworzyć całe tablice wskaźników, np:
int *wskazniki[n];
Do zapamiętania z rozdziału:
- operatory new oraz delete pozwalają dynamicznie tworzyć struktury danych o różnych rozmiarach
- do kolejnych elementów tablicy możemy odwoływać się poprzez inkrementację wskaźnika ustawionego na jej pierwszym elemencie
Zadanie:
* Napisz program wczytujący rozmiary 10 tablic liczb całkowitych. Po wczytywaniu kolejnych program ma sprawdzać, czy nie jest dopuszczalny przekroczony limit pamięci, przeznaczony na tablice (niech będzie to np. 2 MB). Jeśli tak, usuwa tablice, które zostały stworzone jako pierwsze, aż do zwolnienia odpowiedniej ilości pamięci. Jeśli nie, tworzy tablicę. Jeśli rozmiar tablicy przekracza 2 MB, program kończy pracę. Jedna liczba całkowita (int) zajmuje w pamięci 4 bajty. Dodatkowo możesz posłużyć się funkcją sizeof(zmienna), która zwraca rozmiar zmiennej w pamięci (w bajtach).
W następnej części kursu zajmiemy się typami danych, które sami będziemy definiować.