25
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme 1 Dodatak predavanjima: Pokazivači i srodne teme Bez obzira na činjenicu da je C++ uveo novu klasu objekata nazvanu reference, koja umnogome rješava brojne probleme vezane za nerazumijevanje i pogrešnu upotrebu pokazivača, pokazivači se u jeziku C++ još uvijek intenzivno koriste, s obzirom da se reference u jeziku C++ nisu mogle izvesti na način koji bi u potpunosti uklonio potrebu za pokazivačima, kao što je to učinjeno u čistim objektno orijentiranim jezicima, kao što je Java. Naime, činjenica da se od jezika C++ zahtijevala visoka kompatibilnost sa jezikom C, dovela je do toga da je “bijeg” od pokazivača u jeziku C++ ipak nemoguć. S obzirom da je nerazumjevanje koncepta pokazivača čest razlog za brojne probleme u razumijevanju naprednijih koncepata jezika C++, svrha ovog dodatka je da pruži pregled onih znanja o pokazivačima koje je neophodno posjedovati za ispravno razumijevanje predavanja koja kasnije slijede. Većinu tih znanja studenti su trebali steći kroz prethodne kurseve programiranja, ali pitanje je da li su ih zaista stekli, zbog dosta složene materije. To je i glavni razlog zbog koje je ovaj dodatak uopće pisan. Pokazivači su jedna od dvije vrste objekata u jeziku C++ pomoću kojih se može vršiti indirektni pristup drugim objektima (druga vrsta su reference). Pokazivači su u C++ direktno naslijeđeni iz jezika C, u kojem predstavljaju jedine objekte preko kojih je moguć indirektni pristup drugim objektima. U suštini, pokazivači nisu ništa drugo nego promjenljive koje kao svoj sadržaj sadrže adresu nekog drugog objekta (kaže se “pokazuju na neki objekat”), podržane odgovarajućim operatorima koji omogućavaju da se pristupi objektu na koji pokazuju. Pokazivači, dakle, nisu ništa komplicirano. Jedini problem je što pokazivači pružaju izuzetno veliku fleksibilnost, koju je veoma lako zloupotrebiti. Zbog tog razloga se programerima u jeziku C++ savjetuje da dobro prouče reference, i da sve probleme koji se mogu rješavati pomoću referenci zaista i rješavaju pomoću referenci, a ne pomoću pokazivača. Bez obzira na to, u jeziku C++ i dalje postoji mnogo stvari koje je moguće uraditi samo pomoću pokazivača, a koje je nemoguće realizirati niti pomoću referenci, niti na bilo koji drugi način (koji ne koristi pokazivače). Pokazivače nije moguće objasniti i razumjeti bez nešto detaljnijeg objašnjenja o tome kako se promjenljive čuvaju u memoriji računara (između ostalog i zbog toga što kod pokazivača možemo direktno pristupiti informaciji o tome gdje se u memoriji čuva objekat na koji pokazivač pokazuje). Zbog toga ćemo prvo u osnovnim crtama razmotriti način smještanja promjenljivih u računarskoj memoriji. Poznato je da svaka promjenljiva koja se deklarira u programu tokom svog života zauzima određeni prostor u memoriji. Mjesto u memoriji koje je dodijeljeno nekoj promjenljivoj određeno je njenom adresom, koja je fiksna sve dok posmatrana promjenljiva postoji (ako zamislimo da je čitava memorija poput nekog niza, tada adrese možemo zamisliti kao indekse elemenata tog niza). Na primjer, pretpostavimo da imamo sljedeću deklaraciju: int broj; Prilikom nailaska na ovu deklaraciju, rezervira se odgovarajući prostor u memoriji za potrebe smještanja sadržaja promjenljive “broj”, koji naravno ima svoju adresu (koju ćemo nazvati prosto adresa promjenljive broj”). Pretpostavimo, na primjer, da je ovoj promjenljivoj dodijeljena adresa 4325. Tada imamo sljedeću memorijsku sliku: Adrese ... 4322 4323 4324 4325 4326 4327 4328 4329 ... broj Ukoliko nakon ove deklaracije izvršimo dodjelu neke vrijednosti promjenljivoj “broj”, npr. dodjelom broj = 56; efekat ove dodjele biće smještanje vrijednosti 56 u memoriju na adresu 4325 dodijeljenu promjenljivoj broj”, kao što je prikazano na sljedećoj slici:

Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Embed Size (px)

Citation preview

Page 1: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

1

Dodatak predavanjima: Pokazivači i srodne teme Bez obzira na činjenicu da je C++ uveo novu klasu objekata nazvanu reference, koja umnogome

rješava brojne probleme vezane za nerazumijevanje i pogrešnu upotrebu pokazivača, pokazivači se u jeziku C++ još uvijek intenzivno koriste, s obzirom da se reference u jeziku C++ nisu mogle izvesti na način koji bi u potpunosti uklonio potrebu za pokazivačima, kao što je to učinjeno u čistim objektno orijentiranim jezicima, kao što je Java. Naime, činjenica da se od jezika C++ zahtijevala visoka kompatibilnost sa jezikom C, dovela je do toga da je “bijeg” od pokazivača u jeziku C++ ipak nemoguć. S obzirom da je nerazumjevanje koncepta pokazivača čest razlog za brojne probleme u razumijevanju naprednijih koncepata jezika C++, svrha ovog dodatka je da pruži pregled onih znanja o pokazivačima koje je neophodno posjedovati za ispravno razumijevanje predavanja koja kasnije slijede. Većinu tih znanja studenti su trebali steći kroz prethodne kurseve programiranja, ali pitanje je da li su ih zaista stekli, zbog dosta složene materije. To je i glavni razlog zbog koje je ovaj dodatak uopće pisan.

Pokazivači su jedna od dvije vrste objekata u jeziku C++ pomoću kojih se može vršiti indirektni

pristup drugim objektima (druga vrsta su reference). Pokazivači su u C++ direktno naslijeđeni iz jezika C, u kojem predstavljaju jedine objekte preko kojih je moguć indirektni pristup drugim objektima. U suštini, pokazivači nisu ništa drugo nego promjenljive koje kao svoj sadržaj sadrže adresu nekog drugog objekta (kaže se “pokazuju na neki objekat”), podržane odgovarajućim operatorima koji omogućavaju da se pristupi objektu na koji pokazuju. Pokazivači, dakle, nisu ništa komplicirano. Jedini problem je što pokazivači pružaju izuzetno veliku fleksibilnost, koju je veoma lako zloupotrebiti. Zbog tog razloga se programerima u jeziku C++ savjetuje da dobro prouče reference, i da sve probleme koji se mogu rješavati pomoću referenci zaista i rješavaju pomoću referenci, a ne pomoću pokazivača. Bez obzira na to, u jeziku C++ i dalje postoji mnogo stvari koje je moguće uraditi samo pomoću pokazivača, a koje je nemoguće realizirati niti pomoću referenci, niti na bilo koji drugi način (koji ne koristi pokazivače).

Pokazivače nije moguće objasniti i razumjeti bez nešto detaljnijeg objašnjenja o tome kako se

promjenljive čuvaju u memoriji računara (između ostalog i zbog toga što kod pokazivača možemo direktno pristupiti informaciji o tome gdje se u memoriji čuva objekat na koji pokazivač pokazuje). Zbog toga ćemo prvo u osnovnim crtama razmotriti način smještanja promjenljivih u računarskoj memoriji. Poznato je da svaka promjenljiva koja se deklarira u programu tokom svog života zauzima određeni prostor u memoriji. Mjesto u memoriji koje je dodijeljeno nekoj promjenljivoj određeno je njenom adresom, koja je fiksna sve dok posmatrana promjenljiva postoji (ako zamislimo da je čitava memorija poput nekog niza, tada adrese možemo zamisliti kao indekse elemenata tog niza). Na primjer, pretpostavimo da imamo sljedeću deklaraciju:

int broj;

Prilikom nailaska na ovu deklaraciju, rezervira se odgovarajući prostor u memoriji za potrebe smještanja sadržaja promjenljive “broj”, koji naravno ima svoju adresu (koju ćemo nazvati prosto adresa promjenljive “broj”). Pretpostavimo, na primjer, da je ovoj promjenljivoj dodijeljena adresa 4325. Tada imamo sljedeću memorijsku sliku:

Adrese ... 4322 4323 4324 4325 4326 4327 4328 4329 ...

broj

Ukoliko nakon ove deklaracije izvršimo dodjelu neke vrijednosti promjenljivoj “broj”, npr. dodjelom broj = 56;

efekat ove dodjele biće smještanje vrijednosti 56 u memoriju na adresu 4325 dodijeljenu promjenljivoj “broj”, kao što je prikazano na sljedećoj slici:

Page 2: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

2

Adrese ... 4322 4323 4324 4325 4326 4327 4328 4329 ... 56

broj

U stvarnosti je situacija nešto složenija nego na prikazanoj slici. Naime, memorija se obično adresira po bajtima, tako da jedna memorijska lokacija može primiti samo jednobajtnu vrijednost, dok cjelobrojne promjenljive obično zahtijevaju više bajta za memoriranje sadržaja (danas najčešće 4). Stoga će promjenljiva “broj” u stvarnosti zauzeti ne samo jednu, nego više susjednih lokacija (npr. 4325, 4326, 4327 i 4328), pri čemu pod adresom promjenljive smatramo adresu prve od zauzetih lokacija (tj. adresu 4325). Međutim, radi pojednostavljenja diskusije koja slijedi, mi ćemo sadržaj memorije crtati kao da jedna promjenljiva uvijek zauzima jednu memorijsku lokaciju. Ovo pojednostavljenje neće se bitno odraziti na suštinu izlaganja. Također, postoje računarske arhitekture kod kojih organizacija memorije nije linearna (tj. nalik na niz) nego ima složeniju strukturu, npr. poput strukture matrice, tako da adresa nije običan broj nego par brojeva u obliku (red, kolona). Ovakvu situaciju imamo, na primjer, kod Intel-ovih procesora pod starijim operativnim sistemima poput MS DOS-a i Windows-a 3.11, kod kojih je adresa zaista zadana kao par brojeva (koji se redom nazivaju segment i offset). U nastavku izlaganja ćemo uglavnom zanemariti eventualne komplikacije ove prirode i smatraćemo da je memorija linearna, tj. usvojićemo linearni memorijski model (pri čemu ćemo istaći eventualne poteškoće koje mogu nastati ukoliko to nije slučaj).

Razumije se da za pristup promjenljivoj “broj” nije uopće potrebno poznavati na kojoj se adresi u memoriji ona nalazi, jer njenom sadržaju uvijek možemo pristupiti koristeći njeno simboličko ime “broj” (kao što smo uvijek i činili). Međutim, pokazivači kao svoj sadržaj imaju adresu neke druge promjenljive (ili općenito, neke druge l-vrijednosti). Svaki pristup pokazivaču daje upravo ovu adresu, odnosno ne vrši se automatski preusmjeravanje na objekat na koji pokazivač pokazuje (upravo po tome se pokazivači bitno razlikuju od referenci). Za pristup objektu na koji pokazivač pokazuje koristi se posebna operacija, koja se obično naziva dereferenciranje.

Pokazivačke promjenljive se deklariraju tako da se ispred njihovog imena stavi znak “*” (zvjezdica).

Na primjer, sljedeća deklaracija deklarira promjenljivu “pokazivac” koja nije tipa “cijeli broj”, nego “pokazivač na cijeli broj”:

int *pokazivac; Kao i svaka druga promjenljiva, i pokazivačka promjenljiva zauzima određeni prostor u memoriji.

Ukoliko pretpostavimo da je promjenljivoj “pokazivac” dodijeljena npr. adresa 4329, dobijamo sljedeću memorijsku sliku:

Adrese ... 4322 4323 4324 4325 4326 4327 4328 4329 ... 56

broj pokazivac Naravno, ovom deklaracijom još uvijek nije dodijeljena nikakva vrijednost promjenljivoj

“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati brojčane vrijednosti. Stoga kompajler neće dozvoliti dodjelu poput sljedeće:

pokazivac = 4325;

Umjesto toga, pokazivačkim promjenljivim se mogu dodjeljivati samo vrijednosti pokazivačke prirode (tj. vrijednosti koje garantirano predstavljaju adrese nekih drugih objekata). Na primjer, pokazivaču je moguće dodjeliti adresu nekog objekta (npr. promjenljive) uz pomoć unarnog prefiksnog operatora “&”,

Page 3: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

3

koji možemo čitati “adresa od”. Tako, u sljedećem primjeru, promjenljivoj “pokazivac”dodjeljujemo adresu promjenljive “broj” (odnosno vrijednost “4325” u konkretnom primjeru situacije u memoriji):

pokazivac = &broj;

Nakon ove dodjele, situacija u memoriji je kao na sljedećoj slici:

Adrese ... 4322 4323 4324 4325 4326 4327 4328 4329 ... 56 4325

broj pokazivac Ukoliko bismo sada pokušali da ispišemo sadržaj maloprije deklarirane promjenljive “pokazivac”

(koja je deklarirana kao pokazivač), dobili bismo ispis njenog sadržaja, koji nije ništa drugo nego adresa na kojoj je smještena promjenljiva “broj”. U navedenom primjeru, to bi bio broj 4325. Strogo gledano, ukoliko ispis vršimo preko “cout” objekta, ispis neće glasiti “4325“ nego “0x10e5“ zbog konvencije koju podržava “cout” objekat prema kojoj se adrese uvijek ispisuju u heksadekadnom brojnom sistemu, a “0x” je prefiks za heksadekadni brojni sistem u jeziku C++ (dekadni prikaz bismo mogli dobiti eksplicitnom konverzijom u tip “int”, odnosno ispisom izraza poput “int(pokazivac)”). Naravno, tačan ispis striktno zavisi od toga gdje je promjenljiva “broj” zaista smještena u memoriji što se ne može unaprijed znati (značenje ispisa postaje još zapetljanije kod računarskih arhitektura kod kojih memorija nije linearna, u šta ovdje nećemo ulaziti). Zbog toga, sama vrijednost pokazivačke promjenljive najčešće nije od neposredne koristi. Mnogo češće nam je potreban ne sam sadržaj pokazivačke promjenljive, već sadržaj memorijske lokacije na koju pokazivačka promjenljiva pokazuje. Ovaj sadržaj možemo dobiti primjenom unarnog operatora “*”, koji možemo čitati “sadržaj lokacije smještene u” (ili pojednostavljeno samo “sadržaj od”). Na primjer, sljedeća naredba

