Lekcija 0.4
Memorija i pokazivaci
U jezicima c postoji nesto sto se zove pokazivac a funkcija mu je da pokazuje na dio memorije. Memorija koju cemo ovdje spominjati je laicki dio u memoriji koji kompjuter rezervise za rad programa i ta memorija se nakon sto se zauzme nekom varijablom ostaje zauzeta sve do kraja bloka i u slucaju da hocemo da zauzmemo novi dio memorije onda se trazi prvi slobodan dio u memoriji koji je dovoljno velik da primi varijablu (razliciti tipovi imaju razlicitu velicinu, tj. zauzimaju razlicitu kolicinu memorije).
U slucaju jednostavne deklaracije varijable, npr.
int a; u memoriji se trazi slobodna lokacija gdje bi se ta varijabla mogla "smjestiti". Uzmimo na primjer da niz kutija predstavlja neki dio memorije kojoj program ima pristup.
U slucaju da su kutije 0, 1, i 2 zauzete (tj. vec imaju varijablu u "sebi") onda ce se u slucaju gornje deklaracije varijabla a smjestiti u kutiju broj 3. U slucaju ovakve deklaracije varijabla ce najvjerovatnije imati neku vrijednost koja je vec bila u kutiji (a u nekim slucajevima ce biti inicijalizirana na nulu ali se ne treba na to previse oslanjati). Ovdje je zanimljiva jedna stvar a to je da varijabli mozemo saznati adresu uz pomoc operatora &. Ukoliko ovaj operator stavimo ispred varijable onda on kao rezultat vraca adresu na kojoj je varijabla smjestena.
Code: Select all
#include <iostream>
int main()
{
int a(9);
std::cout << "Varijabla a ima vrijednost " << a << " i nalazi se na lokaciji: " << &a;
}
Ovaj kod ispisuje
Code: Select all
Varijabla a ima vrijednost 9 i nalazi se na lokaciji: 0x7ffd09dde77c
Gornji primjer pokazuje da nakon zauzimanja mjesta u memoriji i inicijalizacije varijable na vrijednost 9 naredba std::cout ce uz samu vrijednost varijable ispisati i njenu adresu u memoriji. Ove adrese u vecini slucajeva nisu toliko ni bitne jer svakim pokretanjem program ce naci dio memorije koji je bas u tom trenutku slobodan i ne mora znaciti da ce to biti ista adresa kao i u proslom pokretanju.
Postoje i varijable kojima je zadatak da cuvaju adresu varijable koja nam je zanimljiva i oni se nazivaju pokazivaci. Njihova deklaracija je malo drugacija nego deklaracija "pravih" varijabli a vrsi se na sljedeci nacin
tip_podatka* ime_varijable. Vrijedi opet ista prica da neinicijalizirane varijable je potrebno inicijalizirati na neku vrijednost (u slucaju pokazivaca kompajler sa cpp.sh ce ga inicijalizirati na nulu tj. dobit cemo nullptr ili nul pokazivac) jer u slucaju razlicitih kompajlera rezultati mogu biti razliciti.
Prepravkom gornjeg primjera mozemo koristenjem pokazivaca pristupiti adresi bloka memorije koji zauzima varijabla na sljedeci nacin
Code: Select all
#include <iostream>
int main()
{
int a(9);
//deklaracija pokazivacke varijable
int* b;
//dodjela adrese pokazivackoj varijabli (adresu dobijamo upotrebom adresnog operatora ispred varijable)
b = &a;
std::cout << "Varijabla a ima vrijednost " << a << " i nalazi se na lokaciji: " << b;
}
U slucaju da zelimo da pristupimo vrijednosti koja se nalazi u kutiji putem pokazivaca koristimo operator dereferenciranja ispred pokazivacke promjenjive
*pokazivac; i on kao rezultat daje vrijednost varijable na koju pokazivac pokazuje. U gornjem primjeru ako bismo dodali na
std::cout jos par instrukcija ispred tackazareza i to redom
<< ", a vrijednost varijable na koju pokazivac pokazuje je: " << *b kao rezultat bi dobili sljedeci ispis
Code: Select all
Varijabla a ima vrijednost 9 i nalazi se na lokaciji: 0x7ffd9f7956cc, a vrijednost varijable na koju pokazivac pokazuje je: 9
.
Zanimljivo je dodati da pokusaj ispisivanja adrese nul pokazivaca upotrebom
std::cout << nul_pokazivac; kao rezultat daje nulu a pokusaj pristupanja vrijednosti na koju pokazuje daje gresku i dovodi do kraha programa (jer ne pokazuje ni na koju vrijedost u memoriji).
Pravu jacinu pokazivaci dobijaju u slucaju nizova i tu je njihova upotreba najveca (mada se u novije vrijeme vise koriste napredniji "nizovi" pa se ova funkcionalnost slabije koristi mada je korisno da je upoznamo). U slucaju ispisa elemenata niza (u proslim primjerima) smo imali primjer upotrebe pokazivacke varijable mada to nismo eksplicitno naglasili pa hajmo redom da dodjemo do toga korak po korak.
Prilikom deklaracije niza zauzme se dio u memoriji koji moze primiti toliko elemenata niza (dobar primjer su kutije). Zamislimo da smo deklarisali niz od sest elemenata i da kutije predstavljaju te elemente u memoriji. U slucaju pokusaja ispisa elementa niza (npr. kutije sa brojem tri, tj. cetvrtog elementa) kao rezultat cemo dobiti neku vrijednost koja se zadesila u kutiji (neki kompajleri ce postaviti to na nulu a neki nece). Koristili smo pristup elementima preko for petlje uz pomoc indeksacije a isti rezultat smo mogli dobiti upotrebom pokazivaca jer postoji jedna caka u slucaju nizova a to je da ime niza upotrebljeno samo za sebe predstavlja adresu pocetka tog niza. Tako na primjer ukoliko deklarisemo niz od 6 elemenata i hocemo ispisati adresu pocetka niza to mozemo uraditi na sljedeci nacin:
Code: Select all
#include <iostream>
int main()
{
//vrsimo inicijalizaciju niza od 6 elemenata tako da su pripadne vrijednosti elemenata redom 1,2,3,4,5,6
int a[6]{1,2,3,4,5,6};
std::cout << "Niz se nalazi na lokaciji: " << a;
}
Ispis izgleda ovako
Code: Select all
Niz se nalazi na lokaciji: 0x7ffc633040d0
.
Za pristup elementima smo koristili npr. za pristup trecem elementu
a[2] sto smo mogli dobiti upotrebom pokazivacke logike na zanimljiv nacin. Naime potrebno je koristiti operator dereferenciranja ispred pokazivaca za koji hocemo da vidimo element na koji pokazuje i to je to. U slucaju da zelimo pristupiti trecem elementu mozemo koristiti
*(a+2) i rezultat te akrobacije ce biti element koji se nalazi na trecem mjestu niza.
a[2] i *(a+2) su ekvivalentni a vecinom se koristi prvi pristup.
Neko ce se zapitati sta znaci
a+2, i koja je funkcija takve konstrukcije a laicki receno to znaci uzmi adresu pocetka niza
a i pomjeri se za dva mjesta unaprijed. Isto smo mogli postici koristenjem pokazivaca
b i njegovim pomjeranjem za dva mjesta unaprijed operatorom
++. U tom slucaju kod bi izgledao ovako
Code: Select all
#include <iostream>
int main()
{
int a[6]{1,2,3,4,5,6};
int *b;
b = a;
b++;
b++;
std::cout << "Vrijednost treceg elementa je: " << *b;
}
U ovom slucaju je zanimljivo da smo dodjelu vrsili bez upotrebe adresnog operatora jer kao sto je vec receno ime niza upotrebljeno bez uglastih (uglatih) zagrada daje adresu pocetka niza. U slucaju da smo htjeli adresu treceg elementa bez pomjeranja pokazivaca dva mjesta unaprijed to smo mogli postici upotrebom
b = &a[2];, jer u tom slucaju imamo sljedecu konstrukciju (ekvivalentnu)
*(a + 2) koja kao rezultat daje element koji se nalazi na trecem mjestu niza
a, a adresni operator ispred tog svega
&(*(a+2)) znaci daj adresu elementa koji se nalazi na trecoj poziciji niza.
Code: Select all
#include <iostream>
int main()
{
int a[6]{1,2,3,4,5,6};
int *b;
b = &(*(a+2));
std::cout << "Vrijednost treceg elementa je: " << *b;
}
Ostaje nam jos prenos nizova u funkcije. Da bi nizove prenosili u funkciju moramo koristiti pokazivace ali prilikom prenosa gubi se jedna informacija o nizu a to je velicina niza a to znaci da kao jos jedan parametar moramo prenositi i velicinu samog niza.
Code: Select all
#include <iostream>
//niz u funkciju saljemo tako sto u parametre funkcije navedemo tip_podatka* ime_varijable
void Niz(int *a){
std::cout << "Velicina niza u funkciji je: " << sizeof(a);
}
int main()
{
int *p;
std::cout << "Velicina pokazivaca je: " << sizeof(p) << std::endl;
int a[6]{1,2,3,4,5,6};
std::cout << "Velicina niza prije funkcije je: " << sizeof(a) << std::endl;
//saljemo niz u funkciju a kao sto je ranije naglaseno ime niza upotrijebljeno samo bez uglastih zagrada ima funkciju pokazivaca na prvi element niza pa smo zato i u funkciji Niz morali da koristimo isti tip podatka tj. pokazivac se mora prenositi u pokazivac da bi funkcija radila (moraju biti isti tipovi parametara i onoga sto saljemo u funkciju)
Niz(a);
}
Ovaj program ce ispisati
Code: Select all
Velicina pokazivaca je: 8
Velicina niza prije funkcije je: 24
Velicina niza u funkciji je: 8
Gornji primjer je zanimljiv zbog toga sto u slucaju pokazivaca
p rezultat operatora
sizeof daje rezultat 8 a velicinu niza daje 24. A kad isti niz posaljemo u funkciju onda je velicina tog "niza" ista kao i velicina pokazivaca pa se da zakljuciti da se izgubila informacija o velicini niza. Malo objasnjenje o ovim brojevima, naime velicina u bajtima za pokazivac na cjelobrojnu vrijednost je 8 dok je velicina jednog cjelobrojnog elementa 4 bajta pa onda velicina niza od 6 cjelobrojnih elemenata je 24 jer je 4 * 6 = 24.
Zasto je ovo bitno? Evo jedan primjer, postoji nesto u c++ sto se zove rangovska petlja (range loop) a koristi se gdje god se moze koristiti umjesto obicne for petlje upravo zbog njene izvedbe i jednostavnosti. Naime da bi ispisali gornji niz potrebno je da umjesto klasicnog inicijalizovanja brojaca i kucanja uslova i koraka brojaca i same indeksacije niza unutar for petlje to se jednostavno izvrsi na sljedeci nacin
Code: Select all
int main()
int main()
{
int a[6]{1,2,3,4,5,6};
//koristimo auto kao tip elementa iz kolekcije a jer kompajler moze sam da zakljuci koji je tip da ne kucamo neke konstrukcije koje znaju biti po nekoliko rijeci za tip (npr. kada imamo vektor pametnih pokazivaca na liste dablova :lol: )
for(auto element : a)
std::cout << element << " ";
}
Gornji nacin ispisa i kretanja kroz same elemente je dosta jednostavniji od klasicnog nacina pa se uglavnom koristi. Pokusaj koristenja istog nacina u funkciji niz nece raditi, jer prava istina je da tip podatka nad kojim radi rangovska petlja mora dati rezultat za funkcije
begin i
end (koje redom kao rezultat vracaju iterator na prvi i na iza zadnjeg elementa i tako for petlja zna kad je dostigla kraj i "velicinu" niza). U slucaju prenosa niza u funkciju gubi se funkcionalnost niza pa isti isjecak nece raditi jer smo kao parametre funkcije Niz deklarisali samo pokazivac na cjelobrojnu vrijednost a na pokazivac koji ne pokazuje na konkretni niz ne mogu se iskoristiti
begin i
end pa zato rangovska petlja nece ni raditi u tom slucaju. To je razlog sto i operator
sizeof daje kao rezultat velicinu samog pokazivaca u funkciji a ne niza koji joj je proslijedjen u funkciju.
Isti problem se javlja prilikom vracanja niza iz funkcije jer se citav niz ne moze prenositi a tako ni vracati iz funkcije nego se to mora raditi uz pomoc pokazivaca.
Code: Select all
int* NoviNiz(){
//veoma vazno deklarisati niz da bude staticki
static int a[5]{6,7,8,9,10};
return a;
}
int main()
{
int a[5]{1,2,3,4,5};
int* PokazivacNaNizIzFunkcije;
//dodjeljujemo pokazivacu pokazivac na prvi element novog niza
PokazivacNaNizIzFunkcije = NoviNiz();
std::cout << "Prvi niz ima sljedece elemente: ";
//koristimo rangovsku for pelju
for(auto element : a)
std::cout << element << " ";
//prelazak u novi red
std::cout << std::endl;
std::cout << "A drugi niz ima sljedece elemente: ";
//koristimo rucno brojanje, tj. klasicnu for petlju
for(int i = 0; i < 5; i++){
std::cout << *PokazivacNaNizIzFunkcije << " ";
PokazivacNaNizIzFunkcije++;
}
}
Veoma vazno je da se niz u funkciji deklarise kao staticki jer c++ ima jedno pravilo a to je da nakon svakog bloka (viticaste zagrade) sve varijable obrisu (za sada ce tako biti, dok ne udjemo u naprednije vode) ili jednostavnije ne moze im se pristupiti pa tako to isto vrijedi i za niz u funkciji a upotrebom
static ispred deklaracije kazemo kompajleru ne diraj tu promjenjivu do kraja programa i ona ce ostati u memoriji do kraja programa tj. kompajler je nece obrisati na kraju bloka.
Evo jedan zadatak koji pokazuje da se pokazivaci mogu koristiti i sa lijeve strane znaka jednakosti i tada imaju ulogu varijable na koju pokazuju
Code: Select all
int main()
{
int a, b;
int* p;
a = 5; b = 6;
//pokazivacu dodjeljujemo adresu varijable a
p = &a;
//onome na sta pokazivac p pokazuje dodjeljujemo vrijednost 1
*p = 1;
//program ispisuje 1 6
std::cout << a << " " << b;
}
VEOMA VAZNA NAPOMENA!!!
U slucaju da pokazivac ode van opsega niza dereferenciranjem se mogu dobiti neke vrijednosti ali to nije preporucljivo raditi! Vodite racuna o opsegu niza i pokazivace usmjeravajte samo na niz! Sta ce se desiti ako pokrenemo sljedeci program?
Code: Select all
int main()
{
int a, b;
int* p;
a = 5; b = 6;
p = &a;
//pomjeramo pokazivac za jedan pored varijable a
p++;
//mijenjamo vrijednost te lokacije na 1
*p = 1;
//ispisuje 5 6 1
std::cout << a << " " << b << " " << *p;
}
Gdje taj pokazivac pokazuje tacno? Sta ce on tacno izmijeniti? Eee, na to ni ja ne znam odgovor
.
Evo jedan zadatak za vas koji pratite ovaj uvod u c++, napisite program koji od korisnika trazi da unese 10 elemenata koji su cjelobrojnog tipa u niz. Zatim da od korisnika trazi da ide naprijed ili nazad i koristeci kljucnu rijec "naprijed" ili "nazad" i tako da se krece kroz niz, npr. ako korisnik na pocetku kaze naprijed program treba da ispise drugi element niza, ako kaze nazad treba da ispise prvi element niza. Za kretanje kroz niz koristiti pokazivac i voditi racuna o opsegu (tj. da se ne izadje van niza). Kad korisnik napise "stop" program treba da nacrta slovo V koje ima visinu i duzinu kao broj koji se nalazi u nizu kad je korisnik rekao stop (ako se zaustavi na 16 onda treba da je visina i duzina 16 znakova)
Ovim smo presli najosnovnije stvari koje su potrebne da bi se mogli baviti malo slozenijim problemima. Kao sto sam rekao posto je jezik c++ preopsiran najbolje bi bilo da kako koje tipove podataka spominjem da trazite funkcije koje rade sa tim tipovima podataka i iste koristite pa ako dodje do kakvog problema da postavite pitanje i da vam ja ili neko drugi pokusamo objasniti.
Sva pitanja i zadatke postavljajte na temi
c-osnove-i-ostale-tricarije-pitanja-i-o ... 50648.html .