dodatak - sve sto treba znati o pokazivacima

Upload: hamza-zubaca

Post on 09-Mar-2016

235 views

Category:

Documents


0 download

DESCRIPTION

.....

TRANSCRIPT

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    1

    Dodatak predavanjima: Sve to treba znati o pokazivaima Bez obzira na injenicu da je C++ uveo novu vrstu objekata nazvanu reference, koja umnogome rjeava

    brojne probleme vezane za nerazumijevanje i pogrenu upotrebu pokazivaa, pokazivai se u jeziku C++ jo uvijek intenzivno koriste, s obzirom da se reference u jeziku C++ nisu mogle izvesti na nain koji bi u potpunosti uklonio potrebu za pokazivaima, kao to je to uinjeno u istim objektno orijentiranim jezicima (npr. u Javi). Naime, injenica da se od jezika C++ zahtijevala visoka kompatibilnost sa jezikom C, dovela je do toga da je bijeg od pokazivaa u jeziku C++ ipak nemogu. S obzirom da je nerazumjevanje koncepta pokazivaa est razlog za brojne probleme u razumijevanju naprednijih koncepata jezika C++, svrha ovog dodatka je da prui pregled onih znanja o pokazivaima koje je neophodno posjedovati za ispravno razumijevanje predavanja koja kasnije slijede. Veinu tih znanja studenti su trebali stei kroz prethodne kurseve programiranja, ali pitanje je da li su ih zaista stekli, zbog dosta sloene materije. To je i glavni razlog zbog koje je ovaj dodatak uope pisan.

    Pokazivai su jedna od dvije vrste objekata u jeziku C++ pomou kojih se moe vriti indirektni pristup drugim objektima (druga vrsta su reference). Pokazivai su u C++ direktno naslijeeni iz jezika C, u kojem predstavljaju jedine objekte preko kojih je mogu indirektni pristup drugim objektima. U sutini, pokazivai nisu nita drugo nego promjenljive koje kao svoj sadraj sadre adresu nekog drugog objekta (kae se pokazuju na neki objekat), podrane odgovarajuim operatorima koji omoguavaju da se pristupi objektu na koji pokazuju. Pokazivai, dakle, nisu nita komplicirano. Najvei problem kod poentika je to se esto pobrka sam pokaziva (engl. pointer), sa onim na ta on u tom trenutku pokazuje (engl. pointee). Dodatni problem (i to ne samo kod poetnika) je to to pokazivai pruaju izuzetno veliku fleksibilnost, koju je veoma lako zloupotrebiti. Zbog tog razloga se programerima u jeziku C++ savjetuje da dobro proue reference, i da sve probleme koji se mogu rjeavati pomou referenci zaista i rjeavaju pomou referenci, a ne pomou pokazivaa. Bez obzira na to, u jeziku C++ i dalje postoji mnogo stvari koje je mogue uraditi samo pomou pokazivaa, a koje je nemogue realizirati niti pomou referenci, niti na bilo koji drugi nain (koji ne koristi pokazivae).

    Pokazivae nije mogue objasniti i razumjeti bez neto detaljnijeg objanjenja o tome kako se promjenljive uvaju u memoriji raunara (izmeu ostalog i zbog toga to kod pokazivaa moemo direktno pristupiti informaciji o tome gdje se u memoriji uva objekat na koji pokaziva pokazuje). Zbog toga emo prvo u osnovnim crtama razmotriti nain smjetanja promjenljivih u raunarskoj memoriji. Poznato je da svaka promjenljiva koja se deklarira u programu tokom svog ivota zauzima odreeni prostor u memoriji. Mjesto u memoriji koje je dodijeljeno nekoj promjenljivoj odreeno je njenom adresom. Adresa promjenljive je zapravo neka informacija koja na jednoznaan nain odreuje mjesto u memoriji gdje se ta promjenljiva nalazi, i ona je fiksna sve dok ta promjenljiva postoji. Ta informacija je kod veine raunarskih arhitektura brojane prirode (tj. adrese su obino neki brojevi), mada ne mora nuno da bude. Recimo, kod najveeg broja raunarskih arhitektura memorija je organizovana kao jednodimenzionalni niz memorijskih elija (registara), i tada je adresa prosto redni broj (indeks) odgovarajue memorijske elije u kojoj se uva sadraj promjenljive (ili prve od vie takvih elija u sluaju da sadraj promjenljive ne moe stati u jednu memorijsku eliju). Meutim, sasvim su mogue (i postoje) drugaije memorijske arhitekture kod kojih adresa nije broj. Recimo, postoje memorijski modeli koji nisu organizirani kao jednodimenzionalni ve kao dvodimenzionalni niz (matrica) memorijskih elija, tako da je za odreivanje pozicije neke memorijske elije potrebno poznavati red i kolonu u kojoj se ta elija nalazi, tako da kod takvih memorijskih modela adresa nije broj nego par brojeva (red i kolona). Na primjer, takav memorijski model javlja se kod prvih generacija PC raunara sa starijim modelima Intel-ovih procesora poput 8086, kod kojih su adrese zaista predstavljeni kao parovi brojeva (koji se redom nazivaju segment i offset). tavie, postoje i koncepti asocijativnih memorija kod kojih je svakoj memorijskoj eliji pridruen neki jednoznani identifikator (tag), koji uope ne mora biti numerike prirode, i koji onda preuzima ulogu adrese te elije. ak i ako se ograniimo iskljuivo na raunarske arhitekture kod kojih adrese memorijskih elija zaista jesu brojevi, prilikom rada sa pokazivaima mogu nastati velike greke u reznonovanju ukoliko o adresama razmiljamo kao obinim brojevima. Zbog toga adrese ne treba posmatrati kao brojeve, nego iskljuivo kao informacije koje jednoznano odreuju lokaciju nekog objekta u memoriji, pri emu je tana priroda te informacije manje-vie posve nebitna.

    Pretpostavimo, na primjer, da imamo deklaraciju neke klasine (recimo cjelobrojne) promjenljive poput

    int broj;

    Prilikom nailaska na ovu deklaraciju, rezervira se odgovarajui prostor u memoriji za potrebe smjetanja sadraja promjenljive broj, koji naravno ima svoju adresu (koju emo nazvati prosto adresa promjenljive broj). To emo predstaviti sljedeom memorijskom slikom:

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    2

    broj

    Rezervirani memorijski prostor smo predstavili pravougaonikom, dok upitnici oznaavaju da sadraj rezerviranog memorijskog prostora nije definiran, odnosno u njemu se nalazi isti sadraj koji se u tom dijelu memorije nalazio od ranije, tj. prije izvrene rezervacije (zbog toga je i poetna vrijednst promjenljive broj nedefinirana). Ukoliko nakon ove deklaracije izvrimo dodjelu neke vrijednosti promjenljivoj broj, npr. dodjelom poput

    broj = 56;

    efekat ove dodjele bie smjetanje neke kodirane reprezentacije broja 56 u rezervirani memorijski prostor (za reprezentaciju cijelih brojeva najee se koristi prosti binarni kd, ali sam nain reprezentacije nije bitan za izlaganja koja slijede). To emo predstaviti sljedeom slikom: broj

    Da smo prilikom deklaracije odmah izvrili i inicijalizaciju promjenljive, npr. deklaracijom poput int broj(56); // ili int broj = 56; u C stilu

    ovakvu memorijsku sliku bismo imali odmah po stvaranju promjenljive, odnosno istovremeno sa rezervacijom memorijskog prostora za pamenje sadraja promjenljive broj bilo bi izvreno upisivanje odgovarajueg sadraja u rezervirani prostor.

    Razumije se da za pristup promjenljivoj broj nije uope potrebno poznavati na kojoj se adresi u memoriji ona nalazi, jer njenom sadraju uvijek moemo pristupiti koristei njeno simboliko ime broj (kao to smo uvijek i inili). Meutim, pokazivai su specijalne vrste promjenljivih koje kao svoj sadraj imaju adresu neke druge promjenljive (ili openito, neke druge l-vrijednosti) i u takve promjenljive se ne moe upisivati nita drugo osim adrese. Pokazivake promjenljive se deklariraju tako da se ispred njihovog imena stavi znak * (zvjezdica). Na primjer, sljedea deklaracija deklarira promjenljivu pok koja nije tipa cijeli broj (tj. int), nego pokaziva na cijeli broj (ovaj tip se oznaava kao int *):

    int *pok;

    Kao i svaka druga promjenljiva, i pokazivaka promjenljiva zauzima odreeni prostor u memoriji (te i ona ima svoju adresu). Pretpostavimo li da su na prethodno opisani nain deklarirane cjelobrojna promjenljiva broj i pokazivaka promjenljiva pok, to moemo predstaviti sljedeom memorijskom slikom: broj pok

    Upitnici unutar promjenljive pok govore da prethodna deklaracija nije postavila nikakav inicijalni sadraj u prostor rezerviran za potrebe te promjenljive, nego je on onakav kakav se prosto zatekao odranije u tom prostoru. Tako e ostati sve dok ovoj promjenljivoj ne dodijelimo neku vrijednost. Meutim, pokazivake promjenljive namijenjene su da pamte iskljuivo adrese, i njjma se ne smiju neposredno dodjeljivati brojane vrijednosti (razlozi za to su brojni, a jedan od njih, mada ne i najbitniji, je i to to adrese ne moraju nuno biti brojevi). Stoga kompajler nee dozvoliti dodjelu poput sljedee:

    pok = 4325; // OVO NIJE DOZVOLJENO!

    Umjesto toga, pokazivakim promjenljivim se mogu dodjeljivati samo vrijednosti pokazivake prirode (tj. vrijednosti koje garantirano predstavljaju adrese nekih drugih objekata). Na primjer, pokazivau je mogue dodjeliti adresu nekog objekta (npr. promjenljive) uz pomo unarnog prefiksnog operatora &, koji moemo itati adresa od. Ovaj operator (koji se jo naziva i operator referenciranja) moe se primjenjivati samo na operande koji imaju adrese (tj. na l-vrijednosti), kao to su recimo promjenljive, ali ne i na recimo brojeve ili proizvoljne izraze (tako je, na primjer, konstrukcija poput &broj smislena, ali su konstrukcije poput &15 ili &(broj + 2) besmislene). Stoga, ukoliko izvrimo dodjelu poput

    ???

    56

    56 ???

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    3

    pok = &broj;

    u promjenljvu pok e se smjestiti adresa promjenljive broj (nain kako je ta adresa zaista predstavljena posve je nebitan). Situacija u memoriji sada odgovara sljedeoj slici: broj pok

    Na ovoj slici, da bismo izbjegli sve eventualne pekulacije o tome kako je adresa zaista predstavljena u pokazivakoj promjenljivoj, tu adresu prosto prikazali kao krui od kojeg vodi strelica ka objektu koji odgovara toj adresi. Kae se da sada pokazivaka promjenljiva (engl. pointer) pok pokazuje (engl. points to) na promjenljivu broj, za koju emo rei da je pokazani objekat (engl. pointee) u nedostatku boljeg prevoda za ovu englesku rije.

    Koritenje pokazivakih promjenljivih u aritmetikim izrazima je prilino ogranieno (npr. izraz poput 3 * pok + 2 nije dozvoljen), a ak i u kontekstima u kojima je dozvoljeno njihovo koritenje u aritmetikim izrazima, pravila izvoenja raunskih operacija (aritmetike) sa pokazivaima razlikuju se od klasinih pravila aritmetike sa brojevima (jo jedan razlog da nije dobro doivljavati adrese kao brojeve), o emu e kasnije biti detaljno govora. U svakom sluaju, svaki direktni pristup promjenljivoj pok odnosi se na adresu koja je u njoj pohranjena, a ne na promjenljivu na koju ona pokazuje. Po tome se pokazivai bitno razlikuju od referenci, s obzirom da se svaki pristup referenci automatski preusmjerava na objekat na koji referenca ukazuje (odnosno, kod pokazivaa ovog automatskog preusmjeravanja nema).

    Sam sadraj pokazivake promjenljive nam obino nije od neposredne koristi, nego nam je vanije da pristupimo objektu na koji ona pokazuje. Za tu svrhu koristi se posebna operacija, koja se obino naziva dereferenciranje, a postie se uz pomo unarnog operatora *. Ovaj operator primjenjuje se na pokazivake promjenljive, ili openitije na pokazivake izraze (tj. izraze iji su rezultati adrese, o kojima emo govoriti neto kasnije), a moemo ga itati kao ono na ta pokazuje .... Recimo, ako je pok neka pokazivaka promjenljiva, izraz *pok moemo itati kao ono na ta pokazuje pok. Tako e, ukoliko se prethodno izvre naredbe iz gore napisanih primjera, naredba

    std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    4

    *pok = 100; // Smjeta 100 u ono na ta pokazuje pok (*pok)++; // Uveava sadraj onoga na ta pokazuje pok za 1

    Prva naredba postavlja sadraj memorijske lokacije na koju ukazuje pokazivaka promjenljiva pok na vrijednost 100, dok druga naredba uveava sadraj memorijske lokacije na koju ukazuje pokazivaka promjenljiva pok za jedinicu. U konkretnom primjeru, kada pokazivaka promjenljiva pok pokazuje na promjenljivu broj, efekat ovih naredbi je isti kao da smo neposredno pisali

    broj = 100; // *pok je ovdje faktiki promjenljiva "broj" broj++;

    Drugim rijeima, dereferencirani pokaziva koji pokazuje na neku promjenljivu ponaa se identino kao i promjenljiva na koju pokazuje. Treba jo napomenuti da su u izrazu (*pok)++ zagrade obavezne. Naime, operator ++ ima vei prioritet u odnosu na operator dereferenciranja. Stoga bi se izraz *pok++ interpretirao kao *(pok++), koji je takoer smislen. Uskoro emo razjasniti i sadraj ove druge interpretacije.

    Operatori referenciranja & i dereferenciranja * su meusobno inverzni. Ako je x objekat tipa NekiTip, tada izraz &x ima tip pokaziva na tip NekiTip, a izraz *&x svodi se prosto na x. S druge strane, ukoliko je pok pokaziva tipa pokaziva na tip NekiTip, tada je tip izraza *pok prosto NekiTip, a izraz &*pok svodi se na pok.

    Veoma je bitno istai da znak * upotrijebljen ispred imena promjenljive ima potpuno drugaije znaenje prlikom deklaracije promjenljive i prilikom njene upotrebe. Nerazumijevanje ove injenice uzrok je velikih zbrki kod poetnika. Zaista, prilikom deklaracije promjenljive, zvjezdica ispred njenog imena (npr. u deklaraciji poput int *pok;) oznaava da se radi o pokazivakoj a ne o obinoj promjenljivoj, dok prilikom upotrebe (pokazivake) promjenljive zvjezdica ispred njenog imena (npr. u izrazu poput std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    5

    potrebno pokazivau pok2 dodijeliti adresu nekog drugog objekta). Drugim rijeima, nakon ove dodjele, *pok1 i *pok2 ne samo da imaju iste vrijednosti, nego su oni jedan te isti objekat (inae, pojava da se istom objektu moe pristupiti na vie razliitih naina u programiranju se naziva aliasing). Slino vrijedi za dodjelu pok2 = pok1. Sljedei primjer ilustrira pojavu aliasinga: int broj;

    int *pok1, *pok2;

    pok1 = &broj1; // *pok1 je sada isto to i broj pok2 = pok1; // *pok2 je sada takoer isto to i broj *pok1 = 8; // U promjenljivu broj se smjeta 8 std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    6

    Naime, kako god bila napisana funkcija Razmijeni, ona uvijek radi sa kopijom stvarnih argumenata a i b, tako da ih ona ne moe izmijeniti (napomenimo da je, zahvaljujui referencama, u jeziku C++ mogue postii da se koristi upravo ovakav poziv). U jeziku C jedino to moemo uraditi je da funkciji Razmijeni eksplicitno prenesemo adrese gdje se nalaze odgovarajui stvarni parametri, odnosno u pozivu funkcije moramo eksplicitno uzeti adrese stvarnih parametara pomou adresnog operatora &. To zahtijeva da poziv funkcije izgleda poput sljedeeg:

    Razmijeni(&a, &b);

    Da bi ovakav poziv bio sintaksno ispravan, potrebno je funkciju Razmijeni napisati tako da umjesto cijelih brojeva kao parametre prihvata pokazivae na cijele brojeve. Tada je mogue upotrijebiti operator dereferenciranja da se pomou njega indirektno pristupi sadraju memorijskih lokacija gdje se nalaze stvarni parametri i da se na taj nain izvri razmjena. Na taj nain 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, poetniku nije posve lako shvatiti kako ona radi. Neka su, na primjer, vrijednosti promjenljivih a i b respektivno 5 i 8, i neka je funkcija Razmijeni pozvana na opisani nain (tj. uz eksplicitno navoenje operatora referenciranja & pri pozivu funkcije). Sljedea slika ilustrira situaciju nakon izvravanja svake od naredbi u funkciji Razmijeni: Razmijeni(&a, &b);

    a b pok1 pok2

    int pomocna(*pok1);

    a b pok1 pok2 pomocna

    *pok1 = *pok2;

    a b pok1 pok2 pomocna

    *pok2 = pomocna;

    a b pok1 pok2 pomocna

    Vidimo da su promjenljive a i b zaista razmijenile svoje vrijednosti. S druge strane, u jeziku C++ isti problem moe se rijeiti na mnogo jednostavniji i jasniji nain, koritenjem prenosa parametara po referenci umjesto pokazivaa. Bez obzira na to, ovaj primjer veoma ilustrativno demonstrira na koji nain djeluju pokazivai, a ujedno i demonstrira kako se pokazivai koriste kao parametri funkcija. Interesantno je napomenuti da se potpuno ista situacija u memoriji poput situacije prikazane na prethodnim slikama deava i u rjeenju u kojem se umjesto pokazivaa koristi prenos parametara po referenci, samo to toga programer nije svjestan. U sutini, reference uvedene u jezik C++ se, na izvjestan nain, ponaaju kao veoma dobro prerueni pokazivai.

    5 8

    5 8 5

    8 8 5

    8 5 5

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    7

    Prethodni primjer je takoer veoma pouan, jer emo kod njega razmotriti i neke od tipinih greaka koje mogu nastupiti pri radu sa pokazivaima. Naime, poetnici koji esto brkaju razliku izmeu pokazivaa i pokazanog objekta esto nisu sigurni kad treba staviti zvjezdicu a kada ne. Razmotriemo sada neke od tipinih greaka koje u prethodnom primjeru mogu nastati ukoliko se ne stavi zvjezdica tamo gdje bi trebala, ili se stavi tamo gdje ne bi trebala. Idealna situacija je ukoliko nam kompajler pri tome prijavi greku, jer onda smo barem sigurni da neto nije u redu. Mnogo gora situacija je ukoliko pri tome ne doe do sintaksne greke, ali funkcija ne radi ispravno (ili jo gore, izgleda da radi ispravno, a pravi neku tetu sa strane koja nije odmah vidljiva). Krenimo prvo sa najmanje problematinim grekama, tj. onima koje e dovesti do prijave sintaksne greke. Slijedi spisak ta bismo sve mogli pogrijeiti u prethodnoj funkciji po pitanju upotrebe zvjezdice, a da to dovede do prijave greke:

    int *pomocna(*pok1); // Greka (1) int pomocna(pok1); // Greka (2) pok1 = *pok2; // Greka (3)

    *pok1 = pok2; // Greka (4) pok2 = pomocna; // Greka (5) pok2 = *pomocna; // Greka (6) *pok2 = *pomocna; // Greka (7)

    Konstrukcija (1) je sintaksno neispravna jer se u njoj promjenljiva pomocna deklarira kao pokaziva, a pokuava se inicijalizirati onim na ta pokazuje pokaziva pok, to je cijeli broj (a pokaziva ne moemo inicijalizirati cijelim brojem). U konstrukciji (2) pokuavamo inicijalizicati cjelobrojnu promjenljivu pokazivaem, to takoer nije dozvoljeno. U konstrukcijama (3) i (5) pokuavamo dodijeliti pokazivau cijeli broj, dok u konstrukciji (4) pokuavamo cijelom broju dodijeliti pokaziva. Konano, u konstrukcijama (6) i (7) pokuavamo dereferencirati promjenljivu pomocna koja nije pokazivakog tipa, to je naravno nedozvoljeno, osim ako nismo istovremeno napravili jo jednu greku i ovu promjenljivu takoer deklarirali kao pokazivaku (o posljedicama takve greke govoriemo neto nie u nastavku teksta).

    Sada emo prei na malo podmuklije greke, s obzirom na injenicu da one ostaju nedetektirane od strane kompajlera. Pretpostavimo da smo grekom umjesto dodjele *pok1 = *pok2 napisali dodjelu pok1 = pok2 (koja je u naelu sasvim legalna), odnosno da smo napisali sljedeu funkciju:

    void Razmijeni(int *pok1, int *pok2) {

    int pomocna(*pok1);

    pok1 = pok2; // OVA FUNKCIJA NE RADI ISPRAVNO...

    *pok2 = pomocna;

    }

    Ova funkcija nee raditi ispravno, jer umjesto da kopiramo ono na ta pokazuje pokaziva pok2 tamo gdje pokazuje pokaziva pok1, mi zapravo kopiramo same pokazivae, odnosno adrese koje su u njima sadrane, nakon e doi do aliasinga (oba pokazivaa e pokazivati na istu promjenljivu). Sljedea analiza pokazuje ta e se dogoditi uz iste poetne pretpostavke kao u prethodnoj analizi:

    Razmijeni(&a, &b);

    a b pok1 pok2

    int pomocna(*pok1);

    a b pok1 pok2 pomocna

    pok1 = pok2;

    a b pok1 pok2 pomocna

    5 8

    5 8 5

    5 8 5

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    8

    *pok2 = pomocna;

    a b pok1 pok2 pomocna

    U svakom sluaju, vidimo da sadraji promjenljivih a i b nisu razmijenjeni (nego je zapravo sadraj promjenljive a samo iskopiran u promjenljivu b). Ova greka i nije toliko opasna, jer emo u fazi testiranja barem vidjeti da funkcija ne radi (gore su greke koje se ak ni testiranjem ne mogu uvijek uoiti, a uskoro emo vidjeti da su naalost i takve greke mogue).

    Druga greka koja bi se mogla napraviti je sljedea, u kojoj se promjenljiva pomocna grekom deklarira kao pokazivaka promjenljiva, a ona to ne treba da bude. Naravno, da bi takva funkcija prola sintaksnu provjeru, treba napraviti jo poneku grekicu, recimo staviti zvjezdicu ispred promjenljive pomocna u dodjeli *pok2 = pomocna. Sve u svemu, pretpostavimo da smo grekom napisali ovakvu, sintaksno ispravnu ali logiki nekorektnu funkciju:

    void Razmijeni(int *pok1, int *pok2) {

    int *pomocna(pok1);

    *pok1 = *pok2; // NI OVA FUNKCIJA NE RADI ISPRAVNO...

    *pok2 = *pomocna;

    }

    Kod ove funkcije ve na poetku imamo aliasing uzrokovan injenicom da pokazivai pomocna i pok2 pokazuju na isti objekat. Sljedea analiza pokazuje ta se deava prilikom izvravanja ove funkcije:

    Razmijeni(&a, &b);

    a b pok1 pok2

    int *pomocna(pok1);

    a b pok1 pok2 pomocna

    *pok1 = *pok2;

    a b pok1 pok2 pomocna

    *pok2 = *pomocna;

    a b pok1 pok2 pomocna

    Kao to vidimo, sadraji promjenljivih a i b ni ovdje nisu ispravno razmijenjeni. Varijacijom ove neispravne funkcije moe se dobiti jo nekoliko neispravnih (mada sintaksno korektnih) varijanti. Recimo, umjesto dodjele *pok1 = *pok2 neko bi mogao napisati pogrenu dodjelu pok1 = pok2. Isto tako, neko bi umjesto dodjele *pok2 = *pomocna mogao napisati dodjelu pok2 = pomocna (koja bi sad sintaksno prola, jer je promjenljiva pomocna deklarirana kao pokaziva). I naravno, mogle bi se istovremeno napraviti obje spomenute greke. Kao korisnu vjebu, analizirajte ta bi se desilo u svakom od gore opisanih scenarija.

    5 5 5

    5 8

    5 8

    8 8

    8 8

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    9

    Uglavnom, ni u jednom od gore opisanih scenarija funkcija Razmijeni ne bi radila ono to treba da radi, ali bi njeno testiranje jasno pokazalo da traeni cilj (razmjena sadraja promjenljivih a i b) nije postignut.

    Preimo sada na opis vjerovatno najpodmuklije greke koju bismo mogli napraviti prilikom pisanja ove funkcije. Pretpostavimo da neko nije navikao da vri inicijalizaciju promjenljivih odmah prilikom njihove deklaracije, nego da je vie sklon da umjesto konstrukcije

    int pomocna(*pok1);

    obavi prvo deklaraciju pa dodjelu vrijednosti promjenljivoj pok, odnosno da napie

    int pomocna; pomocna = *pok1;

    Na taj nain dobijamo sljedeu izvedbu funkcije Razmijeni: void Razmijeni(int *pok1, int *pok2) {

    int pomocna;

    pomocna = *pok1; // Ova funkcija je korektna...

    *pok1 = *pok2;

    *pok2 = pomocna;

    }

    Jasno je da je ova funkcija korektna, jer deklaracija promjenljive uz inicijalizaciju ima u konanici isti efekat kao deklaracija promjenljive bez inicijalizacije iza koje odmah slijedi dodjela eljene poetne vrijednosti. Meutim, pretpostavimo da je neko napisao verziju gornje funkcije pri emu je stavio zvjezdice i gdje treba i gdje ne treba, odnosno da je napisao sljedeu (sintaksno ispravnu) funkciju:

    void Razmijeni(int *pok1, int *pok2) {

    int *pomocna;

    *pomocna = *pok1; // OVA FUNKCIJA IMA FATALNU GREKU *pok1 = *pok2; // IAKO PRILIKOM TESTIRANJA MOE

    *pok2 = *pomocna; // IZGLEDATI KAO DA RADI ISPRAVNO!!!

    }

    Ova funkcija je neispravna, ali to je najgore, to vrlo vjerovatno nee biti otkriveno prilikom njenog testiranja. Analizirajmo izvravanje ove funkcije (obratite panju da na poetku pokaziva pomocna nije inicijaliziran tako da pokazuje na sluajnu lokaciju u memoriji, koja je oznaena crticama): Razmijeni(&a, &b);

    a b pok1 pok2

    int *pomocna;

    a b pok1 pok2 pomocna ???

    *pomocna = *pok1;

    a b pok1 pok2 pomocna ???

    *pok1 = *pok2;

    a b pok1 pok2 pomocna ???

    5 8

    5 8

    5 8 5

    8 8 5

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    10

    *pok2 = pomocna;

    a b pok1 pok2 pomocna ???

    Spolja gledano, testiranje funkcije bi moglo pokazati da funkcija radi ispravno, odnoso ona zaista

    razmjenjuje sadraj promjenljivih a i b! U emu je onda problem? Glavni problem je upravo u pristupanju nepoznatom mjestu u memoriji koje nastaje dereferenciranjem neinicijaliziranog pokazivaa, za koji ne znamo gdje pokazuje. Dodjelom *pomocna = *pok1 mi sadraj onoga na ta pokazuje pokaziva pok1 (a to je promjenljiva a) kopiramo tamo gdje pokazuje pokaziva pomocna, ali problem u tome je to mi nemamo nikakve ideje o tome koje je to mjesto u memoriji. Desi li se sluajno da se na tom mjestu u memoriji ne nalazi nita bitno, nee biti nikakvih tetnih posljedica, i izgledae nam da je sve ispravno. Meutim, moe se dogoditi da se u tom dijelu memorije na koji pokaziva pomocna nalaze recimo neki podaci bitni za funkcioniranje operativnog sistema, ili openito neki podaci zabranjeni za pristup. U tom sluaju e vjerovatno operativni sistem prekinuti rad programa, uz obavjetenje da program radi nedozvoljene stvari. Ni to nije najgori scenario, jer tad bar znamo da neto nije u redu. Najgore je ako se na tom mjestu u memoriji na koje sluajno pokaziva pomocna u tom trenutku pokazuje nalazi recimo neka druga promjenljiva istog programa. Ova funkcija e tada poremetiti vrijednost te promjenljive, a da niko toga nee biti svjestan. Posljedice tog napada bie vidljive tek moda mnogo kasnije, kada programu zatreba vrijednost te promjenljve koju je ova funkcija unitila! Ovo je najgora vrsta greaka, jer se teko otkrivaju testiranjem, s obzirom da se njihove mogue posljedice tipino manifestiraju mnogo kasnije u odnosu na trenutak kada je do greke zaista dolo.

    Treba napomenuti da do ove greke ne bi moglo doi da smo inicijalizaciju vrili odmah prilikom deklaracije. Naime, treba primijetiti da u prethodnoj konstrukciji

    int *pomocna; // Problematina konstrukcija... *pomocna = *pok1;

    smisao zvjezdice ispred promjenljive pomocna u prvom i drugom redu nije isti. Stoga ova konstrukcija nije ekvivalentna konstrukciji

    int *pomocna(*pok1); // A ova nije ni sintaksno ispravna...

    koja usput nije ni sintaksno ispravna, jer se pokazivaka promjenljiva pomocna ne moe inicijalizirati cjelobrojnom promjenljivom, a *pok1 se upravo tretira kao cjelobrojna promjenljiva. Sve prethodne (ispravne i neispravne) primjere treba dobro prouiti, jer je njihovo prouavanje najbolji nain da se ispravno shvati smisao pokazivakih promjenljivih i izbjegnu greke pri njihovoj upotrebi.

    Sada emo prei na opis pokazivake aritmetike. Jezici C i C++ pokazivake promjenljive tretiraju pokazivake promjenljive posve drugaije nego obine brojane promjenljive, ak i u situacijama kada se adrese koje su u njima pohranjene zaista mogu zamisliti kao obini brojevi (to je zapravo sluaj kod veine raunarskih arhitektura). Jedan od razloga za to je i injenica da nije dobro pretpostavljati nita 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 izvrava. Posljedica ovoga je da mnoge aritmetike operacije nad pokazivakim promjenljivim nisu dozvoljene, a i one koje jesu dozvoljene interpretiraju se bitno drugaije nego sa obinim brojanim promjenljivim (na nain koji ne ovisi od same arhitekture raunara). Stoga je neophodno pokazivake tipove treba shvatiti kao sasvim posebne tipove, bitno razliite od cjelobrojnih tipova (bez obzira to su veini sluajeva oni interno realizirani upravo kao cijeli brojevi). Na primjer, nije dozvoljeno sabrati, pomnoiti ili podijeliti dva pokazivaa. Razlog za ovu zabranu je injenica da se ne vidi na ta bi smisleno mogao pokazivati pokaziva koji bi se dobio recimo mnoenjem adresa dva druga objekta, ak i uz pretpostavku da su te adrese obini brojevi. Slino, nije dozvoljeno pomnoiti ili podijeliti pokaziva sa brojem, ili obrnuto. Takoer, mada se interna vrijednost pokazivaa moe ispisati na izlazni tok (pri emu se kao rezultat ispisa dobija interna reprezentacija adrese pohranjene u pokazivau u vidu heksadekadnog broja, to za veinu korisnika nije osobito interesantna informacija), nije dozvoljeno njihovo unoenje preko ulaznog toka (npr. tastature). Meutim, dozvoljeno je sabrati pokaziva i cijeli broj (i obratno), zatim oduzeti cijeli broj od pokazivaa, kao i oduzeti jedan pokaziva od drugog. U nastavku emo razmotriti smisao ovih operacija.

    Smisao pokazivake aritmetike oitava se u sluaju kada pokaziva pokazuje na neki element niza (kako operator referenciranja & kao svoj argument prihvata bilo kakvu l-vrijednost, njegov argument moe biti i indeksirani element niza, za razliku od recimo izraza poput &2 ili &(a + 2) koji su besmisleni). Da bismo to ilustrirali, pretpostavimo da imamo sljedee deklaracije:

    8 5 5

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    11

    double niz[5] = {4.23,

    7,

    3.441,

    12.9,

    6.03}; // Ili bez "=" u C++11

    double *p(&niz[2]);

    Ovdje smo inicijalizirali pokaziva p tako da pokazuje na element niza niz[2]. Nakon ovih deklaracija, stanje u memoriji moemo predstaviti ovako: niz p

    Pokaziva p pokazuje na element niz[2], tako da je *p upravo niz[2]. Razmotrimo sada izraz p + 1. Ovaj izraz ponovo predstavlja pokaziva (tako da se i on moe dereferencirati poput svih drugih pokazivaa, odnosno izraz poput *(p + 1) je legalan) ali koji pokazuje na element niz[3]. Dakle, dodavanje jedinice na neki pokaziva koji pokazje na neki element niza daje kao rezultat novi pokaziva koji pokazuje na sljedei element niza u odnosu na element na koji pokazuje izvorni pokaziva. Meutim, treba naglasiti da ak i u sluajevima kada adrese moemo interpretirati kao brojeve, brojana vrijednost adrese koju predstavlja pokaziva p + 1 gotovo sigurno nije jednaka brojanoj vrijednosti adrese koju predstavlja pokaziva p uveanoj za 1! Naime, kod veine raunarskih arhitektura memorija se adresira po bajtima, odnosno uzima se da jedna memorijska elija zauzima tano jedan bajt. S druge strane, promjenljive tipa double sasvim sigurno ne mogu stati u jednu jednobajtnu memorijsku lokaciju (najei nain kodiranja promjenljivih tipa double koji se danas koristi propisan je IEEE 754 standardom, prema kojem se jedna promjenljiva tipa double kodira u 64 bita, odnosno u 8 bajta, to znai da promjenljive tipa double prema tom standardu zauzimaju 8 jednobajtnih memorijskih lokacija). Stoga, ukoliko je brojana vrijednost adrese pohranjene u pokazivau p recimo 1000, tada pokazivau p + 1 ne odgovara adresa 1001 nego recimo 1008, s obzirom da element niza niz[2] ne zauzima samo memorijsku eliju sa adresom 1000, nego 8 memorijskih elija sa adresama od 1000 do 1007 ukljuivo!

    Ukoliko pretpostavimo da je memorijski model linearan (jednodimenzionalan) tako da su adrese interno reprezentirane kao obini brojevi, tada e stvarna adresa koju predstavlja pokaziva p + 1 biti vea od adrese koju predstavlja pokaziva p za broj memorijskih lokacija (tipino broj bajta) koje zauzima objekat na koji pokaziva p pokazuje. Ova konvencija je izuzetno praktina, jer 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 sljedei element istog niza, ma gdje da se on nalazio. to je jo znaajnije, to vrijedi bez obzira kako je memorija organizirana (tj. ak i u sluaju da ona nije organizirana na linearan nain). 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 sadraj pokazivaa p. To i jeste osnovni smisao pokazivake 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 vei odnosno manji za n u odnosu na indeks elementa niza na koji pokazuje pokaziva p. Stoga e sljedea naredba, za niz iz razmatranog primjera, ispisati vrijednosti 3.441, 12.9 i 6.03:

    for(int i = 0; i

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    12

    zapisane u pokazivau pok za onoliko memorijskih lokacija koliko zauzima promjenljiva broj. Meutim, ako memorijski model nije linearan, interpretacije zaista mogu biti svakakve. Stoga se trebamo pridravati standarda koji strogo naglaava da pokazivaku aritmetiku treba koristiti samo ukoliko pokaziva pokazuje na neki element niza. Ovog pravila se zaista moramo pridravati strogo: u nekim sluajevima operativni sistem ima pravo da prekine izvravanje programa u kojem je pokaziva odlutao zbog pogrene primjene pokazivake aritmetike, ak i ako se zalutali pokaziva uope ne dereferencira (ovo se deava iznimno rijetko, ali se deava)!

    Pokazivaka aritmetika nije uvijek definirana ak ni za sluaj pokazivaa koji pokazuju na elemente niza. Postoji jo pravilo koje kae 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 rijeima, 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 sadri 5 elemenata i pokazivaa 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 sadraj je nepredvidljiv, posebno u sluajevima kada memorijski model nije linearan).

    Nad pokazivakim promjenljivim se mogu primjenjivati i operatori +=, =, ++ i ije je znaenje analogno kao za obine promjenljive, samo to se koristi pokazivaka aritmetika. Na primjer, izraz p += 2 e promijeniti sadraj pokazivake promjenljive p tako da pokazuje na element niza koji se nalazi dvije pozicije navie u odnosu na element niza na koji p trenutno pokazuje, dok e izraz p promijeniti sadraj pokazivake promjenljve p tako da pokazuje na prethodni element niza u odnosu na element na koji p trenutno pokazuje (u oba sluaja, p postaje nedefiniran u sluaju da odlutamo izvan prostora koji niz zauzima; eventualno se smijemo pozicionirati tik iza kraja niza). Na taj nain mogue je koristiti pokazivae ija vrijednost tokom izvravanja programa pokazuje na razne elemente niza, kao da se pokaziva kree odnosno klizi kroz elemente niza. Ovakve pokazivae obino nazivamo klizei pokazivai (engl. sliding pointers), kao to je ilustrirano u sljedeem primjeru (u kojem pretpostavljamo da su niz i p deklarirani kao u ranijim primjerima):

    p = &niz[0];

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

    std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    13

    kojem se generira takav pokaziva ukoliko mu njegovo postojanje ugroava rad (to se vrlo vjerovatno nee desiti, ali se moe desiti). Rjeenje ovog problema je posve jednostavno nemojmo generirati takve pokazivae. Na primjer, moemo p na poetku postaviti da pokazuje tano iza posljednjeg elementa niza, tj. na nepostojei element niza niz[5] (to je, zaudo, legalno razloge za to emo shvatiti kasnije) i umanjivati pokaziva prije nego to ga dereferenciramo, kao u sljedeem (legalnom) primjeru:

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

    p--;

    std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    14

    Naime, nakon to p dostigne prvi element niza, vrijednost izraza p postaje nedefinirana, pa je pogotovo nedefinirano ta je rezultat poreenja p >= &niz[0]. Stoga je veoma upitno kada e napisana petlja uope zavriti (i da li e uope zavriti). Ovo nije samo prazno filozofiranje: napisani primjer provjereno nee raditi sa C i C++ kompajlerima za MS DOS operativni sistem (kod kojeg memorijski model nije linearan) u sluajevima kada je niz niz deklariran kao statiki ili globalni. Sigurno je mogue navesti jo mnotvo situacija u kojima ovaj primjer ne radi kako treba. Rjeenje ovog problema je isto kao u ranijim primjerima: ne smijemo dozvoliti da pokazivaka aritmetika dovede do izlaska pokazivaa izvan dozvoljenog opsega. Sljedea 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 trei argument):

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

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    15

    da e on raditi ispravno (inae ga ne bi citirali u mnogim knjigama), ali postoji vjerovatnoa i da nee. To je, bez sumnje, sasvim dovoljan razlog da ne koristimo ovakva rjeenja.

    Bez obzira na tijesnu vezu izmeu pokazivaa i nizova, na imena nizova ne mogu se primijeniti operatori +=, =, ++ i koji mijenjaju sadraj pokazivake promjenljive, odnosno lokaciju na koju oni pokazuju. Imena nizova (sama za sebe, bez indeksiranja) uvijek se tretiraju kao pokazivai na prvi element niza, i to se ne moe 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 klizei pokazivai.

    Treba napomenuti da su jedini izuzeci u kojima se ime niza upotrijebljeno samo za sebe ne konvertira u odgovarajui pokaziva sluajevi kada se ime niza upotrijebi kao argument operatora sizeof ili &. Tako, npr. sizeof niz daje veliinu itavog niza a ne veliinu pokazivaa. Takoer, izraz &niz ne predstavlja adresu pokazivaa, ve adresu prvog elementa niza. Naravno, izrazi niz ili &niz[0] takoer 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 inae vrlo rijetko koristi) je pokaziva na itav niz (odnosno pokaziva na niz kao cjelinu). Drugim rijeima, tipovi izraza niz (ili &niz[0]) i &niz se razlikuju (iako im je vrijednost ista). Pokazivai na nizove kao cjeline koriste se jedino kod dinamike alokacije viedimenzionalnih nizova, i o njima emo govoriti kada budemo obraivali tu tematiku.

    Kako se nizovi po potrebi automatski konvertiraju u pokazivae kada se upotrijebe bez indeksa, to e svaka funkcija koja kao svoj formalni parametar oekuje pokaziva na neki tip kao stvarni parametar prihvatiti niz elemenata tog tipa (naravno, formalni parametar e pri tome pokazivati na prvi element niza). Meutim, interesantno je da vrijedi i obrnuta situacija: funkciji koja kao formalni parametar oekuje niz moemo kao stvarni parametar proslijediti pokaziva odgovarajueg tipa. Zapravo, jezici C i C++ uope ne prave razliku izmeu formalnog parametra tipa pokaziva na tip Tip i formalnog parametra tipa Niz elemenata tipa Tip (u oba sluaja funkcija dobija samo adresu prvog elementa niza). Drugim rijeima, formalni parametri nizovnog tipa, ne samo da se mogu koristiti kao pokazivai, ve oni zaista i jesu pokazivai, bez obzira to u opem sluaju, nizovi i pokazivai nisu u potpunosti ekvivalentni, kao to smo ve naglasili. Zbog tog razloga, operator sizeof nee dati ispravnu veliinu niza ukoliko se primijeni na formalni argument nizovnog tipa (naime, kao rezultat emo dobiti veliinu pokazivaa).

    injenica da su formalni argumenti nizovnog tipa zapravo pokazivai, omoguava nam da funkcije koje obrauju nizove moemo realizirati i preko klizeih pokazivaa. Na primjer, funkciju koja e ispisati na ekranu elemente niza realnih brojeva moemo umjesto uobiajenog naina (sa for-petljom i indeksiranjem elemenata niza) realizirati i na sljedei nain:

    void IspisiNiz(double *pok, int n) {

    for(int i = 0; i < n; i++) std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    16

    da se formalni parametar pok u prethodnoj funkciji mogao deklarirati kao const double *pok, to bi ak bilo veoma preporuljivo. Naime, na taj nain bismo sprijeili eventualne nehotine promjene objekta na koji pokaziva pok pokazuje.

    Na ovom mjestu potrebno je razjasniti neke este zablude vezane za pokazivae na konstantne objekte. Prvo, pokaziva na konstantni objekat nee uiniti 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 nee uiniti promjenljivu broj konstantom. Promjenljiva broj e se i dalje moi mijenjati, ali ne preko pokazivaa p! Ipak, adresu konstantnog objekta (npr. adresu neke konstante) mogue je dodijeliti samo pokazivau na konstantni objekat. Takoer, pokazivau na nekonstantni objekat nije mogue (bez eksplicitne primjene operatora za konverziju tipa) dodijeliti pokaziva na konstantni objekat (ak i ukoliko su tipovi na koje oba pokazivaa pokazuju isti), jer bi nakon takve dodjele preko pokazivaa na nekonstantni objekat mogli promijeniti sadraj konstantnog objekta, to je svakako nekonzistentno. Obrnuta dodjela je legalna, tj. pokazivau na konstantni objekat mogue je dodijeliti pokaziva na nekonstantni objekat istog tipa, s obzirom da takva dodjela ne moe imati nikakve tetne posljedice.

    Treba primijetiti da se sadraj pokazivaa na konstantne objekte moe mijenjati, tj. sam pokaziva nije konstantan. To treba razlikovati od konstantnih pokazivaa, ija se vrijednost ne moe mijenjati, ali se sadraj objekata na koji oni pokazuju moe mijenjati (npr. imena nizova upotrijebljena sama za sebe konvertiraju se upravo u konstantne pokazivae). Konstantni pokazivai mogu se deklarirati na sljedei nain (primijetimo da se kvalifikator const ovdje pie iza zvjezdice):

    double *const p(&niz[2]); // Konstantni pokaziva na nekonstantni objekat

    Primijetimo da se konstantni pokaziva mora odmah inicijalizirati, jer mu se naknadno vrijednost vie ne moe mijenjati. Naravno, sada operacije poput p++ ili p += 2 koje bi promijenile sadraj pokazivaa 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]). Konano, mogue je deklarirati i konstantni pokaziva na konstantni objekat, npr. deklaracijom poput sljedee: const double *const p(&Niz[2]); // Konstantni pokaziva na konstantni objekat

    Moe se postaviti pitanje zato se uope patiti sa pokazivaima i koristiti pokazivaku aritmetiku ukoliko se ista stvar moe obaviti i uz pomo indeksiranja. Ako zanemarimo dinamiku alokaciju memorije i dinamike strukture podataka, o kojima e kasnije biti govora i koji se ne mogu izvesti bez upotrebe pokazivaa, osnovni razlog je efikasnost i fleksibilnost. Naime, mnoge radnje se mogu neposrednije obaviti upotrebom klizeih pokazivaa nego pomou indeksiranja, naroito u sluajevima kada se elementi niza obrauju sekvencijalno, jedan za drugim. Pored toga, pokaziva koji pokazuje na neki element niza sadri 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 klasini kolski primjer koji se navodi u gotovo svim udbenicima za programski jezik C (neto rjee za C++, jer se C++ ne hvali toliko pokazivaima koliko C), koji ilustrira efikasnost upotrebe klizeih pokazivaa. Naime, razmotrimo kako bi se mogla napisati funkcija nazvana KopirajString, sa dva parametra od kojih su oba nizovi znakova, koja kopira sve znakove drugog niza u prvi niz, sve do NUL graninika (odnosno znaka sa ASCII ifrom 0, koja po konvenciji jezika C predstavlja oznaku kraja niza znakova, odnosno stringa), ukljuujui i njega. Zanemarimo ovdje injenicu da praktino identina funkcija, pod imenom strcpy, ve postoji kao standardna funkcija u biblioteci cstring. Vjerovatno najoiglednija verzija funkcije KopirajString mogla bi izgledati ovako (kvalifikator const kod imena nizovnog parametra izvorni samo ukazuje da funkcija ne mijenja sadraj ovog niza):

    void KopirajString(char odredisni[], const char izvorni[]) {

    int i(0);

    while(izvorni[i] != 0) {

    odredisni[i] = izvorni[i];

    i++;

    }

    odredisni[i] = 0; // Smjetanje graninika u odredite }

    Zamijenimo sada parametre odredisni i izvorni klizeim pokazivaima (strogo uzevi, oni to ve i sada jesu, ali emo ipak izmijeniti i njihovu deklaraciju, da jasnije istaknemo namjeru da ih elimo koristiti kao klizee pokazivae). Odmah vidimo da nam indeks i vie nije potreban:

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    17

    void KopirajString(char *odredisni, const char *izvorni) {

    while(*izvorni != 0) {

    *odredisni = *izvorni;

    odredisni++; izvorni++;

    }

    *odredisni = 0;

    }

    Dalje, oba inkrementiranja moemo obaviti u istom izrazu u kojem vrimo dodjelu. Takoer, test razliitosti od 0 moemo izostaviti (zbog automatske konverzije svake nenulte vrijednosti u logiku vrijednost true) ime dobijamo sljedeu verziju funkcije:

    void KopirajString(char *odredisni, const char *izvorni) {

    while(*izvorni) *odredisni++ = *izvorni++;

    *odredisni = 0;

    }

    Konano, kako je u jezicima C i C++ dodjela takoer izraz, rezultat izvrene dodjele je zapravo rezultat izraza *izvorni (inkrementiranje se obavlja naknadno) koji je identian sa izrazom u uvjetu while petlje. Stoga se funkcija moe svesti na sljedei 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 uoiti da e ovako napisana petlja iskopirati i NUL graninik. Ovaj primjer veoma ilustrativno pokazuje u kojoj mjeri se klizei pokazivai mogu iskoristiti za optimizaciju kda (dodue, po cijenu itljivosti i razumljivosti, mada se na pokazivaku aritmetiku programeri brzo naviknu kada je jednom ponu koristiti). Funkcija strcpy iz biblioteke cstring implementirana je upravo ovako. Odnosno, ne ba u potpunosti: prava funkcija strcpy iz biblikoteke cstring implementirana je tano 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;

    }

    Praktino jedina razlika izmeu maloas napisane funkcije KopirajString i (prave) funkcije strcpy je u tome to funkcija strcpy, pored toga to obavlja kopiranje, vraa kao rezultat pokaziva na prvi element niza u koji se vri kopiranje (isto svojstvo posjeduje i funkcija strcat, takoer prisutna u biblioteci cstring, koja nadovezuje niz znakova koji je zadan drugim parametrom na kraj niza znakova koji je predstavljen prvim parametrom, odnosno vri dodavanje znakova iza nul-graninika prvog niza znakova, uz odgovarajue pomjeranje graninika). Ovaj primjer ujedno demonstrira da funkcija moe kao rezultat vratiti pokaziva. Povratna vrijednost koju vraaju funkcije strcpy i strcat najee se ignorira, kao to smo i mi radili u svim dosadanjim primjerima. Meutim, ova povratna vrijednost moe se nekada korisno upotrijebiti za ulanavanje funkcija strcpy i/ili strcat. Na primjer, posmatrajmo sljedeu sekvencu naredbi (uz pretpostavku da su recenica, rijec1, rijec2 i rijec3 propisno deklarirani nizovi znakova): std::strcpy(recenica, rijec1);

    std::strcat(recenica, rijec2);

    std::strcat(recenica, rijec3);

    std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    18

    Postoji velika vjerovatnoa da e ova naredba zaista ispisati pozdrav Dobar dan! na ekran (uskoro emo vidjeti zato), iz ega poetnik moe zakljuiti da je ona ispravna. Meutim, ovakva naredba je strogo zabranjena, jer dovodi do prepisivanja sadraja stringovne konstante. O ovoj temi treba malo detaljnije prodiskutovati, s obzirom da ona moe biti izvor brojnih frustracija. Da bismo bolje objasnili ta se ovdje deava, razmotrimo sljedeu naredbu (u kojoj se, ukoliko e to olakati razumijevanje, umjesto biblioteke funkcije strcpy moe koristiti i maloas napisana funkcija KopirajString): std::strcpy("abc", "def"); // VEOMA PROBLEMATINO!!!

    Mada e ovu konstrukciju kompajler prihvatiti (u boljem sluaju uz prijavu upozorenja), ona zapravo prepisuje sadraj memorije u kojem se uva stringovna konstanta "abc" sa sadrajem stringovne konstante "def". Posljedice ovog su posve nepredvidljive. Jedna mogunost je da e svaka upotreba stringovne konstante "abc" u programu dovesti do efekta kao da je upotrijebljena stringovna konstanta "def". Druga mogunost je da doe do prekida rada programa od strane operativnog sistema (ovo se moe desiti ukoliko kompajler sadraje stringovnih konstanti uva u dijelu memorije u kojem operativni sistem ne dozvoljava nikakav upis). Mada nekome efekat zamjene jedne stringovne konstante drugom moe djelovati kao zgodan trik, njegova upotreba je, ak i u sluaju da je konkretan kompajler i operativni sistem dozvoljavaju, izuzetno loa taktika koju ne bi trebalo koristiti ni po koju cijenu (injenica da je po standardu jezika C ovakav poziv naelno dozvoljen ne znai da ga treba ikada koristiti). Jo gore posljedice moe imati naredba

    std::strcpy("abc", "defghijk"); // FATALNA GREKA!!!

    U ovom sluaju se dua stringovna konstanta "defghijk" pokuava prepisati u dio memorije u kojem se uva kraa stringovna konstanta "abc", to ne samo da e prebrisati ovu stringovnu konstantu, nego e i prebrisati sadraj nekog dijela memorije za koji ne znamo kome pripada (za stringovnu konstantu "abc" rezervirano je tano onoliko prostora koliko ona zauzima, tako da ve prostor koji slijedi moe pripadati nekom drugom). Naravno, posljedice su potpuno nepredvidljive i nikada nisu bezazlene. Kao zakljuak, nikada ne smijemo stringovnu konstantu proslijediti kao stvarni parametar nekoj funkciji koja mijenja sadraj znakovnog niza koji joj je deklariran kao formalni parametar (ovo je tako opasna stvar da bolji kompajleri imaju opciju kojom se moe ukljuiti da se takav pokuaj tretira kao sintaksna greka). Kao konkretan primjer, kao prvi argument funkcije strcpy (odnosno KopirajString) smije se zadavati iskljuivo niz za koji eksplicitno znamo da mu se sadraj smije mijenjati (recimo, neka promjenljiva nizovnog tipa), i koji je dovoljno velik da primi sadraj niza koji se u njega kopira.

    Sada postaje jasno ta nije u redu sa pozivom std::strcat("Dobar ", "dan!"). Naime, ovaj poziv bi pokuao da nadovee stringovnu konstantu "dan!" na kraj onog dijela memorije gdje se uva stringovna konstanta "Dobar ", ime ne samo da bi promijenio sadraj ove stringovne konstante, nego bi i prebrisao sadraj memorijskih lokacija koje joj ne pripadaju. Posljedice ovakvog napada mogu se odraziti znatno kasnije u programu, to moe biti veoma frustrirajue. Ako nam operativni sistem odmah prekine izvoenje takvog programa zbog zabranjenih akcija, jo moemo smatrati da smo imali sree. Stoga, moemo zakljuiti da funkciju strcat ima smisla koristiti samo ukoliko je njen prvi argument niz koji ve sadri neki string (eventualno prazan) i koji je dovoljno veliki da moe prihvatiti string koji e nastati nadovezivanjem. Moemo istu stvar rei i ovako: korisnici koji poznaju neke druge programske jezike mogu pomisliti da funkcija strcat vraa kao rezultat string koji je nastao nadovezivanjem prvog i drugog argumenta (ne mijenjajui svoje argumente), s obzirom da takve funkcije postoje u mnogim programskim jezicima. Meutim, to nije sluaj sa funkcijom strcat iz jezika C++: ona nadovezuje drugi argument na prvi, pri tome mijenjajui prvi argument. U neku ruku, razlika izmeu funkcije strcat i srodnih funkcija u drugim programskim jezicima najlake se moe uporediti sa razlikom koja postoji izmeu izraza x += y i x + y.

    Pokazivaka aritmetika moe se veoma lijepo iskoristiti zajedno sa funkcijama koje barataju sa klasinim nul-terminiranim stringovima u stilu jezika C, poput maloas napisane funkcije KopirajString, odnosno bibliotekih 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 najlake moemo uraditi na jedan od sljedea dva naina (oba su ravnopravna):

    std::strcpy(recenica2, &recenica[9]); std::strcpy(recenica2, recenica + 9);

    Ista tehnika moe se iskoristiti i sa funkcijama koje prihvataju proizvoljne vrste nizova kao parametre, odnosno koje prihvataju pokazivae na proizvoljne tipove. Na primjer, pretpostavimo da elimo iz ranije deklariranog niza realnih brojeva niz ispisati samo drugi, trei i etvrti element (tj. elemente sa indeksima 1, 2

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    19

    i 3). Nema potrebe da prepravljamo ve napisanu funkciju IspisiNiz, s obzirom da ovaj cilj moemo veoma jednostavno ostvariti jednim od sljedea dva poziva, bez obzira da li je funkcija napisana da prihvata pokazivae ili nizove kao svoj prvi argument, i bez obzira da li je napisana koritenjem indeksiranja ili klizeih pokazivaa:

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

    Ova univerzalnost ostvarena je automatskom konverzijom nizova u pokazivae, kao i mogunou da se indeksiranje primjenjuje na pokazivae kao da se radi o nizovima.

    Tijesna veza izmeu nizova i pokazivaa i injenica da se nizovi znakova tretiraju donekle razliito od ostalih nizova ima kao posljedicu da se i pokazivai na znakove tretiraju neznatno drugaije nego pokazivai na druge objekte. Tako, ukoliko je p pokaziva na znak (tj. na tip char), izraz std::cout > p;

    sa tastature e se unijeti jedna rije, i smjestiti u niz recenica poev od desetog znaka, odnosno tano iza teksta Poetak: . I u ovom primjeru smo mogli izbjei uvoenje promjenljive p i pisati direktno izraz poput std::cin >> &recenica[9] ili std::cin >> recenica + 9. Ukoliko bismo umjesto jedne rijei eljeli unijeti itavu liniju teksta, koristili bismo konstrukciju poput std::cin.getline(p, 91) odnosno std::cin.getline(&recenica[9], 91) ili std::cin.getline(recenica + 9, 91) ako pored toga elimo i da izbjegnemo uvoenje promjenljive p. Maksimalna duina 91 odreena je tako to je od maksimalnog kapaciteta od 100 znakova odbijen prostor koji je ve rezerviran za tekst Poetak: .

    Pokazivai i pokazivaka aritmetika omoguavaju prilino mone trikove, koje bi inae bilo znatno tee realizirati. S druge strane, pokazivai mogu biti i veoma opasni, jer lako mogu odlutati u neko nepredvieno

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    20

    podruje memorije, to omoguava da pomou njih indirektno promijenimo praktiki bilo koji dio memorije u kojem su doputene izmjene (posljedice ovakvih intervencija su uglavnom nepredvidljive i esto fatalne). Na primjer, ve smo rekli da je smisao pokazivake aritmetike precizno definiran samo za pokazivae koji pokazuju na neki element niza, i to samo pod uvjetom da novodobijeni pokaziva ponovo pokazuje na neki element niza, ili eventualno tano iza posljednjeg elementa niza. Ukoliko se ne pridravamo ovih pravila, pokazivai mogu odlutati u nedozvoljena podruja memorije. Razmotrimo, na primjer, sljedeu sekvencu naredbi:

    int a(5), *p(&a); p++; // NELEGALNO: p ne pokazuje na element nekog niza!

    *p = 3;

    Ovdje smo prvo deklarirali cjelobrojnu promjenljivu a koju smo inicijalizirali na vrijednost 5 , a

    zatim pokazivaku promjenljivu p koju smo inicijalizirali tako da pokazuje na promjenljivu a (odnosno, ona sada sadri adresu promjenljive a). Nakon toga je na promjenljivu p primijenjen operator ++. Na ta sada pokazuje promjenljiva p? Strogo uzevi, odgovor uope nije definiran u skladu sa pravilima koja po standardu vrijede za pokazivaku aritmetiku, ali u skladu sa nainom kako se pokazivaka aritmetika implementira u veini kompajlera, p e gotovo sigurno pokazivati na prostor u memoriji koji se nalazi neposredno iza prostora koji zauzima promjenljiva a. Nakon toga e dodjela *p = 3 na to mjesto u memoriji upisati vrijednost 3

    . Meutim, ta se nalazi na tom mjestu u memoriji? Odgovor je nepredvidljiv:

    moda neka druga promjenljiva (ovo je najvjerovatnija mogunost), moda nita pametno, a moda neki podatak od vitalne vanosti za rad operativnog sistema! Samim tim, posljedica ove dodjele je nepredvidljiva. U svakom sluaju, dodjelom *p = 3, mi smo zapravo napali memorijski prostor iju svrhu ne znamo, to je strogo zabranjeno. Za pokaziva koji je odlutao tako da vie ne znamo na ta pokazuje kaemo da je divlji pokaziva (engl. wild pointer). To ne mora znaiti da ne znamo koja je adresa pohranjena u njemu, nego samo da ne znamo kome pripada adresa koju on sadri. Divlji pokazivai nikada se ne bi trebali pojaviti u programu. Naalost, greke koje nastaju usljed divljih pokazivaa veoma se teko otkrivaju, a prilino ih je lako napraviti. Zbog toga se u literaturi esto citira da su pokazivai zloesti (engl. pointers are evil), i oni predstavljaju drugi tipini zloesti koji postoji u jezicima C i C++ (prvi zloesti koncept su nizovi, koji postaju zloesti ukoliko indeks niza odluta izvan dozvoljenog opsega). Smatra se da oko 90% svih fatalnih greaka u dananjim profesionalnim programima nastaje upravo zbog ilegalnog pristupa memoriji preko divljih pokazivaa!

    Treba primijetiti da je i pokaziva koji pokazuje tano iza posljednjeg elementa niza takoer divlji pokaziva, jer i on pokazuje na lokaciju koja ne pripada nizu (niti se zna kome ona zapravo pripada). Meutim, standard jezika C++ kae 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 vei od svih pokazivaa koji pokazuju na elemente tog niza. Ipak, to jo uvijek ne znai da takav pokaziva smijemo dereferencirati. Pokuaj njegovog dereferenciranja takoer moe imati nepredvidljive posljedice. Meutim, njega bar smijemo kreirati bez opasnosti. Druge vrste divljih pokazivaa ponekad ne smijemo ni kreirati moe se desiti da i smo njihovo kreiranje dovede do kraha, ak i ukoliko se oni uope ne dereferenciraju (to se, u nekim rijetkim sluajevima, moe desiti npr. ukoliko primjenom pokazivake aritmetike pokaziva odluta ispred prvog elementa niza). Dalje, sve operacije sa njima (npr. poreenje, 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 nee izazvati krah) i smijemo ga porediti sa drugim pokazivaima, jedino ga ne smijemo dereferencirati. Sa drugim divljim pokazivaima ne smijemo raditi apsolutno nita. ak i sm pokuaj njihovog kreiranja (koji tipino nastaje neispravnom upotrebom pokazivake aritmetike) moe biti fatalan po program!

    U vezi sa divljim pokazivaima neophodno je naglasiti jednu nezgodnu osobinu: svi pokazivai su neposredno nakon deklaracije divlji (osim u sluajevima kada se uporedo sa deklaracijom vri i inicijalizacija), sve dok im se eksplicitno ne dodijeli vrijednost! Naime, kao i sve ostale promjenljive, njihova vrijednost je nedefinirana neposredno nakon deklaracije (osim ukoliko se istovremeno vri i inicijalizacija), sve do prve dodjele. Tako je i sa pokazivaima: njihova poetna vrijednost je u odsustvu inicijalizacije nedefinirana, tako da se sve do prve dodjele ne zna ni na ta pokazuju. Na ovo smo se ve ukratko osvrnuli prilikom razmatranja raznih neispravnih realizacija funkcije Razmijeni. Recimo, sljedea 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; // NELEGALNO: Gdje se smjeta ovaj broj 10???

    Slinu situaciju imamo u sljedeem katastrofalno opasnom neispravnom primjeru, koji se veoma esto moe sresti kod poetnika:

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    21

    char *p;

    std::cin >> p; // NELEGALNO: Gdje se smjeta uneseni tekst??? std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    22

    (pri tome se svi pokazivai automatski tretiraju kao logika vrijednost tano, osim nul-pokazivaa koji se tretira kao netaan).

    Nul-pokazivai se takoer esto vraaju kao rezultati iz funkcija koje kao rezultat vraaju pokazivae, u sluaju da nije mogue vratiti neku drugu smislenu vrijednost. Na primjer, biblioteka cstring sadri veoma korisnu funkciju strstr koja prihvata dva nul-terminirana stringa kao parametre. Ova funkcija ispituje da li se drugi string sadri u prvom stringu (npr. string je lijep sadri se u stringu danas je lijep dan). Ukoliko se nalazi, funkcija vraa kao rezultat pokaziva na znak unutar prvog stringa od kojeg poinje tekst identian 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 vraa nul-pokaziva. Sljedei primjer demonstrira kako se ova funkcija moe iskoristiti (obratite panju na igre sa pokazivakom aritmetikom):

    char recenica[100], fraza[100];

    std::cin.getline(recenica, sizeof recenica);

    std::cin.getline(fraza, sizeof fraza);

    char *pozicija(std::strstr(recenica, fraza));

    if(pozicija != nullptr)

    std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    23

    S druge strane, slina konstrukcija nije mogua sa obinim nizovima znakova, nego se moraju koristiti ili dinamiki stringovi (tj. objekti tipa string, uvedeni u jeziku C++), ili konstrukcije poput sljedee:

    char recenica[100];

    std::strcpy(recenica, "Ja sam prva reenica..."); std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    24

    bilo kakav ozbiljniji rad sa stringovima ne koristite niti nul-terminirane nizove znakova niti pokazivae na znakove, ve dinamike stringove, odnosno tip string!

    Mada je reeno da pokazivaima nije mogue eksplicitno dodjeljivati brojeve, niti je mogue pokazivau na jedan tip dodijeliti pokaziva na drugi tip, postoji indirektna mogunost da se ovakva dodjela ipak izvri, pomou operatora za pretvorbu tipova (typecasting operatora). Na primjer, neka imamo sljedee deklaracije:

    int *pok_na_int;

    double *pok_na_double;

    Tada pretvorba tipova dozvoljava, da na sopstveni rizik, izvrimo i ovakve dodjele:

    pok_na_int = (int *)3446; // RIZINO! pok_na_double = (double *)pok_na_int; // RIZINO!

    Prikazane konverzije tipova koriste sintaksu i stil jezika C. Ova sintaksa radi i u jeziku C++, mada se u jeziku C++ preporuuje druga sintaksa, u kojoj se javlja kljuna rije reinterpret_cast (koja djeluje slino kao i static_cast, samo slui za drastine pretvorbe pokazivakih tipova):

    pok_na_int = reinterpret_cast(3446); // RIZINO!

    pok_na_double = reinterpret_cast(pok_na_int); // RIZINO!

    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 (uz pretpostavku da raunar na kojem radimo koristi linearni memorijski model), 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 takoer veoma opasno. Naime, ako se na nekoj lokaciji nalazi cijeli broj, tamo ne moe u isto vrijeme biti realni broj (ne smijemo zaboraviti da se cijeli i realni brojevi zapisuju na posve razliite naine u raunarskoj memoriji, ak i kada imaju istu vrijednost). Pored toga, na razliite tipove pokazivaa pokazivaka 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 moe dereferencirati kao i svaki drugi pokaziva. Stoga su naredbe poput sljedee sasvim legalne (obje su ekvivalentne, s tim to druga radi samo u jeziku C++):

    *(int *)3446 = 50; // RIZINO!

    *reinterpret_cast(3446) = 50; // RIZINO!

    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 omoguavaju ovakvu slobodu u pristupima resursima raunara, to ih ini idealnim za pisanje sistemskog softvera (naravno, za tu svrhu treba izuzetno dobro poznavati arhitekturu raunara i operativnog sistema za koji se pie 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, zahvaljujui pokazivakoj aritmetici, funkcije koje barataju sa nizovima, poput ranije napisane funkcije IspisiNiz, mogue iskoristiti tako da se izvre samo nad dijelom niza, koji ne poinje nuno od prvog elementa. Meutim, sve takve funkcije bile su heterogene po pitanju svojih parametara, odnosno njihovi parametri bili su razliitih 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 praktinije 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 pokazivai na prvi i posljednji element koji e se ispisati. Ovako napisane funkcije su esto ne samo jednostavnije za koritenje, ve i jednostavnije za implementaciju, jer se na taj nain moe efikasno koristiti pokazivaka aritmetika. Zbog nekih tehnikih razloga, bolje je umjesto pokazivaa na posljednji element koristiti pokaziva iza posljednjeg elementa. Na primjer, upravo takva je sljedea 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++)

    std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    25

    Alternativno, mogli smo ak i izbjei koritenje dodatne promjenljive p, recimo ovako:

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

    while(pocetak < iza_kraja) std::cout

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    26

    njihovu konverziju u pokazivae na tip char, s obzirom da je tip char najelementarniji tip podataka (svaki podatak tipa char uvijek zauzima tano jedan bajt), a nakon toga obaviti kopiranje niza kao da se radi o obinom nizu bajtova (odnosno, obaviti kopiranje nizova bajt po bajt, onako kako su oni pohranjeni u memoriji). Meutim, 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 sljedei 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 odreeni tip ili izraz). Naravno, umjesto izraza sizeof(double) moe se koristiti i izraz sizeof niz1[0] ime poziv funkcije postaje neovisan od stvarnog tipa elemenata niza. U sutini, izraz 10 * sizeof(double) bi se ak mogao prosto zamijeniti sa sizeof niz1, jer ono to nas zanima je zapravo broj bajtova koje zauzima niz niz1. Vidimo da smo, na izvjestan nain, uz pomo generikih pokazivaa 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.

    Radi boljeg razumijevanja, navedimo i jo jedan neto sloeniji primjer koji koristi generike pokazivae. Pretpostavimo da treba napisati funkciju IzvrniNiz sa dva parametra niz i br_elemenata koja izvre br_elemenata elemenata niza niz, iji su elementi proizvoljnog tipa, u obrnuti poredak. Ponovo, ovaj problem je vrlo lako rjeiv uz pomo generikih funkcija, ali na ovom mjestu nas ne zanima takvo rjeenje. Ovdje je cilj pokazati kako se ovaj problem moe rijeiti uz pomo generikih pokazivaa. Jasno je da parametar niz treba biti generiki pokaziva. Meutim, 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 uinimo, izvrnuemo i bajtovsku strukturu svakog individualnog elementa niza, to naravno ne smijemo uraditi. Dakle, bajtovska struktura svakog individualnog elementa niza ne smije se naruiti. Meutim, funkcija ne moe ni na kakav nain saznati tip elemenata niza, odakle slijedi da ne moe saznati ni koliko bajtova zauzima svaki od elemenata niza. Ukoliko malo bolje razmislimo o svemu, zakljuiemo da se problem ne moe rijeiti 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, traena funkcija se moe napraviti uz malo igranja sa pokazivakom aritmetikom. Jedno od moguih rjeenja 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 sljedeeg:

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

    Opisani primjeri nisu nimalo jednostavni, i trae izvjesno poznavanje naina na koji su organizirani podaci u raunarskoj memoriji. Najbolje je da probate runo pratiti izvravanje napisanih funkcija na nekom konkretnom primjeru. Napomenimo da su ovi primjeri ubaeni isto sa ciljem pojanjenja prave prirode pokazivaa. Naime, u jeziku C++ ovakve primjere ne treba pisati, s obzirom da se razmotreni problemi mnogo lake rjeavaju pomou pravih generikih funkcija (koje emo uskoro upoznati). Ovakve kvazi-generike

  • Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima: Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu Sve to treba znati o pokazivaima Akademska godina 2014/15

    27

    funkcije tipino zahtijevaju udne ili dopunske parametre (poput parametra vel_elementa u posljednjem primjeru). Pored toga, nemogue je na ovaj nain napraviti funkciju koja bi, recimo, nala najvei element niza, ili sumu svih elemenata u nizu (koja bi radila za nizove proizvoljnih tipova elemenata), s obzirom da na ovaj nain mi uope ne baratamo sa elementima niza, ve sa bajtovima koje tvore elementi niza. Stoga je takav pristup mogu jedino za primjene u kojima tana priroda elemenata niza nije ni bitna (kakva je npr. kopiranje).

    Bez obzira na brojna ogranienja funkcija koje koriste generike pokazivae, 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 identina je posljednjoj napisanoj verziji funkcije KopirajNiz. Drugim rijeima, kopiranje niza niz1 od 10 realnih brojeva u niz niz2 moe se, u principu, obaviti i na sljedei nain:

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

    Takoer, ni funkcije koje kao jedan od parametara zahtijevaju veliinu elemenata niza u bajtovima (poput posljednje verzije funkcije IzvrniNiz), nisu nikakva rijetkost u standardnim bibliotekama. Takva je, na primjer, funkcija qsort iz biblioteke cstdlib, koja slui za sortiranje elemenata niza u odreeni poredak. Programeri u jeziku C intenzivno koriste ovakve funkcije. Mada se one, u naelu, mogu koristiti i u jeziku C++, postoje jaki razlozi da u jeziku C++ ove funkcije ne koristimo (ova napomena namijenjena je najvie onima koji imaju dobro prethodno programersko iskustvo u jeziku C). Naime, sve funkcije zasnovane na generikim pokazivaima svoj rad baziraju na injenici da je manipulacije sa svim tipovima podataka mogue izvoditi bajt po bajt. Ta pretpostavka je tana 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 moe imati fatalne posljedice. Isto vrijedi i za funkciju qsort, koja moe napraviti pravu zbrku ukoliko se pokua primijeniti za sortiranje nizova iji su elementi dinamiki stringovi. Tipovi podataka sa kojima se moe 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 klasini nizovi jesu POD tipovi, dok vektori i dinamiki stringovi nisu.

    Opisani problemi sa koritenjem funkcija iz biblioteka naslijeenih iz jezika C koje koriste generike pokazivae ne treba mnogo da nas zabrinjava, jer za svaku takvu funkciju u jeziku C++ postoje bolje alternative, koje garantirano rade u svim sluajevima. 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 (takoer iz biblioteke algorithm, koju emo kasnije detaljno obraditi). Ove funkcije, ne samo da su univerzalnije od srodnih funkcija naslijeenih iz jezika C, ve su i jednostavnije za upotrebu. Na kraju, ,oramo dati jo samo jednu napomenu. Programeri koji posjeduju bogatije iskustvo u jeziku C, teko se odriu upotrebe funkcije memcpy i njoj srodnih funkcija (poput memset), jer im je poznato da su one, zahvaljujui raznim trikovima na kojima su zasnovane, vrlo efikasne (mnogo efikasnije nego upotreba obine for-petlje). Meutim, bitno je znati da odgovarajue funkcije iz biblioteke algorithm, uvedene u jeziku C++, poput funkcije copy (ili fill, koju treba koristiti umjesto memset), tipino nisu nita 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 osjeati nelagodu prilikom prelaska sa upotrebe funkcija poput memcpy na funkcije poput copy. Jednostavno, treba prihvatiti injenicu: funkcije poput memcpy, memset, qsort i njima sline zaista ne treba koristiti u C++ programima (osim u vrlo iznimnim sluajevima). One su zadrane preteno sa ciljem da omogue kompatibilnost sa ve napisanim programima koji su bili na njima zasnovani!