cout << *pokazivac;

ispisaće vrijednost “56”, jer je sadržaj lokacije na koju pokazuje promjenljiva “pokazivac” upravo 56. Operator “&” naziva se još i operator referenciranja, dok se operator “*” naziva i operator dereferenciranja. Operator referenciranja može se primijeniti isključivo na objekte koji su po svojoj prirodi l-vrijednosti, kao što su recimo promjenljive (s obzirom da samo l-vrijednosti imaju adrese). Ako je “x” objekat tipa “NekiTip”, tada izraz “&x” ima tip “pokazivač na tip NekiTip”. S druge strane, ukoliko je “p” pokazivač tipa “pokazivač na tip NekiTip”, tada je tip izraza “*p” prosto “NekiTip”.

Dereferencirani pokazivač može se koristiti u bilo kojem kontekstu u kojem bi se mogao koristiti bilo koji izraz čiji tip odgovara tipu na koji pokazivač pokazuje. Stoga je sljedeća naredba potpuno legalna, i ispisuje brojeve 57 i 112:

cout << *pokazivac + 1 << endl << 2 * *pokazivac;

U ovoj naredbi trebamo obratiti pažnju na dva detalja. Prvo, operator dereferenciranja ima veći prioritet od aritmetičkih operatora (sabiranja, množenja, itd.) tako da se izraz “*pokazivac + 1” ne interpretira kao “*(pokazivac + 1)” već kao “(*pokazivac) + 1” (kasnije ćemo vidjeti kakvo bi tačno značenje imala prva interpretacija, koja je također smislena). Drugo, u izrazu “2 * *pokazivac” prva zvjezdica predstavlja operator množenja, dok druga zvjezdica predstavlja operator dereferenciranja (odnosno, postoje dva različita operatora istog imena “*”, pri čemu je jedan binarni a drugi unarni). Kako se razmaci mogu izostaviti, istu naredbu smo mogli napisati i ovako:

cout<<*pokazivac+1<<endl<<2**pokazivac; Međutim, na ovaj način je veoma nejasno šta koja zvjezdica predstavlja, pa ovakvo pisanje treba izbjegavati. Vjerovatno najjasnije značenje dobijamo ukoliko upotrijebimo zagrade:

Page 4: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

4

cout << *pokazivac + 1 << endl << 2 * (*pokazivac);

Veoma važna činjenica je da dereferencirani pokazivač predstavlja l-vrijednost, odnosno može se naći sa lijeve strane znaka dodjeljivanja (i, općenito, ponaša se poput promjenljive). Stoga se na dereferencirani pokazivač mogu primijeniti operatori poput “++” i “––”, tako da su sasvim dozvoljene naredbe poput sljedećih: *pokazivac = 100; (*pokazivac)++; Prva naredba postavlja sadržaj lokacije na koju ukazuje promjenljiva “pokazivac” na vrijednost 100, dok druga naredba uvećava sadržaj lokacije na koju ukazuje promjenljiva “pokazivac” za jedinicu. U konkretnom primjeru, kada promjenljiva “pokazivac” pokazuje na promjenljivu “broj”, efekat ovih naredbi je isti kao da smo neposredno pisali broj = 100; broj++; Drugim riječima, dereferencirani pokazivač ponaša se identično kao i promjenljiva na koju on pokazuje. Treba još napomenuti da su u izrazu “(*pokazivac)++” zagrade obavezne. Naime, operator “++” ima veći prioritet u odnosu na operator dereferenciranja. Stoga bi se izraz “*pokazivac++” interpretirao kao “*(pokazivac++)”, koja je također smislena. Uskoro ćemo razjasniti i sadržaj ove druge interpretacije.

Prije nego što se upoznamo sa primjenama pokazivača, bitno je napomenuti da bez obzira što pokazivači i dalje imaju veliku primjenu u jeziku C++, oni se mnogo manje koriste nego što se koriste u jeziku C. Naime, u jeziku C su pokazivači jedini način da se ostvari indirektni pristup nekoj promjenljivoj, dok se ta indirekcija u jeziku C++ često može ostvariti znatno jednostavnije putem referenci, o čemu ćemo kasnije govoriti. Recimo, u jeziku C, ne postoji nikakav način da funkcija promijeni vrijednosti svojih stvarnih parametara. Recimo, ukoliko želimo napisati funkciju “Razmijeni” koja razmjenjuje vrijednosti promjenljivih koje joj se prenose kao argumenti, u C-u ne postoji nikakav način da to izvedemo tako da poziv funkcije izgleda poput sljedećeg:

Razmijeni(a, b);

Naime, kako god bila napisana funkcija “Razmijeni”, ona uvijek radi sa kopijom stvarnih

argumenata “a” i “b”, tako da ih ona ne može izmijeniti (napomenimo da je, zahvaljujući referencama, u jeziku C++ moguće postići da se koristi upravo ovakav poziv). U jeziku C jedino što možemo uraditi je da funkciji “Razmijeni” eksplicitno prenesemo adrese gdje se nalaze odgovarajući stvarni parametri, odnosno u pozivu funkcije moramo eksplicitno uzeti adrese stvarnih parametara pomoću adresnog operatora “&”. To zahtijeva da poziv funkcije izgleda poput sljedećeg:

Razmijeni(&a, &b); Da bi ovakav poziv bio legalan, potrebno je funkciju “Razmijeni” napisati tako da umjesto cijelih

brojeva kao parametre prihvata pokazivače na cijele brojeve. Tada je moguće upotrijebiti operator dereferenciranja da se pomoću njega indirektno pristupi sadržaju memorijskih lokacija gdje se nalaze stvarni parametri i da se na taj način izvrši razmjena. Na taj način funkcija “Razmijeni”, napisana u duhu jezika C, mogla bi izgledati ovako:

void Razmijeni(int *pok1, int *pok2) { int pomocna(*pok1); *pok1 = *pok2; *pok2 = pomocna; } Mada napisana funkcija izgleda dosta jednostavno, početniku nije posve lako shvatiti kako ona radi.

Neka su, na primjer, vrijednosti promjenljivih “a” i “b” respektivno “5” i “8”, i neka je funkcija

Page 5: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

5

“Razmijeni” pozvana na opisani način (tj. uz eksplicitno navođenje operatora referenciranja “&” pri pozivu funkcije). Sljedeća slika ilustrira situaciju nakon izvršavanja svake od naredbi u funkciji “Razmijeni” (prikazane adrese i raspored u memoriji uzeti su kao čisto hipotetički primjer): Razmijeni(&a, &b);

Adrese ... 3144 3145 3146 3147 3148 3149 3150 3151 ... 5 8 3144 3145

a b pok1 pok2

int pomocna(*pok1);

Adrese ... 3144 3145 3146 3147 3148 3149 3150 3151 ... 5 8 3144 3145 5

a b pok1 pok2 pomocna

*pok1 = *pok2;

Adrese ... 3144 3145 3146 3147 3148 3149 3150 3151 ... 8 8 3144 3145 5

a b pok1 pok2 pomocna

*pok2 = pomocna;

Adrese ... 3144 3145 3146 3147 3148 3149 3150 3151 ... 8 5 3144 3145 5

a b pok1 pok2 pomocna

Vidimo da su promjenljive “a” i “b” zaista razmijenile svoje vrijednosti. Kasnije ćemo vidjeti da se u jeziku C++ isti problem može riješiti na mnogo jednostavniji i jasniji način, korištenjem prenosa parametara po referenci umjesto pokazivača. Bez obzira na to, ovaj primjer veoma ilustrativno demonstrira na koji način djeluju pokazivači, a ujedno i demonstrira kako se pokazivači koriste kao parametri funkcija. Interesantno je napomenuti da se potpuno ista situacija u memoriji poput situacije prikazane na prethodnim slikama dešava i u rješenju u kojem se umjesto pokazivača koristi prenos parametara po referenci, samo što toga programer nije svjestan. U suštini, reference uvedene u jezik C++ se, na izvjestan način, ponašaju kao “veoma dobro prerušeni pokazivači”.

Jednom pokazivaču se može dodijeliti drugi pokazivač samo ukoliko oba pokazuju na posve isti tip

(uz neke vrlo rijetke iznimke, na koje ćemo ukazati kada se za to ukaže potreba). Na primjer, ukoliko imamo deklaraciju

int *pok1, *pok2;

tada je dodjela “pok1 = pok2” posve legalna, i nakon nje “pok1” i “pok2” pokazuju na istu lokaciju u memoriji (da bi ova dodjela imala smisla, prethodno je potrebno pokazivaču “pok2” dodijeliti adresu nekog drugog objekta). Slično vrijedi za dodjelu “pok2 = pok1”. S druge strane, ukoliko imamo deklaracije int *pok1; double *pok2;

Page 6: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

6

tada nije dozvoljena niti dodjela “pok1 = pok2” niti dodjela “pok2 = pok1”. Treba razlikovati ove dodjele od dodjela “*pok1 = *pok2” i “*pok2 = *pok1” koje su potpuno legalne, s obzirom da se dereferencirani pokazivači “pok1” i “pok2” ponašaju kao cjelobrojna odnosno realna promjenljiva, a međusobne dodjele između cjelobrojnih i realnih promjenljivih su dozvoljene (uz automatsku konverziju tipa). Također treba primijetiti da se pri deklaraciji zvjezdica treba stavljati ispred svake promjenljive za koju želimo da bude pokazivačka promjenljiva. Tako je u deklaraciji int *a, b; samo promjenljiva “a” pokazivačka promjenljiva (tipa “pokazivač na cijeli broj”, koji skraćeno obilježavamo kao tip “int *”), dok je “b” obična cjelobrojna promjenljiva (tipa “int”).

Mada u svojoj osnovi možemo reći da pokazivačke promjenljive najčešće u sebi interno sadrže također brojeve koji predstavljaju adrese nekih objekata (ovo nije posve tačno na arhitekturama kod kojih organizacija memorije nije linearna), jezici C i C++ pokazivačke promjenljive tretiraju posve drugačije nego obične brojčane promjenljive (jedan od razloga za to je i činjenica da nije dobro pretpostavljati ništa o tome kako je memorija zaista organizirana, s obzirom da bi smisao jezika trebala da što manje ovisi od svojstava hardvera na kojem se program izvršava). Stoga, mnoge aritmetičke operacije nad pokazivačkim promjenljivim nisu dozvoljene, a i one koje jesu dozvoljene interpretiraju se bitno drugačije nego sa brojčanim promjenljivim (na način koji ne ovisi od same arhitekture računara). Pokazivačke tipove treba shvatiti kao sasvim posebne tipove, bitno različite od cjelobrojnih tipova (bez obzira što su oni najčešće interno realizirani upravo kao cijeli brojevi). Na primjer, nije dozvoljeno sabrati, pomnožiti ili podijeliti dva pokazivača. Razlog za ovu zabranu je činjenica da se ne vidi na šta bi smisleno mogao pokazivati pokazivač koji bi se dobio recimo množenjem adresa dva druga objekta, čak i uz pretpostavku da su te adrese obični brojevi. Slično, nije dozvoljeno pomnožiti ili podijeliti pokazivač sa brojem, ili obrnuto. Također, mada se interna vrijednost pokazivača može ispisati na izlazni tok, nije dozvoljeno njihovo unošenje preko ulaznog toka (npr. tastature). Međutim, dozvoljeno je sabrati pokazivač i cijeli broj (i obratno), zatim oduzeti cijeli broj od pokazivača, kao i oduzeti jedan pokazivač od drugog. U nastavku ćemo razmotriti smisao ovih operacija.

Smisao pokazivačke aritmetike očitava se u slučaju kada pokazivač pokazuje na neki element niza

(kako operator referenciranja “&” kao svoj argument prihvata bilo kakvu l-vrijednost, njegov argument može biti i indeksirani element niza, za razliku od recimo izraza poput “&2” ili “&(a + 2)” koji su besmisleni). Pretpostavimo da imamo sljedeće deklaracije:

double niz[5] = {4.23, 7, 3.441, –12.9, 6.03}; double *p(&niz[2]);

Ovdje smo inicijalizirali pokazivač “p” tako da pokazuje na element niza “niz[2]”. Naravno, poput svih drugih promjenljivih, inicijalizacija pokazivačke promjenljive se može obaviti odmah prilikom njene deklaracije. Međutim, razmotrimo sada činjenicu da elementi niza tipa “double” sigurno ne mogu stati u jednu jednobajtnu memorijsku lokaciju. Pretpostavimo npr. da jedan element ovakvog niza zauzima 8 bajta (u skladu sa IEEE 754 standardom, koji se najčešće koristi). Podrazumijevajući linearni model memorije, kao i do sada, memorijska slika nakon ovih deklaracija mogla bi izgledati recimo ovako (adrese su ponovo izabrane posve nasumično):

Adrese ... 370 ÷ 377 378 ÷ 385 386 ÷ 393 394 ÷ 401 402 ÷ 409 ... 550 ... 4.23 7 3.441 –12.9 6.03 386

Niz p

Pokazivač “p” pokazuje na element “niz[2]”, a njegov stvarni sadržaj je adresa 386 (naravno, u skladu sa pretpostavljenim primjerom memorijske slike). Razmotrimo sada izraz “p + 1”. Ovaj izraz ponovo predstavlja pokazivač, ali koji pokazuje na element “niz[3]”. Stoga njegova stvarna vrijednost

Page 7: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

7

