Tablice i wskaźniki
Tablica to ułożone kolejno w pamięci zmienne jednego typu. Coś jakby poustawiać jednakowe pudełka w rzędzie. Ilość tych elementów musisz określić przy deklarowaniu tablicy. Musi być ona stała i znana już w trakcie kompilacji. Poniżej masz przykład deklaracji tablicy składającej się z 10 elementów typu int, które będą ocenami w dzienniku ;-).
int MojeOceny[10];
Widząc coś takiego kompilator zarezerwuje pamięć na 10 elementów typu int, z których każdy ma 4 bajty. Czyli łącznie zostanie zarezerwowane 40 bajtów. Ale w pamięci były przypadkowe liczby [lub zera], a my nie chcemy mieć przypadkowych ocen, tylko same piątki i czwórki, no, może czasem jakaś trójka ;-P. Co zrobić? Trzeba tablicę zainicjować jakimiś wartościami. Robi się to przy deklarowaniu w taki oto sposób:
int MojeOceny[10] = {5,4,4,5,3,5,5,6,3,4};
Wcale o tym nie wiedząc, stosowaliśmy już tablice, choć może trochę "zakamuflowane" ;-). Mam na myśli teksty, które posyłaliśmy do strumienia. Taki tekst to nic innego jak tablica - zmienne typu char ustawione kolejno w pamięci! Pewnie zastanawiało cię już, jakby tu pobrać od użytkownika coś innego niż proste cyferki, na przykład jakiś tekst, nie? :-> Tylko w jakiej zmiennej przechowuje się teksty? Do tego służy właśnie tablica. Można w niej przechować na przykład ciąg znaków. Tak się ją tworzy:
char Napis[30];
Taka deklaracja rezerwuje pamięć na 30 kolejnych bajtów [zmiennych typu char]. Jak myślisz, ile liter tekstu może pomieścić taka tablica 30-elementowa? Nie, niestety nie trzydzieści. Dlaczego tak jest? No więc komputer podczas wypisywania tekstu musi wiedzieć, gdzie ten tekst się kończy. Do oznaczenia końca tekstu używa bajtu o wartości 0 [czyli znaku '\0' zwanego także NULL]. W C++ każdy napis jest zakończony właśnie takim znakiem. Zwykle nie musisz go umieszczać w tekście, kompilator sam o to zadba. Ale właśnie ze względu na ten znak tablica 30-elementowa może pomieścić tylko 29 znaków - bo na 30 miejscu w tablicy będzie ten znak kończący.
Jak już wspomniałem, nie wiadomo co znajdowało się wcześniej w pamięci zarezerwowanej pod tablicę. Dlatego powinniśmy od razu zainicjować taką tablicę jakimiś wartościami, na przykład kolejnymi znakami napisu. Inicjowanie tablicy znaków można zrobić na dwa sposoby. Pierwszy już znasz:
char Napis[30] = {'J','a','k','i','ś',' ','n','a','p','i','s','\0'};
Drugi jest prostszy, bo zamiast wymieniać po jednej literce, podajemy od razu cały tekst [stałą znakową]. Nie musisz też dodawać na końcu znaku '\0', bo kompilator sam to zrobi. Użycie drugiego sposobu wygląda tak:
char Napis[30] = "Jakiś napis";
Jak widzisz, nie musisz wpychać do tablicy wszystkich 30 znaków. Możesz zarezerwować pamięć dla 30 znaków, ale używać raz 15, raz 23, innym razem tylko 2 i nic się nie stanie. Jedyne, czego nie wolno robić, to przekraczać zadeklarowanego rozmiaru tablicy, bo grozi to trudnymi do wykrycia błędami.
No dobra. Mamy już utworzoną tablicę i zainicjowaliśmy ją jakimiś wartościami. Jak się teraz do tych wartości odwoływać? Ponieważ są one ułożone w pamięci sekwencyjnie [czyli po kolei], możemy się do nich odwoływać poprzez indeksy, czyli ich numery porządkowe. Elementy tablicy są numerowane od 0. Pierwszy element ma numer 0, drugi element ma numer 1, i tak dalej. Na przykład żeby wypisać na ekranie trzeci element tablicy, używamy takiej instrukcji:
cout << Napis[2];
Jest to właściwie równoznaczne z instrukcją:
cout << 'k';
bo trzeci element tej tablicy to właśnie znak 'k' [typu char]. Podobnej składni używamy, gdy chcemy zmienić zawartość wybranego elementu tablicy. Na przykład zmiana ósmego znaku wygląda tak:
Napis[7] = 'd';
Spróbuj teraz wyświetlić cały tekst zawarty w tablicy a zobaczysz, że odpowiednia litera rzeczywiście uległa zmianie. Instrukcja:
cout << Napis;
spowoduje teraz wywalenie na ekran napisu "Jakis ndpis". A zgadnij, co by się stało, gdyby zamiast znaku 'd' wstawić w to miejsce tablicy znak '\0'? Spróbuj tak zrobić, a zobaczysz, że tym razem wyświetli się tylko część napisu, bo komputer kończy wypisywanie tekstu gdy natrafi na znak '\0'. Jednak reszta napisu dalej jest w tablicy, nigdzie nie ginie. Wystarczy, że zmienisz ósmy element tablicy spowrotem na znak 'a', a tekst wypisze się znów w całości.
Podczas odwoływania się do elementów tablicy należy bardzo uważać, żeby nie odwoływać się do elementów nieistniejących. Jeśli zarezerwowaliśmy w pamięci miejsce dla tablicy 12-elementowej, nie możemy odwoływać się do jej 19 elementu bo to jest błąd. Kompilator jednak nie zawsze ci o tym powie. Czasami, gdy naruszysz pamięć należącą do innego programu, wywali ci błąd dostępu do pamięci [ang. Acces Violation]. Najczęściej jednak można normalnie zapisywać dane poza tablicą, ale pamiętaj, że takie numery są bardzo niebezpieczne, bo możesz przez przypadek nadpisać zawartość innej zmiennej, której kompilator zarezerwował pamięć tuż obok naszej tablicy, i program się wysypie!
Wskaźniki
No to przejdźmy teraz do wskaźników. Wskaźnik [z ang. pointer] to taka zmienna, która nie przechowuje wartości, lecz jej adres w pamięci. Najprościej mówiąc - wskazuje on miejsce w pamięci, w którym znajduje się zmienna określonego typu. Przypuśćmy, że mamy zmienną Liczba typu int. Deklaracja wskaźnika, który będzie wskazywał na taki typ zmiennej, wygląda tak:
int * Wskaz;
Ta gwiazdka oznacza, że Wskaz nie jest zmienną typu int, lecz wskaźnikiem do takiej zmiennej. Przy czym nie jest ważne, gdzie tą gwiazdkę umieścisz: czy bliżej typu, czy bliżej nazwy, czy nawet równo pośrodku między nimi, byle by tam była. Taki nowoutworzony wskaźnik narazie wskazuje "w maliny", czyli w jakieś nieznane miejsce w pamięci. Zanim go użyjemy, należy go ustawić żeby wskazywał tam gdzie chcemy, czyli na naszą zmienną Liczba.
Wskaz = &Liczba;
Operator & to operator adresu [czasem zwany też operatorem referencji]. Zwraca on adres obiektu, który stoi po jego prawej stronie. Czyli powyższa linijka oznacza: Do wskaźnika Wskaz przypisz adres zmiennej Liczba. Od tej chwili Wskaz wskazuje na miejsce zmiennej Liczba w pamięci.
Zapomnij teraz na chwilę o zmiennej Liczba. Nie wiesz, w jakim miejscu pamięci się ona znajduje. Wiesz o niej tylko tyle, że wskazuje na nią wskaźnik Wskaz. Jak poznać jej zawartość? Musisz wykorzystać do tego celu ten wskaźnik. Zobacz, jak to się robi:
int Cyferka = *Wskaz;
Operator * to operator wyłuskania [zwany też operatorem dereferencji]. Zwraca on to, na co wskazuje wskaźnik stojący po jego prawej stronie. Innymi słowy wyłuskuje on z pamięci to, na co wskazuje wskaźnik. Powyższa linijka oznacza: Przypisz zmiennej Cyferka to, na co wskazuje Wskaz. Jeśli nie dasz operatora wyłuskania, do zmiennej Cyferka trafi adres, na który wskazuje wskaźnik, a nie to co się pod tym adresem znajduje. Kumasz? Myślę, że tak! ;-)
Jeśli ten zapis z gwiazdką wydaje ci się mylący, to spójrz na to tak. Deklaracja zawsze przypomina sposób, w jaki będziemy korzystać ze zmiennej. Jeśli w deklaracji była gwiazdka, to najpewniej w podobny sposób [z użyciem gwiazdki] będziemy się do niej odwoływać. :-) Podobnie było z tablicą i podobnie będzie z referencją, którą poznasz niebawem.
Wskaźnik to takie fajne coś, że możemy go w każdej chwili przestawić na inne miejsce w pamięci. Przykładowo przestawimy go teraz tak, by wskazywał na zmienną Cyferka. Już chyba wiesz, jak się to robi, no nie? ;-) Dla przypomnienia pokażę ci to jeszcze raz.
Wskaz = &Cyferka;
A teraz będzie najciekawsze. W C++ tablica i wskaźnik to to samo! "Jak to?!" - pewnie sobie teraz myślisz. Ano właśnie tak. Nazwa tablicy to nic innego jak wskaźnik na miejsce w pamięci, gdzie ta tablica się zaczyna. Jedyną różnicą jest to, że nazwa tablicy jest wskaźnikiem stałym i dlatego nie można takiego "wskaźnika" przestawić na inne miejsce w pamięci. To dlatego niemożliwy jest taki kod:
char Napis[50] = "Przykładowy tekst";
Napis = "Inny tekst";
bo oznaczało by to przestawienie tego "wskaźnika" na inne miejsce w pamięci [tam, gdzie kompilator umieścił ten drugi tekst]. Wspomniałem już o tym wcześniej. Jeśli zamiast tablicy posłużyłbyś się wskaźnikiem, taka instrukcja jak najbardziej była by możliwa, bo zwykły wskaźnik można przestawiać. Ty też możesz zadeklarować swój wskaźnik jako stały [taki, którego nie da się przestawiać]. Robi się to tak:
char * const Napis = "Tylko tutaj to jest mozliwe";
Ten wskaźnik działa identycznie jak nazwa tablicy - nie da się go przestawiać. Sprawiło to słówko const. Jednak czasami potrzebny jest wskaźnik który przestawiać można, ale nie można modyfikować obszaru pamięci, na który on wskazuje. Do tego celu służy wskaźnik do stałej. Różni się on tym od wskaźnika stałego, że można go przestawiać na inne miejsce pamięci. Deklaruje się go tak:
char const * Napis;
Zauważ, że tym razem słówko const [oznaczające wartość stałą] znajduje się po drugiej stronie gwiazdki. Tym razem już nie wskaźnik, lecz to na co on pokazuje, jest wartością stałą.
Jak już mówiłem, tablica i wskaźnik mają dużo wspólnego. W języku C++ to co można zrobić z użyciem tablicy, można też zrobić z użyciem wskaźników i w dodatku będzie to zwykle działać szybciej! :-) Załóżmy że masz wskaźnik Wskaz pokazujący na zmienne typu char. Pokazuje on na początek tekstu "Przykład" w pamięci [czyli na jego pierwszy znak]. Do poszczególnych znaków możesz się wtedy odwoływać na kilka różnych sposobów. Oto niektóre z nich:
char Literka;
Literka = *Wskaz; //Literka='P'
Literka = *(Wskaz+2); //Literka='z'
Literka = Wskaz[4]; //Literka='k'
Pewnie zastanawia cię użycie nawiasów w drugiej linijce. Czy są one potrzebne?
Zależy od tego, co chcesz uzyskać. Jeśli nie dasz nawiasów, kompilator zrozumie
to tak: Do zmiennej Literka przypisz to na co wskazuje
Wskaz powiększone o liczbę 2. Po prostu operator
* bierze górę nad operatorem + i najpierw wartość
jest wyciągana z pamięci, a dopiero potem zwiększana o 2. Nam jednak
chodziło o odwołanie się do drugiego znaku w tekście, dlatego użyliśmy nawiasów.
Teraz kompilator rozumie to tak: Do zmiennej Literka przypisz to,
co znajduje się w miejscu oddalonym o 2 elementy od tego, na które
wskazuje Wskaz. Można to zapisać prościej, wiedząc że wskaźnik i
tablica to to samo. Przykład takiego zapisu jest w trzeciej linijce. Będziesz stosować te sposoby zamiennie, więc żeby mieć jasność poniżej masz takie małe porównanie.
*Wskaz; Wskaz[0];
*Wskaz + 3; Wskaz[0] + 3;
*(Wskaz+4); Wskaz[4];
Nie na darmo użyłem w powyższym akapicie słowa elementy. Wskaźnik Wskaz pokazuje na elementy typu char, które mają wielkość jednego bajta. Ale co by się stało, gdybyśmy mieli wskaźnik Wskaz2 do pokazywania na elementy typu int, które zajmują 4 bajty? O ile bajtów dalej on pokaże, gdy zwiększymy jego wartość tak jak poniżej?
Wskaz2++;
W tym przypadku adres zapamiętany we wskaźniku nie zwiększy się o jeden, lecz o cztery, bo taki jest rozmiar zmiennej typu int. Czyli przestawienie wskaźnika nie jest o jeden bajt, tylko o jeden ELEMENT! Dobrze to sobie zapamiętaj!
Narazie to tyle. Programik przykładowy do tej lekcji napisz samodzielnie, w ramach zadania domowego :-P. Na przykład taki, który będzie pytał użytkownika o nazwisko, a później pokazywał mu, na jaką literę się to nazwisko zaczyna i dajmy na to jaka jest jego trzecia litera. Czy skorzystasz z tablic, czy ze wskaźników, to już zależy od ciebie.