Upload
leda-link
View
131
Download
1
Embed Size (px)
DESCRIPTION
seminarski rad s 2.god iz struktura podataka
Citation preview
SVEUČILIŠTE U ZAGREBU
FAKULTET ORGANIZACIJE I INFORMATIKE VARAŽDIN
SEMINARSKI RAD IZ KOLEGIJA „STRUKTURE PODATAKA“
TEMA: POKAZIVAČI
LEDA LINK
38180/09-R
U Varaždinu, prosinac, 2010.
SVEUČILIŠTE U ZAGREBU
FAKULTET ORGANIZACIJE I INFORMATIKE VARAŽDIN
Ime i prezime: Leda Link
Redovni student
Broj indeksa: 38180/09-R
SEMINARSKI RAD IZ KOLEGIJA „STRUKTURE PODATAKA“
TEMA: POKAZIVAČI
Nositelj kolegija: prof.dr.sc. Mirko Čubrilo
Asistent: Tihomir Orehovački, mag.inf.
U Varaždinu, prosinac, 2010.
Sadržaj
1. Pokazivači – što se odvija u memoriji..............................................................................................4
2. Uvodno o pokazivačima...................................................................................................................5
3. Dinamička alokacija.........................................................................................................................7
3.1. Naredba „new“ i stvaranje dinamičkih varijabli.......................................................................7
3.2. Naredba „delete“ i brisanje dinamičkih varijabli....................................................................11
4. Dinamički nizovi............................................................................................................................12
5. Pokazivači na pokazivače...............................................................................................................17
6. Polja i pokazivači............................................................................................................................19
7. Vezana lista i pokazivači................................................................................................................21
8. Strukture i pokazivači.....................................................................................................................23
Zaključak................................................................................................................................................25
Literatura................................................................................................................................................26
1. Pokazivači – što se odvija u memoriji
Kako bismo temeljitije objasnili sam pojam pokazivača, potrebno je objasniti što se i
na koji način odvija u memoriji. Naime, memorija je kontinuirani niz bajtova što bi značilo da
svaki bajt ima svoju adresu, odnosno, pobliže, redni broj. Definiranjem varijable će se
rezervirati određeni memorijski prostor na nekim adresama. Za varijablu ćemo reći da je na
nekoj adresi ukoliko je prvi bajt sadržaja varijable pohranjen na toj adresi.
Primjer:
Primjerice, ako tražimo adresu varijable c, pogledat ćemo prvo mjesto koje ona zauzima, a to
će biti adresa 82564. Za varijablu c su rezervirana 4 bajta, počevši od adrese 82564.
2. Uvodno o pokazivačima
Za adresu se često koristi pojam pokazivač (eng. pointer) jer upravo adresa pokazuje
na neki element u memoriji. Kada pokazivač sadrži adresu neke varijable može se reći da
„pokazuje“ na neku varijablu. Dakle, definicija pokazivača bi mogla biti: pokazivač je
varijabla koja sadrži adresu neke druge varijable.
Pokazivači se deklariraju na način da se definira tip podatka na koji pokazivač
pokazuje, te da se ispred imena varijable postavlja operator diferencijacije * koji omogućuje
pokazivaču da pristupi varijabli na koju pokazuje. Operator & daje adresu varijable, a
operator * daje sadržaj lokacije na koju pokazivač pokazuje. Pokazivač može biti deklariran
tako da pokazuje na bilo koji tip podatka. Ipak, adrese koje pokazivač sadrži moraju biti
istoga tipa.
Za razliku od ostalih varijabli, za pokazivače možemo definirati da ne sadrže nikakvu
memorijsku adresu. Nepostojeća se memorijska adresa definira pomoću konstante NULL.
NULL je konstanta iz biblioteke „cstdlib“, a predstavlja naziv za broj 0. Poslijednji element
unutar vezane liste će uvijek imati vrijednost NULL. Također se koristi i naredba „new“
kojom se dinamički alocira prostor. Nadalje, koristi se i naredba „delete“ kojom se dealocira
memorijski prostor na koji pokazuje određeni pokazivač.
Sintaksa:
tip podataka *ime;
Tip podataka je kod pokazivača potreban iz razloga jer je važno znati kolika je
veličina varijable u memorijskom prostoru na koji pokazivač pokazuje, tj. koliko se memorije
treba alocirati.
Pokazivači su često u upotrebi u pisanju programskog koda iz razloga što omogućuju
optimiziranje programskog koda.
Dinamičke varijable mogu mijenjati svoju strukturu na bilo kojem mjestu unutar
programa. Takve dinamičke strukture mogu biti: vezane liste, binarna stabla, stogovi. One se
od statičkih varijabli razlikuju u tome što za njih nećemo rezervirati unaprijed memorijski
prostor, već ćemo ga rezervirati unutar programa po potrebi, odnosno ovisno o količini
podataka koji će se unositi tijekom izvršavanja programa. Dinamičke varijable nemaju
vlastito ime u programu, već imaju definiran samo tip podatka. Da bi se s takvim varijablama
moglo baratati potrebne su nam pokazivačke varijable koje pokazuju na dinamičku varijablu.
U ovom seminarskom radu ću pokazivače prikazati unutar programskog jezika C++ uz par
poveznica s programskim jezikom C.
3. Dinamička alokacija
C++ je znatno osuvremenio mehanizme za dinamičku alokaciju memorijskog prostora
u odnosu na programski jezik C. Primjerice, u C programskom jeziku se dinamička alokacija
ostvarivala pozivom funkcije „malloc“ ili neke slične funkcije iz biblioteke „cstdlib“. Iako
takav način alokacije radi i u programskom jeziku C++, isti nudi puno fleksibilnije načine od
ovog, tako da se u praksi izbjegava korištenje naslijeđenih funkcija iz C programskog jezika.
Također, potreba za dinamičkom alokacijom na takav način je značajno pala nakon uvođenja
dinamičkih tipova podataka poput „vector“ i „string“. Ipak, dinamički tipovi podataka su
izvedeni tipovi podataka koji su definirani u standardnoj bibiloteci jezika C++ koji su
implementirani pomoću dinamičke alokacije memorije, što znači da da ne postoji dinamička
alokacija memorije da ne bi bilo moguće niti kreiranje tipova podataka poput „vector“ i
„string“.
3.1. Naredba „new“ i stvaranje dinamičkih varijabli
U programskom jeziku C++ način za dinamičku alokaciju memorijskog prostora je
pomoću operatora „new“. Postoji nekoliko oblika istog, a što se osnovnog tiče, iza njega treba
slijediti ime nekoga tipa. Nakon toga će taj operator potražiti slobodno mjesto unutar
memorije u koje bi se mogao smjestiti podatak odabranog tipa. Ukoliko se to mjesto pronađe,
operator „new“ će kao rezultat vratiti pokazivač na pronađeno memorijsko mjesto. To mjesto
će se potom označiti kao zauzeto, tako da se taj prostor više neće moći upotrijebiti za neku
drugu namjenu. Ukoliko se dogodi situacija da je cijela memorija zauzeta, C++ će izbaciti
izuzetak, dok se u programskom jeziku C i ranijim verzijama C++ vraćao NULL pokazivač.
Osnovnu upotrebu operatora „new“ možemo prikazati na sljedećem primjeru:
int *pok;
Ovom deklaracijom je definirana pokazivačka varijabla „pok“ koja treba pokazati na
tip podataka „int“, odnosno cijeli broj. Stanje memorije nakon ove deklaracije izgleda ovako:
Potom izvršavamo naredbu pok=new int;
u kojoj operator „new“ traži slobodno mjesto u memoriji koje je dovoljno veliko da prihvati
jedan cijeli broj. Ukoliko se takvo mjesto pronađe, njegova adresa će biti vraćena kao rezultat
operanda „new“ i dodijeljena pokazivaču „pok“, tako da će on u ovom primjeru pokazivati na
adresu 9. Nakon toga će stanje u memoriji izgledati ovako:
Pored toga, lokacija 9 postaje zauzeta, i to u smislu da će ova lokacija biti rezervirana
za upotrebu od strane programa, i ona do daljnjeg neće biti korištena za neku drugu namjenu.
Iz tog je razloga sigurno pristupiti sadržaju iste putem pokazivača „pok“.
Sadržaj dinamičkih varijabli je nakon njihovog deklariranja tipično nedefiniran, sve
dok se ne izvrši prva dodjela vrijednosti putem pokazivača. Preciznije, sadržaj zauzete
lokacije zadržava svoj raniji sadržaj, jedino što se lokacija smatra zauzetom. Ipak, moguće je
inicijalizirati dinamičku varijablu odmah na početku, tijekom njenog stvaranja, tako što iza
oznake tipa u operatoru „new“ u zagradama navedemo izraz koji predstavlja željeno inicijalnu
vrijednost dinamičke varijable.
Primjerice, sljedeća naredba će stvoriti novu cjelobrojnu dinamičku varijablu, te inicijalizirati
njezin sadržaj na vrijednost 5 i postaviti pokazivač „pok“ na nju:
pok=new int(5);
Stanje u memoriji je prikazano na sljedećoj slici:
Razumljivo je da se rezultat operatora „new“ mogao odmah iskoristiti za inicijalizaciju
pokazivača „pok“ već prilikom njegove deklaracije, kao naprimjer:
int *pok=new int(5);
int *pok (new int(5));
Također, za dinamičku varijablu je, kao i za svaku drugu varijablu, moguće vezati
referencu, čime možemo indirektno dodijeliti ime novostvorenoj dinamičkoj varijabli. Tako,
ukoliko je „pok“ pokazivačka varijabla koja pokazuje na novostvorenu dinamičku varijablu,
kao u prethodnom primjeru, moguće je izvršiti sljedeću deklaraciju:
int &dinamicka=*pok;
Na ovaj način je kreirana referenca „dinamicka“ koja je vezana za novostvorenu
dinamičku varijablu, i koju, prema tome, možemo smatrati kao alternativno ime te dinamičke
varijable. Drugi način na koji smo mogli napraviti isto je vezivanjem reference direktno na
dereferencirani rezultat operatora „new“ (bez posredstva dinamičke varijable). Rezultat
operatora „new“ je pokazivač, dereferencirani pokazivač je l-vrijednost, a na svaku l-
vrijednost se može vezati referenca odgovarajućeg tipa:
int &dinamicka=*new int(5);
Ovakva formulacija se ne koristi često, ali može poslužiti jer se dereferencirani
rezultat operatora „new“ može prenijeti u funkciju kao parametar po referenci.
Prilikom korištenja operatora „new“ treba voditi računa o tome da uvijek postoji
mogućnost da se dogodi izuzetak (ukoliko nema dovoljno memorije). Iako je vjerojatnost da u
memoriji neće biti pronađen prostor za jedan cijeli broj (kao u ovom elementarnom primjeru)
vrlo mala, treba paziti na to jer primjerice u nizovima trebamo dinamički alocirati puno više
memorije za koje je vrlo lako moguće da nema dovoljno memorije. Stoga je moguće izvoditi
svaku dinamičku alokaciju unutar „try“ bloka. Tip izuzetka koji će prikazati je tip
„bad_alloc“. Ovo je izvedeni tip podataka, definiran u biblioteci „new“. Tako da, ukoliko
želimo specificirati izuzetke takvoga tipa, možemo koristiti sljedeće:
try {
int *pok=new int(5);
...
}
catch (bad_alloc) {
cout<<“Problem, nema dovoljno memorije.“;
}
Kreiranje individualnih dinamičkih varijabli nije optimalno ukoliko se kreiraju
varijable prostih tipova (npr. cjelobrojne varijable), tako da će kreiranje individualnih
dinamičkih varijabli postati zanimljivo tek kada se razmotre kompleksniji tipovi podataka
poput struktura i klasa. Ipak, pomoću dinamičke alokacije se mogu indirektno kreirati nizovi
čija veličina nije unaprijed zadana. Za tu svrhu se koristi također operator „new“ iza kojeg
ponovo slijedi ime tipa (koje ovaj put predstavlja tip elementa niza), nakon čega u uglatim
zagradama slijedi broj elemenata niza koji kreiramo. Pri tome, traženi broj elemenata niza ne
mora biti konstanta, već može biti proizvoljan izraz.
Operator „new“ će tada potražiti slobodno mjesto u memoriji u koje bi se mogao
smjestiti niz tražene veličine navedenog tipa, i vratiti kao rezultat pokazivač na pronađeno
mjesto u memoriji, ukoliko takvo postoji (u suprotnom će se ispisati izuzetak tipa
„bad_alloc“). Naprimjer, ako je varijabla „pok“ deklarirana kao pokazivač na cijeli broj
(prethodni primjeri), tada će sljedeća naredba potražiti prostor u memoriji koji je dovoljan za
prihvaćanje 5 cjelobrojnih vrijednosti, i u slučaju da pronađe takav prostor, dodijelit će
njegovu adresu pokazivaču „pok“:
pok=new int[5];
3.2. Naredba „delete“ i brisanje dinamičkih varijabli
Dinamičke varijable se mogu ne samo stvarati, već i brisati na zahtjev. Onog trenutka
kada zaključimo da nam dinamička varijabla na koju pokazuje pokazivač više nije potrebna,
možemo ju obrisati primjenom operatora „delete“, iza kojeg se navodi pokazivač koji
pokazuje na dinamičku varijablu koju želimo obrisati. Primjer naredbe:
delete pok;
Ovim primjerom narebe ćemo obrisati dinamičku varijablu na koju pokazivač „pok“
pokazuje. Pri tome je važno spomenuti da će pokazivač „pok“ i dalje pokazivati na istu adresu
na koju je pokazivao i prije, ali će se izbrisati evidencija o tome da je ta lokacija zauzeta.
Dakle, ta lokacija može nakon brisanja dinamičke varijable biti iskorištena od strane nekog
drugog (npr. operacijskog sustava) za neku drugu svrhu (npr. sljedeća primjena operatora
„new“ može ponovo iskorisiti taj isti prostor za stvaranje neke druge dinamičke varijable).
Nakon toga više nije sigurno pristupati sadržaju na koji pokazuje „pok“. Pokazivači
koji pokazuju na objekte koji su obrisani iz bilo kojeg razloga nazivaju se viseći pokazivači
(eng. dangling pointers). Jedan od mogućih načina da kreiramo viseći pokazivač je
eksplicitno brisanje sadržaja na koji pokazivač pokazuje pozivom operatora „delete“, ali
postoje i drugi načini, primjerice, da iz funkcije kao rezultat vratimo pokazivač na neki
lokalni objekt unutar funkcije, koji prestaje postojati po završetku funkcije. Treba paziti na
viseće pokazivače jer su u pravilu vrlo čest uzročnik grešaka u programu koje se načelno vrlo
teško otkrivaju.
Sve dinamičke varijable se automatski brišu po završetku programa. Međutm, bitno je
naglasiti da ni jedna dinamička varijabla neće biti obrisana sama od sebe prije završetka
programa, osim ukoliko je eksplicitno ne obrišemo primjenom operatora „delete“. Pri tome se
one bitno razlikuju od automatskih varijabli, koje se same automatski brišu na kraju bloka u
kojem su definirane.
4. Dinamički nizovi
Za najkvalitetnije razumijevanje pokazivača koristit ću dinamičke nizove jer se
elementima unutar njih može pristupati upravo pomoću pokazivača.
Dinamičke nizove ćemo kreirati na sljedeći način: s obzirom da dinamički nizovi
nemaju imena njima se može pristupati samo pomoću pokazivača koji pokazuju na njihove
elemente. Tako, ukoliko izvršimo sljedeću deklaraciju:
int *dinamicki_niz=new int[100];
stvaramo dva objekta: dinamički niz od 100 cijelih brojeva koji nema ime, te pokazivač
„dinamicki_niz“ koji je inicijaliziran tako da pokazuje na prvi element ovako stvorenog
dinamičkog niza.
Za razliku od običnih nizova koji se mogu inicijalizirati pri deklaraciji, i običnih
dinamičkih varijabli koje se mogu inicijalizirati pri stvaranju, dinamički nizovi se ne mogu
inicijalizirati u trenutku stvaranja (tj. njihov je sadržaj nakon stvaranja nepredvidljiv).
Naravno, inicijalizaciju je moguće uvijek naknadno izvršiti ručno. Također, nije problem
napisati funkciju koja će stvoriti niz, inicijalizirati ga, i vratiti kao rezultat pokazivač na
novostvoreni i inicijalizirani niz. Tada tako napisanu funkciju možemo koristiti za stvaranje
nizova koji će odmah po kreiranju biti i inicijalizirani.
Kada nam neki dinamički niz više nije potreban, možemo ga također obrisati (tj.
osloboditi prostor koji je zauzimao) pomoću operatora “delete”, samo uz neznatno drugačiju
sintaksu u kojoj se koristi par uglatih zagrada. Tako, ukoliko pokazivač “pok” pokazuje na
dinamički niz, brisanje dinamičkog niza realizira se pomoću naredbe:
delete[] pok;
Neophodno je napomenuti da su uglate zagrade važne. Naime, postupci dinamičke
alokacije običnih dinamičkih varijabli i dinamičkih nizova interno se obavljaju na potpuno
drugačije načine, tako da ni postupak njihovog brisanja nije isti. Iako postoje situacije u
kojima bi se dinamički nizovi mogli obrisati primjenom običnog operatora “delete” (bez
uglatih zagrada), takvo brisanje je uvijek veoma rizično, pogotovo ukoliko se radi o nizovima
čiji su elementi složeni tipovi podataka poput struktura i klasa (na primjer, brisanje niza
pomoću običnog operatora “delete” sigurno neće biti obavljeno kako treba ukoliko elementi
niza posjeduju tzv. destruktore). Stoga se treba držati pravila: dinamički nizovi se uvijek
moraju brisati pomoću konstrukcije “delete[]”. Ovdje treba biti posebno oprezan zbog
činjenice da nas kompajler neće upozoriti ne upotrijebimo li uglaste zagrade, s obzirom na
činjenicu da kompajler ne može znati na šta pokazuje pokazivač koji se navodi kao argument
operatoru “delete”.
Već je rečeno da bi se dinamička alokacija memorije trebala vršiti unutar “try” bloka, s
obzirom da se može dogoditi slučaj da alokacija ne uspije. Stoga, sljedeći primjer, koji alocira
dinamički niz čiju veličinu zadaje korisnik, a zatim unosi elemente niza s tipkovnice i ispisuje
ih u obrnutom poretku, ilustrira kako se ispravno treba raditi s dinamičkim nizovima:
try {
int n;
cout << "Koliko želite brojeva? ";
cin >> n;
int *niz = new int[n];
cout << "Unesite brojeve:\n";
for(int i = 0; i < 10; i++) cin >> niz[i];
cout << "Niz brojeva ispisan naopako glasi:\n";
for(int i = 9; i >= 0; i--) cout << niz[i] << endl;
delete[] niz;
}
catch(...) {
cout << "Nema dovoljno memorije!\n";
}
Obavljanje dinamičke alokacije memorije unutar “try” bloka je posebno važno kada se
vrši dinamička alokacija nizova. Naime, alokacija sigurno neće uspjeti ukoliko se zatraži
alokacija niza koji zauzima više prostora nego što iznosi količina slobodne memorije (npr.
prethodni primjer će sigurno izbaciti izuzetak u slučaju da zatražite alociranje niza od recimo
100000000 elemenata).
Treba napomenuti da se rezervacija memorije za čuvanje elemenata vektora također
interno realizira pomoću dinamičke alokacije memorije i operatora “new”. Drugim riječima,
prilikom deklaracije vektora također može doći do izbacivanja izuzetka (tipa “bad_alloc”)
ukoliko količina raspoložive memorije nije dovoljna da se kreira vektor odgovarajućeg
kapaciteta. Zbog toga bi se i deklaracije vektora načelno trebale nalaziti unutar “try” bloka.
Stoga, ukoliko bismo u prethodnom primjeru željeli izbjeći eksplicitnu dinamičku alokaciju
memorije, i umjesto nje koristiti tip “vector”, modificirani primjer trebao bi izgledati ovako:
try {
int n;
cout << "Koliko želite brojeva? ";
cin >> n;
vector<int> niz(n);
cout << "Unesite brojeve:\n";
for(int i = 0; i < 10; i++) cin >> niz[i];
cout << "Niz brojeva ispisan naopako glasi:\n";
for(int i = 9; i >= 0; i--) cout << niz[i] << endl;
}
catch(...) {
cout << "Nema dovoljno memorije!\n";
}
Izuzetak tipa “bad_alloc” također može biti izbačen kao posljedica operacija koje
povećavaju veličinu vektora (poput “push_back” ili “resize”) ukoliko se ne može udovoljiti
zahtjevu za povećanje veličine (zbog nedostatka memorijskog prostora). Možemo primijetiti
jednu suštinsku razliku između primjera koji koristi operator “new” i primjera u kojem se
koristi tip “vector”. Naime, u primjeru zasnovanom na tipu “vector” ne koristi se operator
“delete”. Očigledno, lokalne varijable tipa “vector” se ponašaju kao i sve druge automatske
varijable – njihov kompletan sadržaj se briše nailaskom na kraj bloka u kojem su definirane
(uključujući i oslobađanje memorije koja je bila alocirana za potrebe smještanja njihovih
elemenata).
Slično običnim dinamičkim varijablama, i dinamički nizovi se brišu tek na završetku
programa, ili eksplicitnom upotrebom operatora “delete[]”. Stoga, pri njihovoj upotrebi
također treba voditi računa da ne dođe do curenja memorije, koje može biti znatno ozbiljnije
nego u slučaju običnih dinamičkih varijabli. Naročito treba paziti da dinamički niz koji se
alocira unutar neke funkcije preko pokazivača koji je lokalna varijabla obavezno treba i
obrisati prije završetka funkcije, inače će taj dio memorije ostati trajno zauzet do završetka
programa, i nitko ga neće moći osloboditi (izuzetak nastaje jedino u slučaju ako funkcija
vraća kao rezultat pokazivač na alocirani niz – u tom slučaju onaj tko poziva funkciju ima
mogućnost osloboditi zauzetu memoriju kada ona više nije potrebna). Višestrukim pozivom
takve funkcije (npr. unutar neke petlje) možemo veoma brzo nesvjesno zauzeti svu
raspoloživu memoriju. Dakle, svaka funkcija bi prije svog završetka morala osloboditi svu
memoriju koju je dinamički zauzela (osim ukoliko vraća pokazivač na zauzeti dio memorije),
i to bez obzira kako se funkcija završava: nailaskom na kraj funkcije, naredbom “return”, ili
izbacivanjem izuzetka. Naročito se često zaboravlja da funkcija, prije nego što izbaci
izuzetak, također treba za sobom “počistiti” sve što je krivo napravila, što uključuje i
oslobađanje dinamički alocirane memorije. Još je veći problem ukoliko funkcija koja
dinamički alocira memoriju pozove neku drugu funkciju koja može baciti izuzetak.
Promotrimo, na primjer, sljedeći isječak:
void F(int n) {
int *pok = new int[n];
…
G(n);
…
delete[] pok;
}
U ovom primjeru, funkcija “F” zaista briše kreirani dinamički niz po svom završetku,
ali problem nastaje ukoliko funkcija “G” koju ova funkcija poziva baci izuzetak. Kako se taj
izuzetak ne hvata u funkciji “F”, ona će također biti prekinuta, a zauzeta memorija neće biti
oslobođena. Naravno, prekid funkcije “F” dovodi do automatskog brisanja automatske
lokalne pokazivačke varijable “pok”, ali dinamički niz na čiji početak “pok” pokazuje nikada
se ne briše automatski, već samo eksplicitnim pozivom operatora “delete[]”. Stoga, ukoliko se
operator “delete[]” ne izvrši eksplicitno, zauzeta memorija neće biti oslobođena. Ovaj
problem se može riješiti na sljedeći način:
void F(int n) {
int *pok = new int[n];
…
try {
G(n);
}
catch(...) {
delete[] pok;
throw;
}
…
delete[] pok;
}
U ovom slučaju u funkciji “F” izuzetak koji eventualno izbacuje funkcija “G” hvatamo
samo da bismo mogli izvršiti brisanje zauzete memorije, nakon čega uhvaćeni izuzetak
prosljeđujemo dalje, navođenjem naredbe “throw” bez parametara. Iz ovog primjera vidimo
da je bitno razlikovati sam dinamički niz od pokazivača koji se koristi za pristup njegovim
elementima. Ovdje je potrebno ponovo napomenuti da se opisani problemi ne bi pojavili
ukoliko bismo umjesto dinamičke alokacije memorije koristili automatsku varijablu tipa
“vector” – ona bi automatski bila uništena po završetku funkcije “F”, bez obzira da li je do
njenog završetka došlo na prirodan način, ili bacanjem izuzetka iz funkcije “G”. U suštini, kad
god možemo koristiti tip “vector”, njegovo korištenje je jednostavnije i sigurnije od korištenja
dinamičke alokacije memorije. Stoga, tip “vector” treba koristiti kad god je to moguće –
njegova upotreba sigurno neće nikada dovesti do curenja memorije. Jedini problem je u tome
što to nije uvijek moguće. Na primjer, nije moguće napraviti tip koji se ponaša slično kao tip
“vector” (ili sam tip “vector”) bez upotrebe dinamičke alokacije memorije i dobrog
razumijevanja kao dinamička alokacija memorije funkcionira.
Iz svega što je do sada rečeno može se zaključiti da dinamička alokacija memorije
sama po sebi nije komplicirana, ali da je potrebno preduzeti dosta mjera predostrožnosti da ne
bi došlo do curenja memorije.
Ponašanje operatora “delete” je nedefinirano (i može rezultirati krahom programa)
ukoliko se primijene na pokazivač koji ne pokazuje na prostor koji je zauzet u postupku
dinamičke alokacije memorije. Naročito česta greška je primijeniti operator “delete” na
pokazivač koji pokazuje na prostor koji je već obrisan (tj. na viseći pokazivač). Ovo se, na
primjer može dogoditi ukoliko dva puta uzastopno primijenimo ove operatore na isti
pokazivač (kojem u međuvremenu između dvije primjene operatora “delete” nije dodijeljena
neka druga vrijednost). Da bi se izbjegli ovi problemi veoma dobra ideja je eksplicitno
dodijeliti NULL-pokazivač svakom pokazivaču nakon izvršenog brisanja bloka memorije na
koju on pokazuje. Na primjer, ukoliko “pok” pokazuje na prvi element nekog dinamičkog
niza, brisanje tog niza najbolje je izvesti sljedećom konstrukcijom:
delete[] pok;
pok = 0;
Eksplicitnom dodjelom “pok = 0” zapravo postižemo dva efekta. Prvo, takvom
dodjelom eksplicitno naglašavamo da pokazivač “pok” više ne pokazuje ni na što. Drugo,
ukoliko slučajno ponovo primijenimo operator “delete” na pokazivač “pok”, neće se dogoditi
ništa, jer je on sada NULL-pokazivač.1
1 u poglavlju o dinamičkim nizovima je dosta korišten web dokument sa stranice: http://www.scribd.com/doc/11542041/C-Pokazivaci3
5. Pokazivači na pokazivače
U jeziku C++ pokazivači na pokazivače pretežno se koriste za potrebe dinamičke
alokacije dvodimenzionalnih nizova, dok su se u jeziku C dvojni pokazivači intenzivno
koristili u situacijama u kojima je u jeziku C++ prirodnije upotrijebiti reference na pokazivač
(tj. reference vezane za neki pokazivač). Na primjer, pomoću deklaracije
int *&ref = pok;
deklariramo referencu “ref” vezanu na pokazivačku varijablu “pok” (tako da je “ref” zapravo
referenca na pokazivač). Sada se “ref” ponaša kao alternativno ime za pokazivačku varijablu
“pok”. Važno je obratiti pažnju na redoslijed znakova “*” i “&”. Ukoliko bismo zamijenili
redoslijed ovih znakova, umjesto reference na pokazivač pokušali bismo deklarirati pokazivač
na referencu, što nije dozvoljeno u jeziku C++ (postojanje pokazivača na referencu omogućilo
bi pristup internoj strukturi reference). Inače, deklaracije svih pokazivačkih tipova mnogo su
jasnije ako se čitaju s desna na lijevo (tako da uz takvo čitanje, iz prethodne deklaracije jasno
vidimo da “ref” predstavlja referencu na pokazivač na cijele brojeve).
Reference na pokazivače najčešće se koriste ukoliko je potrebno neki pokazivač
prenijeti po referenci kao parametar u funkciju, što je potrebno npr. ukoliko funkcija treba
izmijeniti sadržaj samog pokazivača (a ne objekta na koji pokazivač pokazuje).
Na primjer, sljedeća funkcija vrši razmjenu dva pokazivača koji su joj proslijeđeni kao stvarni
parametri:
void RazmjenaPok(double *&x, double *&y) {
double *pomocna = x; x = y; y = pomocna;
}
Kako u jeziku C nisu postojale reference, sličan efekt se mogao postići jedino
upotrebom dvojnih pokazivača, kao u sljedećoj funkciji
void RazmjenaPok(double **p, double **q) {
double *pomocna = *p;
*p = *q; *q = pomocna;
}
Naravno, prilikom poziva ovakve funkcije “RazmjenaPok”, kao stvarne argumente
morali bismo navesti adrese pokazivača koje želimo razmijeniti (a ne same pokazivače), pri
čemu nakon uzimanja adrese dobivamo dvojni pokazivač. Drugim riječima, za razmjenu dva
pokazivača “p1” i “p2” (na tip “double”) morali bismo izvršiti sljedeći poziv:
RazmjenaPok(&p1, &p2);
Očigledno, upotreba referenci na pokazivače (kao uostalom i upotreba bilo kakvih
referenci) oslobađa korisnika funkcije potrebe da eksplicitno razmišlja o adresama. Svi do
sada prikazani postupci dinamičke alokacije matrica nisu vodili računa o tome da li su
alokacije zaista uspjele ili nisu. U realnim situacijama moramo i o tome voditi računa, tako da
bismo dinamičku alokaciju matrice realnih brojeva sa “n” redova i “m” kolona zapravo trebali
realizirati ovako:
try {
double **a = new int*[n];
for(int i = 0; i < n; i++) a[i] = 0;
try {
for(int i = 0; i < n; i++) a[i] = new int[m];
}
catch(...) {
for(int i = 0; i < n; i++) delete[] a[i];
delete[] a;
throw;
}
}
catch(...) {
cout << "Problemi s memorijom!\n";
}
6. Polja i pokazivači
Do sada smo duljinu polja uvijek definirali fiksno, unaprijed. Pokazivači nam
omogućuju da duljinu polja definiramo ovisno o parametru u programu tako da alociramo
točno onoliko memorijskog prostora koliko nam je potrebno. U programskom jeziku C i C++
postoji čvrsta veza između pokazivača i polja. Bilo koja operacija koju možemo obaviti preko
indeksa polja, može se obaviti i preko pokazivača. Primjer polja:
int a[10];
definira polje veličine 10, što će predstavljati skup od deset objekata a[0], a[1], a[2]....a[9].
Pri tome a[i] odgovara i-tom elementu polja. Ako je pok pokazivač na cjelobrojnu vrijednost,
deklariran kao
int *pok;
tada naredba
pok=&a[0];
dodjeljuje varijabli pok vrijednost koja je ekvivalentna adresi prvog elementa polja a.
Sad naredba
x=*pok;
prebacuje sadržaj a[0] u x. Ako pok pokazuje na određeni element polja, onda po definiciji
pok+1 pokazuje na sljedeći element, pok+i pokazuje na i-ti element poslije prvog, dok pok-1
pokazuje na i-ti element prije prvog. Stoga ako pok pokazuje na a[0],
*(pok+1)
se odnosi na sadržaj od a[1], pok+i je adresa od a[i], a *(pok+i) je sadržan u a[i].
Na ovaj način je prikazano da tehnika rada s pokazivača nalikuje na rad s indeksima polja.
Ipak, kao što je već unutar ovog seminarskog rada pokazano, mogućnosti pokazivača su
svakako veće.
7. Vezana lista i pokazivači
Svaki element vezane liste sadrži pokazivač s adresom slijedećeg elementa liste.
Pokazivač zadnjeg elementa u listi ima vrijednost NULL, što će pokazati da je na tom mjestu
kraj liste. Ukoliko dodajemo novi element unutar liste, tada ćemo njegovu adresu pridruživati
pokazivaču do zadnjeg elementa liste. To nam pokazuje da broj elemenata nije unaprijed
zadan (zato se vezana lista i smatra dinamičkom strukturom), već se mogu po potrebi dodavati
novi elementi sve dok ima raspoloživog memorijskog prostora.
Vezana lista je vrlo slična polju.
Razlike između liste i polja:
– U polju se elementima pristupa izravno, preko indeksa, dok se elementima
vezane liste pristupa slijedno
– Polju mora broj elemenata biti zadan prije korištenja polja dok se broj
elemenata vezane liste dinamički mijenja
Kod dodavanja novog elementa u vezanu listu trebamo alocirati memorijski prostor na
kojem će se taj element nalaziti. Treba napomenuti da taj memorijski prostor nije ni u kakvoj
vezi s memorijskim prostorom koji je dodjeljen pri alokaciji prethodno dodanih elemenata
liste. Upravo zbog toga je potrebno da elemente vezane liste povezujemo pokazivačima. Jasno
je da će pokazivač nakon dodavanja novog elementa morati pokazivati na upravo dodani
element. No prije nego što mu pridružimo adresu novog elementa, potebno je povezati novi
element ostatkom liste. Ako to nebismo učinili, ostatak liste bi bio izgubljen, te više ne bismo
znali na kojem se on mjestu nalazi. To ćemo učiniti tako da vrijednost pokazivača zapišemo u
slog novododanog elementa liste. Na taj će način pokazivač iz novododanog elementa
pokazivati na sljedeći element vezane liste. Tek nakon što to učinimo možemo preusmjeriti
pokazivač tako da pokazuje na novododani element.
Kod brisanja elemenata s početka liste pokazivač treba preusmjeriti tako da pokazuje
na drugi element u vezanoj listi. No i pri ovoj operaciji, kao i pri dodavanju elementa, treba
paziti na redoslijed. Naime, ako jednostavno preusmjerimo pokazivač na drugi element u
vezanoj listi, onda će se izgubiti adresa elementa kojeg smo upravo izbacili. Taj element nam
više nije potreban, pa nam to izbacivanje nije važno. Međutim, memorijski prostor koji je taj
element zauzimao još uvijek ostaje zauzet, iako se više ne koristi. Takav odnos može
uzrokovati brzo zauzimanje raspoložive memorije. Iz tog razloga je vrlo važno da se svaki
memorijski prostor koji je dinamički alociran nakon korištenja vrati operacijskom sustavu,
odnosno dealocira. Zbog toga se prije nego što se pokazivač preusmjeri, njegova vrijednost,
odnosno adresa prvog elementa u vezanoj listi mora zapisati u pomoćni pokazivač, kako bi se
nakon premještanja pokazivača na sljedeći element liste prostor koji je obrisani element
zauzimao mogao dealocirati.
8. Strukture i pokazivači
Pokazivači se mogu koristiti kao članovi strukture, a deklariraju se na isti način kao i
ostali pokazivači koji nisu dio strukture – operatorom diferencijacije *.
Primjer:
struct koordinate {
int *x;
int *y;
} struktura1;
Ove naredbe će definirati i deklarirati strukturu čija su dva člana pokazivači na tip
podataka int. Kao i kod svih pokazivača, njihovo deklariranje nije dovoljno; moraju im se
pridružiti i adrese varijabli da bi ih se moglo inicijalizirati da pokazuju negdje:
struktura1.x = &lijevogore;
struktura2.y = &desnodolje;
Sad kad su pokazivači inicijalizirani, može se rabiti operator *. Izraz struktura1.x dat
će vrijednost varijable lijevogore, a izraz struktura.y dat će vrijednost varijable desnodolje.
C program može deklarirati i rabiti pokazivače na strukture, kao i na sve ostale tipove
za pohranu podataka. Pokazivači na strukture često se rabe kada se struktura prosljeđuje
funkciji kao argument. Evo kako u programu može načiniti i rabiti pokazivači na strukture.
Prvo, definiranje strukture:
struct adresa {
char ulica[81];
char grad[81];
};
Sada se deklarira pokazivač na tip adresa:
struct adresa *p_adresa;
U ovom trenutku ne treba inicijalizirati pokazivač jer ni struktura još nije deklarirana, nego je
tek definirana. Deklaracija:
struct adresa osoba;
Sada se može inicijalizirati pokazivač:
p_adresa = &osoba; -ova naredba pridružuje adresu osoba u p_adresa.
Recimo, da bismo pridružili vrijednost "Ilica" članu osoba.ulica, napisat ćemo to ovako:
(*p_adresa).ulica = "Ilica";
Ovim primjerom je pokazano da se pokazivači mogu koristiti u kompleksnijim
tipovima podataka kao što su strukture, što je itekako važno jer pokazuje da je upotreba
pokazivača uistinu vrlo raširena.
Zaključak
U ovom seminarskom radu obradila sam temu pokazivača koja je u suštini vrlo
opširna. Iz tog razloga nisam mogla u potpunosti objediniti sve funkcionalnosti pokazivača na
jedno mjesto. Ipak, pokazala sam neke osnovne funkcionalnosti, te prikazala kako se
pokazivači ponašaju unutar polja, vezane liste, dinamičkih nizova, te struktura.
Pokazivači se unutar programskog jezika C i C++ koriste ponajviše iz razloga što
optimiziraju programski kod. Svakako je važno za svaki programski jezik da je omogućen
način kako smanjiti količinu linija koda jer većina zahtjevnijih programskih kodova ima
nemali broj linija. Iz tog razloga su pokazivači ipak vrlo često korišteni.
U programskom jeziku C++, najvažnije primjene pokazivača su:
- prijenos argumenata funkciji po referenci
- implementacija nizova
- implementacija struktura
Literatura
http://grube.web.srk.fer.hr/marcupic/COsnove/Pokazivaci.html
http://grube.web.srk.fer.hr/marcupic/COsnove/Pokazivaci.html
http://www.scribd.com/doc/11542041/C-Pokazivaci3
http://www.scribd.com/doc/11542034/C-Pokazivac-Na-Pokazivac
http://www.cplusplus.com/doc/tutorial/pointers/
http://www.augustcouncil.com/~tgibson/tutorial/ptr.html
http://www.linuxconfig.org/c-understanding-pointers
http://www.exforsys.com/tutorials/c-plus-plus/c-plus-plus-pointers.html
http://en.wikipedia.org/wiki/Pointer_%28computing%29
- svim poveznicama je pristupljeno do 20.12.2010.