nije 387 (386+1) nego 394. Dakle, dodavanje jedinice na neki pokazivač daje kao rezultat novi pokazivač koji pokazuje na sljedeći element niza u odnosu na element na koji pokazuje izvorni pokazivač. Pri tome, stvarna adresa ne mora nužno biti povećana za 1. Zapravo, možemo zaključiti da se stvarna adresa koju sadrži pokazivač (za slučaj linearnog memorijskog modela) uvećava za veličinu objekta na koju pokazivač pokazuje (izraženu u bajtima). Međutim, ovakva konvencija omogućila je da programer ne mora da vodi brigu o tome koliko zaista neki element niza zauzima prostora: ako “p” pokazuje na neki element niza, “p + 1” pokazuje na sljedeći element istog niza, ma gdje da se on nalazio. Što je još značajnije, to vrijedi čak i u slučaju da memorija nije organizirana na linearan način. Izraz “p + 1” uvijek pokazuje na element niza koji neposredno slijedi elementu na koji pokazuje pokazivač “p”, ma kako da je memorija organizirana, i ma kakav da je interni sadržaj pokazivača “p”. To i jeste osnovni smisao pokazivačke aritmetike. Analogno tome, izraz “p – 1” predstavlja pokazivač koji pokazuje na prethodni element niza u odnosu na element na koji pokazuje pokazivač “p” (u navedenom primjeru na element “niz[1]”). Generalno, izraz oblika “p + n” odnosno “p – n” gdje je “p” pokazivač a “n” cijeli broj (odnosno ma kakav cjelobrojni izraz) predstavlja novi pokazivač koji pokazuje na element niza čiji je indeks veći odnosno manji za “n” u odnosu na indeks elementa niza na koji pokazuje pokazivač “p”. Stoga će sljedeća naredba, za niz iz razmatranog primjera, ispisati vrijednosti “3.441”, “−12.9” i “6.03”:

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

Uočite razliku između izraza “*(p + i)” i izraza “*p + i” (odnosno “(*p) + i”) koji, kao što smo ranije vidjeli, imaju posve drugačije značenje. Također, kako je svaki dereferencirani pokazivač l-vrijednost, to su izrazi poput izraza *(p – 1) = 8.5

sasvim smisleni (ovaj izraz će postaviti sadržaj elementa niza “niz[1]” na vrijednost “8.5”).

Bitno je napomenuti da je smisao pokazivačke aritmetike preciziran standardima jezika C i C++ samo za slučaj kada pokazivač pokazuje na neki element nekog niza. Za slučaj kada pokazivač pokazuje na neki drugi objekat (npr. na običnu promjenljivu), smisao pokazivačke aritmetike nije definiran. Na primjer, za ranije definiran pokazivač “pokazivac” koji je pokazivao na cjelobrojnu promjenljivu “broj”, smisao izraza “pokazivac + 1” i “pokazivac – 1” nije preciziran standardom (tj. oni su sintaksno ispravni, ali nije precizirano šta im je vrijednost, stoga se ne smiju koristiti). Za slučaj linearnih memorijskih modela, ovi izrazi bi se gotovo sigurno interpretirali kao pokazivači koji pokazuju na mjesta u memoriji čije su adrese veće odnosno manje od adrese zapisane u pokazivaču “pokazivac” za onoliko bajtova koliko zauzima promjenljiva “broj”. Međutim, ako memorijski model nije linearan, interpretacije zaista mogu biti svakakve. Stoga se trebamo pridržavati standarda koji strogo naglašava da pokazivačku aritmetiku treba koristiti samo ukoliko pokazivač pokazuje na neki element niza. Ovog pravila se zaista moramo pridržavati strogo: u nekim slučajevima operativni sistem ima pravo da prekine izvršavanje programa u kojem je pokazivač “odlutao” zbog pogrešne primjene pokazivačke aritmetike, čak i ako se “zalutali” pokazivač uopće ne dereferencira (ovo se dešava iznimno rijetko, ali se dešava)!

Pokazivačka aritmetika nije uvijek definirana čak ni za slučaj pokazivača koji pokazuju na elemente

niza. Postoji još pravilo koje kaže da su izrazi oblika “p + n” odnosno “p – n” gdje je “p” pokazivač koji pokazuje na neki element niza a “n” cjelobrojni izraz definirani samo ukoliko novodobijeni pokazivač ponovo pokazuje na neki element niza koji zaista postoji u skladu sa specificiranom dimenzijom niza, ili eventualno na mjesto koje se nalazi neposredno iza posljednjeg elementa niza. Drugim riječima, vrijednost izraza “p – n” postaje nedefinirana ukoliko novodobijeni pokazivač “odluta” ispred prvog elementa niza, a vrijednost izraza “p + n” postaje nedefinirana ukoliko novodobijeni pokazivač “odluta” daleko iza posljednjeg elementa niza. Za konkretan primjer niza “niz” iz maloprije navedenog primjera koji sadrži 5 elemenata i pokazivača “p” koji je postavljen da pokazuje na element “niz[2]”, izrazi “p + 1”, “p + 2”, “p + 3”, “p – 1” i “p – 2” su precizno definirani, za razliku od izraza “p + 4” ili “p – 3” koji nisu, jer su “odlutali” u “nepoznate vode” (njihov sadržaj je nepredvidljiv, posebno u slučajevima kada memorijski model nije linearan).

Page 8: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

8

Nad pokazivačkim promjenljivim se mogu primjenjivati i operatori “+=”, “–=”, “++” i “––” čije je značenje analogno kao za obične promjenljive, samo što se koristi pokazivačka aritmetika. Na primjer, izraz “p += 2” će promijeniti sadržaj pokazivačke promjenljive “p” tako da pokazuje na element niza koji se nalazi dvije pozicije naviše u odnosu na element niza na koji “p” trenutno pokazuje, dok će izraz “––p” promijeniti sadržaj pokazivačke promjenljve tako da pokazuje na prethodni element niza u odnosu na element na koji “p” trenutno pokazuje (u oba slučaja, “p” postaje nedefiniran u slučaju da odlutamo izvan prostora koji niz zauzima; eventualno se smijemo pozicionirati tik iza kraja niza). Na taj način moguće je koristiti pokazivače čija vrijednost tokom izvršavanja programa pokazuje na razne elemente niza, kao da se pokazivač “kreće” odnosno “klizi” kroz elemente niza. Ovakve pokazivače obično nazivamo klizeći pokazivači (engl. sliding pointers), kao što je ilustrirano u sljedećem primjeru (u kojem pretpostavljamo da su “niz” i “p” deklarirani kao u ranijim primjerima):

p = &niz[0];

for(int i = 0; i < 5; i++) { cout << *p; p++;

} Dereferenciranje i inkrementiranje (odnosno dekrementiranje) pokazivača se često obavlja u istom

izrazu, što je ilustrirano u sljedećem primjeru (koji je ekvivalentan prethodnom): p = &niz[0];

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

U posljednjem primjeru, izraz “*p++” interpretira se kao izraz “*(p++)” a ne kao izraz “(*p)++” koji ima drugačije, ranije objašnjeno značenje.

Oba navedena primjera su posve legalna i korektna. Međutim, sljedeći primjer, koji bi trebao da ispiše elemente niza u obrnutom poretku, nije legalan (iako je sintaksno ispravan), bez obzira što će u većini slučajeva raditi ispravno:

p = &niz[4]; for(int i = 0; i < 5; i++) { cout << *p; p--;

} Isto vrijedi i za sažetu varijantu ovog primjera:

p = &niz[4]; for(int i = 0; i < 5; i++) cout << *p--;

U čemu je problem? Poteškoća leži u činjenici da nakon završetka ovih sekvenci, pokazivač “p” pokazuje ispred prvog elementa niza, što po standardu nije legalno. Operativni sistem ima pravo da prekine program u kojem se generira takav pokazivač ukoliko mu njegovo postojanje ugrožava rad (to se vrlo vjerovatno neće desiti, ali se može desiti). Rješenje ovog problema je posve jednostavno – nemojmo generirati takve pokazivače. Na primjer, možemo “p” na početku postaviti da pokazuje tačno iza posljednjeg elementa niza, tj. na nepostojeći element niza “niz[5]” (što je, začudo, legalno – na ovom mjestu nećemo ulaziti u razloge zašto) i umanjivati pokazivač prije nego što ga dereferenciramo, kao u sljedećem (legalnom) primjeru:

p = &niz[5]; for(int i = 0; i < 5; i++) { p--; cout << *p;

} ili u njegovoj kompaktnijoj varijanti:

Page 9: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

9

p = &niz[5]; for(int i = 0; i < 5; i++) cout << *--p;

Pokazivači se mogu porediti pomoću operatora “<“, “>”, “<=”, “>=”, “==” i “!=” pod uvjetom da

pokazuju na isti tip. Dva pokazivača na različite tipove (npr. pokazivači na tipove “int” i “double”) ne mogu se porediti, čak i ukoliko su tipovi na koje pokazivači pokazuju uporedivi. Pokušaj poređenja pokazivača koji pokazuju na različite tipove prijavljuje se kao sintaksna greška od strane kompajlera. Zbog toga, pretpostavimo da imamo dva pokazivača koji pokazuju na objekte istog tipa. Rezultat poređenja primjenom operatora “==” i “!=” je uvijek jasno definiran: dva pokazivača su jednaka ako i samo ako pokazuju na isti objekat. Međutim, rezultat poređenja dva pokazivača primjenom ostalih relacionih operatora je precizno definiran standardnom samo za slučaj kada oba pokazivača pokazuju na elemente jednog te istog niza. Na primjer, neka su “p1” i “p2” dva pokazivača koji pokazuju na neke elemente istog niza (eventualno, oni smiju pokazivati i na fiktivni prostor iza posljednjeg elementa niza). Pokazivač “p1” je manji od pokazivača “p2” ukoliko “p1” pokazuje na element niza sa manjim indeksom u odnosu na element na koji pokazuje “p2”, a veći ukoliko pokazuje na element niza sa većim indeksom u odnosu na element na koji pokazuje “p2”. S druge strane, ukoliko pokazivači “p1” i “p2” ne pokazuju na dva elementa istog niza, pojmovi “veći” i “manji” nisu definirani, tako da rezultat poređenja zavisi od implementacije. Stoga se takva poređenja ne smiju koristiti. Ukoliko je memorijski model linearan, sva poređenja pokazivača se obično interno realiziraju prostim poređenjem brojčanih vrijednosti adresa pohranjenih u pokazivaču, tako da su rezultati poređenja praktično uvijek u skladu sa intuicijom, čak i ukoliko kršimo pravila kada je poređenje pokazivača legalno (npr. kod linearnih modela uvijek je manji onaj pokazivač koji pokazuje na nižu adresu). Međutim, kod memorijskih modela koji nisu linearni, svakakva iznenađenja su moguća. Da biste spriječili moguća iznenađenja, samo poštujte pravilo: nemojte porediti pokazivače koji ne pokazuju na elemente istog niza. Također, bitno je uočiti razliku između poređenja poput “p1 == p2“ koje poredi dva pokazivača (koji mogu biti različiti čak i ukoliko su sadržaji lokacija na koje oni pokazuju isti) i poređenja poput “*p1 == *p2“ koje poredi sadržaje lokacija na koje pokazuju dva pokazivača (koji mogu biti isti čak i ukoliko su pokazivači različiti).

Mada nije moguće sabrati, pomnožiti ili podijeliti dva pokazivača, moguće je oduzeti jedan

pokazivač od drugog, pod uvjetom da su oba pokazivača istog tipa. Pri tome, rezultat oduzimanja je, slično kao za slučaj poređenja, precizno definiran samo za slučaj kada oba pokazivača pokazuju na elemente istog niza. Po definiciji, rezultat oduzimanja takva dva pokazivača je cijeli broj (ne pokazivač) koji je jednak razlici indeksa elemenata niza na koje pokazivači pokazuju. Na primjer, ukoliko pokazivač “p1” pokazuje na element niza “niz[4]” a pokazivač “p2” na element niza “niz[1]”, tada je vrijednost izraza “p1 – p2” cijeli broj 3 (tj. 4 – 1). Ovakva definicija obezbjeđuje da vrijedi pravilo da je vrijednost izraza “p1 + (p2 – p1)” uvijek jednaka vrijednosti “p2”, što je u skladu sa intuicijom.

Zahvaljujući mogućnosti poređenja pokazivača, prethodni primjeri za ispis elemenata niza uz

korištenje klizećih pokazivača mogu se napisati tako da se uopće ne koristi pomoćna promjenljiva “i”, već samo klizeći pokazivač. Na primjer, elemente niza u standardnom poretku možemo ispisati ovako:

for(double *p = &niz[0]; p < &niz[5]; p++) cout << *p;

Pri ispisu unazad ponovo treba biti oprezniji, zbog mogućnosti da pokazivač “odluta” ispred prvog elementa niza. Na primjer, sljedeći primjer nije korektan (u skladu sa standardom):

for(double *p = &niz[4]; p >= &niz[0]; p--) cout << *p;

Naime, nakon što “p” dostigne prvi element niza, vrijednost izraza “p––” postaje nedefinirana, pa je pogotovo nedefinirano šta je rezultat poređenja “p >= &niz[0]”. Stoga je veoma upitno kada će napisana petlja uopće završiti (i da li će uopće završiti). Ovo nije samo prazno filozofiranje: napisani primjer provjereno neće raditi sa C i C++ kompajlerima za MS DOS operativni sistem (kod kojeg memorijski model nije linearan) u slučajevima kada je niz “niz” deklariran kao statički ili globalni. Sigurno je moguće navesti još mnoštvo situacija u kojima ovaj primjer ne radi kako treba. Rješenje ovog problema je isto kao u ranijim primjerima: ne smijemo dozvoliti da pokazivačka aritmetika dovede do

Page 10: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

10

izlaska pokazivača izvan dozvoljenog opsega. Sljedeća varijanta je, na primjer, korektna (sjetimo se da se u “for” petlji bilo koji od njena tri argumenta mogu izostaviti – u ovom primjeru izostavljen je treći argument):

for(double *p = &niz[5]; p > &niz[0];) cout << *--p;

Treba naglasiti da su veoma rijetke knjige o programskim jezicima C i C++ u kojima se upozorava na opisanu opasnost pokazivačke aritmetike, koja posebno dolazi do izražaja kada klizeće pokazivače koristimo za kretanje kroz niz naniže.

Pokazivači su tijesno povezani sa nizovima. Naime, kad god se ime niza upotrijebi samo za sebe, bez navođenja indeksa u uglastim zagradama, ono se automatski konvertira u pokazivač (odgovarajućeg tipa) koji pokazuje na prvi element niza (preciznije, u konstantni pokazivač, što ćemo precizirati malo kasnije). Drugim riječima, ukoliko je “niz” neki niz, tada je izraz “niz” potpuno ekvivalentan izrazu “&niz[0]”. Zbog automatske konverzije nizova u pokazivače, slijedi da se pokazivačka aritmetika može primijeniti i na nizove. Razmotrimo, na primjer, izraz “niz + 2”. Ovaj izraz je, na prvi pogled, besmislen. Međutim, on je ekvivalentan izrazu “&niz[0] + 2” koji je, zahvaljujući pokazivačkoj aritmetici (ne zaboravimo da je izraz “&niz[0]“ pokazivač), ekvivalentan izrazu “&niz[2]“, što je lako provjeriti. Dakle, izrazi “niz + 2” i “&niz[2]“ su ekvivalentni. Slijedi da elemente niza “niz” možemo ispisati i ovako:

for(double *p = niz; p < niz + 5; p++) cout << *p;

Naravno, iz istog razloga, sasvim je legalna i sljedeća konstrukcija: for(int i = 0; i < 5; i++) cout << *(niz + i);

Iz ovoga vidimo da su, zahvaljujući pokazivačkoj aritmetici i automatskoj konverziji nizova u pokazivače, izrazi “niz[i]”, i “*(niz + i)” gdje je “niz” ma kakva nizovna promjenljiva zapravo sinonimi. Ovim je u jezicima C i C++ uspostavljena veoma tijesna veza između pokazivača i nizova. Ovi jezici su pomenutu vezu učinili još tijesnijom (na način kakav ne postoji niti u jednom drugom programskom jeziku) uvođenjem konvencije da izrazi oblika “a[n]” i “*(a + n)” uvijek budu potpuni sinonimi, bez obzira da li je “a” ime niza ili pokazivač. Drugim riječima, indeksiranje se može primijeniti i na pokazivače! Jednostavno, izraz oblika “a[n]” u slučaju kada “a” nije ime niza, po definiciji se interpretira kao “*(a + n)”, bez obzira kojeg je tipa “a”. Stoga je izraz “a[n]” ispravan kad god se zbir “a + n” može interpretirati kao pokazivač (jedna pomalo čudna posljedica ove činjenice je da su sinonimi i izrazi poput “niz[3]” i “3[niz]” mada se ovo svojstvo teško može iskoristiti za nešto pametno). Indeksiranje primjenjeno na pokazivače ilustriraćemo sljedećim primjerom koji ispisuje elemente 4.23, 7, 3.441 i –12.9 (uz pretpostavku da je niz “niz” deklariran kao u prethodnim primjerima) i koji pokazuje kako se u jeziku C++ mogu simulirati nizovi sa negativnim indeksima kakvi postoje u nekim drugim programskim jezicima (npr. u Pascalu i FORTRAN-u):

double *pok(niz + 2); for(int i = –2; i <= 1; i++) cout << pok[i];

Podsjetimo se da izraz “niz + 2” ima isto značenje kao i izraz “&niz[2]”. Kasnije ćemo vidjeti i mnoge druge korisne primjene činjenice da se indeksiranje može primijeniti na pokazivače. Međutim, treba napomenuti da sljedeći primjer, koji se čak može naći u nekim knjigama o jezicima C i C++, nije legalan. Programer je htio da iskoristi indeksiranje pokazivača sa ciljem simulacije nizova čiji indeksi počinju od jedinice umjesto od nule:

double *pok(niz – 1); for(int i = 1; i <= 5; i++) cout << pok[i];

Problem sa ovim primjerom je u tome što izraz “niz – 1” nije legalan, u skladu sa već opisanim pravilima vezanim za pokazivačku aritmetiku (isto vrijedi i za njemu ekvivalentan izraz “&niz[–1]”).

Page 11: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

11

Stoga ovaj primjer može ali i ne mora raditi, ovisno od konkretnog kompajlera i operativnog sistema. Postoji velika vjerovatnoća da će on raditi ispravno (inače ga ne bi citirali u mnogim knjigama), ali postoji vjerovatnoća i da neće. To je, bez sumnje, sasvim dovoljan razlog da ne koristimo ovakva rješenja.

Bez obzira na tijesnu vezu između pokazivača i nizova, na imena nizova ne mogu se primijeniti operatori “+=”, “–=”, “++” i “––” koji mijenjaju sadržaj pokazivačke promjenljive, odnosno lokaciju na koju oni pokazuju. Imena nizova (sama za sebe, bez indeksiranja) uvijek se tretiraju kao pokazivači na prvi element niza, i to se ne može promijeniti. Oni su uvijek vezani za prvi element niza. Preciznije, ime niza upotrijebljeno samo za sebe konvertira se u konstantni (nepromjenljivi) pokazivač na prvi element niza. Stoga se imena nizova ne mogu koristiti kao klizeći pokazivači.

Treba napomenuti da su jedini izuzeci u kojima se ime niza upotrijebljeno samo za sebe ne

konvertira u odgovarajući pokazivač slučajevi kada se ime niza upotrijebi kao argument operatora “sizeof” ili “&”. Tako, npr. “sizeof niz” daje veličinu čitavog niza a ne veličinu pokazivača. Također, izraz “&niz” ne predstavlja adresu pokazivača, već adresu prvog elementa niza. Naravno, izrazi “niz” ili “&niz[0]” također predstavljaju adresu prvog elementa niza, ali oni nisu ekvivalentni izrazu “&niz”. Naime, tip izraza “niz” ili “&niz[0]” je “pokazivač na element niza” (npr. pokazivač na cijeli broj). S druge strane, tip izraza “&niz” (koji se inače vrlo rijetko koristi) je “pokazivač na čitav niz” (odnosno “pokazivač na niz kao cjelinu”). Drugim riječima, tipovi izraza “niz” (ili “&niz[0]”) i “&niz” se razlikuju (iako im je vrijednost ista). Pokazivači na “nizove kao cjeline” koriste se jedino kod dinamičke alokacije višedimenzionalnih nizova, i o njima ćemo govoriti kada budemo obrađivali tu tematiku.

Kako se nizovi po potrebi automatski konvertiraju u pokazivače kada se upotrijebe bez indeksa, to će

svaka funkcija koja kao svoj formalni parametar očekuje pokazivač na neki tip kao stvarni parametar prihvatiti niz elemenata tog tipa (naravno, formalni parametar će pri tome pokazivati na prvi element niza). Međutim, interesantno je da vrijedi i obrnuta situacija: funkciji koja kao formalni parametar očekuje niz možemo kao stvarni parametar proslijediti pokazivač odgovarajućeg tipa. Zapravo, jezici C i C++ uopće ne prave razliku između formalnog parametra tipa “pokazivač na tip Tip” i formalnog parametra tipa “Niz elemenata tipa Tip” (u oba slučaja funkcija dobija samo adresu prvog elementa niza). Drugim riječima, formalni parametri nizovnog tipa, ne samo da se mogu koristiti kao pokazivači, već oni zaista i jesu pokazivači, bez obzira što u općem slučaju, nizovi i pokazivači nisu u potpunosti ekvivalentni, kao što smo već naglasili. Zbog tog razloga, operator “sizeof” neće dati ispravnu veličinu niza ukoliko se primijeni na formalni argument nizovnog tipa (naime, kao rezultat ćemo dobiti veličinu pokazivača).

Činjenica da su formalni argumenti nizovnog tipa zapravo pokazivači, omogućava nam da funkcije

koje obrađuju nizove možemo realizirati i preko klizećih pokazivača. Na primjer, funkciju koja će ispisati na ekranu elemente niza realnih brojeva možemo umjesto uobičajenog načina (sa “for” petljom i indeksiranjem elemenata niza) realizirati i na sljedeći način:

void IspisiNiz(double *pokazivac, int n) { for(int i = 0; i < n; i++) cout << *pokazivac++ << endl; }

Ovu funkciju možemo pozvati na posve uobičajeni način: IspisiNiz(niz, 5); Nikakve razlike ne bi bilo ni da smo formalni parametar deklarirali kao “double pokazivac[]”. U oba slučaja on bi se mogao koristiti kao klizeći pokazivač. Dakle, za formalne parametre nizovnog tipa ne vrijedi pravilo da su uvijek vezani isključivo za prvi element niza. Oni se mogu koristiti u potpunosti kao i pravi pokazivači (iz prostog razloga što oni to i jesu). Zapravo, mogućnost da se formalni parametri deklariraju kao da su nizovnog tipa podržana je samo sa ciljem da se jasnije izrazi namjera da funkcija kao stvarni parametar očekuje niz. Deklaracija formalnog parametra kao pokazivača početniku bi svakako djelovala mnogo nejasnije.

Page 12: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

12

Moguće je deklarirati i pokazivače na konstantne objekte. Na primjer, deklaracijom

const double *p; deklariramo pokazivač “p“ na konstantnu realnu vrijednost (tip ovog pokazivača je “const double *”, za razliku od pokazivača na nekonstantnu realnu vrijednost, čiji je tip prosto “double *”). Ovakvi pokazivači omogućavaju da se sadržaj lokacije na koju oni pokazuju čita, ali ne i da se mijenja. Na primjer, sa ovakvim pokazivačem izrazi “cout << *p” i “broj = p[2]” su dozvoljeni (pod uvjetom da je promjenljiva “broj” deklarirana kao realna promjenljiva), ali izrazi “*p = 2.5” i “p[2] = 0” nisu. Odavde odmah vidimo da se formalni parametar “pokazivac” u prethodnoj funkciji mogao deklarirati kao “const double *pokazivac”, što bi čak bilo veoma preporučljivo. Naime, na taj način bismo spriječili eventualne nehotične promjene objekta na koji pokazivač “pokazivac” pokazuje.

Na ovom mjestu potrebno je razjasniti neke česte zablude vezane za pokazivače na konstantne objekte. Prvo, pokazivač na konstantni objekat neće učiniti objekat na koji on pokazuje konstantom. Na primjer, ako je “p” pokazivač na konstantnu realnu vrijednost, a “broj” neka realna (nekonstantna) promjenljiva, dodjela “p = &broj” neće učiniti promjenljivu “broj“ konstantom. Promjenljiva “broj” će se i dalje moći mijenjati, ali ne preko pokazivača “p”! Ipak, adresu konstantnog objekta (npr. adresu neke konstante) moguće je dodijeliti samo pokazivaču na konstantni objekat. Također, pokazivaču na nekonstantni objekat nije moguće (bez eksplicitne primjene operatora za konverziju tipa) dodijeliti pokazivač na konstantni objekat (čak i ukoliko su tipovi na koje oba pokazivača pokazuju isti), jer bi nakon takve dodjele preko pokazivača na nekonstantni objekat mogli promijeniti sadržaj konstantnog objekta, što je svakako nekonzistentno. Obrnuta dodjela je legalna, tj. pokazivaču na konstantni objekat moguće je dodijeliti pokazivač na nekonstantni objekat istog tipa, s obzirom da takva dodjela ne može imati nikakve štetne posljedice.

Treba primijetiti da se sadržaj pokazivača na konstantne objekte može mijenjati, tj. sam pokazivač

nije konstantan. To treba razlikovati od konstantnih pokazivača, čija se vrijednost ne može mijenjati, ali se sadržaj objekata na koji oni pokazuju može mijenjati (npr. imena nizova upotrijebljena sama za sebe konvertiraju se upravo u konstantne pokazivače). Konstantni pokazivači mogu se deklarirati na sljedeći način (primijetimo da se kvalifikator “const” ovdje piše iza zvjezdice): double *const p(&niz[2]); Primijetimo da se konstantni pokazivač mora odmah inicijalizirati, jer mu se naknadno vrijednost više ne može mijenjati. Naravno, sada operacije poput “p++” ili “p += 2” koje bi promijenile sadržaj pokazivača “p” nisu dozvoljene (s obzirom da je “p” konstantan pokazivač), ali je izraz “p + 2” posve legalan (i predstavlja konstantni pokazivač koji pokazuje na element niza “niz[4]”). Konačno, moguće je deklarirati i konstantni pokazivač na konstantni objekat, npr. deklaracijom poput sljedeće: const double *const p(&Niz[2]);

Može se postaviti pitanje zašto se uopće patiti sa pokazivačima i koristiti pokazivačku aritmetiku ukoliko se ista stvar može obaviti i uz pomoć indeksiranja. Ako zanemarimo dinamičku alokaciju memorije i dinamičke strukture podataka, o kojima će kasnije biti govora i koji se ne mogu izvesti bez upotrebe pokazivača, osnovni razlog je efikasnost i fleksibilnost. Naime, mnoge radnje se mogu neposrednije obaviti upotrebom klizećih pokazivača nego pomoću indeksiranja, naročito u slučajevima kada se elementi niza obrađuju sekvencijalno, jedan za drugim. Pored toga, pokazivač koji pokazuje na neki element niza sadrži u sebi cjelokupnu informaciju koja je potrebna da se tom elementu pristupi. Kod upotrebe indeksiranja ta informacija je razbijena u dvije neovisne cjeline: ime niza (odnosno adresa prvog elementa niza) i indeks posmatranog elementa.

Navedimo jedan klasični školski primjer koji se navodi u gotovo svim udžbenicima za programski

jezik C (nešto rjeđe za C++, jer se C++ ne hvali toliko pokazivačima koliko C), koji ilustrira efikasnost upotrebe klizećih pokazivača. Naime, razmotrimo kako bi se mogla napisati funkcija nazvana “KopirajString”, sa dva parametra od kojih su oba nizovi znakova, koja kopira sve znakove drugog

Page 13: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

13

niza u prvi niz, sve do “NUL” graničnika (odnosno znaka sa ASCII šifrom 0, koja po konvenciji jezika C predstavlja oznaku kraja niza znakova, odnosno stringa), uključujući i njega. Zanemarimo ovdje činjenicu da praktično identična funkcija, pod imenom “strcpy”, već postoji kao standardna funkcija u biblioteci “cstring”. Vjerovatno najočiglednija verzija funkcije “KopirajString” mogla bi izgledati ovako (kvalifikator “const” kod imena nizovnog parametra “izvorni” samo ukazuje da funkcija ne mijenja sadržaj ovog niza):

void KopirajString(char odredisni[], const char izvorni[]) { int i(0); while(izvorni[i] != 0) { odredisni[i] = izvorni[i]; i++; } odredisni[i] = 0; } Zamijenimo sada parametre “odredisni” i “izvorni” klizećim pokazivačima (strogo uzevši, oni

to već i sada jesu, ali ćemo ipak izmijeniti i njihovu deklaraciju, da jasnije istaknemo namjeru da ih želimo koristiti kao klizeće pokazivače). Odmah vidimo da nam indeks “i” više nije potreban:

void KopirajString(char *odredisni, const char *izvorni) { while(*izvorni != 0) { *odredisni = *izvorni; odredisni++; izvorni++; } *odredisni = 0; }

Dalje, oba inkrementiranja možemo obaviti u istom izrazu u kojem vršimo dodjelu. Također, test različitosti od 0 možemo izostaviti (zbog automatske konverzije svake nenulte vrijednosti u logičku vrijednost “true”) čime dobijamo sljedeću verziju funkcije:

void KopirajString(char *odredisni, const char *izvorni) { while(*izvorni) *odredisni++ = *izvorni++; *odredisni = 0; }

Konačno, kako je u jezicima C i C++ dodjela također izraz, rezultat izvršene dodjele je zapravo rezultat izraza “*izvorni” (inkrementiranje se obavlja naknadno) koji je identičan sa izrazom u uvjetu “while” petlje. Stoga se funkcija može svesti na sljedeći oblik:

void KopirajString(char *odredisni, const char *izvorni) { while(*odredisni++ = *izvorni++); }

Djeluje nevjerovatno, zar ne!? Primijetimo da je nestala potreba čak i za dodjelom “*Odredisni = 0”, jer je lako uočiti da će ovako napisana petlja iskopirati i “NUL” graničnik. Ovaj primjer veoma ilustrativno pokazuje u kojoj mjeri se klizeći pokazivači mogu iskoristiti za optimizaciju kôda (doduše, po cijenu čitljivosti i razumljivosti, mada se na pokazivačku aritmetiku programeri brzo naviknu kada je jednom počnu koristiti). Funkcija “strcpy” iz biblioteke “cstring” implementirana je upravo ovako. Odnosno, ne baš u potpunosti: prava funkcija “strcpy” iz biblikoteke “cstring” implementirana je tačno ovako (ako zanemarimo da su imena promjenljivih u stvarnoj implementaciji vjerovatno na engleskom jeziku, što je naravno posve nebitno):

char *strcpy(char *odredisni, const char *izvorni) { char *pocetak(odredisni); while(*odredisni++ = *izvorni++); return pocetak; }

Page 14: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

14

Praktično jedina razlika između maločas napisane funkcije “KopirajString” i (prave) funkcije “strcpy” je u tome što funkcija “strcpy”, pored toga što obavlja kopiranje, vraća kao rezultat pokazivač na prvi element niza u koji se vrši kopiranje (isto svojstvo posjeduje i funkcija “strcat”, također prisutna u biblioteci “cstring”, koja nadovezuje niz znakova koji je zadan drugim parametrom na kraj niza znakova koji je predstavljen prvim parametrom, odnosno vrši dodavanje znakova iza nul-graničnika prvog niza znakova, uz odgovarajuće pomjeranje graničnika). Ovaj primjer ujedno demonstrira da funkcija može kao rezultat vratiti pokazivač. Povratna vrijednost koju vraćaju funkcije “strcpy” i “strcat” najčešće se ignorira, kao što smo i mi radili u svim dosadašnjim primjerima. Međutim, ova povratna vrijednost može se nekada korisno upotrijebiti za ulančavanje funkcija “strcpy” i/ili “strcat”. Na primjer, posmatrajmo sljedeću sekvencu naredbi (uz pretpostavku da su “recenica”, “rijec1”, “rijec2” i “rijec3” propisno deklarirani nizovi znakova): strcpy(recenica, rijec1); strcat(recenica, rijec2); strcat(recenica, rijec3); cout << recenica; Zahvaljujući činjenici da funkcije “strcpy” i “strcat” vraćaju pokazivač na odredišni niz, ove četiri naredbe možemo kompaktnije zapisati u vidu sljedeće naredbe: cout << strcat(strcat(strcpy(recenica, rijec1), rijec2), rijec3);

Treba napomenuti da pomenuto svojstvo funkcija “strcat” i “strcpy” može početnika da dovede

u zabludu. Naime, mnogi početnici često isprobaju naredbu poput sljedeće: cout << strcat("Dobar ", "dan!");

Postoji velika vjerovatnoća da će ova naredba zaista ispisati pozdrav “Dobar dan!” na ekran (uskoro ćemo vidjeti zašto), iz čega početnik može zaključiti da je ona ispravna. Međutim, ovakva naredba je strogo zabranjena, jer dovodi do prepisivanja sadržaja stringovne konstante. O ovoj temi treba malo detaljnije prodiskutovati, s obzirom da ona može biti izvor brojnih frustracija. Da bismo bolje objasnili šta se ovdje dešava, razmotrimo sljedeću naredbu (u kojoj se, ukoliko će to olakšati razumijevanje, umjesto bibliotečke funkcije “strcpy” može koristiti i maločas napisana funkcija “KopirajString”): strcpy("abc", "def");

Mada će ovu konstrukciju kompajler prihvatiti (u boljem slučaju uz prijavu upozorenja), ona zapravo prepisuje sadržaj memorije u kojem se čuva stringovna konstanta "abc" sa sadržajem stringovne konstante "def". Posljedice ovog su posve nepredvidljive. Jedna mogućnost je da će svaka upotreba stringovne konstante "abc" u programu dovesti do efekta kao da je upotrijebljena stringovna konstanta "def". Druga mogućnost je da dođe do prekida rada programa od strane operativnog sistema (ovo se može desiti ukoliko kompajler sadržaje stringovnih konstanti čuva u dijelu memorije u kojem operativni sistem ne dozvoljava nikakav upis). Mada nekome efekat zamjene jedne stringovne konstante drugom može djelovati kao “zgodan trik”, njegova upotreba je, čak i u slučaju da je konkretan kompajler i operativni sistem dozvoljavaju, izuzetno loša taktika koju ne bi trebalo koristiti ni po koju cijenu (činjenica da je po standardu jezika C ovakav poziv načelno dozvoljen ne znači da ga treba ikada koristiti). Još gore posljedice može imati naredba

strcpy("abc", "defghijk"); U ovom slučaju se duža stringovna konstanta "defghijk" pokušava prepisati u dio memorije u kojem se čuva kraća stringovna konstanta "abc", što ne samo da će prebrisati ovu stringovnu konstantu, nego će i prebrisati sadržaj nekog dijela memorije za koji ne znamo kome pripada (za stringovnu konstantu "abc" rezervirano je tačno onoliko prostora koliko ona zauzima, tako da već prostor koji slijedi može pripadati nekom drugom). Naravno, posljedice su potpuno nepredvidljive i nikada nisu bezazlene. Kao zaključak, nikada ne smijemo stringovnu konstantu proslijediti kao stvarni parametar nekoj funkciji koja

Page 15: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

15

mijenja sadržaj znakovnog niza koji joj je deklariran kao formalni parametar (ovo je tako opasna stvar da bolji kompajleri imaju opciju kojom se može uključiti da se takav pokušaj tretira kao sintaksna greška). Kao konkretan primjer, kao prvi argument funkcije “strcpy” (odnosno “KopirajString”) smije se zadavati isključivo niz za koji eksplicitno znamo da mu se sadržaj smije mijenjati (recimo, neka promjenljiva nizovnog tipa), i koji je dovoljno velik da primi sadržaj niza koji se u njega kopira.

Nakon ovog objašnjenja, postaje jasno šta nije u redu sa pozivom “strcat("Dobar ", "dan!")”. Naime, ovaj poziv bi pokušao da nadoveže stringovnu konstantu "dan!" na kraj onog dijela memorije gdje se čuva stringovna konstanta "Dobar ", čime ne samo da bi promijenio sadržaj ove stringovne konstante, nego bi i prebrisao sadržaj memorijskih lokacija koje joj ne pripadaju. Posljedice ovakvog “napada” mogu se odraziti znatno kasnije u programu, što može biti veoma frustrirajuće. Ako nam operativni sistem odmah prekine izvođenje takvog programa zbog zabranjenih akcija, još možemo smatrati da smo imali sreće. Stoga, možemo zaključiti da funkciju “strcat” ima smisla koristiti samo ukoliko je njen prvi argument niz koji već sadrži neki string (eventualno prazan) i koji je dovoljno veliki da može prihvatiti string koji će nastati nadovezivanjem. Možemo istu stvar reći i ovako: korisnici koji poznaju neke druge programske jezike mogu pomisliti da funkcija “strcat” vraća kao rezultat string koji je nastao nadovezivanjem prvog i drugog argumenta (ne mijenjajući svoje argumente), s obzirom da takve funkcije postoje u mnogim programskim jezicima. Međutim, to nije slučaj sa funkcijom “strcat” iz jezika C++: ona nadovezuje drugi argument na prvi, pri tome mijenjajući prvi argument. U neku ruku, razlika između funkcije “strcat” i srodnih funkcija u drugim programskim jezicima najlakše se može uporediti sa razlikom koja postoji između izraza “x += y” i “x + y”.

Pokazivačka aritmetika može se veoma lijepo iskoristiti zajedno sa funkcijama koje barataju sa klasičnim nul-terminiranim stringovima u stilu jezika C, poput maločas napisane funkcije “KopirajString”, odnosno bibliotečkih funkcija iz biblioteke “cstring” poput “strcpy”, “strcat”, “strcmp” itd. Na primjer, ukoliko je potrebno iz niza znakova “recenica” izdvojiti sve znakove od desetog nadalje u drugi niz znakova “recenica2”, to najlakše možemo uraditi na jedan od sljedeća dva načina (oba su ravnopravna):

strcpy(recenica2, &recenica[9]); strcpy(recenica2, recenica + 9); Ista tehnika može se iskoristiti i sa funkcijama koje prihvataju proizvoljne vrste nizova kao

parametre, odnosno koje prihvataju pokazivače na proizvoljne tipove. Na primjer, pretpostavimo da želimo iz ranije deklariranog niza realnih brojeva “niz” ispisati samo drugi, treći i četvrti element (tj. elemente sa indeksima 1, 2 i 3). Nema potrebe da prepravljamo već napisanu funkciju “IspisiNiz”, s obzirom da ovaj cilj možemo veoma jednostavno ostvariti jednim od sljedeća dva poziva, bez obzira da li je funkcija napisana da prihvata pokazivače ili nizove kao svoj prvi argument, i bez obzira da li je napisana korištenjem indeksiranja ili klizećih pokazivača:

IspisiNiz(&niz[1], 3); IspisiNiz(niz + 1, 3);

Ova univerzalnost ostvarena je automatskom konverzijom nizova u pokazivače, kao i mogućnošću da se indeksiranje primjenjuje na pokazivače kao da se radi o nizovima.

Tijesna veza između nizova i pokazivača i činjenica da se nizovi znakova tretiraju donekle različito

od ostalih nizova ima kao posljedicu da se i pokazivači na znakove tretiraju neznatno drugačije nego pokazivači na druge objekte. Tako, ukoliko je “p“ pokazivač na znak (tj. na tip “char”), izraz “cout << p” neće ispisati adresu smještenu u pokazivač “p” (kao što bi vrijedilo za sve druge tipove pokazivača), nego redom sve znakove iz memorije počev od adrese smještene u pokazivač “p”, pa sve do “NUL” graničnika (jasno je da se ova konvencija odnosi samo na jezik C++ a ne na C, s obzirom da jezik C ne posjeduje objekat toka “cout”). Na ovaj način je, kao prvo, ostvarena konzistencija sa tretmanom znakovnih nizova od strane objekta izlaznog toka “cout”, a kao drugo, omogućeni su neki interesantni efekti. Na primjer, sljedeća skupina naredbi

Page 16: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

16

char recenica[] = "Tehnike programiranja"; char *p(&recenica[8]); cout << p;

ispisaće tekst “programiranja” na ekran. Naime, nakon izvršene dodjele, pokazivač “p” pokazuje na deveti znak rečenice smještene u niz “recenica” (indeksiranje počinje od nule), a to je upravo prvi znak druge riječi u nizu “recenica”, odnosno početak dijela teksta koji glasi “programiranja”. Naravno, umjesto izraza “&recenica[8]” mogli smo pisati i “recenica + 8” (s obzirom da bi se u ovom primjeru upotreba imena “recenica” bez indeksa automatski konvertirala u pokazivač na prvi element niza, nakon čega bi pokazivačka aritmetika odradila ostatak posla). Također, uopće nismo ni morali uvoditi pokazivačku promjenljivu “p”: mogli smo prosto pisati

char recenica[] = "Principi programiranja"; cout << &recenica[8];

odnosno

char recenica[] = "Principi programiranja"; cout << recenica + 8;

Isti efekat proizvela bi čak i sljedeća naredba (razmislite zašto):

cout << "Principi programiranja" + 8; Pokazivači na znakove su jedini pokazivači koji se mogu koristiti sa ulaznim tokom “cin” (jasno je

da se i ova napomena odnosi samo na jezik C++). Tako, ako je “p” pokazivač na znak, izvršavanjem izraza “cin >> p” znakovi pročitani iz ulaznog toka (do prvog razmaka) smjestiće se redom u memoriju počev od adrese smještene u pokazivaču “p”. Slično vrijedi i za druge konstrukcije za unos, poput “cin.getline(p, 100)”. Zajedno sa pokazivačkom aritmetikom, ovo se može iskoristiti za razne interesantne efekte. Na primjer, pomoću sekvence naredbi

char recenica[100] = "Početak: ", *p(&recenica[9]); cin >> p;

sa tastature će se unijeti jedna riječ, i smjestiti u niz recenica počev od desetog znaka, odnosno tačno iza teksta “Početak: ”. I ovdje smo mogli izbjeći uvođenje promjenljive “p” i pisati direktno izraz poput “cin >> &recenica[9]” ili “cin >> recenica + 9”. Ukoliko bismo umjesto jedne riječi željeli unijeti čitavu liniju teksta, koristili bismo konstrukciju “cin.getline(p, 91)” odnosno, bez uvođenja pomoćne promjenljive “p”, konstrukcije poput “cin.getline(&recenica[9], 91)” odnosno “cin.getline(recenica + 9, 91)”. Maksimalna dužina 91 određena je tako što je od maksimalnog kapaciteta od 100 znakova odbijen prostor koji je već rezerviran za tekst “Početak: ”.

Pokazivači i pokazivačka aritmetika omogućavaju prilično moćne trikove, koje bi inače bilo znatno teže realizirati. S druge strane, pokazivači mogu biti i veoma opasni, jer lako mogu “odlutati” u neko nepredviđeno područje memorije, što omogućava da pomoću njih indirektno promijenimo praktički bilo koji dio memorije u kojem su dopuštene izmjene (posljedice ovakvih intervencija su pretežno nepredvidljive i često fatalne). Na primjer, već smo rekli da je smisao pokazivačke aritmetike precizno definiran samo za pokazivače koji pokazuju na neki element niza, i to samo pod uvjetom da novodobijeni pokazivač ponovo pokazuje na neki element niza, ili eventualno tačno iza posljednjeg elementa niza. Ukoliko se ne pridržavamo ovih pravila, pokazivači mogu “odlutati” u nedozvoljena područja memorije. Razmotrimo, na primjer, sljedeću sekvencu naredbi:

int a(5), *p(&a); p++; *p = 3; Ovdje smo prvo deklarirali cjelobrojnu promjenljivu “a” koju smo inicijalizirali na vrijednost “5 ”, a

zatim pokazivačku promjenljivu “p” koju smo inicijalizirali tako da pokazuje na promjenljivu “a”

Page 17: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

17

(odnosno, ona sada sadrži adresu promjenljive “a”). Nakon toga je na promjenljivu “p” primijenjen operator “++”. Na šta sada pokazuje promjenljiva “p”? Strogo uzevši, odgovor uopće nije definiran u skladu sa pravilima koja po standardu vrijede za pokazivačku aritmetiku, ali u skladu sa načinom kako se pokazivačka aritmetika implementira u većini kompajlera, “p” će gotovo sigurno pokazivati na adresu neposredno iza prostora koji zauzima promjenljiva “a”. Nakon toga će dodjela “*p = 3” na to mjesto u memoriji upisati vrijednost “3 ”. Međutim, šta se nalazi “na tom mjestu u memoriji”? Odgovor je nepredvidljiv: možda neka druga promjenljiva (ova varijanta je najvjerovatnija), možda ništa pametno, a možda neki podatak od vitalne važnosti za rad operativnog sistema! Samim tim, posljedica ove dodjele je nepredvidljiva. U svakom slučaju, dodjelom “*p = 3”, mi smo zapravo “napali” memorijsku lokaciju čiju svrhu ne znamo, što je strogo zabranjeno. Za pokazivač koji je “odlutao” tako da više ne znamo na šta pokazuje kažemo da je divlji pokazivač (engl. wild pointer). To ne mora značiti da ne znamo koja je adresa u njemu, nego samo da ne znamo kome pripada adresa na koju on pokazuje. Divlji pokazivači nikada se ne bi trebali pojaviti u programu. Nažalost, greške koje nastaju usljed divljih pokazivača veoma se teško otkrivaju, a prilično ih je lako napraviti. Zbog toga se u literaturi često citira da su “pokazivači zločesti” (engl. pointers are evil), i oni predstavljaju drugi tipični “zločesti” koji postoji u jezicima C i C++ (prvi “zločesti” koncept su nizovi, koji postaju “zločesti” ukoliko indeks niza “odluta” izvan dozvoljenog opsega). Smatra se da oko 90% svih fatalnih grešaka u današnjim profesionalnim programima nastaje upravo zbog ilegalnog pristupa memoriji preko divljih pokazivača!

Treba primijetiti da je i pokazivač koji pokazuje tačno iza posljednjeg elementa niza također divlji

pokazivač, jer i on pokazuje na lokaciju koja ne pripada nizu (niti se zna kome ona zapravo pripada). Međutim, standard jezika C++ kaže da je takav pokazivač legalan. Nije li ovo kontradikcija? Stvar je u tome što je dozvoljeno generirati odnosno kreirati pokazivač koji pokazuje iza posljednjeg elementa niza. Pored toga, garantira se da je on veći od svih pokazivača koji pokazuju na elemente tog niza. Ipak, to još uvijek ne znači da takav pokazivač smijemo dereferencirati. Pokušaj njegovog dereferenciranja također može imati nepredvidljive posljedice. Međutim, njega bar smijemo kreirati bez opasnosti. Druge vrste divljih pokazivača ponekad ne smijemo ni kreirati – može se desiti da i sâmo njihovo kreiranje dovede do kraha, čak i ukoliko se oni uopće ne dereferenciraju (to se, u nekim rijetkim slučajevima, može desiti npr. ukoliko primjenom pokazivačke aritmetike pokazivač odluta ispred prvog elementa niza). Dalje, sve operacije sa njima (npr. poređenje, itd.) potpuno su nedefinirane. U tom smislu, pokazivač koji pokazuje neposredno iza elementa niza legalan je u smislu da ga smijemo kreirati (tj. njegovo kreiranje garantirano neće izazvati krah) i smijemo ga porediti sa drugim pokazivačima, jedino ga ne smijemo dereferencirati. Sa drugim divljim pokazivačima ne smijemo raditi apsolutno ništa. Čak i sâm pokušaj njihovog kreiranja (koji tipično nastaje neispravnom upotrebom pokazivačke aritmetike) može biti fatalan po program!

U vezi sa divljim pokazivačima neophodno je naglasiti jednu nezgodnu osobinu: svi pokazivači su

neposredno nakon deklaracije divlji, sve dok im se eksplicitno ne dodijeli vrijednost! Naime, kao i sve ostale promjenljive, njihova vrijednost je nedefinirana neposredno nakon deklaracije, sve do prve dodjele (osim u slučajevima kada se uporedo sa dodjelom vrši i inicijalizacija). Tako je i sa pokazivačima: njihova početna vrijednost je nedefinirana, tako da se sve do prve dodjele ne zna ni na šta pokazuju! Na primjer, sljedeća sekvenca naredbi ima nepredvidljive posljedice jer “napada” nepoznati dio memorije, s obzirom da se ne zna na šta pokazivač “p” pokazuje:

int *p; *p = 10;

Sličnu situaciju imamo u sljedećem katastrofalno opasnom neispravnom primjeru, koji se veoma često može sresti kod početnika:

char *p; cin >> p; cout << p;

Mada onome ko isproba navedeni primjer može izgledati da on radi korektno (odnosno da ispisuje na ekran riječ prethodno unesenu sa tastature), on sadrži katastrofalnu grešku. Naime, kako je “p”

Page 18: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

18

neinicijaliziran pokazivač, njegov sadržaj je nepredvidljiv, pa je i potpuno nepredvidljivo gdje će se uopće u memoriji smjestiti znakovi pročitani sa tastature! Može se desiti da se program koji sadrži ovakve naredbe “sruši” tek nakon više sati, ukoliko se ispostavi da su smješteni znakovi “upropastili” neke bitne podatke u memoriji. Može se desiti da u startu “p” pokazuje na neki dio memorije zabranjen za upis od strane operativnog sistema. U tom slučaju program će se srušiti odmah (zapravo, operativni sistem će prekinuti njegovo izvršavanje). U tom slučaju čak možemo reći da smo imali sreće!

Opisani problem nije “specijalnost” samo jezika C++. Sa sličnim problemom se početnici susreću u

jeziku C. Naime, prethodni problematični primjer, napisan u duhu jezika C, izgledao bi otprilike ovako: char *p; scanf("%s", p); printf("%s", p);

Da li ste ikada napisali nešto poput ovoga? Sasvim je vjerovatno da jeste. Da li ste nakon toga bili ubjeđeni da se smije ovako pisati? Ukoliko ste već napisali tako nešto, skoro ste bili sigurni da smije. Sad znate ne samo da ne smije, nego da je u pitanju katastrofalna greška. Čovjek uči na greškama i uči dok je živ, ali je žalosno što ovakve primjere navode (misleći da su ispravni) čak i neki pojedinci koji bi druge trebali učiti osnovama programiranja...

Već smo rekli da pokazivačima nije dozvoljeno eksplicitno dodjeljivati brojeve. Međutim, postoji

jedan broj koji se smije dodijeliti svakom tipu pokazivača: broj nula (0). Pokazivač kojem je dodijeljena vrijednost 0 naziva se nul-pokazivač i često se obilježava sa “NULL” (ne “NUL”), tako da mnoge biblioteke funkcija (poput “cstring”, “cstdlib” itd.) definiraju simboličku konstantu “NULL” koja ima vrijednost 0 (pri čemu je ipak bitno znati da je konstanta “NULL” pokazivačkog a ne brojčanog tipa, čime se sprečava da se ova konstanta koristi u ne-pokazivačkim kontekstima). Mi ćemo ipak radije koristiti čistu vrijednost 0 umjesto konstante “NULL”, da ne bi pravili nepotrebne pretpostavke o uključenosti pojedinih biblioteka u program.

Po konvenciji, smatra se da nul-pokazivač ne pokazuje ni na šta. Obično se pokazivač inicijalizira na

nulu kada eksplicitno želimo da naglasimo da pokazivač još uvijek ne pokazuje ni na šta konkretno. Na primjer:

int *p(0);

Naravno, istu stvar smo mogli uraditi i ovako, u duhu jezika C:

int *p = 0;

Na taj način lako možemo kasnije ispitati u programu da li pokazivač pokazuje na nešto smisleno tako što ćemo prosto testirati da li je vrijednost pokazivača nula. Pored očiglednih testova tipa “p == 0” odnosno “p != 0”, sintaksa jezika C i C++ dozvoljava i samo navođenje izraza “!p” odnosno “p” unutar uvjeta “if” naredbe (pri tome se svi pokazivači automatski tretiraju kao logička vrijednost “tačno”, osim nul-pokazivača koji se tretira kao “netačan”). Naravno, vrijednost “0” možemo pokazivaču dodijeliti bilo kada, a ne samo prilikom inicijalizacije. To obično radimo kada želimo da kažemo da pokazivač više ne pokazuje na neki konkretan objekat.

Konvencija da se pokazivač koji ne pokazuje ni na šta eksplicitno inicijalizira na nulu može učiniti divlje pokazivače “manje divljim”. Oni su i dalje divlji u smislu da ne pokazuju ni na šta smisleno, tako da ih ne smijemo dereferencirati. Međutim, njihov sadržaj nije nepredvidljiv, nego je tačno određen, tako da programski možemo testirati (poređenjem sa nulom) da li on pokazuje na nešto smisleno ili ne. Interesantno je da standard jezika C++ ne insistira da nul-pokazivači budu implementirani tako da im sadržaj zaista bude nula (tj. da čuvaju u sebi adresu nula). Postoje jaki razlozi (tehničke prirode) zbog kojeg se ovo ne insistira. Međutim, kako god da su oni interno implementirani, dodjela nule pokazivaču i njihovo poređenje sa nulom mora da radi tačno onako kako treba (tako da korisnik ne treba da brine o implementaciji)!

Page 19: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

19

Nul-pokazivači se također često vraćaju kao rezultati iz funkcija koje kao rezultat vraćaju pokazivače, u slučaju da nije moguće vratiti neku drugu smislenu vrijednost. Na primjer, biblioteka “cstring” sadrži veoma korisnu funkciju “strstr” koja prihvata dva nul-terminirana stringa kao parametre. Ova funkcija ispituje da li se drugi string sadrži u prvom stringu (npr. string “je lijep” sadrži se u stringu “danas je lijep dan”). Ukoliko se nalazi, funkcija vraća kao rezultat pokazivač na znak unutar prvog stringa od kojeg počinje tekst identičan drugom stringu (u navedenom primjeru, to bi bio pokazivač na prvu pojavu slova “j ” u stringu “danas je lijep dan”). U suprotnom, funkcija kao rezultat vraća nul-pokazivač. Sljedeći primjer demonstrira kako se ova funkcija može iskoristiti (obratite pažnju na igre sa pokazivačkom aritmetikom):

char recenica[100], fraza[100]; cin.getline(recenica, sizeof recenica); cin.getline(fraza, sizeof fraza); char *pozicija(strstr(recenica, fraza)); if(pozicija != 0) cout << "Navedena fraza se nalazi u rečenici počev od " << pozicija - recenica + 1 << ". znaka\n"; else cout << "Navedena fraza se ne nalazi u rečenici\n";

Pored funkcije “strstr”, biblioteka “cstring“ sadrži i sličnu funkciju “strchr”, samo što je

njen drugi parametar znak a ne string (prihvata se i brojčana vrijednost, sa značenjem ASCII šifre). Ona vraća pokazivač na prvu pojavu navedenog znaka u stringu, ili nul-pokazivač ukoliko takvog znaka nema. Ova funkcija se može zgodno iskoristiti da se dobije pokazivač na “NUL” graničnik stringa (za tu svrhu, funkciji “strchr“ treba proslijediti nulu kao drugi parametar). Biblioteka “cstring” sadrži još mnoštvo korisnih funkcija koje kao rezultat vraćaju pokazivače, koje nećemo opisivati s obzirom da bi njihov opis zauzeo previše prostora. Napomenimo da su sve ove funkcije namijenjene isključivo za rad sa klasičnim, nul-terminiranim stringovima, tj. stringovima na način kako ih tretira jezik C. Funkcije slične namjene postoje i za dinamičke stringove (tj. objekte tipa “string” uvedene u jeziku C++), samo što se koriste na nešto drugačiji način i one nikada ne vraćaju pokazivače kao rezultate (pokazivače i dinamičke stringove nije dobro koristiti u istom kontekstu, jer su oni konceptualno različiti). Zainteresirani se upućuju se na mnogobrojnu literaturu koja detaljno obrađuje standardnu biblioteku “string” i operacije sa dinamičkim stringovima. Vrijedi još napomenuti da je, u slučaju potrebe, legalno uzeti adresu nekog elementa dinamičkog stringa i sa tako dobijenim pokazivačem (koji je tipa pokazivač na znak, isto kao i u slučaju klasičnih nul-terminiranih stringova) koristiti pokazivačku aritmetiku (tj. garantira se da se elementi dinamičkih stringova također čuvaju u memoriji jedan za drugim). Tako dobijene adrese su garantirano ispravne sve dok se dužina stringa ne promijeni, nakon čega više ne moraju biti (s obzirom da promjena veličine stringa može dovesti do pomjeranja njegovih elemenata u memoriji).

Pošto je cilj ovog dodatka detaljno upoznavanje sa pokazivačima i njihovim svojstvima, recimo još

nekoliko riječi o pokazivačima na znakove. Interesantno je da se pokazivači na konstantne znakove mogu inicijalizirati tako da pokazuju na neku stringovnu konstantu, kao da se radi o nizovima znakova, koji se mogu inicijalizirati na sličan način. Na primjer:

const char *p = "Ovo je neki string..."; cout << p;

Ovdje je također moguće (i preporučljivo) koristiti sintaksu sa zagradama:

const char *p("Ovo je neki string..."); cout << p;

Ovakvim pokazivačima je čak moguće i naknadno “dodijeliti” stringovne konstante, pri čemu ovakva “dodjela” naravno ne dodjeljuje samu stringovnu konstantu pokazivačkoj promjenljivoj (ona nema “prostora” da primi cijeli niz znakova u sebe), već adresu stringovne konstante. Međutim, bez obzira na

Page 20: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

20

to, ovakva “poludodjela” može, zajedno sa specijalnim tretmanom pokazivača na znakove, proizvesti interesantne efekte. Na primjer, sljedeći primjer je posve legalan:

const char *recenica; recenica = "Ja sam prva rečenica..."; cout << recenica << endl; recenica = "A ja sam druga rečenica..."; cout << recenica << endl;

S druge strane, slična konstrukcija nije moguća sa običnim nizovima znakova, nego da se moraju koristiti ili dinamički stringovi (tj. objekti tipa “string”, uvedeni u jeziku C++) ili konstrukcije poput sljedeće:

char recenica[100]; strcpy(recenica, "Ja sam prva rečenica..."); cout << recenica << endl; strcpy(recenica, "A ja sam druga rečenica..."); cout << recenica << endl; Veoma je važno uočiti bitnu razliku između navedena dva primjera. U prvom primjeru, “recenica”

je pokazivač, koji prvo pokazuje na jednu stringovnu konstantu, a zatim na drugu. S druge strane, u drugom primjeru, “recenica” je niz znakova (za koji je rezerviran prostor od 100 mjesta za znakove), u koji se prvo kopira jedna stringovna konstanta, a zatim druga. Ova razlika je veoma važna, mada na prvi pogled djeluje nebitna, jer je krajnji efekat isti. Međutim, kako je u prvom primjeru “recenica” pokazivač na konstantne znakove, izmjena sadržaja na koji on pokazuje nije moguća. Stoga bi u primjeru poput sljedećeg prevodilac prijavio grešku: const char *ime; ime = "Elma"; ime[0] = 'A'; // Ilegalno: "ime" pokazuje na KONSTANTNE znakove! cout << ime;

Na ovome bi sva priča o pokazivačima na znakove mogla da završi da nije jedne nevolje. Naime, po posljednjem standardu jezika C++ kompajleri ne bi trebali da dozvole da se običnim pokazivačima na (nekonstantne) znakove dodjeljuju adrese stringovnih konstanti. Kažemo “ne bi trebali”, jer gotovo svi raspoloživi kompajleri za C++ dozvoljavaju takve dodjele (s obzirom da su one dugi niz godina načelno bile dozvoljene, bez obzira što nisu smjele da budu – po ranijim standardima stringovne konstante zapravo uopće nisu bile konstante). Stoga će sljedeći primjer “proći nekažnjeno” kroz većinu raspoloživih C++ kompajlera (i praktično sve kompajlere za C, s obzirom da jezik C ima znatno “blaži” tretman konstanti, odnosno ne tretira “konstantnost” tako strogo kao jezik C++):

char *ime;

ime = "Elma"; ime[0] = 'A'; cout << "Elma";

“Repertoar” mogućih posljedica ove skupine naredbi je raznovrstan. Možda se program “sruši” zbog toga što “ime” pokazuje na stringovnu konstantu koja je možda smještena u dio memorije koji je zabranjen za upis. Ukoliko to nije slučaj, gotovo sigurno da ćemo na ekranu umjesto “Elma” imati ispisano “Alma”, bez obzira što naredba ispisa eksplicitno ispisuje tekst “Elma”. Naime, obje pojave stringovne konstante “Elma” gotovo sigurno se u memoriji čuvaju na istom mjestu, pa će se izmjena sadržaja stringovne “konstante” (izmjena konstante je već sama po sebi apsurdna) odraziti na svaku pojavu te stringovne konstante. Navedimo još jedan katastrofalan primjer koji se može sresti kod početnika (i koji kompajler ne bi trebao uopće da dozvoli): char *recenica; recenica = "Ja sam recenica..."; …

cin >> recenica;

Page 21: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

21

Postavlja se pitanje gdje će se u memoriju smjestiti niz znakova koji unesemo sa tastature? Odgovor je jasan: na najgore moguće mjesto – preko znakova stringovne konstante “Ja sam rečenica...”! Pored toga, ukoliko je uneseni niz znakova duži od dužine te stringovne konstante, višak znakova će “pojesti” i sadržaj memorije koji se nalazi iza ove stringovne konstante! Ukoliko nam operativni sistem odmah prekine ovakav program, imamo mnogo sreće. Gora varijanta je da program počne neispravno raditi tek nakon nekoliko sati, kad mu zatreba sadržaj uništenog dijela memorije!

Iz svega što je rečeno bitno je shvatiti da se dodjela stringovnih konstanti pokazivačima na znakove

nipošto ne smije shvatiti kao lijepa i elegantna zamjena za funkciju “strcpy” (kako se propagira u nekim udžbenicima za jezik C), nego kao nešto što treba koristiti sa izuzetnim oprezom, i to obavezno sa kvalifikatorom “const”. Na taj način će nas kompajler upozoriti pokušamo li izvršiti nedozvoljeni “napad” na memoriju. Napomenimo da su ovi komentari uglavnom namijenjeni onima koji imaju određena predznanja u jeziku C i žele da ta znanja nastave koristiti u jeziku C++. S druge strane, onima kojima programski jezik C nije “jača strana”, kao i onima kojima je C++ prvi programski jezik sa kojim se susreću, savjetuje se da za bilo kakav ozbiljniji rad sa stringovima ne koristite niti nul-terminirane nizove znakova niti pokazivače na znakove, već dinamičke stringove, odnosno tip “string”!

Mada je rečeno da pokazivačima nije moguće eksplicitno dodjeljivati brojeve, niti je moguće

pokazivaču na jedan tip dodijeliti pokazivač na drugi tip, postoji indirektna mogućnost da se ovakva dodjela ipak izvrši, pomoću operatora za pretvorbu tipova (typecasting operatora). Na primjer, neka imamo sljedeće deklaracije:

int *pok_na_int; double *pok_na_double;

Tada pretvorba tipova dozvoljava, da na sopstveni rizik, izvršimo i ovakve dodjele: pok_na_int = (int *)3446; pok_na_double = (double *)pok_na_int; Prikazane konverzije tipova koriste sintaksu i stil jezika C. Ova sintaksa radi i u jeziku C++, mada se u jeziku C++ preporučuje druga sintaksa, u kojoj se javlja ključna riječ “reinterpret_cast” (koja djeluje slično kao i “static_cast”, samo služi za drastične pretvorbe pokazivačkih tipova): pok_na_int = reinterpret_cast<int *>(3446); pok_na_double = reinterpret_cast<double *>(pok_na_int);

Bez obzira koja se sintaksa koristi, ovakve dodjele su veoma opasne. Prvom dodjelom smo eksplicitno postavili pokazivač “pok_na_int” da pokazuje na adresu 3446, mada ne znamo šta se tamo nalazi. Drugom dodjelom smo postavili pokazivač “pok_na_double” da pokazuje na istu adresu na koju pokazuje “pok_na_int”, što je također veoma opasno. Naime, ako se na nekoj lokaciji nalazi cijeli broj, tamo ne može u isto vrijeme biti realni broj (ne smijemo zaboraviti da se cijeli i realni brojevi zapisuju na posve različite načine u računarskoj memoriji, čak i kada imaju istu vrijednost). Pored toga, na različite tipove pokazivača pokazivačka aritmetika ne djeluje isto. Stoga su ovakve pretvorbe ostavljene samo onima koji veoma dobro znaju šta njima žele da postignu. Treba napomenuti da se pretvorbom broja u pokazivač dobija pokazivač, koji se dalje može dereferencirati kao i svaki drugi pokazivač. Stoga su naredbe poput sljedeće sasvim legalne (obje su ekvivalentne, s tim što druga radi samo u jeziku C++): *(int *)3446 = 50; *reinterpret_cast<int *>(3446) = 50; Ove naredbe će na adresu 3446 u memoriju smjestiti broj 50 (posljedice ovakvih akcija preuzima programer, i u “normalnim” programima ih ne treba koristiti). Jezici C i C++ su rijetki jezici koji omogućavaju ovakvu slobodu u pristupima resursima računara, što ih čini idealnim za pisanje sistemskog softvera (naravno, za tu svrhu treba izuzetno dobro poznavati arhitekturu računara i

Page 22: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

22

operativnog sistema za koji se piše program). Stoga, ukoliko želite koristiti ovakve konstrukcije, provjerite da li ste sigurni da znate šta radite. Ukoliko utvrdite da ste sigurni, provjerite da li ste sigurno sigurni. Na kraju, ukoliko ste sigurni da ste sigurno sigurni, opet ne radite to ako zaista ne morate!

Već smo vidjeli da je, zahvaljujući pokazivačkoj aritmetici, funkcije koje barataju sa nizovima, poput ranije napisane funkcije “IspisiNiz”, moguće iskoristiti tako da se izvrše samo nad dijelom niza, koji ne počinje nužno od prvog elementa. Međutim, sve takve funkcije bile su heterogene po pitanju svojih parametara, odnosno njihovi parametri bili su različitih tipova (na primjer, jedan parametar funkcije “IspisiNiz” bio je sam niz, dok je drugi parametar bio broj elemenata koje treba ispisati). Za mnoge primjene je praktičnije imati funkcije sa homogenom strukturom parametrara, odnosno funkcije čiji su parametri istog tipa. Na primjer, funkcija “IspisiNiz” mogla bi se napisati tako da njeni parametri budu pokazivači na prvi i posljednji element koji će se ispisati. Ovako napisane funkcije su često ne samo jednostavnije za korištenje, već i jednostavnije za implementaciju, jer se na taj način može efikasno koristiti pokazivačka aritmetika. Zbog nekih tehničkih razloga, bolje je umjesto pokazivača na posljednji element koristiti pokazivač iza posljednjeg elementa. Na primjer, takva je sljedeća verzija funkcije “IspisiNiz”, u kojoj parametri “pocetak” i “iza_kraja” predstavljaju respektivno pokazivač na prvi element niza koji treba ispisati i pokazivač koji pokazuje iza posljednjeg elementa koji treba ispisati:

void IspisiNiz(double *pocetak, double *iza_kraja) {

for(double *p = pocetak; p < iza_kraja; p++) cout << *p << endl; }

Recimo, ukoliko želimo ispisati elemente niza realnih brojeva “niz” sa indeksima 3, 4, 5, 6 i 7, možemo koristiti sljedeći poziv: IspisiNiz(niz + 3, niz + 8);

Također, ukoliko isti niz ima “n” elemenata, tada čitav niz možemo ispisati pozivom poput

IspisiNiz(niz, niz + n); Iz posljednjeg primjera je ujedno vidljivo zbog čega je praktično da drugi parametar pokazuje iza posljednjeg elementa, a ne tačno na njega (ovo nije jedini razlog). Istu funkciju bismo mogli iskoristiti i za ispis elementata vektora (mislimo na tip “vector” uveden u jeziku C++), ukoliko eksplicitno uzmemo adrese pojedinih elemenata pomoću operatora “&”. Na primjer, sljedeća dva poziva su analogna prethodnim pozivima za slučaj kada umjesto niza “niz” treba ispisati elemente vektora “v” (u istom opsegu kao u prethodnim primjerima):

IspisiNiz(&v[3], &v[8]);

IspisiNiz(&v[0], &v[n]); Kasnije ćemo vidjeti da standardna biblioteka “algorithm” posjeduje čitavo mnoštvo funkcija za manipulaciju nizovima i vektorima (i drugim srodnim strukturama podataka koje zbog nedostatka prostora nećemo obrađivati) koje koriste upravo ovakvu konvenciju o parametrima koja je iskorištena u posljednjoj verziji funkcije “IspisiNiz”.

Interesantno je napomenuti da postoje ne samo pokazivači na jednostavne objekte, kao što su cijeli i realni brojevi ili znakovi, već je moguće napraviti pokazivače na praktično bilo koje objekte. Tako npr. postoje pokazivači na nizove, pokazivači na funkcije pa i pokazivači na pokazivače (tzv. dvojni pokazivači). Također se mogu napraviti nizovi pokazivača, reference na pokazivače (samo u jeziku C++) i druge komplicirane strukture. Sa nekim od ovakvih složenijih pokazivačkih tipova upoznaćemo se kasnije u okviru ovog kursa.

Page 23: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

23

Izlaganje o prostim pokazivačkim tipovima završićemo kratkim opisom tzv. generičkih pokazivača. Ovi pokazivači se jako mnogo koriste u jeziku C, dok je njihova upotreba u jeziku C++ danas svedena na najnužniji minimum. Ipak, njihov opis može pomoći razumijevanju prave prirode pokazivačkih tipova. Generički pokazivači se definiraju kao pokazivači na tip “void”, i za njih se smatra da je nepoznato na koji tip tačno pokazuju. Stoga se oni ne mogu dereferencirati, niti se sa njima može vršiti pokazivačka aritmetika. Zapravo, ovi pokazivači su još ograničeniji: sa njima se ne može raditi praktično ništa sve dok se pomoću operatora za pretvorbu tipova ne pretvore u pokazivač u neki konkretan tip. Međutim, generičkom pokazivaču se može dodijeliti ma koji drugi pokazivač, i funkcija koja prima kao formalni parametar generički pokazivač prihvatiće kao stvarni parametar bilo koji pokazivač. Stoga su se generički pokazivači intenzivno koristili prije nego što su u jezik C++ uvedene generičke (šablonske) funkcije, o kojima ćemo govoriti kasnije u toku ovog kursa. Naime, oni su ranije bili jedini način da se naprave funkcije koje mogu prihvatati nizove različitih tipova kao parametre (u jeziku C oni su i dalje jedini način da se naprave funkcije koje, makar u izvjesnoj mjeri, posjeduju neka svojstva generičkih funkcija). Razmotrimo jedan konkretan primjer. Neka je potrebno napisati funkciju “KopirajNiz” koja posjeduje tri parametra “odredisni”, “izvorni” i “br_elemenata” i koja kopira “br_elemenata” elemenata iz niza “izvorni” u niz “odredisni”. Neka je dalje tu funkciju potrebno napisati tako da radi sa nizovima čiji su elementi proizvoljnog tipa. Uz pomoć generičkih funkcija, kakve danas postoje u jeziku C++, takvu funkciju je posve lako napisati, što ćemo vidjeti uskoro u okviru ovog kursa. Međutim, na ovom mjestu nas zanima kako se isti problem može donekle riješiti bez upotrebe generičkih funkcija. Za tu svrhu će nam poslužiti generički pokazivači. Naime, ako bismo formalne parametre “odredisni” i “izvorni” deklarirali tako da budu generički pokazivači (odnosno, pokazivači na “void”), takva funkcija bi kao odgovarajuće stvarne parametre prihvatala bilo kakav pokazivač, pa samim tim (zbog automatske konverzije nizova u pokazivače) i bilo kakav niz. Dakle, takva funkcija bi zaista prihvatala nizove proizvoljnog tipa kao stvarne parametre. Međutim, sa generičkim pokazivačima se ne može raditi ništa dok se ne izvrši njihova pretvorba u pokazivač na neki konkretan tip. Sad se javlja prirodno pitanje u koji tip izvršiti njihovu pretvorbu. Naime, nijedna klasična funkcija ne može ni na kakav način saznati kojeg su tipa njeni stvarni parametri (generičke funkcije to mogu, ali ovdje ne govorimo o njima). Stoga je najprirodnije izvršiti njihovu konverziju u pokazivače na tip “char”, s obzirom da je tip “char” najelementarniji tip podataka (svaki podatak tipa “char” uvijek zauzima tačno jedan bajt), a nakon toga obaviti kopiranje niza kao da se radi o običnom nizu bajtova (odnosno, obaviti kopiranje nizova bajt po bajt, onako kako su oni pohranjeni u memoriji). Međutim, tada se umjesto broja elemenata koje treba kopirati, kao parametar mora zadavati broj bajtova koje želimo kopirati. Tako napisana funkcija “KopirajNiz” mogla bi izgledati recimo ovako:

void KopirajNiz(void *odredisni, void *izvorni, int br_bajtova) { for(int i = 0; i < br_bajtova; i++) ((char *)odredisni)[i] = ((char *)izvorni)[i];

}

Sad, ako na primjer želimo kopirati 10 elemenata niza realnih brojeva “niz1” u niz “niz2”, mogli bismo koristiti sljedeći poziv: KopirajNiz(niz2, niz1, 10 * sizeof(double));

Primijetimo kako je ovdje upotrijebljen operator “sizeof”, sa ciljem pretvaranja broja elemenata u broj bajtova (podsjetimo se da operator “sizeof” daje kao rezultat broj bajtova koji zauzima određeni tip ili izraz). Naravno, umjesto izraza “sizeof(double)” mogli smo koristiti i izraz “sizeof niz1[0]” čime bismo poziv funkcije učinili neovisnim od stvarnog tipa elemenata niza. U suštini, čak smo i čitav izraz “10 * sizeof(double)” mogli prosto zamijeniti sa “sizeof niz1”, jer je broj bajtova niza “niz1” ono što nas zapravo zanima. Vidimo da smo, na izvjestan način, uz pomoć generičkih pokazivača uspjeli napraviti funkciju koja kao argumente prihvata nizove proizvoljnih tipova, ali po cijenu da umjesto broja elemenata koji se kopiraju moramo zadavati broj bajtova koji se kopiraju. Uskoro ćemo vidjeti da to nije jedini nedostatak ovakvog pristupa.

Page 24: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

24

Radi boljeg razumijevanja, navedimo i još jedan nešto složeniji primjer koji koristi generičke pokazivače. Pretpostavimo da treba napisati funkciju “IzvrniNiz” sa dva parametra “niz” i “br_elemenata” koja izvrće “br_elemenata” elemenata niza “niz”, čiji su elementi proizvoljnog tipa, u obrnuti poredak. Ponovo, ovaj problem je vrlo lako rješiv uz pomoć generičkih funkcija, ali na ovom mjestu nas ne zanima takvo rješenje. Ovdje je cilj pokazati kako se ovaj problem može riješiti uz pomoć generičkih pokazivača. Jasno je da parametar “niz” treba biti generički pokazivač. Međutim, ovdje se javlja dodatni problem u odnosu na funkciju iz prethodnog primjera. Da bismo izvrnuli elemente niza u obrnuti poredak, ne smijemo prosto izvrnuti bajtove od kojih je niz formiran u obrnuti poredak. Naime, ukoliko to učinimo, izvrnućemo i bajtovsku strukturu svakog individualnog elementa niza, što naravno ne smijemo uraditi. Dakle, bajtovska struktura svakog individualnog elementa niza ne smije se narušiti. Međutim, funkcija ne može ni na kakav način saznati tip elemenata niza, odakle slijedi da ne može saznati ni koliko bajtova zauzima svaki od elemenata niza. Ukoliko malo bolje razmislimo o svemu, zaključićemo da se problem ne može riješiti ukoliko u funkciju ne uvedemo dodatni parametar (nazvan recimo “vel_elementa”) koji govori upravo koliko bajtova zauzima svaki od elemenata niza. Uz pomoć ovakvog dopunskog parametra, tražena funkcija se može napraviti uz malo “igranja” sa pokazivačkom aritmetikom. Jedno od mogućih rješenja moglo bi izgledati recimo ovako:

void IzvrniNiz(void *niz, int br_elemenata, int vel_elementa) { for(int i = 0; i < br_elemenata / 2; i++) { char *p1((char *)niz + i * vel_elementa); char *p2((char *)niz + (br_elemenata - i - 1) * vel_elementa); for(int j = 0; j < vel_elementa; j++) { char pomocna(p1[j]);

p1[j] = p2[j]; p2[j] = pomocna; } }

}

Ukoliko sada želimo izvrnuti elemente niza “niz1” od 10 elemenata u obrnuti poredak, mogli bismo koristiti poziv poput sljedećeg:

IzvrniNiz(niz1, 10, sizeof niz1[0]);

Opisani primjeri nisu nimalo jednostavni, i traže izvjesno poznavanje načina na koji su organizirani podaci u računarskoj memoriji. Najbolje je da probate ručno pratiti izvršavanje napisanih funkcija na nekom konkretnom primjeru. Napomenimo da su ovi primjeri ubačeni čisto sa ciljem pojašnjenja prave prirode pokazivača. Naime, u jeziku C++ ovakve primjere ne treba pisati, s obzirom da se razmotreni problemi mnogo lakše rješavaju pomoću pravih generičkih funkcija (koje ćemo uskoro upoznati). Ovakve kvazi-generičke funkcije tipično zahtijevaju čudne ili dopunske parametre (poput parametra “vel_elementa” u posljednjem primjeru). Pored toga, nemoguće je na ovaj način napraviti funkciju koja bi, recimo, našla najveći element niza, ili sumu svih elemenata u nizu (koja bi radila za nizove proizvoljnih tipova elemenata), s obzirom da na ovaj način mi uopće ne baratamo sa elementima niza, već sa bajtovima koje tvore elementi niza. Stoga je takav pristup moguć jedino za primjene u kojima tačna priroda elemenata niza nije ni bitna (kakva je npr. kopiranje).

Bez obzira na brojna ograničenja funkcija koje koriste generičke pokazivače, one se mnogo koriste u

jeziku C (jer u njemu bolja alternativa i ne postoji). Stoga mnoge funkcije iz biblioteka koje dolaze uz jezik C koriste ovaj pristup. Kako je jezik C++ naslijedio sve biblioteke koje su postojale u jeziku C, one postoje i u jeziku C++. Na primjer, funkcija “memcpy” iz biblioteke “cstring” identična je posljednjoj napisanoj verziji funkcije “KopirajNiz”. Drugim riječima, kopiranje niza “niz1” od 10 realnih brojeva u niz “niz2” može se, u principu, obaviti i na sljedeći način:

memcpy(niz2, niz1, 10 * sizeof niz1[0]);

Također, ni funkcije koje kao jedan od parametara zahtijevaju veličinu elemenata niza u bajtovima (poput posljednje verzije funkcije “IzvrniNiz”), nisu nikakva rijetkost u standardnim bibliotekama.

Page 25: Dodatak - Pokazivaci i srodne teme · PDF file“pokazivac”, tako da je njen sadržaj inicijalno nedefiniran. Međutim, pokazivačkim promjenljivim ne smiju se neposredno dodjeljivati

Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Pokazivaci i srodne teme

25

Takva je, na primjer, funkcija “qsort” iz biblioteke “cstdlib”, koja služi za sortiranje elemenata niza u određeni poredak. Programeri u jeziku C intenzivno koriste ovakve funkcije. Mada se one, u načelu, mogu koristiti i u jeziku C++, postoje jaki razlozi da u jeziku C++ ove funkcije ne koristimo (ova napomena namijenjena je najviše onima koji imaju dobro prethodno programersko iskustvo u jeziku C). Naime, sve funkcije zasnovane na generičkim pokazivačima svoj rad baziraju na činjenici da je manipulacije sa svim tipovima podataka moguće izvoditi bajt po bajt. Ta pretpostavka je tačna za sve tipove podataka koji postoje u jeziku C, ali ne i za neke novije tipove podataka koji su uvedeni u jeziku C++ (kao što su svi tipovi podataka koji koriste tzv. vlastiti konstruktor kopije). Na primjer, mada će funkcija “memcpy” bespijekorno raditi za kopiranje nizova čiji su elementi tipa “double”, njena primjena na nizove čiji su elementi tipa “string” može imati fatalne posljedice. Isto vrijedi i za funkciju “qsort”, koja može napraviti pravu zbrku ukoliko se pokuša primijeniti za sortiranje nizova čiji su elementi dinamički stringovi. Tipovi podataka sa kojima se može sigurno manipulirati pristupanjem individualnim bajtima koje tvore njihovu strukturu nazivaju se POD tipovi (od engl. Plain Old Data). Na primjer, prosti tipovi kao što su “int”, “double” i klasični nizovi jesu POD tipovi, dok vektori i dinamički stringovi nisu.

Opisani problemi sa korištenjem funkcija iz biblioteka naslijeđenih iz jezika C koje koriste generičke pokazivače ne treba mnogo da nas zabrinjava, jer za svaku takvu funkciju u jeziku C++ postoje bolje alternative, koje garantirano rade u svim slučajevima. Tako, na primjer, u jeziku C++ umjesto funkcije “memcpy” treba koristiti funkciju “copy” iz biblioteke “algorithm”, o kojoj ćemo govoriti kasnije, dok umjesto funkcije “qsort” treba koristiti funkciju “sort” (također iz biblioteke “algorithm”, koju ćemo kasnije detaljno obraditi). Ove funkcije, ne samo da su univerzalnije od srodnih funkcija naslijeđenih iz jezika C, već su i jednostavnije za upotrebu. Na kraju, moramo dati još samo jednu napomenu. Programeri koji posjeduju bogatije iskustvo u jeziku C, teško se odriču upotrebe funkcije “memcpy” i njoj srodnih funkcija (poput “memset”), jer im je poznato da su one, zahvaljujući raznim trikovima na kojima su zasnovane, vrlo efikasne (mnogo efikasnije nego upotreba obične “for” petlje). Međutim, bitno je znati da odgovarajuće funkcije iz biblioteke “algorithm”, uvedene u jeziku C++, poput funkcije “copy” (ili “fill”, koju treba koristiti umjesto “memset”), tipično nisu ništa manje efikasne. Zapravo, u mnogim implementacijama biblioteke “algorithm”, poziv funkcije poput “copy” automatski se prevodi u poziv funkcije poput “memcpy” ukoliko se ustanovi da je takav poziv siguran, odnosno da tipovi podataka sa kojima se barata predstavljaju POD tipove. Stoga, programeri koji sa jezika C prelaze na C++ ne trebaju osjećati nelagodu prilikom prelaska sa upotrebe funkcija poput “memcpy” na funkcije poput “copy”. Jednostavno, treba prihvatiti činjenicu: funkcije poput “memcpy”, “memset”, “qsort” i njima slične zaista ne treba koristiti u C++ programima (osim u vrlo iznimnim slučajevima). One su zadržane pretežno sa ciljem da omoguće kompatibilnost sa već napisanim programima koji su bili na njima zasnovani!