Skripta informatičke škole Proanima
Lekcija 1
Uvod u C++
Danas ćemo naučiti: • • •
Zašto je C++ postao industrijski standard za razvoj softvera Korake potrebne za razvoj C++ programa Kako unijeti, kompajlirati i povezati naš prvi C++ program
Kratka povijest jezika C++ Programski jezici su prošli kroz dramatičnu evoluciju od vremena prvih elektronskih računala koja su se koristila za proračune i razbijanje šifri za vrijeme Drugog svjetskog rata. U ranim danima, programeri su radili u najprimitivnijem programskom jeziku: strojnom jeziku. Te instrukcije su bile prikazane slijedom dugačkih brojeva prikazanih nulama i jedinicama. Uskoro je razvijen assembler kako bi povezao teško pamtljive brojeve za svaku naredbu i asocirao ih sa engleskim riječima, tzv. mnemonicima poput ADD ili MOV. S vremenom, razvili su se viši programski jezici, poput BASICa i COBOLa, Ti jezici su omogućavali programerima da rade s nečim što aproksimira riječi i rečenice, npr. Let A = 100. Te instrukcije se prevode nazad u strojni jezik uz pomoć programa koje nazivamo interpreteri i kompajleri. Interpreter prevodi program dok ga čita, pretvarajući programsku instrukciju, odnosno kod direktno u akcije. Kompajler prevodi kod u neku vrstu prijelaznog oblika, mješanca između strojnog jezika i jezika u kojem razvijamo program. Taj proces se zove kompajliranje i proizvodi objektni kod. Kompajler nakon toga obično poziva linker koji objektni kod pretvara u izvršnu datoteku, odnosno u program.
Programi Obratite pažnju na to da se riječ program koristi za dve potpuno različite vrste datoteka. Programom nazivamo i naš izvorni kod (engl. source kod), ali i izvršnu datoteku koja je nastala kao proces kompajliranja i linkanja izvršne datoteke.
Proceduralno, strukturirano i objektno-orjentirano programiranje Do nedavno, programi su bili smatrani slijedom procedura koje vrše zadane radnje nad podacima. Procedura, ili funkcija je kruto definiran slijed specifičnih instrukcija. Sami podaci su bili potpuno odvojeni od procedura, i čitav trik programiranja se sastoji u praćenju koja funkcija je zvala koju i koji podaci su bili promjenjeni u tom procesu. Kako bi se unio nekakav red u potencijalno konfuznu situaciju, izmišljeno je strukturirano programiranje. Osnovna ideja koja se krije iza pojma strukturiranog programiranja jest stara «podjeli pa vladaj». Računalni program je smatran nizom zadaća koje treba izvršiti. Svaka zadaća koje je prekompleksna za jednostavan opis bi trebala biti «razbijena» u niz manjih zadataka, sve dok zadaci nisu zadovoljavajuće mali ali i «samodostatni» u smislu da su takvi dijelovi koda lako razumljivi.
Kao primjer, računanje prosječne plaće svih uposlenika u tvrtci je prilično kompleksan zadatak. Možemo ga, međutim razbiti na podzadatke: 1. 2. 3. 4.
Saznati koliko svaka osoba zarađuje. Zbrojiti sve uposlene. Zbrojiti sve plaće. Podijeliti sa brojem zaposlenih.
Zbrojiti sve plaće može biti rastavljeno na: 1. 2. 3. 4.
Uzimanje karton uposlenog. Pristupiti podatku o iznosu plaće Dodati plaću u ukupnu sumu. Uzeti karton od slijedećeg zaposlenog.
Strukturirano programiranje ostaje enormno koristan i efikasan pristup za rješavanje kompleksnih problema. U kasnim 1980im su se, svejdno, počeli otkrivati nedostatci takvog pristupa i s vremenom su postajali sve očitiji. Prvo, potpuno je neprirodno razmišljati o našim podacima (npr., popis zaposlenih) i onome što možemo učiniti s njima (sortirati, uređivati, itd.) kao o odvojenim procesima. Drugo, programeri su se često nalazili u situaciji da traže rješenja za stare probleme, ili kako mi volimo to reći, «otkrivali su toplu vodu». Rodila se «Ponovno upotrijebi» (engl. reusability) ideja za stvaranjem komponenti koje imaju poznata svojstva, te o mogućnosti da takve komponente jednostavno umećemo u naš kod. To je učinjeno po analogiji hardwareskog svijeta. Kad vam se pokvari procesor u računalu, jednostavno zamijenite procesor, ne morate ga projektirati, niti dobro poznavati njegovu internu arhitekturu kako bi on ipak bio u stanju obavljati svoju funkciju. U softwareskom svijetu se do tada takva analogija nije mogla povući. Nadalje, staromodni programi su forsirali korisnika da prolazi kroz «korak-po-korak» seriju ekrana. Moderni događajem pokretani (engl. event driven) programi predstavljaju sve izbore odmah i reagiraju na odgovarajuću korisnikovu akciju. Objektno-orjentirano programiranje pokušava odgovoriti na te potrebe, pružajući tehnike za upravljanje kompleksnim problemima, podržavanjem ponovnog korištenja komponenti i grupranjem podataka i zadatcima koji manipuliraju tim podacima. Srž objektno orjentiranog programiranja je u tretiranju podataka i procedura koje djeluju na podatke kao jednoga «objekta», samodostatne jedinke koja ima vlastiti identitet i određene karakteristike.
Razvojni ciklus Da je svaki program kojeg ste ukucali ikad radio iz prve, to bi bio potpun razvojni ciklus. Ukucate program, kompajlirate ga, linkate i pokrenete izvršnu datoteku. Na žalost, svaki program, ma kako trivijalan on bio će najvjorvatnije sadržavati određene pogreške, odn. bugove. Neke pogreške će uzrokovati greške prilikom kompajliranja, neke tek prilikom linkanja, dok će se neke pojaviti tek u izvršnoj datoteci. Bilo kada da se to dogodi, bit će potrebno ukloniti pogrešku iz koda, te ponovo kreirati izvršnu datoteku i pokrenuti ju. Razvojni ciklus je okvirno prikazan na slijedećoj shemi:
hello.cpp --- naš prvi C++ program 1: #include
2: 3: 4: int main(); 5: { 6: cout <<»Hello World!\n»; 7: return 0; 8: }
Nakon što smo unesli program i kreirali izvršnu datoteku, pokušajmo ju pokrenuti. Na ekranu bi se trebalo pojaviti: Hello World!
Kviz 1. Koja je razlika između interpretera i kompajlera? 2. Kako kompajlirate izvršni kod sa svojim kompajlerom? 3. Što čini linker? 4. Koji su koraci u razvojnom ciklusu?
Vježbe 1. pogledajte u slijedeći program i pokušajte ustanoviti što on radi bez njegova pokretanja. 1: #include 2: int main() 3: { 4: int x = 5; 5: int y = 7; 6: cout «\n»; 7: cout << x + y << « « << x * y; 8: cout «\n»; 9:return 0; 10: } 2. Unesite program sa 1. vježbe, kompajlirajte ga i povežite. Što on čini ? Da li sradi ono što ste mislili? 3. Unesite slijedeći program i kompajlirajte ga. Koja će se greška pojaviti? 1: include 2: int main() 3: { 4: cout << «Hello World\n»; 5: return 0; 6: } 4. Popravite grešku u 3. vježbi, rekompajlirajte , linkajte i pokrenite program. Što on radi?
Lekcija 2
Dijelovi C++ programa
C++ programi se sastoje od objekata, funkcija, varijabli i drugih komponenti. Većina ovih lekcija se bavi detaljnim objašnjavanjem tih djelova, ali kako bi dobili predožbu kako program radi, moramo analizirati neki program koji radi. Danas ćemo proučavati dijelove C++ programa. Čak i jednostavan program poput Hello.cpp iz 1. lekcije ima mnoge zanimljive dijelove. Za početak prisjetimo se kako je Hello.cpp izgledao: Listing 2.1. HELLO.CPP demonstracija dijelova C++ programa 1: #include 2: 3: int main() 4: { 5: cout << "Hello World!\n"; 6: return 0; 7: } Hello World! U liniji 1, datoteka iostream.h je uključena u naš program. Prvi znak je # simbol, što je zapravo signal pretprocesoru. Svaki put kad startate kompajler, pretprocesor se pokreće. On "čita" vaš source kod, tražeći linije koje počinju s simbolom povisilice (#), i obavlja zadane radnje prije nego se kompajler pokrene. include je pretprocesorska instrukcija koja kaže, "Ono što slijedi je ime datoteke. Pronađi tu datoteku i pročitaj ju prije nego kreneš s prevođenjem programa." kose zagrade oko imena datoteke kažu pretprocesoru da traži u svim definiranim mjestima tu datoteku. Ako vam je kompajler pravilno podešen, uglate zagrade će uzrokovati da pretprocesor traži datoteku iostream.h u direktoriju koji sadrži sve H datoteke za vaš kompajler. Datoteka iostream.h (Input-Output-Stream) je upetrebljena prilikom ukucavanja cout, koja pomaže prilikom ispisa na ekran. Efekt linije 1 je isti kao da ste cijelu datoteku iostream prepisali u program prije nego što ste počeli unositi vaš program. Linija 3 počinje s pravim programom, odnosno s funkcijom nazvanom main(). Svaki C++ program mora imati main() funkciju. Uopćeno govoreći, funkcija je blok naredbi koja obavlja jednu ili više radnji. Obično se funkicje pozivaju unutar drugih funkcija, ali main() je drugačiji. Kad pokrenete program, main() se poziva automatski. Sve funkcije počinju s otvorenom zagradom ({) a završavaju, naravno sa zatvorenom zagradom (}). Zagrade od main() funkcije su na linijama 4 i 7. Ssve između njih se smatra dijelom funkcije. Srž ovog programa sadržana je u liniji 5. Objekt cout je upotrebljen za ispis poruke na ekran. Baviti ćemo se objektima više u lekciji 6. Evo kako se cout koristi: Otipkate riječ cout, prateći je s operatorom koji se naziva output redirection operator (<<). Sve što slijedi taj operator, preusmjerava (engl. redirection=preusmjeravanje) se na ekran. ako želite nizove znakova ispisati na ekran, svakako ih zatvorite navodnicima ("), kako je prikazano u liniji 5. Zadnja dva znaka, \n, kažu cout objektu da stave novu liniju nakon što se ispiše poruka Hello World!. Taj posebni kod će biti detaljno analiziran u 17 lekciji, kad ćemo kompletno obrađivati cout objekt. Svi ANSI uskladivi programi deklariraju main() kao cjelobrojnu funkciju (int). Ta vrijednost se "vraća" operativnom sistemu kad program dođe do kraja. Listing 2.2.Upotreba cout. 1: 2: 3: 4: 5:
// Listing 2.2 using cout #include int main() {
6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: }
cout << "Hello there.\n"; cout << "Here is 5: " << 5 << "\n"; cout << "The manipulator endl writes a new line to the screen." << endl; cout << "Here is a very big number:\t" << 70000 << endl; cout << "Here is the sum of 8 and 5:\t" << 8+5 << endl; cout << "Here's a fraction:\t\t" << (float) 5/8 << endl; cout << "And a very very big number:\t" << (double) 7000 * 7000 << endl; cout << "Don't forget to replace Jesse Liberty with your name...\n"; cout << "Jesse Liberty is a C++ programmer!\n"; return 0;
Hello there. Here is 5: 5 The manipulator endl writes a new line to the screen. Here is a very big number: 70000 Here is the sum of 8 and 5: 13 Here's a fraction: 0.625 And a very very big number: 4.9e+07 Don't forget to replace Jesse Liberty with your name... Jesse Liberty is a C++ programmer!
Komentari Kad pišete program, uvijek je vam je jasno što pokušavate napraviti u kojem dijelu. Smješno je to da—mjesec poslije, kad se vratite programu, može nam djelovati dosta zbunjujuće i nejasno. Za borbu protiv zbunjenosti, a i za olakšavanje drugima da razumiju naš kod, željeti ćete koristiti komentare. Komentari su običan tekst, u potpunosti ignoriran od strane kompajlera, ali koji obavještava potencijalnog programera (ili nas same) što radimo u određenim dijelovima programa.
Tipovi komentara C++ komentari dolaze u dva oblika: the double-slash (//) komentar, i slash-star (/*) komentar. Double-slash komentar, kojeg ćemo često zvati C++ komentarom kaže kompajleru da ignorira sve što slijedi do kraja linije.
Slash-star komentar kaže kompajleru da ignorira sve iza (/*) dok ne naleti na star-slash (*/) oznaku. Ovakvi komentari se često nazivaju i C komentarima. Svaki /* mora imati i zatvarajući */. Listing 2.3 demonstracija upotrebe komentara, pokazuje da oni ne utječu na procesiranje programa. 1: #include 2: 3: int main() 4: { 5: /* this is a comment 6: and it extends until the closing 7: star-slash comment mark */ 8: cout << "Hello World!\n"; 9: // this comment ends at the end of the line 10: cout << "That comment ended!\n"; 11: 12: // double slash comments can be alone on a line 13: /* as can slash-star comments */ 14: return 0; 15: } Hello World! That comment ended!
Komentari na vrhu datoteke Dobra je ideja staviti blok komentara na vrh svake datoteke koju stvarate. Točan stil takvog komentara je stvar osobnog ukusa, ali bi svako zaglavlje takve vrste trebalo sadržavati barem slijedeće informacije: Ime funkcije ili programa. Ime datoteke. Što funkcija ili program radi. Opis kako program radi. Ime autora. Povijest prepravki (engl. revision history)—zabilješke o svakoj napravljenoj promjeni. Koji kompajleri, linkeri, i ostali aliti su korišteni prilikom stvaranja programa. Eventualne dodatne zabilješke. Npr., slijedeći blok komentara bi mogao stajati na vrhu Hello World programa. /************************************************************ Program: Hello World File: Hello.cpp Function: Main (complete program listing in this file) Description: Prints the words "Hello world" to the screen Author: Jesse Liberty (jl) Environment: Turbo C++ version 4, 486/66 32mb RAM, Windows 3.1 DOS 6.0. EasyWin module. Notes: This is an introductory, sample program. Revisions: 1.00 10/1/94 (jl) First release 1.01 10/2/94 (jl) Capitalized "World" ************************************************************/
Funkcije Iako je i main() funkcija, zbog svoje (ne)običnosti nam to i nije dobar primjer. Tipične fumkcije su zovu i stvaraju prilikom izvršenja vašega programa. Program se izvršava liniju po liniju onako kako se pojavljuje u vašem izvršnom kodu sve dok ne "naleti" na funkciju. Kad se to dogodi, program iskoči iz svog redoslijeda, te izvršava funkciju.. Kada funkcija obavi svoj posao, program ponovo preuzima kontrolu u liniji koda koji direktno slijedi iza poziva funkcije.
Dobra analogija za ovo je oštrenje olovke. Ako crtate sliku, i vrh olovke se slomi, morate prestati crtati, naoštriti olovku, te se potom vratiti crtanju. Kad program treba uslugu, može pozvati funkciju koja tu uslugu pruža, te potom nastaviti gdje je stao sa radom. Slijedeći program demonstrira tu ideju. Listing 2.4. Demonstracija poziva funkcije. 1: #include 2: 3: // function Demonstration Function 4: // prints out a useful message 5: void DemonstrationFunction() 6: { 7: cout << "In Demonstration Function\n"; 8: } 9: 10: // function main - prints out a message, then 11: // calls DemonstrationFunction, then prints out 12: // a second message. 13: int main() 14: { 15: cout << "In main\n" ; 16: DemonstrationFunction(); 17: cout << "Back in main\n"; 18: return 0; 19: } In main In Demonstration Function Back in main
Upotreba funkcija Funkcije ili vraćaju vrijenost ili vraćaju prazninu (engl. void), što u biti znači da ne vraćaju ništa. Funkcija koja zbraja dva broja bi mogla vraćati sumu, i prema tome bila bi definirana kao cjelobrojna funkcija (engl. integer, int). Funkcija koja samo ispisuje poruku i nema šta za vratiti bi bila deklarirana kao void funkcija. Funkcija se sastoji od zaglavlja (header) i tijela. Zaglavlje se sastoji od povratnog tipa, imena funkcije i ulaznih parametara te funkcije. Tako bi, npr. funkcija za zbrajanje dva broja bila deklarirana kao: int Sum(int a, int b) Parametar je deklaracija tipa vrijednosti kojeg dajemo funkciji. Stvarne vrijednosti koje će biti predane prilikom izvršenja nazivamo argumenti. Često se parametri i argumenti koriste i kao sinonimi. Tijelo funkcije se sastoji od vitičaste zagrade, nitijedne ili više naredbi, te zatvarajuće zagrade. Funkcija također može vratiti vrijednost, koristeći return naredbu. Ta naredba će također uzrokovati izlazak iz funkcije. Ako ne stavite return naredbu, funkcija će automatski vraćati void. Povratna vrijednost mora biti istoga tipa kao deklaracija funkcije. Listing 2.5 demonstrira funkciju koja uzima dva cjelobrojna parametra i vraća cjelobrojnu vrijednost. Za sada se ne opterećujte previše o tipovima podataka, njih ćemo proučavati u lekciji 3. Listing 2.5. FUNC.CPP demonstracija jednostavne funkcije. 1: 2: 3:
#include int Add (int x, int y) { 4: 5: cout << "In Add(), received " << x << " and " << y << "\n"; 6: return (x+y); 7: } 8: 9: int main() 10: { 11: cout << "I'm in main()!\n";
12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: }
int a, b, c; cout << "Enter two numbers: "; cin >> a; cin >> b; cout << "\nCalling Add()\n"; c=Add(a,b); cout << "\nBack in main().\n"; cout << "c was set to " << c; cout << "\nExiting...\n\n"; return 0;
I'm in main()! Enter two numbers: 3 5 Calling Add() In Add(), received 3 and 5 Back in main(). c was set to 8 Exiting...
Kviz 1. Koja je razlika između kompajlera i pretprocesora? 2. Zašto je funkcija main() posebna? 3. Koja su dva tipa komentara i po čemu se razlikuju? 4. Mogu li komentari biti duži od jedne linije?
Vježbe 1. Napišite program koji ispisuje "Ja volim C++" na ekran. 2. Napišite najmanji program koji se može prevesti, linkati i pokrenuti. 3. Unesite slijedeći program i kompajlirajte ga. Gdje je greška? Možete li ju popraviti? 1: #include 2: void main() 3: { 4: cout << Is there a bug here?"; 5: } 4. Popravite pogrešku sa 3. vježbe i pokrenite program.
Lekcija 3
Varijable i konstante
Programi često trebaju pohraniti podatke koje koriste. Varijable i konstante nude različite načine za predstavljanje podataka i njihovo manipuliranje. Danas ćemo naučiti • Kako deklarirati i definirati varijable i konstante. • Kako dodijeliti vrijednosti varijablama i manipulirati s tim vrijednostima. • Kako ispisati vrijednost varijable na ekran.
Što je varijabla? U C++, kao i u većini programsih jezika, varijabla je mjesto za pohranjivanje informacije. Varijabla je lokacija u memoriji računala u koju možete pohraniti vrijednost i kasnije je odatle pročitati. Računalna memorija se može zamisliti kao niz kutijica. Svaka kutijica je jedna u nizu mnoštva poredanih kutijica. Svaka kutijica ima pridružen broj i na svakoj slijedećoj kutijici taj broj je uvećan za 1. Te brojeve nazivamo memorijskim adresama. Varijabla rezervira jednu ili više kutijica u koje smješta vrijednost.
Ime vaše varijable (na primjer, myVariable) je etiketa na jednoj od tih kutijica, tako da ju možete jednostavno naći bez potrebe za znanjem njene stvarne adrese. Slika 3.1 je shematski prikaz te ideje. Kao što vidite sa slike, myVariable počinje na adresi 103. Ovisno o veličini myVariable, ona može zauzeti jednu ili više memorijskih adresa. myVariabl e
Varijabla RAM Adresa
100
101
102
103
104
105
Slika3.1. Shematski prikaz memorije.
Rezerviranje memorije Kad definirate varijablu u C++, morate reći kompajleru koju podatka će sadržavati: cijeli broj, karakter, itd. Ta informacija govori kompajleru koliko prostora da rezervira u memoriji ovisno o vrijednosti koju želite pohraniti u varijabli. Svaka kutijica je velika jedan byte. Ako vrsta podatka kojeg spremate treba 2 bytea, kompajler će ih rezervirati uz ime vaše varijable. Tip varijable (npr., integer—cijeli broj) govori kompajleru upravo to: koliko memorije treba sačuvati za varijablu. Budući da računala koriste bitove i byteove za predstavljanje vrijednosti, te budući da se računalna memorija mjeri u byteovima, bitno je da razumijete osnovne koncepte vezane uz te termine. Ne bi bilo loše ni podsjetiti se matematike vezane uz dekadske, binarne i heksadecimalne brojevne sustave.
Veličina varijabli Na istoj vrsti računala, svaka varijabla zauzima istu količinu prostora. Sama implementacija ovisi o arhitekturi računala, njegovu procesoru, operativnom sistemu i kompajleru kojeg koristimo. Char varijabla (služi za čuvanje znaakova) najčešće zauzima jedan byte. Short integer je dva bytea na većini računala, dok je long integer obično četiri bytea dug. Int varijable (bez oznake short i long) može biti ili 2 bytea ili 4. Listing 3.1 bi trebao pomoći prilikom određivanja točne veličine svih tipova na vašem računalu. Listing 3.1. Određivanje veličine tipova varijabli na vašem računalu. 1: #include 2: 3: int main() 4: { 5: cout << "The size of an int is:\t\t" << sizeof(int) << " bytes.\n"; 6: cout << "The size of a short int is:\t" << sizeof(short) << " bytes.\n"; 7: cout << "The size of a long int is:\t" << sizeof(long) << " bytes.\n"; 8: cout << "The size of a char is:\t\t" << sizeof(char) << " bytes.\n"; 9: cout << "The size of a float is:\t\t" << sizeof(float) << " bytes.\n"; 10: cout << "The size of a double is:\t" << sizeof(double) << " bytes.\n"; 11: 12: return 0; 13: } Output: The size of an int is: 2 bytes. The size of a short int is: 2 bytes. The size of a long int is: 4 bytes. The size of a char is: 1 bytes. The size of a float is: 4 bytes. The size of a double is: 8 bytes.
Analiza: Većina stvari na listingu 3.1 bi tebala biti prilično poznata. Jedino novo svojstvo koje koristimo je sizeof() funkcijama u linijama 5 do 10. sizeof() je standardna funkcija koja dolazi s vašim kompajlerom i ona nam govori veličinu objekta kojeg joj prosljedimo kao parametar. Npr., u liniji 5 ključna riječ int je proslijeđena u sizeof(). Koristeći sizeof(), u stanju smo odrediti da je na našem računalu int jednak short int-u, što iznosi 2 bytea.
Integeri s predznakom i bez njega (engl. signed i unsigned) Dodatno, svi cjeli brojevi dolaze u dva oblika: signed i unsigned. Ideja iza koja se krije iza toga je da ponekad trebate negativne brojeve, a ponekad ne trebate. Cjeli brojevi (i short i long) bez riječi "unsigned" su pretpostavljeni kao signed. Signed integeri su ili negativni ili pozitivni. Unsigned integeri su uvijek pozitivni. Budući da je isti broj byteova dodijeljen i za signed i za unsigned brojeve, najveći broj koji možemo pohraniti u unsigned integer je dvostruko veći od onoga pohranjenog u signed integer.. Npr., unsigned short integer može baratati brojevima od 0 do 65535. Pola od te količine će otići na negativne brojeve, pa stoga signed short može predstavljati brojeve samo od –32,768 do 32,768.
Osnovni tipovi varijabli Još nekoliko tipova varijabli je ugrađeno u C++. Oni mogu biti podijeljeni na cjelobrojne varijable (tip o kojem smo da sad govorili), realne odn. floating point varijable, te znakovne varijable. Floating-point varijable sadržavaju brojeve koji mogu biti prikazani kao razlomci, odnosno to su realni brojevi. Znakovne varijable drže jedan byte i služe za pohranu jednog od 256 znakova i simbola ASCII seta znakova. Novi izraz: ASCII set znakova je standardizirani set znakova koji se koristi na računalima. ASCII je akronim od American Standard Code for Information Interchange. Tipovi varijabli korišteni u C++ programima su opisani u tablici 3.1. Ova tablica prikazuje tip varijable, koliko mjesa zauzima u računalu (ovisi o tipu računala—okvirno), te koje vrijednosti mogu biti pohranjene u te varijable. Vrijednosti koje mogu biti pohranjene su određene veličinom tipa varijable, pa provjerite izlaz za program sa listinga 3.1. Tablica 3.1. Tipovi varijabli.
Type unsigned short int short int unsigned long int long int int (16 bit) int (32 bit) unsigned int (16 bit) unsigned int (32 bit) char
Size 2 bytea 2 bytea 4 bytea 4 bytea 2 bytea 4 bytea 2 bytea 4 bytea 1 byte
Values 0 do 65,535 -32,768 do 32,767 0 do 4,294,967,295 -2,147,483,648 do 2,147,483,647 -32,768 do 32,767 -2,147,483,648 do 2,147,483,647 0 do 65,535 0 do 4,294,967,295 256 znakovnih vrijednosti
float double
4 bytea 8 bytea
1.2e-38 do 3.4e38 2.2e-308 do 1.8e308
Definiranje varijable Možete kreirati ili definirati varijablu ispisujući prvo njezin tip, te nakon jednog ili više razmaka njezino ime praćeno znakom ";". Ime varijable može biti praktički bilo kakva kombinacija slova i brojeva, ali ne smije sadržavati prazna mjesta. Legakna imena varijabli su npr., x, J23qrsnf, i mojeGodine. Dobro ime varijable nam govori čemu služi varijabla, te nam samim tim olakšava praćenje tjeka našeg programa.Slijedeća izjava definira cjelobrojnu varijablu zvanu myAge: int myAge; Izbjegavajte imena poput J23sqfrn za vaše varijable, te ograničite upotrebu varijabli od samo jednog znaka (kao x ili i) na one koje se koriste samo kratko. Pokušavajte koristiti opisna imena poput mojeGodine ili kolikoDana. Takva imena se lakše razumijevaju tri tjedna kasnije dok se lupate po glavi pokušavajući shvatiti što ste željeli postići u toj i toj liniji koda.
Pokušajmo s malim eksperimentom: Pogodite što ovaj program radi bazirano na prvih par linija koda: Primjer 1: main() { unsigned short x; unsigned short y; ULONG z; z = x * y; } Primjer 2: main () { unsigned short Width; unsigned short Length; unsigned short Area; Area = Width * Length; } Jasno, drugi program je mnogo lakše razumljiv od prvog, a nepogodnost ukucavanja dugačkih imena varijabli brzo blijedi s rastom čitljivosti programa.
Osjetljivost na velika i mala slova C++ razlikuje velika i mala slova. Drugim rječima, varijable imena Godine i GODINE su dvije različite varijable. Postoje različite konvencije prilikom imenovanja varijabli, i iako nije važno koju metodu koristite, bitno je da budete dosljedni u svom programu. Mnogi programeri koriste samo mala slova za varijable. Ako je opsi varijable sadržan u dvije riječi, popularni načini imenovanja su moj_auto ili mojAuto.
Ključne riječi Neke riječi su rezervirane od strane jezika C++ i zato ih ne smijemo koristiti kao imena varijabli. To su tzv., ključne riječi koje se koriste za kontrolu vašega programa, poput if, while, for, ili main. Priručnik vašeg kompajleera sigurno sadrži popis svih ključnih riječi, ali općenito, svako razumno ime za varijablu gotovo sigurno neće biti ključna riječ.
Stvaranje više varijabli odjednom Moguće je kreirati više varijabli istoga tipa u jednoj naredbi, navodeći tip, te zatim pobrojati sve varijable tog tipa odvojene zarezom. Npr. : unsigned int myAge, myWeight; long area, width, length; Kao što vidite, myAge i myWeight su obje deklarirane kao unsigned integer varijable. Druga linija deklarira tri individualne long varijable imena area, width, i length. Tip (long) je dodijeljen svim varijablama, pa nemožete mješati tipove u jednoj definicijskoj naredbi.
Dodjeljivanje vrijednosti vašim varijablama Vrijednost dodjeljujemo varijabli koristeći operator pridruživanja (=). Tako bi, primjerice, dodjelili 5 varijabli imena Width tipkajući: unsigned short Width; Width = 5; Moguće je i kombinirati te korake i inicijalizirati Width prilikom definicije unoseći: unsigned short Width = 5; Evo još nekoliko primjera deklaracije i inicijalizacije varijabli: long width = 5, length = 7; int myAge = 39, yourAge, hisAge = 40; Listing 3.2 pokazuje gotov program, spreman za prevođenje, koji računa površinu pravokutnika i ispisuje ju na ekran. 1: 2: 3: 4: 5: 6: 7: 8:
// Demonstration of variables #include int main() { unsigned short int Width = 5, Length; Length = 10;
9: // create an unsigned short and initialize with result 10: // of multiplying Width by Length 11: unsigned short int Area = Width * Length; 12: 13: cout << "Width:" << Width << "\n"; 14: cout << "Length: " << Length << endl; 15: cout << "Area: " << Area << endl; 16: return 0; 17: } Width:5 Length: 10 Area: 50
typedef Zamorno je i podložno greškama stalno ponavljati npr. unsigned short int. C++ nam omogućuje kreiranje aliasa za tu frazu korištenjem ključne riječi typedef, koja dolazi od "definicija tipa" (engl. type definition). U stvarnosti, zapravo kreiramo sinonim, važno je to razlikovati od stvaranja novog tipa (što ćemo raditi u lekciji 6). typdef se upotrebljava upisivanjem riječi typedef, praćene postojećim tipom i potom novim imenom. Na primjer: typedef unsigned short int USHORT stvara novo ime USHORT koje možemo koristiti bilo gdje gdje smo dosad pisali unsigned short int. Listing 3.3 je prepisani listing 3.2, ali koji koristi definiciju tipa USHORT umjesto unsigned short int.
Listing 3.3. Demonstracija typedef. 1: // ***************** 2: // Demonstrates typedef keyword 3: #include 4: 5: typedef unsigned short int USHORT; //typedef defined 6: 7: void main() 8: { 9: USHORT Width = 5; 10: USHORT Length; 11: Length = 10; 12: USHORT Area = Width * Length; 13: cout << "Width:" << Width << "\n"; 14: cout << "Length: " << Length << endl; 15: cout << "Area: " << Area <<endl; 16: } Output: Width:5 Length: 10 Area: 50
Kada koristiti short a kada long Veliki izvor konfuzije za novopečenog C++ programera je kada deklarirati varijablu tipa long, a kada kao short. Pravilo je da, ako postoji ikoja šansa da će vrijednost premašiti doseg varijable, koristimo veći tip. Kada unsigned integer dosegne maksimalnu vrijednost, on jednostavno, počne brojati od početka, slično brojaču kilometara u automobilu. Listing 3.4 pokazuje što se događa kad stavimo preveliku vrijdnost u short integer. Listing 3.4. Domonstracija stavljanja prevelikog broja u unsigned integer. 1: #include 2: int main() 3: { 4: unsigned short int smallNumber; 5: smallNumber = 65535; 6: cout << "small number:" << smallNumber << endl; 7: smallNumber++; 8: cout << "small number:" << smallNumber << endl; 9: smallNumber++; 10: cout << "small number:" << smallNumber << endl; 11: return 0; 12: } Output: small number:65535 small number:0
small number:1
Signed integer je drugačiji oblik cjelobrojni oblik u kojoj se pola vrijednosti čuva za pozitivne a pola za negativne brojeve. Listing 3.5 pokazuje što se događa kad dodamo 1 na najveći pozitivni broj u unsigned short integer varijabli. Listing 3.5. Demonstarcija stavljanja prevelikog broja u signed integer. 1: #include 2: int main() 3: { 4: short int smallNumber; 5: smallNumber = 32767; 6: cout << "small number:" << smallNumber << endl; 7: smallNumber++; 8: cout << "small number:" << smallNumber << endl; 9: smallNumber++; 10: cout << "small number:" << smallNumber << endl; 11: return 0; 12: } Output: small number:32767 small number:-32768 small number:-32767
Znakovi Znakovne varijable (tip char) su obično 1 byte, dovoljno za čuvanje 256 varijednosti. Znak može biti interpertiran malim brojem (0-255) kao pripadajući član ASCII seta. Unutar ASCII tablice, slovo "a" iima dodjeljen broj 97. Sva mala i velika slova, svi brojevi, te interpunkcijski znakovi imaju dodijeljene vrijednosti između 1 i 128. Listing 3.6. Ispis znakova baziran na njihovim brojevima u ASCII tablici 1: #include 2: int main() 3: { 4: for (int i = 32; i<128; i++) 5: cout << (char) i; 6: return 0; 7: } Output: !"#$%G'()*+,./0123456789:;<>?@ABCDEFGHIJKLMNOP _QRSTUVWXYZ[\]^'abcdefghijklmnopqrstuvwxyz<|>~s
Specijalni znakovi \n new line \t tab \b backspace \" double quote \' single quote \? question mark \\ backslash
Konstante Kao i varijable, i konstante su mjesta za pohranu podataka. Za razliku od varijabli, konstante se ne mogu mijenjati, odn., ne možemo im tjekom izvođenja programa dodavati novu vrijednost. Postoje dva načina za deklariranje konstanti u C++ jeziku. Stari način je korištenje pretprocesorske direktive #define, npr: #define studentsPerClass 15 Primjetite da studentsPerClass is nema određeni tip (int, char, itd.). #define radi jednostavnu zamjenu teksta svaki put kad vam se u programu pojavi riječ studentsPerClass, pretprocesor stavlja 15 umjesto toga. Noviji način za definiranje konstanti glasi: const unsigned short int studentsPerClass = 15;
Enumerirane konstante Enumerairane konstante omogućuju nam kreiranje novih tipova , te potom definiranje varijabli čija je sadržina ograničena na niz mogućih vrijednosti. Npr, možemo deklarirati COLOR kao enumeraciju, i potom definirati pet vrijednosti za boju: RED, BLUE, GREEN, WHITE, i BLACK. Npr: enum COLOR { RED, BLUE, GREEN, WHITE, BLACK }; Ova naredba izvodi dvije zadaće: 1. Stvara novu enumeraciju zvanu COLOR. 2. Pravi RED simboličkom konstantom s vrijednosti 0, BLUE ima vrijednost 1, itd... svaka enumerirana konstanta ima cjelobrojnu vrijednost. Ako drugačije ne navedete, prva konstanta ima vrijednost 0, a ostale rastu za 1. Svaka konstanta može biti i inicijalizirana na vlastitu vrijednost, a one koje nisu će biti za 1 veće od prethodno navedene. Pa ako napišete enum Color { RED=100, BLUE, GREEN=500, WHITE, BLACK=700 }; RED će imati vrijednost 100; BLUE, 101; GREEN, 500; WHITE, 501; i BLACK, 700. Listing 3.7. Demonstracija enumeriranih konstanti. 1: #include 2: int main() 3: { 4: enum Days { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }; 5: 6: Days DayOff; 7: int x; 8: 9: 10: 11: 12:
cout << "What day would you like off (0-6)? "; cin >> x; DayOff = Days(x);
13: if (DayOff == Sunday || DayOff == Saturday) 14: cout << "\nYou're already off on weekends!\n"; 15: else 16: cout << "\nOkay, I'll put in the vacation day.\n"; 17: return 0; 18: } Output: What day would you like off (0-6)? 1 Okay, I'll put in the vacation day. What day would you like off (0-6)? 0 You're already off on weekends!
Kviz 1. Koja je razlika između integer i floating point varijable?
2. 3. 4. 6.
Koje su razlike među unsigned short int i long int? Što je prednost kod upotrebe const ključne riječi u odnosu na #define? Kako razlikujemo dobro od lošeg imena varijable? Uz slijedeći enum, koja će biti vrijednost za BLUE? enum COLOR { WHITE, BLACK = 100, RED, BLUE, GREEN = 300 };
7. Koje od slijedećih varijabli su dobre, koje loše, a koje nelegalne? a. Age b. !ex c. R79J d. TotalIncome e. __Invalid
Vježbe 1. Koji bi bio pravi tip varijable za pohranu slijedećih informacija? a. Vaše godine. b. Poštanski broj vašega grada. c. Broj zvijezda u galaksiji. d. Prosječna količina padalina za siječanj. 2. Izmislite dobra imena varijabli za te informacije. 3. Deklarirajte konstantu za pi kao 3.14159. 4. Deklarirajte float varijablu i inicijalizirajte ju koristeći pi konstantu.
Lekcija 4
Izrazi i naredbe
Naredbe U C++ jeziku, naredba kontrolira slijedom izvršavanja, procjenjuje izraze, ili ne čini ništa (null statement). Sve C++ naredbe završavaju sa točka-zarezom, čak i null statement, koji je u stvari samo točka-zarez. Jedna od najuobičajenijih naredbi je slijedeće pridruživanje: x = a + b; Za razliku od algebre, ova naredba ne znači da je x jednako a+b. Ovo se čita "pridruži vrijednost sume od a i b u x", ili "pridruži u x, a+b". Iako ova naredba čini dve stvari, to je jedna naredba i zato ima jedan ";". Operator pridruživanja ("=") pridružuje što god je na desnoj strani znaka jednakosti, onome što se nalazi s njegove lijeve strane.
Umetanje praznina Praznine (tabovi, razmaci, i nove linije) se obično ignoriraju u naredbama. Naredba pridruživanja o kojoj smo maloprije raspravljali može biti napisana kao x=a+b; ili kao x
=a
+
b
;
Iako je ova posljednja varijanta poptuno ispravna glede valjanosti koda, isto tako je i potpuno blesava. Praznine bi trebale vaš program činiti čitljivijim i lakšim za održavanje, a ne za kreiranje zastrašujućeg potpuno nečitkog koda. U ovome, kao i u svemu ostalome, C++ pruža vam moć; na vama je da ju iskoristite razboritim odlukama.
Blokovi naredbi Na svako mjesto na koje možete staviti naredbu, možete staviti i složenu naredbu također zvanu i blok. Blok počinje s vitičastom zagradom ({) i završava s njom (}). Iako svaka naredba u bloku mora završiti s ";", sam blok ne završava s točka-zarezom. Na primjer { temp = a; a = b; b = temp; } Ovaj blok djeluje kao jedna naredba i zamjenjuje vrijednosti u varijablama a i b.
Izrazi U C++ izrazom smatramo sve što vraća neku vrijednost. Tako, na primjer, 3+2 vraća vrijednost 5 i time je izraz. Svi izrazi su naredbe. Raznolikost koda koji se naziva izrazom mogla bi vas iznenaditi. Evo tri primjera: 3.2 // vraća vrijednost 3.2 PI // float const koja vraća vrijednost 3.14 SecondsPerMinute // int const koja vraća 60
Složeni izrazi x = a + b; ne samo da zbraja a i b nego i dodjeljuje rezultat u x, ali i vraća vrijednost toga ipridruživanja (vrijednost od x). Tako je i ova naredba zapravo izraz. Budući da je izraz, ona može biti i na desnoj strani operatora pridjeljivanja: y = x = a + b; Ova se linija izvršava slijedećim redosljedom: zbrajaju se a i b. Rezultat izraza a+b se pridružuje varijabli x. Pridružuje se rezultat izraza pridruživanja x = a + b u y. Ako su a, b, x, i y cijeli brojevi, te ako a ima vrijednost 2, a b ima vrijednost 5, i u x i u y će biti pridružena vrijednost 7. Listing 4.1. Izvršavanje složenih izraza. 1: #include 2: int main() 3: { 4: int a=0, b=0, x=0, y=35; 5: cout << "a: " << a << " b: " << b; 6: cout << " x: " << x << " y: " << y << endl; 7: a = 9; 8: b = 7; 9: y = x = a+b; 10: cout << "a: " << a << " b: " << b; 11: cout << " x: " << x << " y: " << y << endl; 12: return 0; 13: } Output: a: 0 b: 0 x: 0 y: 35 a: 9 b: 7 x: 16 y: 16
Operatori Operator je simbol koji uzrokuje da kompajler poduzme akciju. Operatori djeluju na operande, a u C++ svi operandi su izrazi. U C++ postoji nekoliko različitih kategorija operatora. Dvije od tih kategorija su operatori pridruživanja i matematički operatori.
Operator pridruživanja Operator pridruživanja (=) uzrokuje da operand s lijeve strane operatora pridruživanja promjeni svoju vrijednost u onu koja se
nalazi s njegove desne strane. Izraz x = a + b; pridružuje vrijednost koja je rezultat zbrajanja a i b operandu x. Operand koji legalno može biti na lijevoj strani operatora pridruživanja se zove lvalue. Onaj koji može biti na desnoj strani se zove (pogodili ste) rvalue. Konstante su r-value. One ne mogu biti l-value. Tako, možete pisati x = 35;
// ok
ali nije dozvoljeno 35 = x;
// error, not an lvalue!
Matematički operatori Postoji pet matematičkih operatora: zbrajanje (+), oduzimanje (-), množenje (*), dijeljenje (/), i modul (%). Zbrajanje i oduzimanje rade onako kako bi i očekivali, iako oduzimanje s neoznačenim cjelim brojevima može dati neočekivane rezultate, ako je rezultat negativan broj. Vidjeli smo nešto tako u prošloj lekciji kad je dolazilo do preopterećenja varijabli. Listing 4.2. Demonstracija oduzimanja i preopterećenje. 1: // Listing 4.2 - demonstrates subtraction and 2: // integer overflow 3: #include 4: 5: int main() 6: { 7: unsigned int difference; 8: unsigned int bigNumber = 100; 9: unsigned int smallNumber = 50; 10: difference = bigNumber - smallNumber; 11: cout << "Difference is: " << difference; 12: difference = smallNumber - bigNumber; 13: cout << "\nNow difference is: " << difference <<endl; 14: return 0; 15: } Output: Difference is: 50 Now difference is: 4294967246
Cjelobrojno dijeljenje i moduli Cjelobrojno dijeljenje je malo drugačije od uobičajenog. Kad podjelite 21 sa 4, rezultat je realan broj (broj sa ostatkom). Cijeli brojevi nemaju ostatke, pa je on izgubljen. Rezultat bi prema tome bio 5. Za dobivanje ostatka , radimo 21 modul 4 (21 % 4) i rezultet je 1. Prema tome, modul nam daje ostatak kod cjelobrojnog dijeljenja. Traženje modula može biti vrlo korisno. Na primjer, želite ispisati naredbu na svakoj destoj akciji. Savaki broj čija vrijednost je 0 kad vadimo modul 10 i tog broja je višekratnik broja 10. Tako je 1 % 10 jenako 1, 2 % 10 je 2 itd., sve do 10 % 10, čiji je rezultat 0. 11 % 10 je ponovo 1 i uzorak se ponavlja sve do slijedećeg višekratnika a to je 20. Tu ćemo tehniku često koristiti, a više o njoj u lekciji 7.
Kombiniranje pridruživanja i matematičkih operatora. Nije neuobičajeno željeti dodati vrijednost varijabli i rezultat dodijeliti istoj varijabli. Ako imate varijablu myAge i želite joj povećati vrijednost za 2, možete napisati int myAge = 5; int temp; temp = myAge + 2; // add 5 + 2 and put it in temp
myAge = temp;
// put it back in myAge
Ova je metoda, zapravo gubitak vremena i prostora. U C++, možete istu varijablu koristiti na obje strane operatora pridruživanja, pa prethodno postaje myAge = myAge + 2; što je već mnogo bolje. U algebri bi ovakav izraz bio besmislen, ali ga C++ tumači ka "dodaj 2 u vrijenost od myAge i pridruži rezultat u myAge. Još jednostavnije za pisati, ali malo teže za čitati je myAge += 2; Operator obnavljajućeg pridruživanja (engl. "self-assigned addition")--(+=) dodaje rvalue u lvalue i zatim pridružuje rezultat u lvalue. Ako je myAge imao vrijdenost 4 na početku, imao bbi 6 nakon ove naredbe. Također postoje i operatori obnavljajućeg oduzimanja (-=), dijeljenja (/=), množenja (*=), i modula (%=).
Inkrement i dekrement Najčešća vrijednost za zbrajanje i potom ponovno pridruživanje je 1. U C++ je povećanje vrijednosti za 1 zvano inkrementiranje, a smanjenje za 1 je zvano dekrementiranje. Postoje i specijalni operatori za izvođenje tih akcija. Tako, ako želite varijablu C inkrementirati, koristili bi slijedeću naredbu: C++;
// Start with C and increment it.
Ova naredba je ekvivalentna čitljivijoj naredbi C = C + 1; što se može pisati i kao, već sada to i znate C += 1;
Prefiks i postfiks I inkrement operator (++) i dekrement operator (--) dolaze u dva oblika; prefiks i postfiks. Prefiks varijanta se piše prije imena varijable (++myAge); postfiks se piše nakon varijable (myAge++). U jednostavnoj naredbi, nije bitno koji će te koristiti, ali u složenoj, kad inkrementirate (ili dekrementirate) varijablu i rezultat pridružujete drugoj varijabli, ima bitnu razliku. Prefiks operator se izvodi prije pridurživanja, a postfiks nakon. Semantika prefiksa je: inkrementiraj vrijednost i onda ju pošalji. Semantika postfiksa je drugačija: Pošalji vrijednost i onda inkrementiraj original. To na početku može biti konfuzno, ali ako je x cijeli broj čija je vrijednost 5 i vi napišete int a = ++x; rekli ste kompajleru da inkrementira x (čineći ga 6) i zatim tu vrijednost dodjeljuje varijabli a. Prema tome, sada su i a i x jednake 6. Ako nakon toga napišemo int b = x++; sada smo rekli kompajleru da dodijeli vrijednost od x (6) varijabli b, vrati se nazad i inkrementira x. Sada nam je b jednako 6, ali x ima vrijednost 7. Listing 4.3 prikazuje upotrebu i implikacije oba tipa. Listing 4.3. Demonstarcija prefiks i postfiks operatora. 1: 2: 3: 4: 5: 6: 7: 8: 9:
// Listing 4.3 - demonstrates use of // prefix and postfix increment and // decrement operators #include int main() { int myAge = 39; // initialize two integers int yourAge = 39; cout << "I am: " << myAge << " years old.\n";
10: cout << "You are: " << yourAge << " years old\n"; 11: myAge++; // postfix increment 12: ++yourAge; // prefix increment 13: cout << "One year passes...\n"; 14: cout << "I am: " << myAge << " years old.\n"; 15: cout << "You are: " << yourAge << " years old\n"; 16: cout << "Another year passes\n"; 17: cout << "I am: " << myAge++ << " years old.\n"; 18: cout << "You are: " << ++yourAge << " years old\n"; 19: cout << "Let's print it again.\n"; 20: cout << "I am: " << myAge << " years old.\n"; 21: cout << "You are: " << yourAge << " years old\n"; 22: return 0; 23: } Output: I am 39 years old You are 39 years old One year passes I am 40 years old You are 40 years old Another year passes I am 40 years old You are 41 years old Let's print it again I am 41 years old You are 41 years old
Redosljed izvođenja operatora u složenim naredbama x = 5 + 3 * 8; što se prvo obavlja, zbrajanje ili množenje? Ako se prvo obavlja zbrajanje, rješenje je 8 * 8, ili 64. Ako se prvo izvodi množenje, odgovor je 5+24, odnosno 29. Svaki operator ima svoj prioritet izvršavanja. Množenje ima veći prioritet nego zbrajanje, pa je vrijednost gornjeg izraza 29. Kad dva matematička operatora imaju isti prioritet, oni se izvršavaju s lijeva na desno. Tako se u x = 5 + 3 + 8 * 9 + 6 * 4; prvo provodi množenje, s lijeva na desno. 8 * 9 = 72, i 6 * 4 = 24. Sada je izraz u biti x = 5 + 3 + 72 + 24; Sada zbrajamo, s lijevana desno 5 + 3 = 8; 8 + 72 = 80; 80 + 24 = 104. Budite pažljivi prilikom pisanja izraza. Neki operatori, poput pridruživanja se izvršavaju s desna na lijeva. Pogledajte izraz TotalSeconds = NumMinutesToThink + NumMinutesToType * 60 U ovom izrazu ne želite pomnožiti NumMinutesToType varijablu sa 60 i potom ju zbrojiti sa NumMinutesToThink. Vi želite zbrojiti dvije varijable i potom ih pomnožiti sa 60 kako bi dobili rezultat u sekundama. U tom slučaju, koristimo zagrade da promjenimo redosljed prioriteta. Elementi unutar zagrada se izvršavaju s višim prioritetom nego bilo koji drugi matematički operatori. Prema tome TotalSeconds = (NumMinutesToThink + NumMinutesToType) * 60 će postići ono što želite.
Gnježdenje zagrada Za kompleksne izraze, možda ćete morati ugnjezditi zagrade jedne unutar drugih. Npr: TotalPersonSeconds = ( ( (NumMinutesToThink + NumMinutesToType) * 60) * (PeopleInTheOffice + PeopleOnVacation) ) Složeni izrazi se čitaju iznutra prema van. Prvo se zbrajaju NumMinutesToThink i NumMinutesToType, budući da su u "najunutrašnjijim" zagradama. Nakon toga njihova se suma množi sa 60. Slijedeće, PeopleInTheOffice se zbraja s PeopleOnVacation. Konačno, ukupan broj ljudi se množi s ukupnim brojem sekundi. Ovaj primjer povlači za sobom važnu temu. Ovaj izraz je jednostavan za razumijevanje računalu, ali vrlo težak za čitanje, razumjevanje i mijenjanje. Evo istog izraza ponovo napisanog, koji koristi privremene integer varijable: TotalMinutes = NumMinutesToThink + NumMinutesToType; TotalSeconds = TotalMinutes * 60; TotalPeople = PeopleInTheOffice + PeopleOnVacation; TotalPersonSeconds = TotalPeople * TotalSeconds; Ovaj primjer je duži za ukucavanje i koristi više privremenih varijabli, ali je mnogo jednostavniji za razumijevanje. Dodajmo još komentar s objašnjenjem koda i promjenimo 60 u konstantu, te smo dobili kod koji je jednostavan za razumijevanje i, što je još važnije, održavanje.
Priroda Istine U C++, nula se smatralaži, a sve ostale vrijednosti se smatraju istinama, iako se istina obični predstavlja brojem 1. Prema tome ako je izraz neistinit (false), on je jednak nuli.
Relacijski operatori Relacijski operatori se koriste za određivanje kada su dva broja jednaka, ili dali je jedan broj veći od drugog. Svaka relacijska naredba poprima ili 1(TRUE) ili 0(FALSE).
Ako cjelobrojna varijabla ima vrijednost 39, a yourAge ima vrijednost 40, možete odrediti jesu li oni jednaki koristeći relacijski operator jednakosti: myAge == yourAge; // is the value in myAge the same as in yourAge? Ovaj izraz poprima vrijednost 0, odnosno laž, budući da varijable nisu jednake. Izraz myAge > yourAge; // is myAge greater than yourAge? poprima 0 odn. laž. Upozorenje: Mnogi C++ programeri početnici brkaju operator pridruživanja (=) s operatorom jednakosti (==). To može prouzročiti gadne bugove u programu. Postoji šest relacijskih operatora: jednako (==), manje od (<), veće od (>), manje ili jednako (<=), veće ili jednako (>=), te različito (!=). Tablica 4.1. Relacijski operatori.
Ime Equals
Operato r ==
Not Equals
!=
Greater Than
>
Greater Than or Equals
>=
Less Than
<
Less Than or Equals
<=
Primjer 100 == 50; 50 == 50; 100 != 50; 50 != 50; 100 > 50; 50 > 50; 100 >= 50; 50 >= 50; 100 < 50; 50 < 50; 100 <= 50; 50 <= 50;
Vrijednos t false true true false true false true true false false false true
if naredba Obično vaš program izvršava liniju po liniju redosljedom kojim se pojavljuje u vašem izvornom kodu. if naredba omogućuje vam testiranje uvjeta (npr. da li su dve varijable jednake) i grana se u odvojene dijelove koda ovisno o rezultatu. Najjednostavniji oblik if naredbe glasi: if (izraz) naredba; Izraz unutar zagrada može biti bilo koji izraz, ali obično sadrži jedan od relacijskih izraza. Ako izraz poprimi vrijednost 0, smatra se neistinom, i naredba se preskače. Ako ima bilo koju drugu vrijednost, smatramo ga istinitim, i naredba se izvršava. Pogledajmo slijedeći primjer: if (bigNumber > smallNumber) bigNumber = smallNumber; Ovaj kod uspoređuje bigNumber i smallNumber. Ako je bigNumber veći, slijedeća linija postavlja njegovu vrijednost na onu od koju ima smallNumber. Budući da je blok naredbi omeđen vitičastim zagradama potpuno jednak jednoj naredbi, slijedeći oblik grananja je vrlo čest i moćan: if (expression)
{ statement1; statement2; statement3; } Evo primjera takve upotrebe: if (bigNumber > smallNumber) { bigNumber = smallNumber; cout << "bigNumber: " << bigNumber << "\n"; cout << "smallNumber: " << smallNumber << "\n"; } Listing 4.4. Demonstracija grananja baziranog na relacijskim operatorima. 1: 2: 3: 4: 5: 6: 7: 8: 9:
// Listing 4.4 - demonstrates if statement // used with relational operators #include int main() { int RedSoxScore, YankeesScore; cout << "Enter the score for the Red Sox: "; cin >> RedSoxScore;
10: 11: 12: 13: 14: 15: 16: 17:
cout << "\nEnter the score for the Yankees: "; cin >> YankeesScore;
18: 19: 20: 21: 22: 23:
if (RedSoxScore < YankeesScore) { cout << "Go Yankees!\n"; cout << "Happy days in New York!\n"; }
24: 25: 26: 27: 28: 29:
if (RedSoxScore == YankeesScore) { cout << "A tie? Naah, can't be.\n"; cout << "Give me the real score for the Yanks: "; cin >> YankeesScore;
cout << "\n"; if (RedSoxScore > YankeesScore) cout << "Go Sox!\n";
30: 31: 32:
if (RedSoxScore > YankeesScore) cout << "Knew it! Go Sox!";
33: 34: 35:
if (YankeesScore > RedSoxScore) cout << "Knew it! Go Yanks!";
36: 37: 38: 39:
if (YankeesScore == RedSoxScore) cout << "Wow, it really was a tie!"; }
40: cout << "\nThanks for telling me.\n"; 41: return 0; 42: } Output: Enter the score for the Red Sox: 10 Enter the score for the Yankees: 10 A tie? Naah, can't be Give me the real score for the Yanks: 8 Knew it! Go Sox! Thanks for telling me.
else često će naš program ići jednom granom ako je uvijet istinit, a drugom ako je lažan. Na listingu 4.3 mogli smo ispisati poruku (Go Sox!) ako je prvi test (RedSoxScore > Yankees) davao TRUE, a drugačiju poruku (Go Yanks!) ako je bio FALSE. Dosad prikazana metoda radi fino, ali je pomalo nepraktična. Ključna riječ else može generirati mnogo čitljiviji kod: if (izraz) naredba; else naredba; Listing 4.5. Demonstracija ključne riječi else. 1: // Listing 4.5 - demonstrates if statement 2: // with else clause 3: #include 4: int main() 5: { 6: int firstNumber, secondNumber; 7: cout << "Please enter a big number: "; 8: cin >> firstNumber; 9: cout << "\nPlease enter a smaller number: "; 10: cin >> secondNumber; 11: if (firstNumber > secondNumber) 12: cout << "\nThanks!\n"; 13: else 14: cout << "\nOops. The second is bigger!"; 15: 16: return 0; 17: } Output: Please enter a big number: 10 Please enter a smaller number: 12 Oops. The second is bigger!
Kompleksne if naredbe Listing 4.6. Složena, ugnježdena if naredba. 1: 2: 3: 4: 5: 6: 7: 8: 9:
// Listing 4.5 - a complex nested // if statement #include int main() { // Ask for two numbers // Assign the numbers to bigNumber and littleNumber // If bigNumber is bigger than littleNumber, // see if they are evenly divisible
10: 11:
// If they are, see if they are the same number
12: 13: 14: 15: 16: 17: 18:
int firstNumber, secondNumber; cout << "Enter two numbers.\nFirst: "; cin >> firstNumber; cout << "\nSecond: "; cin >> secondNumber; cout << "\n\n";
19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: }
if (firstNumber >= secondNumber) { if ( (firstNumber % secondNumber) == 0) // evenly divisible? { if (firstNumber == secondNumber) cout << "They are the same!\n"; else cout << "They are evenly divisible!\n"; } else cout << "They are not evenly divisible!\n"; } else cout << "Hey! The second one is larger!\n"; return 0;
Output: Enter two numbers. First: 10 Second: 2 They are evenly divisible!
Upotreba zagrada unutar ugnježdenih if-ova if (x > y) if (x < z) x = y;
// if x is bigger than y // and if x is smaller than z // then set x to the value in z
Kod pisanja velikih ugnježdenih naredbi izostavljanje vitičastih zagrada može uzrokovati popriličnu zbrku. Listing 4.7 ilustrira problem: 1: // Listing 4.7 - demonstrates why braces 2: // are important in nested if statements 3: #include 4: int main() 5: { 6: int x; 7: cout << "Enter a number less than 10 or greater than 100: "; 8: cin >> x; 9: cout << "\n"; 10: 11: if (x > 10) 12: if (x > 100) 13: cout << "More than 100, Thanks!\n"; 14: else // not the else intended! 15: cout << "Less than 10, Thanks!\n"; 16: 17: return 0; 18: } Output: Enter a number less than 10 or greater than 100: 20 Less than 10, Thanks!
Listing 4.8. Demonstracija pravilne upotrebe vitičastih zagrada unutar if naredbe 1: // Listing 4.8 - demonstrates proper use of braces 2: // in nested if statements 3: #include 4: int main() 5: { 6: int x; 7: cout << "Enter a number less than 10 or greater than 100: "; 8: cin >> x; 9: cout << "\n"; 10: 11: if (x > 10) 12: { 13: if (x > 100) 14: cout << "More than 100, Thanks!\n"; 15: } 16: else // not the else intended! 17: cout << "Less than 10, Thanks!\n"; 18: return 0; 19: } Output: Enter a number less than 10 or greater than 100: 20
Logički operatori Često nam je potrebno postaviti više relacijskih pitanja odjednom. "Jeli istina da je x veći od y i da je također y veći od z?". Zamislite sofisticirani alarm sa slijedećom logikom: "Ako se aktivira alarm na vratima I više je od
šest popodne I NIJE praznik, ILI vikend, tada zovi policiju." U C++ tri logička operatora se koriste prilikom takvih ispitivanja. To su: AND OR NOT
&& || !
izraz1 && izraz2 izraz1 || izraz2 !izraz
Logički I (engl. AND) Logička AND naredba procjenjuje dva izraza, te ako su obadva istinita, i logička AND naredba je istinita. Ako je istina da ste gladni I ako je istina da imate novaca, TADA je istina da možete kupiti ručak. Prema tome, if ( (x == 5) && (y == 5) ) će dati TRUE ako su i x i y varijable jednake 5, a dat će FALSE u bilo kojem drugom slučaju.
Logički ILI Logička OR naredba procjenjuje dva izraza. Ako je bilo koji istinit, izjava je istinita. Ako imate novce ILI imate kreditnu karticu, možete platiti račun. Ne trebaju vam i novci i kreditna kartica; dovoljno je samo jedno iako ne bi bilo loše imati i jedno i drug. Prema tome, if ( (x == 5) || (y == 5) ) daje TRUE ako bilo x bilo y sadrži vrijednost 5, ili ako su obadva jednaka 5.
Logički NOT Logička NOT naredba daje istinu ako je izraz kojeg pručavamo lažan. ako je izraz lažan, vrijednost testa je TRUE!. Prema tome, if ( !(x == 5) ) je istina ako je x različit od 5. Ovo je isto kao da smo napisali if (x != 5)
Relacijski prioriteti Relacijski operatori i logički operatori u C++ izrazima vraćaju vrijednost 1(TRUE) ili 0(FALSE). Kao i svi izrazi, oni imaju svoje prioritete koji određuju koja će se relacija ispitivati prva. Ta činjenica je bitna prilikom određivanja vrijednosti naredbe if ( x > 5 && y > 5 || z > 5) Može biti da je programer želio dobiti TRUE ako su i x i y veći od 5 ili je z veći od 5. Ili je možda ideja da dobijemo TRUE samo ako je x veći od 5 i ako je istina da je ili y veći od 5 ili je z veći od 5. Iako prioriteti određuju koje će se operacije izvršavati prve, korištenje zagrada može pojednostavniti izraze: if ( (x > 5) && (y > 5 || z > 5) )
često koristimo if (x)
// if x is true (nonzero)
x = 0; što je u principu if (x != 0)
// if x is nonzero
x = 0; I ove dvije naredbe su ekvivalentne: if (!x) if (x == 0)
// if x is false (zero) // if x is zero
Druga je ipak jednostavnija za razumijevanje i eksplicitnija.
Kondicionalni operator Kondicionalni operator (?:) je jedini operator u C++ koji koristi tri izraza: (izraz1) ? (izraz2) : (izraz3) Ova linija se čita kao "Ako je izraz1 istina, vrati vrijednost u izraz2, inače ju vrati u izraz3. Tipično se ta vrijednost dodjeljuje varijabli. Listing 4.9. Demonstarcija kondicionalnog operatora. 1: // Listing 4.9 - demonstrates the conditional operator 2: // 3: #include 4: int main() 5: { 6: int x, y, z; 7: cout << "Enter two numbers.\n"; 8: cout << "First: "; 9: cin >> x; 10: cout << "\nSecond: "; 11: cin >> y; 12: cout << "\n"; 13: 14: 15: 16: 17: 18:
if (x > y) z = x; else z = y;
19: cout << "z: " << z; 20: cout << "\n"; 21: 22: z = (x > y) ? x : y; 23: 24: cout << "z: " << z; 25: cout << "\n"; 26: return 0; 27: } Output: Enter two numbers. First: 5 Second: 8 z: 8 z: 8
Kviz 1. 2. 3. 4. 5.
Što je izraz? Da li je x = 5 + 7 izraz? Koja mu je vrijednost? Kolika je vrijednost od 201 / 4? Kolika je vrijednost od 201 % 4? Ako su myAge, a, i b svi int varijable, koje su njihove vrijednosti nakon: myAge = 39; a = myAge++; b = ++myAge; 6. Kolika je vrijednost od 8+2*3? 7. Koja je razlika između x = 3 i x == 3? 8. Da li slijedeće vrijednosti poprimaju TRUE ili FALSE? a. 0 b. 1 c. -1 d. x = 0
e. x == 0 // pretpostavimo da x ima vrijednost 0
Vježbe 1. Napišite jednu naredbu koja ispituje dvije cjelobrojne vrijednosti i mijenja veću u manju, koristeći samo jedan else uvjet. 2. Proučite slijedeći program. Zamislite unošenje tri broja, i napišite kakav izlaz očekujete. 1: #include 2: int main() 3: { 4: int a, b, c; 5: cout << "Please enter three numbers\n"; 6: cout << "a: "; 7: cin >> a; 8: cout << "\nb: "; 9: cin >> b; 10: cout << "\nc: "; 11: cin >> c; 12: 13: if (c = (a-b)) 14: {cout << "a: "; 15: cout << a; 16: cout << "minus b: "; 17: cout << b; 18: cout << "equals c: "; 19: cout << c << endl;} 20: else 21: cout << "a-b does not equal c: " << endl; 22: return 0; 23: } 3. Upišite program iz vježbe 2, te kreirajte exe datoteku. Unesite brojeve 20, 10 i 50 . Jeste li dobili ono što ste očekivali? Zašto niste?
4. Pogledajte program i anticipirajte njegov izlaz: 1: #include 2: int main() 3: { 4: int a = 1, b = 1, c; 5: if (c = (a-b)) 6: cout << "The value of c is: " << c; 7: return 0; 8: } 5. Unesite, kompajlirajte, povežite i pokrenite program sa vježbe 4. Što je izlaz i zašto?
Lekcija 5
Funkcije
Iako je objekto orjentirano programiranje svoju pažnju sa funkcija usmjerilo prema objektima, funkcije i dalje ostaju ključne komponente svakog programa. Danas ćete naučiti • • • •
Što je funkcija i koji su njezini dijelovi Kako deklarirati i definirati funkcije Kako proslijediti parametre u funkciju Kako vratiti vrijednost iz funkcije
Što je funkcija? Funkcija je vrsta potprograma koji djeluje na podacima i vraća određenu vrijednost. Svaki C++ program ima bar jednu funkciju, main(). Kad pokrenete vaš program, main() se pokreće automatski. main() može zvati druge funkcije koje opet mogu pozivati druge. Svaka funkcija ima svoje ime, i kad se to ime pojavi unutar programa, izvršenje programa se premješta u tijelo funkcije. Kada funkcija završi, program nastavlja s radom na mjestu gdje je i prekinuo, odnosno u liniji koja slijedi nakon poziva funkcije. Dobro dizajnirane funkcije izvršavaju specifične i lako razumljive zadaće. Komplicirani zadaci bi trebali biti razbijeni u višestruke funkcije, gdje jedna funkcija po potrebi može pozivati drugu. Funkcije dolaze u dva oblika: korisnički definirane i ugrađene. Ugrađene su one koji standardno dolaze u paketu sa vašim kompajlerom.
Deklaracija i definicija funkcije Upotreba funkcija u vašem programu uvjetuje da prvo deklarirate funkciju, a potom da definirate funkciju. Deklaracija kazuje kompajleru ime, povratni tip funkcije, te njezine parametre. Definicija kazuje kompajleru kako funkcija radi. Niti jedna funkcija ne može biti pozvana iz programa, odnosno bilo koje druge funkcije ako prethodno nije bila deklarirana. Deklaracija funkcije se često naziva i njenim prototipom.
Deklariranje funkcije Postoje tri načina za deklariranje funkcije: • • •
Napišete svoju datoteku prototipa i potom koristite #include direktivu da ju uključite u vaš program Napišete prototip u datoteku u kojoj se funkcija koristi Definirate funkciju prije nego što ju pozove bilo koja druga funkcija. Kad to učinite definicija postaje ujedni i njezina deklaracija.
Iako možete definirati funkciju prije njezine uporabe, i time izbjeći neophodno kreiranje funkcijskog prototipa, to nije dobra programerska praksa iz tri razloga: • • •
Prvo, loša je ideja zahtjevati da se funkcije unutar datoteke pojavljuju određenim redosljedom. Čineći to otežavate si održavanje i daljnje izmjene programa. Drugo, moguće je da funkcija A() mora pozvati funkciju B(), ali u određenim okolnostima i B() također mora zvati funkciju A(). Nije moguće definirati funkciju A() prije funkcije B() i također definirati funkciju B() prije definicije funkcije A(), pa barem jedna od njih mora biti deklarirana u svakom slučaju. Treće, funkcijski prototipovi olakšavaju otkrivanje i uklanjanje grešaka. Ako vaš prototip deklarira da funkcija uzima određen niz parametara, ili da vraća određeni tip podatka, te ako potom vaša funkcija to ne učini, kompajler će primjetiti nepravilnost i označiti ju.
Funkcijski prototipovi Mnoge ugrađene funkcije koje koristite imaju svoje prototipe već upisane u datoteke koje uključujete u programe s #include direktivom. Za funkcije koje sami napišete, vi morate upisati i funkcijski prototip.
Funkcijski prototip je naredba, što znači da završava s točka-zarezom. Sastoji se od povratnog tipa funkcije, imena i liste parametara. Lista parametara je lista svih parametara i njihovih tipova, odvojenih zarezima. Funkcijski prototip i definicija funkcije moraju imati potpuno jednake povratne tipove, ime i listu parametara. Ako dođe do razlike, kompajler će nam javiti grešku. Primjetite, da funkcijski prototip ne mora sadržavati imena parametara, već samo njihove tipove, npr: long Area(int, int); Ovaj prototip deklarira funkciju imena Area() koja vraća long vrijednost i ima dva parematra, obadva cjelobrojna. Iako je ovo potpuno legalno, dodavanje imena parametrima čini funkciju mnogo čitljivijom: long Area(int length, int width); Sad je očitije što funkcija radi i koji su joj parametri. Primjetite također da svaka funkcija ima svoj povratni tip. Ako on nije naveden, funkcija se po defaultu dekalrira kao int. Program će vam ipak biti čitljiviji ako eksplicitno navedete povratni tip za svaku funkciju, uključujući i main(). Listing 5.1. Deklaracija i definicija funkcije te upotreba iste. 1: // Listing 5.1 - demonstrates the use of function prototypes 2: 3: typedef unsigned short USHORT; 4: #include 5: USHORT FindArea(USHORT length, USHORT width); //function prototype 6: 7: int main() 8: { 9: USHORT lengthOfYard; 10: USHORT widthOfYard; 11: USHORT areaOfYard; 12: 13: cout << "\nHow wide is your yard? "; 14: cin >> widthOfYard; 15: cout << "\nHow long is your yard? "; 16: cin >> lengthOfYard; 17: 18: areaOfYard= FindArea(lengthOfYard,widthOfYard); 19: 20: cout << "\nYour yard is "; 21: cout << areaOfYard; 22: cout << " square feet\n\n"; 23: return 0; 24: } 25: 26: USHORT FindArea(USHORT l, USHORT w) 27: { 28: return l * w; 29: } Output: How wide is your yard? 100 How long is your yard? 200 Your yard is 20000 square feet Primjeri funkcijskih prototipa long FindArea(long length, long width); void PrintMessage(int messageNumber); int GetChoice(); BadFunction();
Primjeri definicija funkcije long Area(long l, long w) { return l * w; } void PrintMessage(int whichMsg) { if (whichMsg == 0) cout << "Hello.\n"; if (whichMsg == 1) cout << "Goodbye.\n"; if (whichMsg > 1) cout << "I'm confused.\n"; }
Lokalne varijable Ne samo da možete dodjelivati varijable funkciji, nego također možete deklarirati varijable u tijelu same funkcije. Tako deklarirane varijable nazivamo lokalnim varijablama, stoga što se mogu koristiti samo u funkcijama u kojima su deklarirane, znači imaju ograničenu, lokalnu upotrebu. Čim izađemo iz funkcije, takve varijable nam nisu dostupne. Lokalne varijable su definirane kao i sve druge varijable. Parametri proslijeđeni funkciji se također smatraju lokalnim varijablama i mogu biti upotrebljeni isto kao da su definirani unutar tijela same funkcije. Listing 5.2 je primjer upotrebe parametara i lokalno definiranih varijabli unutar funkcije. Listing 5.2. Upotreba lokalnih varijabli i parametara. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
#include float Convert(float); int main() { float TempFer; float TempCel; cout << "Please enter the temperature in Fahrenheit: "; cin >> TempFer; TempCel = Convert(TempFer); cout << "\nHere's the temperature in Celsius: "; cout << TempCel << endl; return 0; }
17: float Convert(float TempFer) 18: { 19: float TempCel; 20: TempCel = ((TempFer - 32) * 5) / 9; 21: return TempCel; 22: } Output: Please enter the temperature in Fahrenheit: 212 Here's the temperature in Celsius: 100 Please enter the temperature in Fahrenheit: 32 Here's the temperature in Celsius: 0 Please enter the temperature in Fahrenheit: 85 Here's the temperature in Celsius: 29.4444 Evo iste verzije sa drugačijim nazivima varijabli: 1: 2: 3: 4: 5: 6: 7: 8:
#include float Convert(float); int main() { float TempFer; float TempCel;
9: 10: 11: 12: 13: 14: 15:
cout << "Please enter the temperature in Fahrenheit: "; cin >> TempFer; TempCel = Convert(TempFer); cout << "\nHere's the temperature in Celsius: "; cout << TempCel << endl; }
16:
float Convert(float Fer)
17: 18: 19: 20: 21:
{ float Cel; Cel = ((Fer - 32) * 5) / 9; return Cel; }
Novi izraz: Varijable imaju svoj doseg (engl. scope), koji određuje koliko dugo će ta varijabla biti dostupna unutar programa i gdje joj se može pristupiti. Varijable deklarirane unutar bloka su dostupne samo u tom bloku, in "ne postoje" čim blok završi. Globalne varijable imaju globalni doseg i dostupne su bilo gdje unutar našega programa.
Globalne varijable Varijable definirane izvan bilo koje funkcije imaju globalan doseg i time su dostupne u bilo kojoj funkciji našeg programa, uključujući i main(). Lokalne varijable istog imena kao i globalne ne mijenjaju globalne. Ipak, one ih skrivaju unutar tijela funkcije u kojima su definirane. Listing 5.3. Demonstracija globalnih i lokalnih varijabli. 1: #include 2: void myFunction(); // prototype 3: 4: int x = 5, y = 7; // global variables 5: int main() 6: { 7: 8: cout << "x from main: " << x << "\n"; 9: cout << "y from main: " << y << "\n\n"; 10: myFunction(); 11: cout << "Back from myFunction!\n\n"; 12: cout << "x from main: " << x << "\n"; 13: cout << "y from main: " << y << "\n"; 14: return 0; 15: } 16: 17: void myFunction() 18: { 19: int y = 10; 20: 21: cout << "x from myFunction: " << x << "\n"; 22: cout << "y from myFunction: " << y << "\n\n"; 23: } Output: x from main: 5 y from main: 7 x from myFunction: 5 y from myFunction: 10 Back from myFunction! x from main: 5 y from main: 7
Globalne varijable: Upozorenje U C++, globalne varijable su dozvoljene ali se gotovo nikada ne koriste. C++ je izrastao iz C, a u C-u se globalne varijable ono što nazivamo "nužno zlo". Neophodne su stoga što programer ponekad želi učiniti podatke dostupnima svim funkcijama. Globalne varijable su opasne stoga što su to dijeljeni podaci, i jedna ih funkcija može promjeniti na način koji je nevidljiv drugoj. To može i najčešće stvarno i uzrokuje pogreške koje je jako teško otkriti. Listing 5.4. Variajable dostupne unutar bloka. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
// Listing 5.4 - demonstrates variables // scoped within a block #include void myFunc(); int main() { int x = 5; cout << "\nIn main x is: " << x; myFunc(); cout << "\nBack in main, x is: " << x; return 0; }
19: void myFunc() 20: { 21: 22: 23: 24:
int x = 8; cout << "\nIn myFunc, local x: " << x << endl;
25: { 26: cout << "\nIn block in myFunc, x is: " << x; 27: 28: int x = 9; 29: 30: cout << "\nVery local x: " << x; 31: } 32: 33: cout << "\nOut of block, in myFunc, x: " << x << endl; 34: } Output: In main x is: 5 In myFunc, local x: 8 In block in myFunc, x is: 8 Very local x: 9 Out of block, in myFunc, x: 8 Back in main, x is: 5 Nepotrebno je reći, program bi bio mnogo razumljiviji da su tim varijablama zadana različita imena! Iako nema limita na veličinu same funkcije, dobro dizajnirane funkcije su obično malene. Mnogi programeri savjetuju da veličina jedne funkcije nikad ne smije prelaziti veličinu ekrana. Pogotovo u našim prvim koracima, moramo se truditi da funkcije budu što je kraće moguće kako bi ih lakše razumjeli i održavali.
Upotreba funkcija kao parametara drugim funkcijama Iako je legalno za jednu funkciju da kao parametar uzima drugu funkciju, to nam kod može napraviti nečitkim i teško razumljivim. Kao primjer, recimo da imate funkcije double(), triple(), square(), i cube(), od kojih svaka vraća vrijednost. Mogli biste napisati Answer = (double(triple(square(cube(myValue))))); Alternativa je dodjelivanje varijable prilikom svakog koraka: unsigned long myValue = 2; unsigned long cubed = cube(myValue); // cubed = 8 unsigned long squared = square(cubed); // squared = 64 unsigned long tripled = triple(squared); // tripled = 196 unsigned long Answer = double(tripled); // Answer = 392
Parameteri su lokalne varijable Argumenti predani funkciji su lokalni za tu funkciju. Promjene napravljene na argumentima ne utječu na vrijednosti iz pozivne funkcije. To se još i naziva "passing by value", što znači da se lokalna kopija svakog argumenta pravi unutar funkcije. Te lokalne kopije se tretiraju kao i sve ostale lokalne varijable. Listing 5.5 nam to ilustrira. Listing 5.5. Demonstracija prenošenja po vrijednosti. 1: // Listing 5.5 - demonstrates passing by value 2: 3: #include 4: 5: void swap(int x, int y); 6: 7: int main() 8: { 9: int x = 5, y = 10; 10: 11: 12: 13: 14: 15: 16:
cout << "Main. Before swap, x: " << x << " y: " << y << "\n"; swap(x,y); cout << "Main. After swap, x: " << x << " y: " << y << "\n"; return 0; }
17: void swap (int x, int y) 18: { 19: int temp; 20: 21: cout << "Swap. Before swap, x: " << x << " y: " << y << "\n"; 22: 23: temp = x; 24: x = y; 25: y = temp; 26: 27: cout << "Swap. After swap, x: " << x << " y: " << y << "\n"; 28: 29: } Output: Main. Before swap, x: 5 y: 10 Swap. Before swap, x: 5 y: 10 Swap. After swap, x: 10 y: 5 Main. After swap, x: 5 y: 10
Povratne vrijednosti Funkcija vraća ili neku vrijednost ili void. Void je signal kompajleru da nikakva vrijednost neće biti vraćena.
Za vraćanje neke vrijednosti iz funkcije, napišemo ključnu riječ return koju slijedi vrijednost koju želimo vratiti. Sama vrijednost može biti i izraz koji vraća vrijednost. Na primjer, ovo su sve legalne return naredbe return 5; return (x > 5); return (MyFunction()); Legalno je imati više od jedne return naredbe unutar funkcije. Listing 5.6 demonstrira tu ideju. Listing 5.6. Demonstracija višestrukih return naredbi. 1: // Listing 5.6 - demonstrates multiple return 2: // statements 3: 4: #include 5: 6: int Doubler(int AmountToDouble); 7: 8: int main() 9: { 10: 11: int result = 0; 12: int input; 13: 14: cout << "Enter a number between 0 and 10,000 to double: "; 15: cin >> input; 16: 17: cout << "\nBefore doubler is called... "; 18: cout << "\ninput: " << input << " doubled: " << result << "\n"; 19: 20: result = Doubler(input); 21: 22: cout << "\nBack from Doubler...\n"; 23: cout << "\ninput: " << input << " doubled: " << result << "\n"; 24: 25: 26: return 0; 27: } 28: 29: int Doubler(int original) 30: { 31: if (original <= 10000) 32: return original * 2; 33: else 34: return -1; 35: cout << "You can't get here!\n"; 36: } Output: Enter a number between 0 and 10,000 to double: 9000 Before doubler is called... input: 9000 doubled: 0 Back from doubler... input: 9000 doubled: 18000 Enter a number between 0 and 10,000 to double: 11000 Before doubler is called... input: 11000 doubled: 0 Back from doubler... input: 11000 doubled: -1
Default parametri Za svaki parametar koji deklarirate u funkcijkom prototipu i definiciji, pozivajuća funkcija mora predati vrijednost. Predana vrijednost mora biti deklariranog tipa. Prema tome, ako imate funkciju deklariranu kao
long myFunction(int); funkcija zapravo mora uzeti cjelobrojnu varijablu. Ako se definicija funkcije razlikuje ili ako ne predate cijeli broj funkciji, dobit ćete grešku prilikom kompajliranja. Jedina iznimka tome pravilu je kad funkcijski prototip deklarira podrazumijevanu vrijednost za taj parametar. Podrazumijevana vrijednost je ona koja će se koristiti ako ne proslijedimo nikakvu vrijednost funkciji. Prethodna deklaracija mogla bi biti napisana long myFunction (int x = 50); Ovaj prototip govori, "myFunction() vraća long i uzima integer parametar. Ako argument nije isporučen, koristi default vrijednost 50." Budući da imena parametara nisu obavezna u funkcijskim prototipima, ova deklaracija bi mogla biti napisana i kao long myFunction (int = 50); Svi ili samo neki funkcijski parametri mogu imati dodjeljene podrazumijevane vrijednosti. Jedino ograničenje glasi: Ako neki parametar nema deklariranu default vrijednost, ne može ga imati niti njemu prethodni parametar. U funkcijskom prototipu poput ovog long myFunction (int Param1, int Param2, int Param3); možete dodjeliti default vrijednost u Param2 samo ako ste ju dodjelili i za Param3. Param1 ju može imati samo ako su dodijeljene i za Param2 i za Param3. Listing 5.7 je demonstracija upotrebu podrazumijevanih vrijednosti.
Listing 5.7. Demonstracija podrazumijevanih vrijednosti parametara. 1: // Listing 5.7 - demonstrates use 2: // of default parameter values 3: 4: #include 5: 6: int AreaCube(int length, int width = 25, int height = 1); 7: 8: int main() 9: { 10: int length = 100; 11: int width = 50; 12: int height = 2; 13: int area; 14: 15: area = AreaCube(length, width, height); 16: cout << "First area equals: " << area << "\n"; 17: 18: area = AreaCube(length, width); 19: cout << "Second time area equals: " << area << "\n"; 20: 21: area = AreaCube(length); 22: cout << "Third time area equals: " << area << "\n"; 23: return 0; 24: } 25: 26: AreaCube(int length, int width, int height) 27: { 28: 29: return (length * width * height); 30: } Output: First area equals: 10000 Second time area equals: 5000 Third time area equals: 2500
Preopterećenje funkcija C++ nam omogućuje kreiranje više funkcija sa istim imenom. To se zove preopterećenje funkcija (engl. function overloading). Funkcija se mora razlikovati po listi parametara, različitim tipovima parametara, ili oboje. Ovo je primjer: int myFunction (int, int); int myFunction (long, long); int myFunction (long); myFunction() je preopterećena s tri različite liste paramatara. Prva i druga se razlikuju po tipovima parametara, a treća po broju parametara. Povratni tip može biti isti ili različit za preopterećene funkcije. Primjetite da dve funkcije istog imena i liste paramaetara a različitih povratnih tipova uzrokuju grešku prilikom kompajliranja. Novi izraz: Preopterećenje funkcija još i nazivamo funkcijskim polimorfizmom. Poli znači više, a morf znači oblik. Dakle, ta funkcija ima više oblika. Polimorfizam nam omogućuje, npr., da napravimo funkciju Prosjek() koja može računati prosjek i za integere, i za double vrijednosti, i za sve ostale, a sve to bez potrebe za kreiranjem individualnih imena poput ProsjekInt(), ProsjekFloat(), ProsjekDouble()...
Recimo da napišete funkciju koja duplira koliki joj god ulazni podatak date. Želite joj moći predati int, long, float, ili double. Bez preopterećenja, morali biste napraviti četiri različita imena funkcije: int DoubleInt(int); long DoubleLong(long); float DoubleFloat(float); double DoubleDouble(double); S preopterećenjem, napraviti ćete slijedeću deklaraciju: int Double(int); long Double(long); float Double(float); double Double(double); To je lakše za čitanje i lakše za upotrebu. Ne morate brinuti o tome koju funkciju pozvati; vi samo predate varijablu, i prava funkcija se poziva automatski. Listing 5.8 ilustrira upotrebu preopterećenja funkcija. Listing 5.8. Demonstracija polimorfizma funkcija. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41:
// Listing 5.8 - demonstrates // function polymorphism #include int Double(int); long Double(long); float Double(float); double Double(double); int main() { int myInt = 6500; long myLong = 65000; float myFloat = 6.5F; double myDouble = 6.5e20; int doubledInt; long doubledLong; float doubledFloat; double doubledDouble; cout << "myInt: " << myInt << "\n"; cout << "myLong: " << myLong << "\n"; cout << "myFloat: " << myFloat << "\n"; cout << "myDouble: " << myDouble << "\n"; doubledInt = Double(myInt); doubledLong = Double(myLong); doubledFloat = Double(myFloat); doubledDouble = Double(myDouble); cout << "doubledInt: " << doubledInt << "\n"; cout << "doubledLong: " << doubledLong << "\n"; cout << "doubledFloat: " << doubledFloat << "\n"; cout << "doubledDouble: " << doubledDouble << "\n"; return 0; } int Double(int original)
42: { 43: cout << "In Double(int)\n"; 44: return 2 * original; 45: } 46: 47: long Double(long original) 48: { 49: cout << "In Double(long)\n"; 50: return 2 * original; 51: } 52: 53: float Double(float original) 54: { 55: cout << "In Double(float)\n"; 56: return 2 * original; 57: } 58: 59: double Double(double original) 60: { 61: cout << "In Double(double)\n"; 62: return 2 * original; 63: } Output: myInt: 6500 myLong: 65000 myFloat: 6.5 myDouble: 6.5e+20 In Double(int) In Double(long) In Double(float) In Double(double) DoubledInt: 13000 DoubledLong: 130000 DoubledFloat: 13 DoubledDouble: 1.3e+21
Umetnute (engl. inline) funkcije Kad definirate funkciju, kompajler obično kreira samo jedan niz pripadajućih instrukcija u memoriji. Kad pozovete funkciju, izvršenej programa skače na te instrukcije, te kad funkcija obavi radnju, skače se nazad u slijedeći red pozivne funkcije. Ako pozovete funkciju 10 puta, vaš program svaki puta skače na isti niz instrukcija. To znači da postoji samo jedna kopija vaše funkcije ane 10. Prilikom skakanja u i iz funkcija dolazi do malog gubitka performansi. Za malene funkcije se može dobiti na brzini ako možemo izbjeći te skokove. Ako je funkcija deklarirana s ključnom riječi inline, kompajler ne kreira "pravu" funkciju: on kopira kod iz inline funkcije direktno u pozivnu funkciju. Ne radi se nikakav skok; kao da smo napisali cijelu funkciju unutar pozivne funkcije. Primjetite da inline funkcije mogu donesti brzinu ali uz popriličnu cijenu. Cijena je povećanje duljine izvršne datoteke, budući da sad možemo imati isti kod dupliran unutar našeg programa onoliko puta koliko se poziva određena funkcija. Listing 5.9. Demonstracija inline funkcije. 1: // Listing 5.9 - demonstrates inline functions 2: 3: #include 4: 5: inline int Double(int); 6: 7: int main() 8: { 9: int target; 10: 11: cout << "Enter a number to work with: "; 12: cin >> target; 13: cout << "\n"; 14: 15: target = Double(target); 16: cout << "Target: " << target << endl; 17: 18: target = Double(target); 19: cout << "Target: " << target << endl; 20: 21: 22: target = Double(target); 23: cout << "Target: " << target << endl; 24: return 0; 25: } 26: 27: int Double(int target) 28: { 29: return 2*target; 30: } Output: Enter a number to work with: 20 Target: 40 Target: 80 Target: 160
Rekurzija Funkcija može pozivati i samu sebe. To se zove rekurzija, i ona može biti direktna ili indirektna. Direktna je kad funkcija poziva samu seba. Indirektna je kad funkcija poziva funkciju koja potom poziva ovu prvu. Neki problemi se puno lakše rješavaju rekurzijom, obično on u kojima djelujemo na podatke i na isti način i razultat. Oba tipa rekurzije dolaze u dva podoblika: oni koji kad tad završe i proizvedu odgovor, te oni koji
nikad ne završe i proizvedu grešku prilikom izvršenja. Programeri smatraju da su ove druge prilično zabavne (dok god se događaju nekom drugom). Važno je napomenuti da kad god funkcija pzove samu sebe, pokrene se nova kopija funkcije. Lokalne varijable u drugoj funkciji su neovisne od onih u prvoj, i ne mogu direktno utjecati jedne na druge. Za ilustraciju rješenja problema upotrebom rekurzije, pogledati ćemo Fibonaccijev niz: 1,1,2,3,5,8,13,21,34... Svaki broj, poslije drugog, je suma dva prethodna broja. Fibonaccijev problem bi bio određivanje koji je dvanaesti broj u nizu. Jedan način za rješenje tog problema je pažljivo proučavanje niza. Prva dva broja su 1. Svaki slijedeći broj je suma prethodna dva broja. Prema tome, sedmi broj je zbroj petog i šestog broja niza. Uopćeno, n-ti broj je suma od n-2 i n-1 dok god je n>2. Rekurzivna funkcija treba uvijet za zaustavljanje. Nešto se mora dogoditi što bi uzrokovalo program da prestane s rekurzijom, ili neće nikad zavšiti s radom. U Fibonaccijevom nizu, n<3 je uvjet za zaustavljanje. Algoritam bi bio slijedeći: 1. Zamolite korisnika za položaj u nizu. 2. Pozovemo fib() funkciju s tom pozicijom, dajući joj unešenu vrijednost. 3. fib() funkcija isiptuje argument (n). Ako je n<3 vraća 1; inače, fib() poziva samu seb (rekurzivno) dodjeljujući vrijednost n-2, poziva se ponovo dodjelujući n-1, i vraća nam sumu. Ako pozovete fib(1), vraća nam 1. Ako pozovete fib(2), vraća 1, Ako pozovete fib(3), vraća nam sumu od poziva fib(2) i fib(1). Budući da da fib(2) vraća 1 i fib(1) vraća 1, fib(3)će vratiti 2. Ako pozovete fib(4), vratiti će nam sumu od fib(3) i fib(2). Već smo ustanovili da fib(3) vraća 2 (pozivajući fib(2) i fib(1)), pa će fib(4) zbrojiti vrrijednosti i time vratiti 3, što je četvrti broj niza. Ova metoda nije najefikasniji način za rješenje toga problema (u fib(20) funkcija fib se poziva 13 529 puta!!!), ali radi. Svaki put kad se pozove fib(), rezervira se određena količina memorije. Kad izađe iz nje, memorija se oslobodi. Prilikom rekurzije, memorija se rezervira prije nego što se oslobađa, pa ovaj program može brzo "potrošiti" svu rezerviranu memoriju. Listing 5.10 implementira fib() funkciju. Upozorenje: Prilikom pokretanja listinga 5.10, koristite malen broj (manji od 20). Zbog upotrebe rekurzije, ovaj program zauzima mnogo memorije.
Listing 5.10. Demonstracija rekurzije za račun Fibonaccijevog niza. 1: // Listing 5.10 - demonstrates recursion 2: // Fibonacci find. 3: // Finds the nth Fibonacci number 4: // Uses this algorithm: Fib(n) = fib(n-1) + fib(n-2) 5: // Stop conditions: n = 2 || n = 1 6: 7: #include 8: 9: int fib(int n); 10: 11: int main() 12: { 13: 14: int n, answer; 15: cout << "Enter number to find: "; 16: cin >> n; 17: 18: cout << "\n\n"; 19: 20: answer = fib(n); 21: 22: cout << answer << " is the " << n << "th Fibonacci number\n"; 23: return 0; 24: } 25: 26: int fib (int n) 27: { 28: cout << "Processing fib(" << n << ")... "; 29: 30: if (n < 3 ) 31: { 32: cout << "Return 1!\n"; 33: return (1); 34: } 35: else 36: { 37: cout << "Call fib(" << n-2 << ") and fib(" << n-1 << ").\n"; 38: return( fib(n-2) + fib(n-1)); 39: } 40: } Output: Enter number to find: 5 Processing fib(5)... Call fib(3) and fib(4). Processing fib(3)... Call fib(1) and fib(2). Processing fib(1)... Return 1! Processing fib(2)... Return 1! Processing fib(4)... Call fib(2) and fib(3). Processing fib(2)... Return 1! Processing fib(3)... Call fib(1) and fib(2). Processing fib(1)... Return 1! Processing fib(2)... Return 1! 5 is the 5th Fibonacci number
Rekurzija se ne koristi često u C++ programiranju, ali može biti moćno i elegantno oruđe za određene potrebe.
Kviz 1. Koja je razlika između prototipa funkcije i definicije funkcije?
2. Moraju li se imena parametara podudarati u prototipu, definiciji i pozivu funkcije? 3. ako funkcije ne vraća nikakvu vrijednost, kako ćemo ju deklarirati? 4. Ako ne deklariramo povratni tip funkcije, koji tiš će biti podrazumijevan? 5. Što je lokalna varijabla? 6. Što je doseg (engl. scope)? 7. Što je rekurzija? 8. Kad koristimo globalne varijable? 9. Što je preopterećenje funkcija? 10. Što je polimorfizam?
Vježbe 1. Napišite prototip za funkciju imena Perimeter(), koja vraća unsigned long int i ima dva parametra, oba unsigned short int. 2. Napišite definiciju za funkciju Perimeter() iz vježbe 1. Dva parametra predstavljaju dužinu i širinu pravokutnika. Neka funkcija vraća perimetar (dvostruka dužina pomnožena dvostrukom širinom). 3. BUG BUSTER: Što ne valja s funkcijom u slijedećem kodu? #include void myFunc(unsigned short int x); int main() { unsigned short int x, y; y = myFunc(int); cout << "x: " << x << " y: " << y << "\n"; } void myFunc(unsigned short int x) { return (4*x); } 4. BUG BUSTER: Što ne valja s funkcijom u slijedećem kodu? #include int myFunc(unsigned short int x); int main() { unsigned short int x, y; y = myFunc(x); cout << "x: " << x << " y: " << y << "\n"; } int myFunc(unsigned short int x); { return (4*x); } 5. Napišite funkciju koja uzima dva unsigned short integer argumenta i vraća rezultat dijeljenja prvog sa drugim. Ako je drugi broj nula, ne radi dijeljenje nego vrati –1. 6. Napišite program koji traži korisnika da unese dva broja i zove funkciju iz prethodne vježbe. Neka ispiše rješenje, odnosno poruku o grešci ako dobije –1. 7. Napišite program koji traži unos broja i potencije. Napišite rekurzivnu funkciju koja računa potenciju zadanog broja. Npr., ako je broj 2, a potencija 4, funkcija treba vratiti 16.
Lekcija 6
Osnovne klase
Klase proširuju ugrađena svojstva jezika C++ kako bi vam pomogle u predstavljanju i rješavanju složenih problem. Danas ćete naučiti: • • •
•
Što su klase i objekti. Kako definirati ovu klasu i stvarati objekte te klase. Što su funkcijski članovi i podatkovni članovi (engl member function & member data). Što su konstruktori i kako se koriste.
Stvaranje novih tipova Već ste naučili i različitim tipovima varijabli, uključujući i unsigned integer i charakter tipove. Tip varijable nam govori podosta o njoj. Npr., ako deklarirate Height i Width kao unsigned integer, tada znate da svaka od njih može sadržavati broj između 0 i 65535, pod pretpostavkom da je integer dvobajtni podatak. To je pravo značenje kad kažemo da je varijabla unisgned integer. Pokušavanje spremanja drugačijeg tipa podatka u varijablu uzrokuje grešku. Vi ne možete spremiti svoje ime u unsigned integer i ne biste trebali niti pokušavati. Uopćeno govoreći, tip je kategorija. U C++ jeziku program može stvarati nove tipove podatka, i svaki od ti novih tipova može imati svu funkcionalnost i snagu ugrađenih tipova.
Zašto stvarati novi tip? Programi se obični pišu kako bismo riješili neke probleme iz realnog svijeta, kao npr., praćenjem osobnih dohodaka uposlenika ili simuliranjem rada sustava za grijanje. Iako je moguće rješavati složene probleme koristeći programe koji se koriste ugrađenim tipovima podataka, puno je lakše nositi se sa kompleksnim problemima ako možete stvarati "opise" objekata o kojima govorite. Drugim riječima, simuliranje rada sustava za grijanje je jednostavnije ako stvorite varijable koje predstavljaju sobe, senzore za grijanje, termostate i bojlere. Što bolje varijable opisuju stvarnost, lakše nam je napisati program.
Klase i članovi Nove tipove stvarate deklarirajući klasu. Klasa je kolekcija varijabli—često različitih tipova—kombinirana sa grupom pripadnih funkcija. Jedan način za razmišljanje o autu je kao o kolekciji kotača, vrata, sjedala, prozora i tako dalje. Drugi način razmišljanja o autu je što on može: može se gibati, ubrzavati, usporavati, stati, parkirati, itd. Klasa nam omogućuje da enkapsuliramo, odn. grupiramo, te dijelove i različite funkcije u jednu kolekciju, koju nazivamo objekt. Enkapsulacija svega što znamo o autu u jednu klasu ima brojne prednosti za programera. Sve se nalazi na jednom mjestu, što olakšava pozivanje, kopiranje i manipuliranje podacima. Isto tako korisnici klase—odnosno dijelovi programa koji ju pozivaju, mogu koristiti objekt bez briganja o tome šta se nalazi unutra i kako radi. Klasa može biti načinjena od bilo koje kombinacije tipova varijabli i ostalih tipova klasa. Varijable unutar klase nazivamo podatkovnim članovima (engl. data members, ili data variables). Klasa Automobil mogla bi imati podatkovne članove koji predstavljaju sjedala, tip radio uređaja, gume, itd. Funkcije unutar klase obično manipuliraju podatkovnim članovima. Njih nazivamo funkcijskim članovima ili metodama (engl. member functions, methods). Metode klase Automobil moglebi biti Start(), Koči(). Klasa Mačak bi mogla imati podatkovne članove koji predstavljaju godine i težinu; njezine metode mogle bi biti Spavaj(), Mjau(), LoviMiša().
Deklariranje klase Za deklariranje klase, koristimo ključnu riječ class, koju slijedi vitičasta zagrada, te potom lista podatkovnih članova i metoda te klase. Zatvaramo deklaraciju sa zatvarajućom vitičastom zagradom i točka-zarezom. Evo deklaracija klase zvane Cat: class Cat { unsigned int itsAge;
unsigned int itsWeight; Meow(); }; Deklaracija ove klase ne alocira memoriju za samu klasu. Ona samo govori kompajleru što je to Cat, koje sadrži podatke (itsAge i itsWeight), te što može raditi (Meow()). Također govori kompajleru koliko je velik Cat— odnosno koliko mjesta kompajler treba rezervirati za svaku mačku koju stvorimo.
Definiranje objekta Objekt vašeg novog tipa definirate isto kao i cjelobrojnu varijablu: unsigned int GrossWeight; // definicija neoznačenog cijelog broja Cat Frisky; // definicija Mačke Ovaj kod definira variablu zvanu Gross Weight koja je cjelobrojnog neoznačenog tipa. Također definira Frisky, što je zapravo objekt čija klasa (ili tip) je Cat.
Klase i objekti Vi se niti u stvarnom životu ne igrate s definicijom mačke; igrate se s individualnim mačkama. Naravno da postoji razlika između poimanja mačke, te one određene mačke koja se u tome trenutku izležava u vašoj dnevnoj sobi. Na isti način C++ pravi razliku između klase Cat, što je u stvari ideja mačke, te svakog individualnog Cat objekta. Prema tome, Frisky je objekt tipa Cat na isti način na koji je GrossWeight varijabla tipa unisgned int.
Pristupanje članovima klase Jednom kad definirate Cat objekt..na primjer, Frisky—koristeći operator točke (engl. dot) (.) pristupate članovima toga objekta. Prema tome, za dodjeljivanje broja 50 u Friskijev Weight podatkovni član, napisali biste Frisky.Weight = 50; Na isti način, za poziv Meow() funkcije, napisali biste Frisky.Meow(); Kad se koristite metodama klase, vi pozivate metodu. U ovom primjeru, vi pozivate Meow() na mačku Frisky. Pridruživanje objektima, a ne klasama U C++, vi ne pridružujete vrijednosti tipovima, nego varijablama. Npr., ne biste nikad napisali int = 5;
// pogrešno
Kompajler bi ovo proglasio pogreškom, budući da ne možete dodijeliti 5 integeru. Umjesto toga, morate definirati varijablu i dodjeliti 5 toj varijabli. Na primjer, int x; x = 5;
// definira x kao cijeli broj // postavi vrijednost varijable x na 5
Isto tako, ne biste nikad napisali Cat.age=5;
// pogreška
??? Kompajler bi ovo proglasio pogreškom budući da nemožete dodjeliti 5 age dijelu klase Cat. Umjesto toga, moramo definirati Cat objekt i dodjeliti 5 tome objektu. Na primjer, Cat Frisky; Frisky.age = 5;
// isto kao int x; // isto kao x = 5;
Ako ne deklarirate, klasa neće imati Pokušajte sa slijedećim eksperimentom: Pronađite nekog trogodišnjaka i pokažite mu mačka. Tada recite, "Ovo je Tom. Tom zna jedan trik. Tom, viči vau." Dijete će se nasmijati i reći, "Ne, ludo, mačke ne laju."
Da ste napisali Cat Frisky; Frisky.Bark()
// napravi mačku imena Frisky // reci Friskiju da laje
kompajler ne bi rekao, mačke ne laju. Kompajler znna da Frisky ne laje budući da klasa Cat nema Bark() funkciju.
Privatni protiv javnih Dodatne ključne riječi se koriste u deklaraciji klase. Dvije najvažnije su public i private. Svi članovi klase—i podaci i metode—su privatni po defaultu. Privatni članovi mogu biti pristupani samo iz metoda klase u kojoj se nalaze. Javni članovi su dostupni svakom objektu te klase. Ova razlika je i važna i zbunjujuća. Kako bismo to malo razjasnili, pogledajmo primjer iz ranijeg dijela lekcije: class Cat { unsigned int itsAge; unsigned int itsWeight; Meow(); }; U ovoj deklaraciji, itsAge, itsWeight, i Meow() su privatni, budući da su svi pripadni članovi klase privatni po defaultu. To znači da, ukoliko ne navedete drugačije, oni su privatni. Kako bilo, ako napišete Cat Boots; Boots.itsAge=5;
// greška! ne može se pristupati privatnim podacima!
kompajler će to proglasiti greškom. U stvari vi ste gornjom deklaracijom rekli kompajeru, "pristupat ću itsAge, itsWeight i Meow() samo iz funkcijskih članova same klase." A ovdje ste pristupili itsAge podatkovnom članu Boots objekta izvan Cat metode. Samo zato što je Boots objekt klase Cat, ne znači da možete pristupat njegovim privatnim dijelovima. Ovo je izvor neopisive konfuzije za novopečenog C++ programera. Već vas čujem kako urlate, "Hej! Upravo sam rekao da je Boots mačka. Zašto Boots ne može pristupiti vlatitim godinama?" Odgovor je da Boots može, ali vi ne možete. Boots, sa vlastitim metodama može pristupati svim svojim dijelovima—i javnim i privatnim. Iako ste vi kreirali klasu Cat, to ne znači da možete vidjeti ili mijenjati one dijelove koji su privatni. Način da koristite klasu Cat i da joj možete pristupati podatkovnim članovima je class Cat { public: unsigned int itsAge; unsigned int itsWeight; Meow(); }; Sada su itsAge, itsWeight, i Meow() svi postali javni. Boots.itsAge=5 kompajlira se bez problema. Listing 6.1. Pristupanje javnim članovima jednostavne klase. 1: 2: 3: 4: 5: 6: 7: 8: 9:
// Demonstrates declaration of a class and // definition of an object of the class, #include // for cout class Cat { public: int itsAge;
// declare the class object // members which follow are public
10: int itsWeight; 11: }; 12: 13: 14: void main() 15: { 16: Cat Frisky; 17: Frisky.itsAge = 5; // assign to the member variable 18: cout << "Frisky is a cat who is " ; 19: cout << Frisky.itsAge << " years old.\n"; 20: Output: Frisky is a cat who is 5 years old. Pažnja: Pokušajte staviti liniju 8 u komentar (//) i rekompajlirajte. Dobit ćete grešku u liniji 17 budući da itsAge više neće imati javni pristup. Podrazumijevana vrijednost za klase je privatan pristup.
Načiniti podatkovne članove privatnima Kao opće pravilo dizajna, trebali biste učiniti navedeno u naslovu. Prema tome, morate kreirati javne funkcije znane kao metode pristupa (engl. accessor methods) za postavljanje i dobivanje privatnih varijabli. Ove pristupne metode su funkcijski članovi koji drugi dijelovi programa zovu za postavljanje i dobivanje privatnih varijabli. Zašto se gnjaviti s ovim dodatnim nivoom indirektnog pristupa? Na kraju, nije li jednostavnije koristiti se podacima nego pristupati im preko pristupnih funkcija. Pristupne funkcije omogućuju nam da razdvojimo detalje o tome kako su podaci pohranjeni i kako se koriste. To vam omogućuje da promjenite način spremanja podataka bez potrebe za ponovnim pisanjem funkcija koje koriste podatke. Ako funkcija koja treba znati starost mačka pristupa varijabli itsAge direktno, ta bi funkcija morala biti ponovno napisana ako vi kao autor Cat klase odlučite promijeniti način na koji su podaci spremljeni. Međutim, ako koristite funkciju GetAge(), vaša Cat clasa će jednostavno vratiti vrijednost bez obzira kako ćete doći do starosti. Pozivna funkcija ne mora znati spremate li taj podatak kao unsigned integer ili long, ili računate li sve kako bi trebalo. Ta tehnika bitno olakšava održavanje programa. Daje vašem kodu dulji život budući da promjene u samom dizajnu ne čine vaš program zastarjelim. Listing 6.2 pokazuje Cat klasu modificiranu tako da sadrži privatne podatkovne članove i javne metode pristupa. Primjetite da ovo nije komletan program nego samo klasa. Listing 6.2. Klasa sa metodama pristupa. 1: // Cat class declaration 2: // Data members are private, public accessor methods 3: // mediate setting and getting the values of the private data 4: 5: class Cat 6: { 7: public: 8: // public accessors 9: unsigned int GetAge(); 10: void SetAge(unsigned int Age); 11: 12: 13: 14:
unsigned int GetWeight(); void SetWeight(unsigned int Weight);
15: 16: 17: 18:
// public member functions Meow(); // private member data
19: private: 20: unsigned int itsAge; 21: unsigned int itsWeight; 22: 23: };
Za postavljanje Friskijevih godina, pridružili biste vrijednost SetAge() metodi, kao u Cat Frisky; Frisky.SetAge(5);
// postavi Friskijevu starost koristeći javni pristup
Privatnost protiv sigurnosti Deklariranjem metoda i podataka privatnima omogućeno je kompajleru da nađe programerske greške prije nego one postanu bugovi. Svaki programer može lagano izbjeći privatnost ako to želi. Stroustrup, izumitelj jezikaC++, je rekao "C++ mehanizmi kontrole pristupa pružaju zaštitu od nezgoda—ne od prevara." class ključna riječ Sintaksa za class ključnu riječ je slijedeća. class class_name { // access control keywords here // class variables and methods declared here }; Primjer 1 class Cat { public: unsigned int Age; unsigned int Weight; void Meow(); }; Cat Frisky; Frisky.Age = 8; Frisky.Weight = 18; Frisky.Meow(); Primjer 2 class Car { public:
// the next five are public
void Start(); void Accelerate(); void Brake(); void SetYear(int year); int GetYear(); private: int Year; Char Model [255]; }; Car OldFaithful;
// the rest is private
// end of class declaration // make an instance of car
int bought; // a local variable of type int OldFaithful.SetYear(84) ; // assign 84 to the year bought = OldFaithful.GetYear(); // set bought to 84 OldFaithful.Start(); // call the start method
Implementacija metoda klase Kao što ste vidjeli, pristupna funkcija pruža javno sučelje za privatne podatkovne članove neke klase. Svaka pristupna funkcija, zajedno sa bilo kojom drugom metodom koju deklarirate mora imati svoju implementaciju. Imlementacijom nazivamo definicju funkcije. Definicija funkcijskog člana počinje s imenom klase, kojeg slijede dvije dvotočke, ime funkcije, te njezini parametri. Listing 6.3 pokazuje kompletnu deklaraciju jednostavne Cat klase i implementacije pristupne funkcije i jednog općeg funkcijskog člana. Listing 6.3. Implementacija metoda jednostavne klase. 1: // Demonstrates declaration of a class and 2: // definition of class methods, 3: 4: #include // for cout 5: 6: class Cat // begin declaration of the class 7: { 8: public: // begin public section 9: int GetAge(); // accessor function 10: void SetAge (int age); // accessor function 11: void Meow(); // general function 12: private: // begin private section 13: int itsAge; // member variable 14: }; 15: 16: // GetAge, Public accessor function 17: // returns value of itsAge member 18: int Cat::GetAge() 19: { 20: return itsAge; 21: } 22: 23: // definition of SetAge, public 24: // accessor function 25: // returns sets itsAge member 26: void Cat::SetAge(int age) 27: { 28: // set member variable its age to 29: // value passed in by parameter age 30: itsAge = age; 31: } 32: 33: // definition of Meow method 34: // returns: void 35: // parameters: None 36: // action: Prints "meow" to screen 37: void Cat::Meow() 38: { 39: cout << "Meow.\n"; 40: } 41: 42: // create a cat, set its age, have it 43: // meow, tell us its age, then meow again. 44: int main() 45: {
46: Cat Frisky; 47: Frisky.SetAge(5); 48: Frisky.Meow(); 49: cout << "Frisky is a cat who is " ; 50: cout << Frisky.GetAge() << " years old.\n"; 51: Frisky.Meow(); 52; return 0; 53: } Output: Meow. Frisky is a cat who is 5 years old. Meow.
Konstruktori i destruktori Postoje dva načina za deklariranje cjelobrojne varijable. Možete deklarirati varijablu i poslije joj tjekom izviđenja program dodijeliti vrijednost. Na primjer, int Weight; ... Weight = 7;
// definicija varijable // drugi kod ide ovdje // pridruživanje vrijednosti varijabli
Ili možete definirati cijeli broj i odmah ga inicijalizirati. Na primjer, int Weight = 7;
// definiraj i inicijaliziraj na 7
Inicijalizacija kombinira definiciju u svom prvom pridruživanju. Ništa vas ne zaustavlja da joj kasnije promjenite vrijednost. Inicijalizacija osigurava da vaša varijabla nikad nema besmislenu vrijednost. Kako inicijalizirati podatkovne članove klase? Klase imaju specijalan funkcijski član zvan konstruktor. Konstruktor može uzeti potrebne parametre, ali nema nikakvu povratnu vrijednost—čak niti void. Konstruktor je metoda klase s istim imenom kao i sama klasa. Kad god deklarirate konstruktor, trebati će vam i destruktor. Kao što konstruktor kreira i inicijalizira objekte vaše klase, destruktori "čiste teren" nakon vašeg objekta i oslobađaju alociranu memoriju. Destruktor uvijek ima ime klase predvođeno tildom (~). Destruktor ne preuzima nikakve argumente i nema povratnu vrijednost. Prema tome deklaracija Cat uključuje ~Cat();
Podrazumijevani konstruktori i destruktori Ako ne deklarirate konstruktor i destruktor, kompajler će ih sam napraviti za vas. Podrazumijevani konstruktor i destruktor ne preuzimaju argumente i ne čine ništa. Kakva je korist iz konstruktora koji ne čini ništa? To je, u stvari, više stvar forme, odn. formalnosti. Svi objeekti moraju biti konstruirani i destruirani, pa se ove "niš korist" funkcije pozivaju u pravom trenutku. Kako bilo, deklariranje objekta bez prosljeđivanja parametara, kao u Cat Rags;
// Rags ne dobija nikakve parametre
uzrokuje postojanje konstruktora u obliku Cat(); Kad definirate objekt neke klase, konstruktor se poziva. Da Cat konstruktor uzima dva parametra, mogli ste definirati Cat objekt pišući Cat Frisky (5,7); Da konstruktor uzima jedan parametar, napisali biste Cat Frisky (3); U slučaju da konstruktor ne uzima parametre uopće, izostavili biste zagrade i napisali Cat Frisky ; Ovo je iznimka pravilu koji kaže da sve funkcije moraju imati zagrade, čak i one bez parametara. No zbog toga ste u stanju napisati Cat Frisky; što je u stvari poziv podrazumijevanom (default) konstruktoru. Budući da on ne prima parametre, zagrade su izostavljene. No vi ne morate koristiti podrazumijevani konstruktor kojeg kompajler kreira. Slobodni ste napisati vlastiti konstruktor sa ili bez parametara. Čak i konstruktori bez parametara mogu imati tijelo funkcije u kojoj inicijaliziraju svoje objekte ili čine neku drugu aktivnost. Kao stvar lijepog ponašanja, ako deklarirate konstruktor, uvijek deklarirajte i destruktor, čak i ako on ništa ne radi. Istina je da će i podrazumijevani destruktor pravilno raditi, ne boli deklarirati vlastiti. To čini vaš kod čitljivijim. Listing 6.4. Upotreba konstruktora i destruktora. 1: // Demonstrates declaration of a constructors and 2: // destructor for the Cat class 3: 4: #include // for cout 5: 6: class Cat // begin declaration of the class 7: { 8: public: // begin public section 9: Cat(int initialAge); // constructor 10: ~Cat(); // destructor 11: int GetAge(); // accessor function 12: void SetAge(int age); // accessor function 13: void Meow(); 14: private: // begin private section 15: int itsAge; // member variable 16: }; 17: 18: // constructor of Cat, 19: Cat::Cat(int initialAge) 20: { 21: itsAge = initialAge; 22: } 23:
24: Cat::~Cat() // destructor, takes no action 25: { 26: } 27: 28: // GetAge, Public accessor function 29: // returns value of itsAge member 30: int Cat::GetAge() 31: { 32: return itsAge; 33: } 34: 35: // Definition of SetAge, public 36: // accessor function 37: 38: void Cat::SetAge(int age) 39: { 40: // set member variable its age to 41: // value passed in by parameter age 42: itsAge = age; 43: } 44: 45: // definition of Meow method 46: // returns: void 47: // parameters: None 48: // action: Prints "meow" to screen 49: void Cat::Meow() 50: { 51: cout << "Meow.\n"; 52: } 53: 54: // create a cat, set its age, have it 55 // meow, tell us its age, then meow again. 56: int main() 57: { 58: Cat Frisky(5); 59: Frisky.Meow(); 60: cout << "Frisky is a cat who is " ; 61: cout << Frisky.GetAge() << " years old.\n"; 62: Frisky.Meow(); 63: Frisky.SetAge(7); 64: cout << "Now Frisky is " ; 65: cout << Frisky.GetAge() << " years old.\n"; 66; return 0; 67: } Output: Meow. Frisky is a cat who is 5 years old. Meow. Now Frisky is 7 years old.
Konstantni funkcijski članovi Ako deklarirate metodu klase kao const, vi obećajete da ta metoda neće promijeniti vrijednost bilo kojeg člana klase. Za deklarirane metode konstantom samo stavite ključnu riječ const nakon zagrade, a prije točke-zareza. Ova deklaracija konstantnog funkcijskog člana NekaFunkcija() ne uzima nikakve argumente i vraća void. Izgleda ovako: void SomeFunction() const; Pristupne funkcije često deklariramo konstantnima koristeći const modifikator. Cat klasa ima dve pristupne funkcije:
void SetAge(int anAge); int GetAge(); SetAge() ne može biti const budući da mijenja padatkovni član, varijablu itsAge. GetAge() može, i trebao bi, biti const budući da uopće ne mijenja klasu. GetAge() jednostavno vraća trenutnu vrijednost podatkovnog člana itsAge. Prema tome deklaracija tih dviju funkcija bi trebala glasiti: void SetAge(int anAge); int GetAge() const; Ako deklarirate funkciju konstantnom, a njezina implementacija utječe na objekt mjenjajući vrijednost jednog ili više njegovih članova, kompajler će javiti grešku. Na primjer, da ste napisali GetAge() na takav način da broji koliko smo puta upitali Cat klasu za godine, generirali bi kompajlersku pogrešku. To je stoga jer bi mijenjali Cat objekt pozivanjem određene metode. Dobra je programerska praksa za deklariranje metoda kao const kad god je to moguće. Svaki put kad to učinite, omogućujete kompajleru da hvata vaše pogreške, umjesto da dozvolite njihovo pretvaranje u bugove koji će se pojavljivati tek prilikom pokretanja vašeg programa.
Sučelje protiv implementacije Kao što ste naučili, klijenti su ddijelovi programa koji kreiraju i koriste objekte vaše klase. Možete razmišljati o sučelju prema vašoj klasi—deklaraciji klase—kao o ugovoru između tih klijenata. Ugovor kazuje koje podatke vaša klasa ima dostupne i kako će se ponašati. Na primjer, u deklaraciji Cat klase, vi stvarate ugovor da će svaka mačka imati podatkovni član itsAge koji može biti inivcijaliziran u svom konstruktoru, dodjeljen preko vlastite SetAge() pristupne funkcije, i pročitan preko GetAge() akcesora. Također obećajete da će svaka mačka znati kako da mjauče, odn. imati metodu Meow(). Ako proglasite GetAge() konstantnom funkcijom—kao što biste trebali—ugovor također obećaje da GetAge() neće promjeniti mačku za koju je pozvan. C++ kompajler javiti će vam grešku svaki put kad prekršite ugovor. Listing 6.5 demonstrira program koji se neda kompajlirati zbog kršenja pravila definiranih ugovorom.
UPOZORENJE: Listing 6.5 se ne može kompajlirati! Listing 6.5.Demonstracija kršenja sučelja (engl. interface) 1: // Demonstrates compiler errors 2: 3: 4: #include // for cout 5: 6: class Cat 7: { 8: public: 9: Cat(int initialAge); 10: ~Cat(); 11: int GetAge() const; // const accessor function 12: void SetAge (int age); 13: void Meow(); 14: private: 15: int itsAge; 16: }; 17: 18: // constructor of Cat, 19: Cat::Cat(int initialAge) 20: { 21: itsAge = initialAge; 21: cout << "Cat Constructor\n"; 22: } 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34:
Cat::~Cat() // destructor, takes no action { cout << "Cat Destructor\n"; } // GetAge, const function // but we violate const! int Cat::GetAge() const { return (itsAge++); // violates const! }
35: // definition of SetAge, public 36: // accessor function 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53:
void Cat::SetAge(int age) { // set member variable its age to // value passed in by parameter age itsAge = age; } // definition of Meow method // returns: void // parameters: None // action: Prints "meow" to screen void Cat::Meow() { cout << "Meow.\n"; }
54: // demonstrate various violations of the 55 // interface, and resulting compiler errors 56: int main() 57: { 58: Cat Frisky; // doesn't match declaration 59: Frisky.Meow(); 60: Frisky.Bark(); // No, silly, cat's can't bark. 61: Frisky.itsAge = 7; // itsAge is private 62: return 0; 63: }
Zašto koristiti kompajler za hvatanje pogrešaka? Iako bi bilo prekrasno kreirati 100% pravilan kod, malo tko je to i stvarno sposoban. Stoga su programeri razvili sistem koji olakšava otkrivanje i ispravljanje bugova rano u procesu stvaranja programa. Iako su kompajlerske pogreške često frustrirajuće, one su mnogo bolje nego njhova alternativa. C++ je prilično krut jezik, što se sintakse tiče, i dok vam neki jezici (poput Visual Basica, na primjer) omogućuju kršenje vlastitih pravila bez prijavljivanja grešaka kod kompajliranja, rezultirajuća izvršna datoteka će se srušiti baš kad je to najkritičnije.
Gdje staviti deklaracije klasa i definicije metoda Svaka funkcija koju deklarirate za vašu klasu mora imati i definiciju. Definicijom također nazivamo i funkcijsku implementaciju. Kao i sve druge funkcije, definicija metode unutar klase ima zaglavlje i tijelo funkcije. Definicija se mora nalaziti u datoteci koju kompajler može naći. Većina C++ kompajlera očekuje da datoteka završava s .c ili .cpp. Mi ćemo koristiti .cpp, ali provjerite u dokumentaciji kompajlera na kojem radite što mu više odgovara. Slobodni ste stavljati i deklaracije u takove datoteke, ali to nije dobra programerska praksa. Konvencija koju najviše programera prihvaća je stavljanje deklaracija u ono što nazivamo datotekom zaglavlja (engl. header file), obično sa istim imenom ali ekstenzijom .h, .hp, ili .hpp. Mi ćemo ih obično nazivati .hpp, ali i ovdje trebate provjeriti kompajlerove postavke. Na primjer, stavit ćete deklaraciju Cat klase u datoteku naziva cat.hpp, a definiciju metoda klase u datoteku zvanu cat.cpp. Tada ćete spojiti datoteku zaglavlja sa .cpp datotekom stavljajući slijedeći kod na vrh cat.cpp: #include Cat.hpp Ovime kažemo kompajleru da pročita Cat.hpp u datoteku, kao da ste u tom trenutku utipkali njegov sadržaj. Zašto se gnjaviti s odvojenim datotekama kad ćete ih ionako ponovno spojiti? Većinom vremena, klijenti vaše klase ne mare o specifičnostima implementacije. Čitanje datoteke zaglavlja govori im sve što trebaju znati, te mogu ignorirati implementacijske datoteke.
ZAPAMTI: Deklaracija klase govori kompajleru što klasa je, koje podatke sadrži, te koje funkcije ima. Deklaracija klase se naziva i njenim sučeljem (engl. interface) stoga što govori korisniku kako komunicirati s objektom. Sučelje se obično sprema u .hpp datoteku, što je skračenica od engl. header C++ file. Definicija funkcije kazuje kompajleru kako funkcija radi. Definiciju funkcije zovemo implementacijom metoda klase, i držimo ju u .cpp datoteci. Detalji vezani uz implementaciju se tiču isključivo autora klase. Klijenti te klase— odn. dijelovi programa koji ju koriste—ne moraju znati, niti mariti, kako je funkcija implementirana.
Inline implementacija Baš kao što možete tražiti od kompajlera da i običnu funkciju umetne inline, možete i metodu klase načiniti inline. Ključna riječ inline se umeće ispred povratnog tipa. Inline implementacija GetWeight() funkcije, na primjer, izgleda ovako: inline int Cat::GetWeight() { return itsWeight;
// return the Weight data member
} Također možete staviti definiciju funkcije u deklaraciju same klase, što automatski čini funkciju inline. Na primjer, class Cat { public: int GetWeight() { return itsWeight; } void SetWeight(int aWeight); };
// inline
Obratite pozornost na sintaksu GetWeight() definicije. Tijelo inline funkcije počinje odmah nakon deklaracije metode klase; ne postoji točka-zarez nakon zagrade. Kao i kod svake druge funkcije, definicija počinje s otvorenom vitičastom zagradom i završava zatvorenom vitičastom zagradom. Mogli ste, naravno napisati deklaraciju i ovako class Cat { public: int GetWeight() { return itsWeight; } // inline void SetWeight(int aWeight); }; Listing 6.6 i 6.7 ponovno kreiraju Cat klasu, ali ovaj put deklaraciju drže u cat.hpp, a implementaciju funkcija u cat.cpp. Listing 6.7 također mijenja pristupnu funkciju Meow() u inline.
Listing 6.6. Cat class deklaracija u cat.hpp 1: #include 2: class Cat 3: { 4: public: 5: Cat (int initialAge); 6: ~Cat(); 7: int GetAge() { return itsAge;} // inline! 8: void SetAge (int age) { itsAge = age;} // inline! 9: void Meow() { cout << "Meow.\n";} // inline! 10: private: 11: int itsAge; 12: }; Listing 6.7. Cat implementacija u cat.cpp. 1: // Demonstrates inline functions 2: // and inclusion of header files 3: 4: #include "cat.hpp" // be sure to include the header files! 5: 6: 7: Cat::Cat(int initialAge) //constructor 8: { 9: itsAge = initialAge; 10: } 11: 12: Cat::~Cat() //destructor, takes no action 13: { 14: } 15: 16: // Create a cat, set its age, have it 17: // meow, tell us its age, then meow again. 18: int main() 19: { 20: Cat Frisky(5); 21: Frisky.Meow(); 22: cout << "Frisky is a cat who is " ; 23: cout << Frisky.GetAge() << " years old.\n"; 24: Frisky.Meow(); 25: Frisky.SetAge(7); 26: cout << "Now Frisky is " ; 27: cout << Frisky.GetAge() << " years old.\n"; 28: return 0; 29: } Output: Meow. Frisky is a cat who is 5 years old. Meow. Now Frisky is 7 years old.
Klase s drugim klasama kao podatkovnim članovima Nije neuobičajeno stvarati kompleksne klase deklarirajući jednostavne klase i umetajući ih u deklaraciju komplicanijh klasa. Na primjer mđete deklarirati klasu Kotač, Motor, i Transmisija, itd., te ih kombinirati unutar klase Automobil. Ta deklaracija ima svoj odnos. Auto ima motor, kotače i transmisiju.
Proučite slijedeći primjer. Pravokutnik se sastoji od linija. Linija je definirana sa dvije točke. Točka je definirana s x i y koordinatama. Listing 6.8 pokazuje kompletnu deklaraciju Rectangle klase, kako bi se mogla pojaviti u rectangle.hpp. Budući da je pravokutnik definiran sa četiri linije koje spajaju četiri točke, a svaka točka označava položaj na x i y osi, prvo deklariramo Point klasu, koja sadrži x i y koordinate za svaku točku. Listing 6.9 pokazuje potpunu deklaraciju obje klase. Listing 6.8. Deklariranje kompleksne klase. 1: // Begin Rect.hpp 2: #include 3: class Point // holds x,y coordinates 4: { 5: // no constructor, use default 6: public: 7: void SetX(int x) { itsX = x; } 8: void SetY(int y) { itsY = y; } 9: int GetX()const { return itsX;} 10: int GetY()const { return itsY;} 11: private: 12: int itsX; 13: int itsY; 14: }; // end of Point class declaration 15: 16: 17: class Rectangle 18: { 19: public: 20: Rectangle (int top, int left, int bottom, int right); 21: ~Rectangle () {} 22: 23: 24: 25: 26: 27:
int GetTop() const { return itsTop; } int GetLeft() const { return itsLeft; } int GetBottom() const { return itsBottom; } int GetRight() const { return itsRight; }
28: 29: 30: 31: 32:
Point Point Point Point
33: 34: 35: 36: 37:
void SetUpperLeft(Point Location) {itsUpperLeft = Location;} void SetLowerLeft(Point Location) {itsLowerLeft = Location;} void SetUpperRight(Point Location) {itsUpperRight = Location;} void SetLowerRight(Point Location) {itsLowerRight = Location;}
38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50:
void SetTop(int top) { itsTop = top; } void SetLeft (int left) { itsLeft = left; } void SetBottom (int bottom) { itsBottom = bottom; } void SetRight (int right) { itsRight = right; }
GetUpperLeft() const { return itsUpperLeft; } GetLowerLeft() const { return itsLowerLeft; } GetUpperRight() const { return itsUpperRight; } GetLowerRight() const { return itsLowerRight; }
int GetArea() const; private: Point itsUpperLeft; Point itsUpperRight; Point itsLowerLeft; Point itsLowerRight; int itsTop;
51: int itsLeft; 52: int itsBottom; 53: int itsRight; 54: }; 55: // end Rect.hpp Listing 6.9. RECT.CPP. 1: 2: 3: 4: 5: 6: 7: 8: 9:
// Begin rect.cpp #include "rect.hpp" Rectangle::Rectangle(int top, int left, int bottom, int right) { itsTop = top; itsLeft = left; itsBottom = bottom; itsRight = right;
10: 11: 12:
itsUpperLeft.SetX(left); itsUpperLeft.SetY(top);
13: 14: 15:
itsUpperRight.SetX(right); itsUpperRight.SetY(top);
16: 17: 18:
itsLowerLeft.SetX(left); itsLowerLeft.SetY(bottom);
19: itsLowerRight.SetX(right); 20: itsLowerRight.SetY(bottom); 21: } 22: 23: 24: // compute area of the rectangle by finding corners, 25: // establish width and height and then multiply 26: int Rectangle::GetArea() const 27: { 28: int Width = itsRight-itsLeft; 29: int Height = itsTop - itsBottom; 30: return (Width * Height); 31: } 32: 33: int main() 34: { 35: //initialize a local Rectangle variable 36: Rectangle MyRectangle (100, 20, 50, 80 ); 37: 38: int Area = MyRectangle.GetArea(); 39: 40: cout << "Area: " << Area << "\n"; 41: cout << "Upper Left X Coordinate: "; 42: cout << MyRectangle.GetUpperLeft().GetX(); 43: return 0; 44: } Output: Area: 3000 Upper Left X Coordinate: 20 RECT.HPP je prikazan na listingu 6.8. Samo gledajući u datoteku zaglavlja, koja sadrži deklaraciju Rectangle klase, programer zna da funkcija GetArea() vraća int. Kojim čudom GetArea() to radi nije nas briga (kao
korisnika te klase). U stvari, autor Rectangle klase može promjeniti GetArea() bez utjecaja na programe koji se služe Rectangle klasom.
Strukture Vrlo blizak rod class ključne riječi je ključna riječ struct, koja se koristi za deklaraciju strukture. U C++, struktura je isto što i klasa, osim što su članovi strukture javni po defaultu. Možete deklarirati strukturu u potpunosti jednako kao i klasu, te joj možete dati ista imena i članove. U stvari, slijedite li pravila lijepoga ponašanja uvijek ekplicitno navodeći privatne i javne dijelove vaše klase, neće biti nikakve razlike.
Zašto dvije ključne riječi rade istu stvar Vjerojatno se pitate zašto dvije ključne riječi rade istu stvar. To je nešto što se može okarakterizirati povijesnom pogreškom. Kad je C++ razvijan, napravljen je kao nadgradnja na C jezik. C ima strukture, iako one ne mogu sadržavati metode, odn. funkcije. Bjarne Stroustrup, kreator jezika C++, je nadgradio strukture, ali je promjenio ime u class kako bi naglasio novu proširenu funkcionalnost.
Kviz 1. 2. 3. 4. 5. 6. 7. 8. 9.
Što je to dot operator i zašto ga koristimo? Što rezervira memoriju—deklaracija ili definicija? Je li deklaracija klase njezino sučeljeee ili njezina implementacija? Koja je razlika između privatnih i javnih podatkovnih članova? Mogu li funkcijski članovi biti privatni? Mogu li podatkovni članovi biti javni? Ako deklarirate dva Cat objekta, mogu li oni imati različite vrijednosti unutar itsAge podatkovnog člana? Da li deklaracija klase završava sa ";" ? A definicija metode ? Kako bi zaglavlje za Cat funkciju imena Meow izgledalo, pod pretpostavkom da ne uzima nikakve parametre i vraća void? 10.Koju funkciju pozivamo za inicijalizaciju klase?
Vježbe 1. Napišite kod koji deklarira klasu zvanu Employee sa slijedećim podatkovnim članovima: age, yearsOfService, and Salary. 2. Promjenite Employee klasu tako da joj podatkovni članovi budu privatni, a pružite javne pristupne metode za dobivanje i postavljanje svakog od podatkovnih članova. 3. Napišite program koji na osnovi gornje klase stvaradva zaposlena, postavlja njihove godine, godine rada i plaću, te ispisuje njihove vrijednosti. 4. Nastavljajući vježbu 3, pružite metodu za Employee koja izvještava koliko tisuća dolara zarađuje, zaokruženo na najbližu 1000. 5. Promjenite Emplyee klasu tako da možete inicijalizirati godine, godine službe i plaću kad kreirate uposlenika. 6. BUG BUSTERS: Šta ne valja sa slijedećom deklaracijom? class Square { public: int Side; } 7. BUG BUSTERS: Zašto je slijedeća deklaracija klase beskorisna? class Cat { int GetAge()const; private: int itsAge; }; 8. BUG BUSTERS: Koja će tri buga u ovome kodu kompajler pronaći? class TV { public: void SetStation(int Station); int GetStation() const; private: int itsStation; }; main() { TV myTV; myTV.itsStation = 9; TV.SetStation(10); TV myOtherTv(2); }
Lekcija 7
Još o programskom toku
Programi obavljaju većinu svog posla s grananjem i petljama (engl. branch & loop). U lekciji 4 smo naučili kako granati program koristeći if naredbu. Danas ćemo naučiti • • •
Što su petlje i kako se koriste Kako napraviti različite petlje Alternativu duboko ugnježdenim if/else naredbama
Petlje Mnogi programerski problemi se rješavaju neprekidnim djelovanjem nad istim podacima. Postoje dva načina da se ovo postigne: rekurzija (rasprava u prošloj lekciji) i iteracija. Iteracija označava stalno ponavljanje jedne te iste stvari. Osnovna metoda iteracije je petlja.
Korijeni petlji: goto naredba U primitivnim danima rane računalne znanosti programi su bili vrlo "prljavo" pisani. Petlje su se sastojale od labele, nekoliko naredbi i skoka. U C++, labele je samo ime koje prati dvotočka (:). Labelu postavljamo lijevo od legalne C++ naredbe i skok postižemo stavljajući goto kojeg prati ime labele. Listing 7.1 nam ilustrira takvu petlju. Listing 7.1. Petlja uz pomoć ključne riječi goto. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
// Listing 7.1 // Looping with goto #include int main() { int counter = 0; // initialize counter loop: counter ++; // top of the loop cout << "counter: " << counter << "\n"; if (counter < 5) // test the value goto loop; // jump to the top
14: cout << "Complete. Counter: " << counter << ".\n"; 15: return 0; 16: } Output: counter: 1 counter: 2 counter: 3 counter: 4 counter: 5 Complete. Counter: 5.
Zašto je goto prognan Goto je primio neke zbilja poražavajuče kritike, i to prilično zasluženo. goto naredba može uzrokovati skok na bilo koju lokaciju vašeg programa, unaprijed ili unazad. Neprimjerena upotreba goto naredbe je uzrokovala patetično nečitke programe, znane i pod nzivom "špageti kod". Zbog toga profesori informatike provode zadnjih 20 godina uporno govoreći učenicima "Nikad, ikad, a niti tada ne koristite goto! To je Zlo!"
Kako bi se izbjegla upotreba naredbe goto, sofisticiraniji i bolje kontrolirani načini za upotrebu petlji su pronađeni: for, while, i do...while. Upotreba tih naredbi čini programe razumljivijima, te uistinu čini goto suvišnim. Ali, uz sve štetne nuspojave, goto je vrlo moćna naredba, ponekad i strahovito korisna kad nemamo drugog rješenja pa ju je ANSI komitet odlučio zadržati u jeziku. Ali kako se kaže, "djeco, ne pokušavajte to kod kuće". goto naredba Dakle, za korištenje goto naredbe utipkati ćete goto i ime labele. To uzrokuje neuvjetovan skok na labelu. Primjer if (value > 10)
goto end;if (value < 10)
goto end;cout << "value is Â10!";end:cout << "done";
UPOZORENJE: Upotreba goto naredbe je gotovo uvijek znak lošega dizajna. Najbolji savjet je da ju potpuno izbjegavate.
while petlje while petlja uzrokuje da vaš program ponavlja slijed naredbi dok je god početni uvjet istinit. U listingu 7.1, brojač je inkrementiran dok nije postao jednak 5. Listing 7.2 pokazuje isti program prepisan da koristi prednosti while petlje. Listing 7.2. while petlja. 1: 2: 3: 4: 5: 6: 7: 8: 9:
// Listing 7.2 // Looping with while #include int main() { int counter = 0;
// initialize the condition
10: 11: 12: 13: 14: 15:
while(counter < 5) // test condition still true { counter++; // body of the loop cout << "counter: " << counter << "\n"; }
16: 17: 18: }
cout << "Complete. Counter: " << counter << ".\n"; return 0;
while naredba Sintaksa while naredbe je slijedeća: while (uvjet) naredba; uvjet je bilo koji C++ izraz, a naredba je bilo koja legalna C++ naredba ili blok naredbi. Kad god je uvjet istinit, naredba se izvrši, te se uvjet ponovno provjerava. To se ponavlja dok na uvjet ne vrati laž. U tom trenutku petlja završava i izvšenje se nastavlja na prvoj slijedećoj naredbi. Primjer // broji do 10 int x = 0; while (x < 10) cout << "X: " << x++;
Još kompliciranije while naredbe Testirani uvjet unutar while petlje može biti kompleksan kao bilo kakav C++ izraz. To može uključivati izraze proizvedene uporabom logičkih && (I), || (ILI), i ! (NE) operatora. Listing 7.3 je malo kompleksniji primjer while naredbe. Listing 7.3. Složene while petlje. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
// Listing 7.3 // Complex while statements #include int main() { unsigned short small; unsigned long large; const unsigned short MAXSMALL=65535;
12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
cout << "Enter a small number: "; cin >> small; cout << "Enter a large number: "; cin >> large;
22: 23: 24: 25: 26: 27: 28: 29: 30:
{
cout << "small: " << small << "..."; // for each iteration, test three conditions while (small < large && large > 0 && small < MAXSMALL)
if (small % 5000 == 0) // write a dot every 5k lines cout << "."; small++; large-=2; }
31: cout << "\nSmall: " << small << " Large: " << large << endl; 32: return 0; 33: } Output: Enter a small number: 2 Enter a large number: 100000
small: 2......... Small: 33335 Large: 33334
continue i break Ponekad ćete željeti skočiti na početak while petlje prije nego li se izvrše sve naredbe unutar nje. continue naredba čini upravo to, ona skače na početak petlje. A ponekad ćete željeti i izaći iz petlje prije nego se svi uvjeti zadovolje. break naredba trenutno izlazi iz while petlje, i izvršenje programa se nastavlja od zatvarajuće vitičaste zagrade. Listing 7.4 demonstrira upotrebu ovih naredbi. Ovaj put igra je postala kompliciranija. Korisnik unosi mali i veliki broj, broj skoka i ciljani broj. Mali broj će se povećavati za 1, veliki će se smanjivati za 2. Međutim, dekrementiranje će biti preskočenio svaki put kad je mali broj višekratnik skok broja i ciljanog broja. Igra završava kad mali broj postane veći od velikog. Ako veliki broj postane ciljani direktno, ipisuje se poruka i igra završava.Cilj korisnika je da pogodi ciljani broj velikog broja koji će zaustaviti igru. Listing 7.4. break i continue. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
// Listing 7.4 // Demonstrates break and continue
14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37:
cout << "Enter a small number: "; cin >> small; cout << "Enter a large number: "; cin >> large; cout << "Enter a skip number: "; cin >> skip; cout << "Enter a target number: "; cin >> target;
38: 39: 40: 41:
#include int main() { unsigned short small; unsigned long large; unsigned long skip; unsigned long target; const unsigned short MAXSMALL=65535;
cout << "\n"; // set up 3 stop conditions for the loop while (small < large && large > 0 && small < 65535) { small++; if (small % skip == 0) // skip the decrement? { cout << "skipping on " << small << endl; continue; } if (large == target) // exact match for the target? { cout << "Target reached!"; break;
42: 43: 44: 45: 46:
} large-=2; }
// end of while loop
47: cout << "\nSmall: " << small << " Large: " << large << endl; 48: return 0; 49: } Output: Enter a small number: 2 Enter a large number: 20 Enter a skip number: 4 Enter a target number: 6 skipping on 4 skipping on 8 Small: 10 Large: 8 PAŽNJA: I continue i brak trebaju bit upotrebljavani s pažnjom. Oni su najopasnije naredbe uz goto, iz istog razloga. Programe koji nenadano mijenjaju smjer je teže pratiti.
while (1) petlja Testirani uvjet u while petlji, rekli smo, mora biti pravilan C++ izraz. Dok god je uvjet istinit, while petlja će se vrtiti. Možete kreirati petlju koja nikad ne završava, tzv. "mrtvu" petlju stavljajući broj 1 kao testirani uvjet. Budući da je 1 uvijek istina ta se petlja neće prekinuti ukoliko unutar nje ne stavimo brak naredbu. Listing 7.5 demonstrira brojanje do 10 koristeći takav oblik petlje. Listing 7.5. while (1)petlja. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
// Listing 7.5 // Demonstrates a while true loop #include int main() { int counter = 0; while (1) { counter ++; if (counter > 10) break; } cout << "Counter: " << counter << "\n"; return 0; }
Output: Counter: 11
do...while petlja Može se dogoditi da se tijelo while petlje nikad ne izvede. While naredba provjerava uvjet prije izvršavanja bilo koje od naredbi, i ako se uvijet pokaže neistinitim, cijelo tijelo while petlje se preskače. Listing 7.6 ilustrira ovo. Listing 7.6. Preskakanje tijela while petlje 1: 2: 3:
// Listing 7.6 // Demonstrates skipping the body of // the while loop when the condition is false.
4: 5: #include 6: 7: int main() 8: { 9: int counter; 10: cout << "How many hellos?: "; 11: cin >> counter; 12: while (counter > 0) 13: { 14: cout << "Hello!\n"; 15: counter--; 16: } 17: cout << "Counter is OutPut: " << counter; 18: return 0; 19: } Output: How many hellos?: 2 Hello! Hello! Counter is OutPut: 0 How many hellos?: 0 Counter is OutPut: 0 Što ako biste željeli osigurati da se poruka Hello ispiše barem jedanput? While petlja to ne može postići sama. Zato si možemo pripomoći sa if naredbom prije ulaska u while petlju: if (counter < 1) // force a minimum value counter = 1; ali ovo je očito "naštimavanje", ružno i neelegantno rješenje.
do...while do...while petlja izvrši tijelo petlje prije testiranja uvjeta i osigurava nam da će se tijelo barem jedanput izvršiti. Listing 7.7 je prepisani listing 7.6, ali koji koristi do...while petlju. Listing 7.7. Demonstracija do...while petlje. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: }
// Listing 7.7 // Demonstrates do while #include int main() { int counter; cout << "How many hellos? "; cin >> counter; do { cout << "Hello\n"; counter--; } while (counter >0 ); cout << "Counter is: " << counter << endl; return 0;
Output: How many hellos? 2 Hello Hello Counter is: 0
do...while nareba Sintaksa za do...while naredbu je slijedeća: do naredba while (uvjet); Primjer 1 // count to 10 int x = 0; do cout << "X: " << x++; while (x < 10) Primjer 2 // print lowercase alphabet. char ch = `a'; do { cout << ch << ` `; ch++; } while ( ch <= `z' );
for petlja Često u while petljama ispitujemo početni uvjet, te ako je istinit inkrementiramo određenu varijablu ili ju na neki drugi način mijenjamo. Listing 7.8 nam to demonstrira. Listing 7.8. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
// Listing 7.8 // Looping with while #include int main() { int counter = 0; while(counter < 5) { counter++; cout << "Looping! "; }
16: cout << "\nCounter: " << counter << ".\n"; 17: return 0; 18: } Output: Looping! Looping! Looping! Looping! Looping! Counter: 5.
Često je takve slučajeve elegantnije riješiti for petljom, kao u listingu 7.9 Listing 7.9. demonstriranje for petlje. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
// Listing 7.9 // Looping with for #include int main() { int counter; for (counter = 0; counter < 5; counter++) cout << "Looping! ";
12: cout << "\nCounter: " << counter << ".\n"; 13: return 0; 14: } Output: Looping! Looping! Looping! Looping! Looping! Counter: 5.
for naredba Sintaksa for naredbe glasi: for (inicijalizacija; test; akcija ) naredba; Inicijalizacija nam služi za postavljanje brojača na početnu vrijednost. Test je bilo koji C++ izraz i on se provjerava prilikom svakog prolaska kroz petlju. Ako je on istinit, akcija u zaglavlju se izvršava (obično inkrementiramo brojač) i nakon toga se izvršava naredba unutar for petlje. Primjer 1 // ispisuje Hello deset puta for (int i = 0; i<10; i++) cout << "Hello! "; Primjer 2 for (int i = 0; i < 10; i++) { cout << "Hello!" << endl; cout << "the value of i is: " << i << endl; }
Napredne for petlje for naredbe su moćne i fleksibilne. Tri neovisne naredbe (inicijalizacija, test, akcija) omogućuju nam bezbroj varijacija i otvaraju neograničene mogućnosti. For petlja radi slijedećim redosljedom: 1. Izvodi inicijalizacijsku operaciju. 2. Provjerava uvjet. 3. Ako uvjet vraća vrijednost TRUE, izvršava akciju navedenu u petlji. Moguće je i višestruke inicijalizacije i inkrementacije navoditi unutar for naredbe, kao u slijedećem primjeru. Listing 7.10. Demonstracija višestrukih naredbi u for petlji. 1: //listing 7.10 2: // demonstrates multiple statements in 3: // for loops 4: 5: #include 6: 7: int main() 8: { 9: for (int i=0, j=0; i<3; i++, j++) 10: cout << "i: " << i << " j: " << j << endl; 11: return 0; 12: } Output: i: 0 j: 0 i: 1 j: 1 i: 2 j: 2 Za kreiranje for petlje koja se ponaša istovjetno lwhile petlji, izostaviti ćemo prvu i treću naredbu. Listing 7.11 ilustrira tu ideju. Listing 7.11. Null naredbe u for petlji. 1: 2: 3: 4: 5: 6: 7: 8: 9:
// Listing 7.11 // For loops with null statements #include int main() { int counter = 0;
10: 11: 12: 13: 14: 15:
for( ; counter < 5; ) { counter++; cout << "Looping! "; }
16: 17: 18: }
cout << "\nCounter: " << counter << ".\n"; return 0;
output: Looping! Looping! Looping! Looping! Looping! Counter: 5.
Još jednom, C++ nam pruža čitav niz načina da postignemo istu stvar.. Niti jedan iskusan C++ programer ne bi na ovaj način (zlo)upotrebljavao for petlju, ali nam je to ilustracija fleksibilnosti same for naredbe. U stvari, moguće je kreirati for petlju s nijednom od tri naredbe. Listing 7.12 ilustrira nam kako.
Listing 7.12. Ilustracija prazne for petlje. 1: //Listing 7.12 illustrating 2: //empty for loop statement 3: 4: #include 5: 6: int main() 7: { 8: int counter=0; // initialization 9: int max; 10: cout << "How many hellos?"; 11: cin >> max; 12: for (;;) // a for loop that doesn't end 13: { 14: if (counter < max) // test 15: { 16: cout << "Hello!\n"; 17: counter++; // increment 18: } 19: else 20: break; 21: } 22: return 0; 23: } Output: How many hellos?3 Hello! Hello! Hello! Iako je ovaj konkretan primjer pomalo apsurdan, postoje trenutci kada su for(;;) i while(1) petlje upravo ono što trebate. Vidjet ćete jednu razumnu upotrebu takve petlje kad budemo diskutirali o switch naredbi malo kasnije.
Prazno tijelo for petlje Toliko se može napraviti u zaglavlju for naredbe, da nam ponekad čak neće biti potrebno tijelo s naredbama. U tom slučaju, ne zaboravite staviti nul naredbu (;) u tijelo petlje. Točkazarez može biti u istom retku kao i zaglavlje, ali to se lako zaboravlja. Listing 7.13 ilustrira nam nul tijelo unutar for petlje.
Listing 7.13. Ilustracija nul naredbe u for petlji 1: 2: 3: 4:
//Listing 7.13 //Demonstrates null statement // as body of for loop
5: #include 6: int main() 7: { 8: for (int i = 0; i<5; cout << "i: " << i++ << endl) 9: ; 10: return 0; 11: } Output: i: 0 i: 1 i: 2 i: 3 i: 4 Primjetite da ovo nije lijepo dizajnirana for petlja: akcijska naredba je pretrpana i nerazumljiva. Bolje bi bilo to napisati kao 8: 9:
for (int i = 0; i<5; i++) cout << "i: " << i << endl;
Dok tijelo čini u potpunosti istu stvar, ovaj primjer je lakše razumljiv.
Ugnježdene petlje Petlje mogu biti ugnježdene, s jednom petljom koja stoji u tijelu druge. Unutašnja petlja će se u potpunosti izvrtiti prilikom svakog izvršavanja vanjske petlje. Listing 7.14 ilustrira umetanje oznaka u matricu uporabom ugnježdenih petlji. Listing 7.14. Ilustracija ugnježdenih petlji. 1: //Listing 7.14 2: //Illustrates nested for loops 3: 4: #include 5: 6: int main() 7: { 8: int rows, columns; 9: char theChar; 10: cout << "How many rows? "; 11: cin >> rows; 12: cout << "How many columns? "; 13: cin >> columns; 14: cout << "What character? "; 15: cin >> theChar; 16: for (int i = 0; i
xxxxxxxxxxxx xxxxxxxxxxxx xxxxxxxxxxxx xxxxxxxxxxxx
Sažetak: petlje U lekciji t, "Funkcije", naučili ste rješiti problem Fibonaccijeva niza korištenjem rekurzije. Kao kratak sažetak, prisjetimo se da takav niz počinje brojevima 1, 1, 2, 3, i slijedećim brojevima koji su sama prethodna dva broja: 1,1,2,3,5,8,13,21,34... n-ti Fibonaccijev broj je suma n-1 i n-2 Fibonaccijeva broja. Zadatak vam je da napišete isti program ali ovaj put ne koristeći rekurzivnu metodu nego metodu iteracije.
switch naredbe U lekciji 4 smo naučili koristiti if i if/else naredbe. One mogu postati prilično konfuzne kad su preduboko ugnježdene, a C++ nam nudi i alternativu. Za razliku od if, koji pruža jednu vrijednost, switch naredbe nam omogućuju grananje ovisno o više različitih vrijednosti. Opći oblik switch naredbe je: switch (izraz) { case vrijednostJedan: naredba; break; case vrijednostDva: naredbat; break; .... case vrijednostN: naredba; break; default: }
naredba;
Ako jedna od case vrijednosti daje iti rezultat kao i izraz, izvršava se navedeni blok naredbi i iskače se iz petlje. Ako se niti jedna vrijednost ne poklapa, izvršava se default blok naredbi, ako je naveden. U slučaju da default blok nije naveden cijela petlja se preskače. Listing 7.16. Demonstracija switch naredbe 1: //Listing 7.16 2: // Demonstrates switch statement 3: 4: #include 5: 6: int main() 7: { 8: unsigned short int number; 9: cout << "Enter a number between 1 and 5: "; 10: cin >> number; 11: switch (number) 12: { 13: case 0: cout << "Too small, sorry!"; 14: break; 15: case 5: cout << "Good job!\n"; // fall through 16: case 4: cout << "Nice Pick!\n"; // fall through 17: case 3: cout << "Excellent!\n"; // fall through 18: case 2: cout << "Masterful!\n"; // fall through 19: case 1: cout << "Incredible!\n"; 20: break; 21: default: cout << "Too large!\n"; 22: break; 23: } 24: cout << "\n\n"; 25: return 0; 26: } Output: Enter a number between 1 and 5: 3 Excellent! Masterful! Incredible! Enter a number between 1 and 5: 8 Too large!
Uporaba switch naredbe za menije Listing 7.17 vraća for(;;) petlju u fokus. To su takozvane vječne petlje, koje se izvršavaju dok ne nalete na break naredbu. Vječnu petlju koristimo za kreiranje menija, gdje korisnik ima mogućnost odabira određene akcije koja se izvrši a potom se vraćamo na meni i tako sve dok ne odlućijo izaći iz programa. Listing 7.17. Demonstracija vječne petlje 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43:
//Listing 7.17 //Using a forever loop to manage //user interaction #include // types & defines enum BOOL { FALSE, TRUE }; typedef unsigned short int USHORT; // prototypes USHORT menu(); void DoTaskOne(); void DoTaskMany(USHORT); int main() { BOOL exit = FALSE; for (;;) { USHORT choice = menu(); switch(choice) { case (1): DoTaskOne(); break; case (2): DoTaskMany(2); break; case (3): DoTaskMany(3); break; case (4): continue; // redundant! break; case (5): exit=TRUE; break; default: cout << "Please select again!\n"; break; } // end switch
44: 45: 46: 47: 48: 49:
if (exit) break; } // end forever return 0; } // end main()
50:
USHORT menu()
51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69:
{ USHORT choice; cout << " **** Menu ****\n\n"; cout << "(1) Choice one.\n"; cout << "(2) Choice two.\n"; cout << "(3) Choice three.\n"; cout << "(4) Redisplay menu.\n"; cout << "(5) Quit.\n\n"; cout << ": "; cin >> choice; return choice; } void DoTaskOne() { cout << "Task One!\n"; }
70: void DoTaskMany(USHORT which) 71: { 72: if (which == 2) 73: cout << "Task Two!\n"; 74: else 75: cout << "Task Three!\n"; 76: } Output: **** Menu **** (1)Choice one. (2)Choice two. (3)Choice three. (4)Redisplay menu. (5)Quit. :1 Task One! **** Menu **** (1)Choice one. (2)Choice two. (3)Choice three. (4)Redisplay menu. (5)Quit. :3 Task Three! **** Menu **** (1)Choice one. (2)Choice two. (3)Choice three. (4)Redisplay menu. (5)Quit. :5
Kviz 1. Kako inicijaliziramo više od jedne varijeble u for petlji?
2. 3. 4. 5. 6.
Zašto izbjegavamo goto naredbu? Je li moguće napisati for petlju s tijelom koje se nikad ne izvršava? Je li moguće umetniti for petlju unutar druge for petlje? Je li moguće napraviti petlju koja nikad ne završava? Dajte primjer. Što se događa kad kreirate petlju koja nikad ne završava?
Vježbe 1. Što je vrijednost od x naredbe kad petlja dođe do kraja? for (int x = 0; x < 100; x++) 2. 3. 4. 5. 6.
Napišite ugnježdenu for petlju koja ispisuje uzorak 0 na površini 10*10 znakova. Napišite for naredbu koja će brojati od 100 do 200 za 2. Napišitewhile petlju koja će brojati od 100 do 200 za 2. Napišite do...while petlju koja bi brojala od 100 to 200 za 2. BUG BUSTERS: Što ne valja s ovim kodom? int counter = 0 while (counter < 10) { cout << "counter: " << counter; }
7. BUG BUSTERS: Što ne valja s ovim kodom? for (int counter = 0; counter < 10; counter++); cout << counter << " "; 8. BUG BUSTERS: Što ne valja s ovim kodom? int counter = 100; while (counter < 10) { cout << "counter now: " << counter; counter--; } 9. BUG BUSTERS: Što ne valja s ovim kodom? cout << "Enter a number between 0 and 5: "; cin >> theNumber; switch (theNumber) { case 0: doZero(); case 1: case 2: case 3: case 4: case 5:
// fall through // fall through // fall through // fall through
doOneToFive(); break; default: doDefault(); break; }
Lekcija 8
Pokazivači
Jedno od najmoćnijih svojstava C++ jezika je sposobnost direktnog manipuliranja memorijom korištenjem pokazivača (engl. pointer). Danas ćete naučiti • Što su pokazivači. • Kako deklarirati i koristiti pokazivače. • Što je to slobodno pohranjivanje i kako manipulirati memorijom. Pokazivači su za početnika vrlo konfuzni, posebice stoga što nije odmah očito zašto su nam uopće potrebni. Iako ova lekcija detaljno objašnjava njihovu ulogu i djelovanje, u potpunosti ćete razumjeti potrebu za pokazivačima tek tjekom nadolazećih lekcija.
Što je pokazivač? Za razumijevanje pokazivača potrebno nam je osnovno poznavanje računalne memorije. Računalna memorija je podijeljena u sekvencijalno pobrojane memorijske lokacije. Svaka varijabla je pohranjena na njoj jedinstvenu lokaciju u memoriji, znanoj kao adresa varijable. Različita računala koriste različite sheme adresiranja memorije. Obično programeri ne trebaju znati tičnu adresu bilo koje varijable, zato što se kompajler brine o takvim detaljima. Ako želite svejedno znati tu informaciju, možete koristiti, address of operator (&), kako je ilustrirano na listingu 8.1. Listing 8.1. Demonstracija adrese varijabli. 1: // Listing 8.1 Demonstrates address of operator 2: // and addresses of local variables 3: 4: #include 5: 6: int main() 7: { 8: unsigned short shortVar=5; 9: unsigned long longVar=65535; 10: long sVar = -65535; 11: 12: cout << "shortVar:\t" << shortVar; 13: cout << " Address of shortVar:\t"; 14: cout << &shortVar _<< "\n"; 15: 16: cout << "longVar:\t" << longVar; 17: cout << " Address of longVar:\t" ; 18: cout << &longVar _<< "\n"; 19: 20: cout << "sVar:\t" << sVar; 21: cout << " Address of sVar:\t" ; 22: cout << &sVar _<< "\n"; 23: 24: return 0; 25: } Output: shortVar: 5 Address of shortVar: 0x8fc9:fff4 longVar: 65535 Address of longVar: 0x8fc9:fff2 sVar: -65535 Address of sVar: 0x8fc9:ffee (Vaš ispis može izgledati drugačije.) Ne postoji razlog za praćenje prave adrese svih varijabli koje koristite. Ono što je vama bitno jest to da svaka od njih ima svoju adrsu, te da rezervira određenu količinu memorije, ovisno o tipu podatka kojeg čuva.
Spremanje adrese u pokazivač Svaka varijabla ima adresu. Čak i bez poznavanja specifične adrese zadane varijable, vi možete pohraniti tu vrijednost u pokazivač. Na primjer, pretpostavimo da je howOld integer varijabla. Za deklariranje pokazivača zvanog pAge za čuvanje njezine adrese, napisali biste int *pAge = 0; Ovime deklariramo pAge kao pokazivač na int. Odnsno, pAge je deklariran da drži adresu od int. Primjetite da je pAge varijabla kao i svaka druga. Kad deklarirate integer varijablu, ona je postavljena da čuva cijeli broj. Kad deklarirate pokazivač na varijablu pAge, on je spreman za čuvanje adrese. U ovom primjeru pAge je inicijaliziran na 0. Pokazivač čija vrijednost je nula, zove se null pointer. Svi pokazivači, prilikom kreiranja, trebaju biti inicijalizirani na nešto. Ako ne znate što bi trebali dodjeliti pokazivaču, dodijelite mu 0. Neinicijalizirani pokazivač nazivamo divljim pokazivačem (engl. wild pointer). Takvi pokazivači su veoma opasni. Ako inicijalizirate pokazivač na 0, morate specificirati pridruživanje adrese od howOld u pAge. Evo primjera kako to postići: unsigned short int howOld = 50; // make a variable unsigned short int * pAge = 0; // make a pointer pAge = &howOld; // put howOld's address in pAge U ovom trenutku, pAge ima kao svoju vrijednost ima adresu od howOld. howOld, i dalje ima vrijednost 50. Ovo smo mogli i kraće napisati unsigned short int howOld = 50; // make a variable unsigned short int * pAge = &howOld; // make pointer to howOld pAge je sada pokazivač na adresu howOld varijable. Uporabom pAge, mi možemo ustvari odrediti vrijednost od howOld, što je u ovom slučaju 50. Pristupanje howOld varijabli uporabom pokazivača pAge zovemo indirekcija stoga što indirektno pristupamo varijabli howOld koristeći pAge. Kasnije ćete vidjeti kako koristiti indirekciju za pristupanje vrijednosti varijable.
Imena pokazivača Pravila za imenovanje pokazivača su ista kao i za sve ostale varijable u C++ jeziku. Mi ćemo kroz lekcije slijediti konvenciju imenovanja pokazivača s početinim slvom p, kao i pAge ili pNumber.
Operator indirekcije Operator indirekcije (*) se također zove i operator dereferenciranja (engl. dereferencing, indirection operator). Kada pokazivač dereferenciramo, zapravo dobijamo vrijednost spremljenu na adresi pokazivača, a ne samu adresu. Normalne varijable pružaju nam direktan pristup vlastitim vrijednostima. Ako kreirate novu varijablu tipa unsigned short int imena yourAge, a potom zaželite pridružiti vrijednost iz howOld toj novoj varijabli, napisat ćete unsigned short int yourAge; yourAge = howOld; Pokazivač nam omogućuje indirektan pristup varijabli čiju adresu pohranjuje. Za pridruživanje vrijednosti iz howOld novoj varijabli yourAge preko pokazivača pAge, napisati ćete unsigned short int yourAge; yourAge = *pAge; Indirekcijski operator (*) ispred varijable pAge znači "vrijednost pohranjena na". Ovo pridruživanje nam kaže, "Uzmi vrijednost pohranjenu na adresi na koju pokazuje pAge i pridruži ju u yourAge." Pažnja: Indirekcijski operator (*) se upotrebljava na dva različita načina s pokazivačima: deklaracijom i dereferenciranjem. Kada pokazivač deklariramo, zvjezdica indicira da je to pokazivać, a ne normalna varijabla. Na primjer, unsigned short * pAge = 0; // make a pointer to an unsigned short Kada pokazivač dereferenciramo, indirekcijski operator indicira da vrijednost pri memorijskoj lokaciji pohranjenoj u pokazivaču treba biti pročitana, a ne sama adresa. *pAge = 5; // assign 5 to the value at pAge Također primjetite da isti znak (*) koristimo i kao operator množenja. Kompajler zna kojeg operatora pozvati, ovisno o kontekstu.
Pokazivači, adrese i varijable Važno je razlikovati sam pokazivač, adresu koju on sadrži, i vrijednost na adresi koju sadrži pokazivač. Ovo je uzrok mnogobrojnih zabuna o pokazivačima. Pogledajte slijedeći odlomak koda: int theVariable = 5; int * pPointer = &theVariable ; theVariable je deklariran kao cjelobrojna varijabla inicijalizirana vrijednošću 5. pPointer je deklariran kao pokazivač na cjeli broj, inicijaliziran je s adresom od theVariable. pPointer je pokazivač. Adresa koju pPointer čuva je adresa od theVariable. Vrijednost na adresi koju pokazivač čuva je 5.
Manipuliranje podacima upotrebom pokazivača Kada jednom pokazivač pridružimo adresi varijable, možemo ga koristiti za pristupanje podacima te varijable. Listing 8.2 demonstrira kako je adresa lokalne varijable dodjeljena pokazivaču i kako pokazivač manipulira vrijednostima te varijable.
Listing 8.2. Manipulirajne podacima uporabom pokazivača. 1: // Listing 8.2 Using pointers 2: 3: #include 4: 5: typedef unsigned short int USHORT; 6: int main() 7: { 8: USHORT myAge; // a variable 9: USHORT * pAge = 0; // a pointer 10: myAge = 5; 11: cout << "myAge: " << myAge << "\n"; 12: 13: pAge = &myAge; // assign address of myAge to pAge 14: 15: cout << "*pAge: " << *pAge << "\n\n"; 16: 17: cout << "*pAge = 7\n"; 18: 19: *pAge = 7; // sets myAge to 7 20: 21: cout << "*pAge: " << *pAge << "\n"; 22: cout << "myAge: " << myAge << "\n\n"; 23: 24: 25: cout << "myAge = 9\n"; 26: 27: myAge = 9; 28: 29: cout << "myAge: " << myAge << "\n"; 30: cout << "*pAge: " << *pAge << "\n"; 31: 32: return 0; 33: } Output: myAge: 5 *pAge: 5 *pAge = 7 *pAge: 7 myAge: 7 myAge = 9 myAge: 9 *pAge: 9
Ispitivanje adresa Pokazivači vam omogućuju manipuliranje adresama čak i kad ne znate njihovu stvarnu vrijednost. Nakon ovog eksperimenta uzimat ćete zdravo za gotovo da dodjeljivanje adrese varijable pokazivaču uistinu sadrži adresu varijable kao svoju vrijednost. Ali samo ovaj put ćemo to i u praksi potvrditi. Listing 8.3. Otkrivanje što je spremljeno u pokazivaču. 1: 2: 3: 4: 5: 6: 7:
// Listing 8.3 What is stored in a pointer. #include typedef unsigned short int USHORT; int main() {
8: unsigned short int myAge = 5, yourAge = 10; 9: unsigned short int * pAge = &myAge; // a pointer 10: 11: cout << "myAge:\t" << myAge << "\tyourAge:\t" << yourAge << "\n"; 12: cout << "&myAge:\t" << &myAge << "\t&yourAge:\t" << &yourAge <<"\n"; 13: 14: cout << "pAge:\t" << pAge << "\n"; 15: cout << "*pAge:\t" << *pAge << "\n"; 16: 17: pAge = &yourAge; // reassign the pointer 18: 19: cout << "myAge:\t" << myAge << "\tyourAge:\t" << yourAge << "\n"; 20: cout << "&myAge:\t" << &myAge << "\t&yourAge:\t" << &yourAge <<"\n"; 21: 22: cout << "pAge:\t" << pAge << "\n"; 23: cout << "*pAge:\t" << *pAge << "\n"; 24: 25: cout << "&pAge:\t" << &pAge << "\n"; 26: return 0; 27: } Output: myAge: 5 yourAge: 10 &myAge: 0x355C &yourAge: 0x355E pAge: 0x355C *pAge: 5 myAge: 5 yourAge: 10 &myAge: 0x355C &yourAge: 0x355E pAge: 0x355E *pAge: 10 &pAge: 0x355A
Pokazivači Za deklariranje pokazivača, napišite tip varijable, napišite tip varijable ili objekta čija adresa će biti pohranjena u pokazivaču, pa zatim pointer operator (*) i ime pokazivača. Na primjer, unsigned short int * pPointer = 0; Za pridruživanje ili inicijaliziranje pokazivača, pridružite mu ime varijable sa address of operatorom (&). Na primjer, unsigned short int theVariable = 5; unsigned short int * pPointer = & theVariable; Za dereferenciranje pokazivača, pridružite ime pokazivača sa operatorom dereferenciranja. NA primjer, unsigned short int theValue = *pPointer
Zašto koristiti pokazivače? Do sada ste naučili kako dodijeliti adresu varijable nekom pokazivaču. U praksi, zapravo, to nikada nećete raditi. Nakon svega, zašto se gnjaviti s pokazivačima kad već imate varijablu sa pristupom željenoj vrijednosti? Jedini razlog za prikazanu vrstu manipulacije pokazivačima je demonstracija funkcioniranja pokazivača. Sada kad ste upoznati sa samom sintaksom, možete ih dobro iskoristiti. Pokazivači se najčešće koriste za slijedeće tri zadaće: • Upravljanje podacima u slobodnom dijelu memorije (engl. free store). • Pristupanje podatkovnim članovima i funkcijama neke klase. • Prijenos varijabli po vrijednosti u funkciju. Ostatak lekcije fokusira se na upravljanje podacima u slobdnom dijelu memorije. U narednoj lekciji naučiti ćemo prenositi varijable po referenci.
Stog i slobodna memorija. Memoriju u grubo možemo podijeliti na 5 kategorija (naravno, ovo se odnosi na dostupnu memoriju za naš program tjekom programiranja): • Prostor za globalna imena • Slobodni dio memorije • Registri • Prostor za kod • Stog Lokalne varijable se smještaju na stog, zajedno sa funkcijskim parametrima. Kod je u prostoru za kod, a globalne varijable, naravno, zauzimaju prostor za globalne varijable. Registri se koriste za internu obradu podataka, kao npr., vođenje brige o vrhu stoga i pokazivaču na instrukcije. Gotovo sva preostala memorija se dodjeljuje slobodnom dijelu memorije, kojeg često nazivamo engleskom riječi heap (gomila, hrpa). Problem sa lokalnim varijablama je u tome što one ne traju. Kada funkcija dođe do returna, sve lokalne varijable se odbacuju. Globalne varijable rješavaju taj problem s cijenom neometanog pristupa iz bilo kojeg dijela programa, što vodi u stvaranje nečitkog i teškog za održavanje koda. Stavljanje podataka u slobodni prostor rješava oba problema. Možete misliti o slobodnom prostoru kao o masivnom bloku memorije u kojem tisuće sekvencijalno adresiranih kockica čekaju vaše podatke. Međutim ove kockice ne možete imenovati za razliku od onih koje smještamo u stogu. Morate pitati adresu kockice koju rezervirate, te potom u pravom trenutku proslijediti adresu pokazivaču. Jedan način da ovo sagledate je analogija. Recimo da vam je prijatelj dao svoj telefonski broj. Vi dođete doma, ubacite telefonski broj u memoriju telefona i bacite papirić. Ako pritisnete gumb, njegov telefon će zazvoniti i vaš će se prijatelj javiti. Vi više ne znate telefonski broj, te možda ne znate ni gdje je telefon lociran, ali vam taj gumb daje pristup do vašeg prijatelja.. Vaš prijatelj su u stvari podaci u slobodnom dijelu memorije. Vi ne znate gdje su oni, ali znate doći do njih. Do njih dolazite koristeći njihovu adresu—u našem slučaju telefonski broj. Vi ne morate znati broj, dovoljno je da ga stavite u pokazivač (gumb). Pokazivač vam daje pristup vašim podacima ne gnjaveći vas detaljima.
Stog se automatski čisti kad funkcija vrati vrijednost. Sve lokalne varijable odlaze izvan dosega i uklanjaju se sa stoga. Slobodni prostor se ne čisti dok program ne završi, pa je vaša dužnost osloboditi memoriju koju ste rezervirali kad vam više ne treba. Prednost slobodnog prostora je u tome što memorija koju ste rezervirali ostaje dostupna dok ju eksplicitno ne oslobodite. Ako rezervirate memoriju u slobodnom dijelu dok ste u funkciji, memorija će biti dostupna i kada funkcija završi. Prednost pristupa memoriji na ovaj način u odnosu na globalne varijable je u tome što jedino funkcije s pristupom pokazivaču imaju pristup podacima. To nam omogućuje čvrstu kontrolu sučelja nad tim podacima, te eliminira problem funkcije koja mijenja podatke na neočekivan i nepredviđen način. Kako biste to mogli postići, morate biti sposobni kreirati pokazivač na područje slobodne memorije, te da predajete taj pokazivač među funkcijama. Slijedeći odjeljak nam opisuje kako se to radi.
new Memoriju alocirate na slobodnom prostoru koristeći ključnu riječ new. new slijedi tip objekta kojeg želite alocirati, kako bi kompajler znao koliko mjesta treba zauzeti. Tako na primjer, new unsigned short int alocira dva bytea na slobodnom prostoru, a new long alocira četiri. Povratna vrijednost iz new je memorijska adresa. Ona mora biti dodijeljena pokazivaču. Za kreiranje unsigned short u slobodnom spremištu, možete napisati unsigned short int * pPointer; pPointer = new unsigned short int; Vi naravno možete inicijalizirati pokazivač prilikom kreiranja sa unsigned short int * pPointer = new unsigned short int; U svakom slučaju, pPointer sada pokazuje na unsigned short int na slobodnom dijelu memorije. Možete ga koristiti kao i svaki drugi pokazivač na varijablu i dodjeliti vrijednost na taj dio memorije pišući *pPointer = 72; Ovo znači, "Stavi 72 kao vrijednost u pPointer," ili "Pridruži vrijednost 72 u područje na slobodnom dijelu memorije na koje pokazuje pokazivač pPointer." Ako new ne može kreirati memoriju na slobodnom spremištu (memorija je, na kraju krajeva, limitirani resurs), vratiti će null pointer. Vi morate provjeriti vaš pokazivač svaki put kad zatražite novu memoriju.
UPOZORENJE: Svaki put kad kad alocirate memoriju koristeći ključnu riječ new, morate provjeriti kako bi bili sigurni da pokazivač nije nula.
delete Kada završite sa upotrebom memorije, morate pozvati naredbu delete na taj pointer. delete vraća memoriju slobodnom spremniku. Zapamtite da je sam pokazivač—za razliku od memorije na koju pokazuje—lokalna varijabla. Kada funkcija u kojoj je deklariran obavi svoje, taj pokazivač odlazi iz dosega i biva izgubljen. Memorija alocirana sa new se svejedno ne oslobađa automatski.Ta memorija postaje nedostupna—situacija koju nazivamo "curenjem memorije" (engl. memory leak). Tako ju zovemo zato što je ta memorija nepovratno izgubljena sve dok program ne završi. To je kao da je memorija iscurila iz vašeg računala. Za vraćanje memorije slobodnom spremniku, koristite ključnu riječ delete. Na primjer, delete pPointer; Kad obrišete pokazivač, vi ustvari oslobađate memoriju čija adresa je spremljena u pokazivaču. Vi kažete, "Vrati slobodnom spremniku memoriju na koju pokazivač pokazuje." Pokazivač je i dalje pokazivač te mu može biti dodijeljena nova vrijednost. Listing 8.4 demonstrira alociranje varijable u slobodnom spremniku, korištenje te varijable, a zatim njezino brisanje.
UPOZORENJE: Kad pozovete delete za pokazivač, memorija na koju on pokazuje je oslobođena. Ponovni poziv delete na isti pokazivač uzrokuje rušenje programa! Kad obrišete pokazivač, postavite ga na nulu (null). Pozivanje delete za null pointer je garantirano sigurno. Na primjer: Animal *pDog = new Animal; delete pDog; //oslobađa memoriju pDog = 0; //postavlja pointer na nulu //... delete pDog; //bezopasno
Listing 8.4. Alociranje, upotreba i brisanje pokazivača. 1: // Listing 8.4 2: // Allocating and deleting a pointer 3: 4: #include 5: int main() 6: { 7: int localVariable = 5; 8: int * pLocal= &localVariable; 9: int * pHeap = new int; 10: if (pHeap == NULL) 11: { 12: cout << "Error! No memory for pHeap!!"; 13: return 0; 14: } 15: *pHeap = 7; 16: cout << "localVariable: " << localVariable << "\n"; 17: cout << "*pLocal: " << *pLocal << "\n"; 18: cout << "*pHeap: " << *pHeap << "\n"; 19: delete pHeap; 20: pHeap = new int; 21: if (pHeap == NULL) 22: { 23: cout << "Error! No memory for pHeap!!"; 24: return 0; 25: } 26: *pHeap = 9; 27: cout << "*pHeap: " << *pHeap << "\n"; 28: delete pHeap; 29: return 0; 30: } Output: localVariable: 5 *pLocal: 5 *pHeap: 7 *pHeap: 9
Curenje memorije Drugi način na koji biste mogli nenamjerno uzrokovati curenje memorije je repridruživajući vaš pokazivač prije nego ste obrisali memoriju na koju pokazuje. Pogledajte slijedeći fragmet koda: 1: 2: 3: 4:
unsigned short int * pPointer = new unsigned short int; *pPointer = 72; pPointer = new unsigned short int; *pPointer = 84;
Linija 1 kreira pPointer i pridružuje ga adresi u području unutar slobodnog spremnika. Linija 2 stavlja 72 u to memorijsko područje. Linija 3 repridružuje pPointer na drugo memorijsko pdruje, a linija 4 stavlja vrijednost 84 u to novo područje. Originalno područje, ono u kojem je vrijednost 72, je nedostupno budući da pokazivač
sada pokazuje na drugo memorijsko područje. Ne postoji način da ponovo pristupimo originalnoj memoriji, niti postoji način da ju oslobodimo prije nego program završi. Kod je trebao biti ovako napisan: 1: unsigned short int * pPointer = new unsigned short int; 2: *pPointer = 72; 3: delete pPointer; 4: pPointer = new unsigned short int; 5: *pPointer = 84; Sad je memorija na koju originalno pokazuje pokazivač pPointer obrisana, pa prema tome i slobodna, u liniji 3. Pažnja: Za svaki put kad u programu pozovete new, trebao bi biti postojati i jedan poziv za delete. Važno je pratiti koji pokazivač posjeduje memorijsko područje i osigurati da se memorija oslobodi kad nam više nije potrebna.
Stvaranje objekata u slobodnom spremištu Baš kao što možete kreirati pokazivač na cijeli broj, možete kreirati i pokazivač na bilo koji objekt. Ako ste deklarirali objekt tipa Cat, možete deklarirati pokazivač na tu klasu te time napraviti instancu Cat objekta u slobodnom spremniku, isto kao što ste dosad kreirali na stogu. Sintaksa je ista kao i za cijele brojeve: Cat *pCat = new Cat; Ovime pozivam podrazumijevani konstruktor—onaj bez parametara. Konstruktor se poziva svaki put kad kreiramo novi objekt (kako na stogu tako i na slobodnom spremištu).
Brisanje objekata Kad pozovete delete za pokazivač na objekt spremljen u slobodnom spremištu, poziva se destruktor objekta prije nego oslobodimo memoriju. To pruža priliku vašoj klasi da počisti za sobom, baš kao što čini uništavajući objekte na stogu. Listing 8.5 prikazuje stvaranje i brisanje objekta na slobodnom spremištu. Listing 8.5. Stvaranje i brisanje objekata na slobodnom spremištu. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11 12 13 14 15 16 17 18 19 20 21 22
// Listing 8.5 // Creating objects on the free store #include class SimpleCat { public: SimpleCat(); ~SimpleCat(); private: int itsAge; }; SimpleCat::SimpleCat() { cout << "Constructor called.\n"; itsAge = 1; } SimpleCat::~SimpleCat() {
23 cout << "Destructor called.\n"; 24 } 25 26 int main() 27 { 28 cout << "SimpleCat Frisky...\n"; 29 SimpleCat Frisky; 30 cout << "SimpleCat *pRags = new SimpleCat...\n"; 31 SimpleCat * pRags = new SimpleCat; 32 cout << "delete pRags...\n"; 33 delete pRags; 34 cout << "Exiting, watch Frisky go...\n"; 35 return 0; 36 } Output: SimpleCat Frisky... Constructor called. SimpleCat *pRags = new SimpleCat.. Constructor called. delete pRags... Destructor called. Exiting, watch Frisky go... Destructor called.
Pristupanje podatkovnim članovima Podatkovnim i funkcijskim članovima pristupali ste koristeći dot (.) operator za Cat objekte kreirane lokalno. Za pristup Cat objektu na slobodnom prostoru, prvo morate dereferencirati pokazivač i pozvati dot operator na objekt na kojeg pokazuje pokazivač. Prema tome, za pristup GetAge funkcijskom članu napisali biste (*pRags).GetAge(); Zagrade nam osguravaju kako bi bili sigurni da će pRags biti dereferenciran prije nego li pristupi GetAge() funkciji. Budući da je ovo nepraktično, C++ nam pruža skraćeni operator za indirektan pristup: tzv. points-to operator (->), koji nastaje utipkavanjem minusa (-) kojeg slijedi veće-od simbol (>). C++ tretira ovo kao jedan simbol, a listing 8.6 demonstrira pristupanje funkcijskim i podatkovnim članovima objekta kreiranog u slobodnom spremniku. Listing 8.6. 1: // Listing 8.6 2: // Accessing data members of objects on the heap 3: 4: #include 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat() {itsAge = 2; } 10: ~SimpleCat() {} 11: int GetAge() const { return itsAge; } 12: void SetAge(int age) { itsAge = age; } 13: private: 14: int itsAge; 15: }; 16: 17: int main() 18: {
19: SimpleCat * Frisky = new SimpleCat; 20: cout << "Frisky is " << Frisky->GetAge() << " years old\n"; 21: Frisky->SetAge(5); 22: cout << "Frisky is " << Frisky->GetAge() << " years old\n"; 23: delete Frisky; 24: return 0; 25: } Output: Frisky is 2 years old Frisky is 5 years old
Podatkovni članovi na slobodnom spremniku Jedan ili više podatkovnih članova klase može biti pokazivač na objekt u slobodnom spremištu. Memorija može biti alocirana u konstruktoru klase ili jednoj od metoda, te može biti obrisan sa svojim destruktorom, kako listing 8.7 prikazuje. Listing 8.7. Pokazivači kao podatkovni članovi. 1: // Listing 8.7 2: // Pointers as data members 3: 4: #include 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(); 10: ~SimpleCat(); 11: int GetAge() const { return *itsAge; } 12: void SetAge(int age) { *itsAge = age; } 13: 14: int GetWeight() const { return *itsWeight; } 15: void setWeight (int weight) { *itsWeight = weight; } 16: 17: private: 18: int * itsAge; 19: int * itsWeight; 20: }; 21: 22: SimpleCat::SimpleCat() 23: { 24: itsAge = new int(2); 25: itsWeight = new int(5); 26: } 27: 28: SimpleCat::~SimpleCat() 29: { 30: delete itsAge; 31: delete itsWeight; 32: } 33: 34: int main() 35: { 36: SimpleCat *Frisky = new SimpleCat; 37: cout << "Frisky is " << Frisky->GetAge() << " years old\n"; 38: Frisky->SetAge(5); 39: cout << "Frisky is " << Frisky->GetAge() << " years old\n"; 40: delete Frisky; 41: return 0;
42: } Output: Frisky is 2 years old Frisky is 5 years old Analiza: Klasa SimpleCat je deklarirana da ima dve varijable—obje su pokazivači na cijele brojeve—u linijama 14 i 15. Konstruktor (linije 22-26) inicijalizira pokazivače na memoriju u slobodnom spremniku i na njihove default vrijednosti. Destruktor (linije 28-32) čisti alociranu memoriju. Budući da je riječ o destruktoru, nema potrebe za postavljanjem tih pokazivača na null, jer nam ionako neće više biti dostupni. Ovo je jedno od sigurnih mjesta za kršenje pravila da obrisani pokazivač moramo postaviti na null, iako pridržavanje pravila ne bi škodilo. Pozivna funkcija (u ovom slučaju, main()) je nesvjesna da su itsAge i itsWeight ustvari pokazivači na memoriju slobodnog spremišta. main() nastavlja zvati GetAge() i SetAge(), pa su detalji oko upravljanja memorijom skriveni u implementaciji klase—onako kako to i treba biti. Kad je Frisky obrisan u liniji 40, njegov destruktor je pozvan. Destruktor briše svakog od pokazivačkih članova. Ako ovi pokazuju na objekt druge korisnički definirane klase, i njihovi se destruktori pozivaju.
this pokazivač Svaki funkcijski član klase ima skriveni parametar: this pointer. this pokazuje na individualni objekt. Prema tome, svaki poziv GetAge() ili SetAge()funkciji, uzrokuje uključivanje this pokazivača kao skrivenog parametra. Moguće je koristiti i this pointer eksplicitno, kao što to pokazuje listing 8.8. Listing 8.8. Upotreba this pokazivača. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35:
// Listing 8.8 // Using the this pointer #include class Rectangle { public: Rectangle(); ~Rectangle(); void SetLength(int length) { this->itsLength = length; } int GetLength() const { return this->itsLength; } void SetWidth(int width) { itsWidth = width; } int GetWidth() const { return itsWidth; } private: int itsLength; int itsWidth; }; Rectangle::Rectangle() { itsWidth = 5; itsLength = 10; } Rectangle::~Rectangle() {} int main() { Rectangle theRect; cout << "theRect is " << theRect.GetLength() << " feet long.\n"; cout << "theRect is " << theRect.GetWidth() << " feet wide.\n"; theRect.SetLength(20);
36: theRect.SetWidth(10); 37: cout << "theRect is " << theRect.GetLength()<< " feet long.\n"; 38: cout << "theRect is " << theRect.GetWidth()<< " feet wide.\n"; 39: return 0; 40: } Output: theRect is 10 feet long. theRect is 5 feet long. theRect is 20 feet long. theRect is 10 feet long. Analiza: SetLength() i GetLength() pristupne funkcije eksplicitno koriste this pointer kako bi pristupile podatkovnim članovima Rectangle objekta. SetWidth i GetWidth pristupnici ne koriste. Nema nikakve razlike u njihovu ponašanju, iako je njihova sintaksa razumljivija. Da je to sve što nam donosi this pointer, ne bi bilo smisla gnjaviti vas s tim. Međutim, this pointer, ima sva svojstva pokazivača, dakle, sprema memorijsku adresu objekta. Kao takav, on može biti moćno oruđe. Vidjeti ćete praktičnu vrijednost this pokazivača u lekciji 10, "Napredne funkcije", kad budemo diskutirali o preopterećenju operatora. Za sada, vaš cilj je saznanje da this pointer postoji i da shvatite što je on: pokazivač na sam objekt. Ne morate brinuti oko kreiranja ili brisanja this poikazivača. Kompajler se brine o tome.
"Viseći" pokazivači (engl. stray or dangling pointers) Gadan i teško otkriv izvor bugova su viseći pokazivači. Viseći pokazivač se stvara kad pozovete delete na pokazivač, time oslobađajući memoriju na koju pokazuje, a kasnije ga pokušate upotrijebiti da mu prethodno niste pridružili novu vrijednost. To je kao da je vaš prijatelj promjenio telefonski broj (recimo da je prešao sa CRONETA na VIP mrežu ), a vi idalje pritišćete gumb s programiranim brojem u vašem telefonu. Ukratko, pazite da ne koristite pokazivač nakon što ste pozvali naredbu delete za njega. Pokazivač još uvijek pokazuje na stari dio memorije, ali kompajler je slobodan staviti druge podatke tamo; to može uzrokovati rušenje vašeg programa. Što je još gore, vaš program se može lijepo ponašati prilikom toga poziva, ali se srušiti nekoliko minuta kasnije. To se zove vremenska bomba (engl. time bomb), i uopće nije zabavno. Kako biste bili sigurni, nakon što obrišete pokazivač, postavite ga na null (0). Time ćete ga potpuno razoružati.
const pokazivači Možete koristiti ključnu riječ const za pokazivače stavljajući je prije tipa, nakon tipa, ili na oba mjesta. Na primjer, sve ovo su legalne deklaracije: const int * pOne; int * const pTwo; const int * const pThree; pOne je pokazivač na konstantan integer. Vrijednost na koju pokazuje ne može biti promijenjena. pTwo je konstantan pokazivač na cijeli broj. Sam broj može biti promijenjen, ali pTwo ne može pokazivati na neki drugi objekt ili varijablu. pThree je konstantan pokazivač na konstantni cijeli broj. Vrijednost na koju pokazuje ne može biti promijenjena, a pThree ne može pokazivati ni na što drugo. Trik je u tome da uvijek gledamo desno od riječi const kako bi vidjeli što proglašavamo konstantom. Ako je desno od const tip varijable, vrijednost će morati biti konstantna. Ako je varijabla s desne strane ključne riječi const, pokazivač je ono što proglašavamo konstantom. const int * p1; // the int pointed to is constant int * const p2; // p2 is constant, it can't point to anything else
const pokazivači i const funkcijski članovi U lekciji 6, "Osnovne klase", naučili ste da možete riječ const primjeniti i na funkcijski član. Kada je funkcija deklarirana kao const, kompajler javlja greškom svaki pokušaj mijenjanja podataka unutar objekta iz te funkcije.
Ako deklarirate pokazivač na const objekt, jedine metode koje možete pozvati s tim pokazivačem su const metode. Listing 8.10. Upotreba pokazivača na const objektima. 1: // Listing 8.10 2: // Using pointers with const methods 3: 4: #include 5: 6: class Rectangle 7: { 8: public: 9: Rectangle(); 10: ~Rectangle(); 11: void SetLength(int length) { itsLength = length; } 12: int GetLength() const { return itsLength; } 13: 14: void SetWidth(int width) { itsWidth = width; } 15: int GetWidth() const { return itsWidth; } 16: 17: private: 18: int itsLength; 19: int itsWidth; 20: }; 21: 22: Rectangle::Rectangle(): 23: itsWidth(5), 24: itsLength(10) 25: {} 26: 27: Rectangle::~Rectangle() 28: {} 29: 30: int main() 31: { 32: Rectangle* pRect = new Rectangle; 33: const Rectangle * pConstRect = new Rectangle; 34: Rectangle * const pConstPtr = new Rectangle; 35: 36: cout << "pRect width: " << pRect->GetWidth() << " feet\n"; 37: cout << "pConstRect width: " << pConstRect->GetWidth() << " feet\n"; 38: cout << "pConstPtr width: " << pConstPtr->GetWidth() << " feet\n"; 39: 40: pRect->SetWidth(10); 41: // pConstRect->SetWidth(10); 42: pConstPtr->SetWidth(10); 43: 44: cout << "pRect width: " << pRect->GetWidth() << " feet\n"; 45: cout << "pConstRect width: " << pConstRect->GetWidth() << " feet\n"; 46: cout << "pConstPtr width: " << pConstPtr->GetWidth() << " feet\n"; 47: return 0; 48: } Output: pRect width: 5 feet pConstRect width: 5 feet pConstPtr width: 5 feet pRect width: 10 feet pConstRect width: 5 feet pConstPtr width: 10 feet
Analiza: Linije 6-20 deklariraju Rectangle. Linija 15 deklarira GetWidth() člansku metodu kao const. Linija 32 deklarira pokazivač na Rectangle. Linija 33 deklarira pConstRect, što je pokazivač na konstantu Rectangle. Linija 34 deklarira pConstPtr, što je konstantan pokazivač na Rectangle. Linije 36-38 ispisuju njihove vrijednosti. U liniji 40, pRect koristimo za postavljanje širine pravokutnika na 10. U linij 41, pConstRect bi bio upotrebljen, ali je deklariran da pokazuje na konstantu Rectangle. Prema tome, on ne može legalno pozvati nekonstantan funkcijski član; zato je umetnut u komentar. U liniji 38, pConstPtr zove SetAge(). pConstPtr je deklariran kao konstantan pokazivač na pravokutnik. Drugim riječima pokazivač je konstanta i ne može pokazivati ni na što drugo osim pravokutnika, ali pravokutnik nije konstanta. const this pokazivači Kad deklarirate objekt kao const, vi efektivno deklarirate this pointer kao pokazivač na const objekt. const this pokazivač može se koristiti samo sa const funkcijskim članovima. Konstantni objekti i konstantni pokazivači bit će i naša sutrašnja tema, kad budemo raspravljali o referencama na konstantne objekte.
Kviz 1. Koji operator koristimo za određivanje adrese od varijable? 2. Koji operator koristimo za pronalaženje vrijednosti pohranjene na adresi na koju pokazuje pokazivač? 3. Što je pokazivač? 4. Koja je razlika između adres pohranjene u pokazivaču i vrijednosti na toj adresi? 5. Koja je razlika između operatora indirekcije i adress of operatora? 6. Koja je razlika između const int * ptrOne i int * const ptrTwo?
Vježbe 1. Što ove deklaracije rade? a. int * pOne; b. int vTwo; c. int * pThree = &vTwo; 2. Ako imate unsigned short varijablu imena yourAge, kako biste deklarirali pokazivač za manipuliranje sa yourAge? 3. Pridružite vrijednost 50 varijabli e yourAge koristeći pokazivač koji ste deklarirali u vježbi 2. 4. BUG BUSTERS: Što ne valja s slijedećim kodom? #include int main() { int *pInt; *pInt = 9; cout << "The value at pInt: " << *pInt; return 0; } 6. BUG BUSTERS: Što ne valja sa slijedećim kodom? int main() { int SomeVariable = 5; cout << "SomeVariable: " << SomeVariable << "\n"; int *pVar = & SomeVariable; pVar = 9; cout << "SomeVariable: " << *pVar << "\n"; return 0;
}
Lekcija 9
Reference
Jučer ste naučili kako koristiti pokazivače za manipuliranje objektima u slobodnom spremniku i kako ih pozivati preko njihove adrese. Reference, naša današnja teme, daju vam gotovo svu moć kao i pokazivači, ali s mnogo jednostavnijom sintaksom. Danas ćemo naučiti slijedeće • Što su reference. • Kako se referenca razlikuje od pokazivača. • Kako kreirati i koristiti reference. • Koja su njihova ograničenja. • Kako prosljeđivati vrijednosti i objekte u i iz funkcija preko referenci.
Što je referenca? Referenca je zapravo alias; kad kreirate referencu, inicijalizirate ju s imenom drugog, ciljanog objekta. Od tog trenutka, referenca nam služi kao alternativno ime za ciljani objekt, te sve što radite na referenci ima svoje posljedice na ciljanom objektu. Referencu kreirate pišući tip ciljanog objekta, kojeg slijedi operator reference (&), kojeg slijedi ime reference. Referanca može imati bilo koje ime legalno za varijablu, ali u našim lekcijama ćemo sve reference označavati prefiksom "r". Tako, ako imate integer varijablu imena someInt, možete napraviti referencu na tu varijablu pišući slijedeće: int &rSomeRef = someInt; Ovo čitamo kao "rSomeRef je referenca na integer koja je inicijalizirana da se odnosi na someInt." Listing 9.1 nam pokazuje kako se reference kreiraju i koriste. PAŽNJA: Primjetite da je operator reference (&) isti simbol kao i operator adrese. To nisu isti operatori, iako su, jasno je, na određeni način povezani.
Listing 9.1. Kreiranje i upotreba referenci. 1: //Listing 9.1 2: // Demonstrating the use of References 3: 4: #include 5: 6: int main() 7: { 8: int intOne; 9: int &rSomeRef = intOne; 10: 11: intOne = 5; 12: cout << "intOne: " << intOne << endl; 13: cout << "rSomeRef: " << rSomeRef << endl; 14: 15: rSomeRef = 7; 16: cout << "intOne: " << intOne << endl; 17: cout << "rSomeRef: " << rSomeRef << endl; 18: return 0; 19: } Output: intOne: 5 rSomeRef: 5 intOne: 7
rSomeRef: 7 Analiza: U liniji 8, lokalna int varijabla, intOne, je deklarirana. U liniji 9, referenca na int, rSomeRef, je deklarirana i inicijalizirana da se odnosi na intOne. Ako deklarirate referencu, ali ju ne inicijalizirate, dobit ćete greku prilikom kompajliranja. Reference moraju biti inicijalizirane. U liniji 11, intOne varijabli je pridružena vrijednost 5. Na linijama 12 i 13, vrijednosti u intOne i rSomeRef se ispisuju, i naravno, one su iste. U liniji 15, 7 je pridružen u rSomeRef. Budući da je riječ o referenci, drugm imenu za varijablu intOne, i time je vrijednost 7 u stvari dodijeljena u intOne, kao što se vidi na ispisu u redovima 16 i 17.
Upotreba operatora adrese & na referencama Ako pitate referencu za njezinu adresu, ona vraća adresu varijable na koju je povezana. To je stoga što je, rekli smo, sama referenca samo alias, drugo ime za varijablu. Listing 9.2 nam to i demonstrira. Listing 9.2. Adresa reference. 1: //Listing 9.2 2: // Demonstrating the use of References 3: 4: #include 5: 6: int main() 7: { 8: int intOne; 9: int &rSomeRef = intOne; 10: 11: intOne = 5; 12: cout << "intOne: " << intOne << endl; 13: cout << "rSomeRef: " << rSomeRef << endl; 14: 15: cout << "&intOne: " << &intOne << endl; 16: cout << "&rSomeRef: " << &rSomeRef << endl; 17: 18: return 0; 19: } Output: intOne: 5 rSomeRef: 5 &intOne: 0x3500 &rSomeRef: 0x3500
Analiza: Još jednom je rSomeRef inicijaliziran i referenciran na intOne. Ovaj put ispisujemo adrese dvaju varijabli, i one su identične. C++ nam ne pruža način pristupa adresi same reference budući da to nema nikakvoga značenja, kao što bi imalo da koristite pokazivač ili neku drugu varijablu. Reference se inicijaliziraju prilikom kreiranja, i uvijek služe kao sinonim za njihov cilj, čak i kad primjenjujemo address of operator. Na primjer, ako imate klasu zvanu President, mođda želite deklarirati instancu te klase kako slijedi: President William_Jefferson_Clinton; Onda bi mogli deklarirati referencu na President i inicijalizirati ju na taj objekt: President &Bill_Clinton = William_Jefferson_Clinton; Postoji samo jedan President; oba identifikatora se odnose na isti objekt iste klase. Svaku akciju koju pokrenete preko Bill_Clinton imat će efekt i na William_Jefferson_Clinton također. Uočite razliku između & simbola u liniji 9 listinga 9.2, koja deklarirareferencu na int nazvanu rSomeRef, a & simbole u linijama 15 i 16, koji vraćaju adresu od integer varijable intOne i reference rSomeRef. Normalno, prilikom realne upotrebe reference nećete koristiti address of operator. Jednostavno ćete koristiti referencu kao da je riječ o ciljanoj varijabli. To se vidi u liniji 13. Čak i iskusni C++ programeri, koji znaju pravilo da reference ne mogu biti promijenjene i uvijek su alijasi na istu varijablu, mogu biti zbunjeni s pitanjem "Što se događa kad pokušavamo "repridružiti" referencu. Ono što
izgleda kao ponovno pridruživanje, zapravo je pridruživanje nove vrijednosti ciljanoj varijabli. Listing 9.3 nam to ilustrira. Listing 9.3. Pridruživanje reference. 1: //Listing 9.3 2: //Reassigning a reference 3: 4: #include 5: 6: int main() 7: { 8: int intOne; 9: int &rSomeRef = intOne; 10: 11: intOne = 5; 12: cout << "intOne:\t" << intOne << endl; 13: cout << "rSomeRef:\t" << rSomeRef << endl; 14: cout << "&intOne:\t" << &intOne << endl; 15: cout << "&rSomeRef:\t" << &rSomeRef << endl; 16: 17: int intTwo = 8; 18: rSomeRef = intTwo; // not what you think! 19: cout << "\nintOne:\t" << intOne << endl; 20: cout << "intTwo:\t" << intTwo << endl; 21: cout << "rSomeRef:\t" << rSomeRef << endl; 22: cout << "&intOne:\t" << &intOne << endl; 23: cout << "&intTwo:\t" << &intTwo << endl; 24: cout << "&rSomeRef:\t" << &rSomeRef << endl; 25: return 0; 26: } Output: intOne: 5 rSomeRef: 5 &intOne: 0x213e &rSomeRef: 0x213e intOne: 8 intTwo: 8 rSomeRef: 8 &intOne: 0x213e &intTwo: 0x2130 &rSomeRef: 0x213e Analiza: Još jednom, cjelobrojna varijabla i referenca na cijeli broj su deklarirani, u linijama 8 i 9. Integeru je pridružena vrijednost 5 u liniji 11, i vrijednosti i njihove adrese se ispisuju u linijama 12-15. U liniji 17, nova varijabla, intTwo, je kreirana i inicijalizirana s vrijednošću 8. U liniji 18, programer pokušava pridružiti rSomeRef da sada bude alias na vaijablu intTwo, ali to se ne događa. Što se zapravo događa? U stvari rSomeRef ostaje alias na intOne, pa je zapravo to pridruživanje jednako ovom: intOne = intTwo; Sada nam je jasno da vrijednosti intOne i rSomeRef prilikom ispisa (linije 19-21) imaju istu vrijednost kao i intTwo. U stvari, kad im ispisujemo adrese na linijama 22-24, primjetite da rSomeRef nastavlja pokazivati na intOne a ne na intTwo.
Što može biti referencirano? Bilo koji objekt može biti referenciran, uključujući i korisnički definirane objekte. Primjetite da kreirate referencu na objekt, a ne na klasu. Vi nećete napisati: int & rIntRef = int; // wrong Vi morate inicijalizirati rIntRef na određeni cijeli broj, kao npr.: int howBig = 200;
int & rIntRef = howBig; Isto tako, ne inicijalizirate referencu na CAT: CAT & rCatRef = CAT; // wrong Vi morate inicijalizirati rCatRef na određeni CAT objekt: CAT frisky; CAT & rCatRef = frisky; Reference na objekte se koriste baš kao i sami objekti. Podatkovnim i funkcijskim članovima pristupamo koristeći ormalni pristupni operator (.), I baš isto kao i sa ugrađenim tipovima, reference služe kao alias na objekt. Listing 9.4 ilustrira taj slučaj. Listing 9.4. Reference na objekte. 1: // Listing 9.4 2: // References to class objects 3: 4: #include 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat (int age, int weight); 10: ~SimpleCat() {} 11: int GetAge() { return itsAge; } 12: int GetWeight() { return itsWeight; } 13: private: 14: int itsAge; 15: int itsWeight; 16: }; 17: 18: SimpleCat::SimpleCat(int age, int weight) 19: { 20: itsAge = age; 21: itsWeight = weight; 22: } 23: 24: int main() 25: { 26: SimpleCat Frisky(5,8); 27: SimpleCat & rCat = Frisky; 28: 29: cout << "Frisky is: "; 30: cout << Frisky.GetAge() << " years old. \n"; 31: cout << "And Frisky weighs: "; 32: cout << rCat.GetWeight() << " pounds. \n"; 33: return 0; 34: } Output: Frisky is: 5 years old. And Frisky weighs 8 pounds. Analiza: U liniji 26, Frisky je deklariran kao objekt SimpleCat klase. U liniji 27, SimpleCat referenca, rCat, je deklarirana i inicijalizirana da se odnosi na Friskija. U linijama 30 i 32, SimpleCat pristupnim metodama pristupamo prvo preko SimpleCat objekta, a potom preko njegove reference. Primjetite da je pristup identičan. Ponovno, referenca na alias je stvaran objekt.
Reference Referencu deklariramo pišući njen tip, kojeg slijedi operator reference (&), kojeg slijedi ime reference. Reference moraju biti inicijalizirane prilikom kreiranja. Primjer 1
int hisAge; int &rAge = hisAge; Primjer 2 CAT boots; CAT &rCatRef = boots;
Nul pokazivači i nul reference Kada pokazivači nisu inicijalizirani, odn. kada su obrisani, trebala biti im pridružena nula (0). Ovo ne vrijedi za reference. U stvari, referenca ne može biti nula, a program s referencom na nul objektom se smatra nepravilnim programom. Kad je program nepravilan, nepredviđene stvari se mogu dogoditi. Većina kompajlera će podržavati nul objekte bez mnogo prigovaranja, rušeći se samo ako nakon toga pokušate upotrijebiti objekt na neki način. Iskorištavanje ove osobine svejedno ne zvuči kao naročito dobra ideja. Kad prebacite svoj program na drugi stroj ili kompajler, misteriozni bugovi se mogu razviti ako koristite nul objekte.
Prosljeđivanje argumenata funkcije po referenci U lekciji 5, "Funkcije" naučili ste da funkcije imaju dva ograničenja: argumenti se prosljeđuju po vrijednosti (engl. passing by value), i return izjava može vraćati samo jednu vrijednost. Proslijeđivanje vrijednosti u funkciju po referenci može izbjeći oba ograničenja. U C++ jeziku, proslijeđivanje po referenci (engl. passing by reference) postižemo na dva načina: upotrebom pokazivača i upotrebom referenci. Sintaksa je različita ali je konačni rezultat jednak. Umjesto kopije vrijednosti koja se kreira unutar dosega same funkcije, pravi originalni objekt se prosljeđuje funkciji. PAŽNJA: Ako ste pažljivo čitali dosadašnje lekcije, naučili ste da funkcije svoje parametre postavljaju u dio memorije kojeg zovemo stog. Kad je funkciji proslijeđena vrijednost preko reference (bilo korištenjem pokazivača, bilo referenci), samo adresa objekta se stavlja na stog, a ne cijeli objekt. U stvari, na nekim računalima se adresa čuva u registru i ništa ne ide na stog. U svakom slučaju, kompajler zna kako doći do originalnog objekta i promjene se vrše tamo a ne na njegovoj kopiji.
Proslijeđivanje objekata po referenci omogućuje funkcijama da mijenjaju objekt na kojeg se odnose. Pogledajte listing 5.5 u petoj lekciji i prisjetite se kako swap() funkcija nije utjecala na vrijednosti pozivajuće funkcije. Listing 5.5 je ponovno napisan kao listing 9.5 kako ne biste morali kopati po papirima. Listing 9.5. Demonstracija prosljeđivanja po vrijednosti. 1: //Listing 9.5 Demonstrates passing by value 2: 3: #include 4: 5: void swap(int x, int y); 6: 7: int main() 8: { 9: int x = 5, y = 10; 10: 11: cout << "Main. Before swap, x: " << x << " y: " << y << "\n"; 12: swap(x,y); 13: cout << "Main. After swap, x: " << x << " y: " << y << "\n"; 14: return 0; 15: } 16: 17: void swap (int x, int y) 18: { 19: int temp;
20: 21: cout << "Swap. Before swap, x: " << x << " y: " << y << "\n"; 22: 23: temp = x; 24: x = y; 25: y = temp; 26: 27: cout << "Swap. After swap, x: " << x << " y: " << y << "\n"; 28: 29: } Output: Main. Before swap, x: 5 y: 10 Swap. Before swap, x: 5 y: 10 Swap. After swap, x: 10 y: 5 Main. After swap, x: 5 y: 10 Problem kod ovoga primjera je u tome što se x i y prenose po vrijednosti u swap() funkciju. To znači da su lokalne kopije tih varijabli napravljene unutar same funkcije. Ono što želite je prijenos x i y po referenci. Postoje dva načina za rješavanje tog problema u C++ jeziku. Možete parametre swap() funkcije proglasiti pokazivačima na orginalne vrijednosti, ili možete proslijediti reference na orginalne vrijednosti.
Pravljenje swap() funkcije s pokazivačima Kada prenesete pokazivač, vi zapravo prenosite adresu objekta, pa time funkcija "zna" manipulirati vrijednošću na toj adresi. kako biste swap() promijenili, morate ju deklarirati da prima dva int pokazivača. Tada, dereferenciranjem pokazivača, vrijednosti od x i y će zapravo biti zamijenjene. Listing 9.6 je demonstracija te ideje. Listing 9.6. Prijenos po referenci korištenjem pokazivača. 1: //Listing 9.6 Demonstrates passing by reference 2: 3: #include 4: 5: void swap(int *x, int *y); 6: 7: int main() 8: { 9: int x = 5, y = 10; 10: 11: cout << "Main. Before swap, x: " << x << " y: " << y << "\n"; 12: swap(&x,&y); 13: cout << "Main. After swap, x: " << x << " y: " << y << "\n"; 14: return 0; 15: } 16 17: void swap (int *px, int *py) 18: { 19: int temp; 20: 21: cout << "Swap. Before swap, *px: " << *px << " *py: " << *py << "\n"; 22: 23: temp = *px; 24: *px = *py; 25: *py = temp; 26: 27: cout << "Swap. After swap, *px: " << *px << " *py: " << *py << "\n"; 28: 29: } Output: Main. Before swap, x: 5 y: 10
Swap. Before swap, *px: 5 *py: 10 Swap. After swap, *px: 10 *py: 5 Main. After swap, x: 10 y: 5 Analiza: Bingo! Uspjeli smo! U liniji 5, prototip swap() funkcije je promijenjen kako bi ukazao da su ulazni parametri pokazivači na int umjesto samih int varijabli. Kada pozovemo swap() izu linije 12, adrese od x i y su proslijeđene kao argumenti. U liniji 19, lokalna varijabla, temp, je deklarirana u swap() funkciji. Temp ne mora biti pokazivač, on će samo zadržati vrijednost od *px (odnosno, vrijednost x iz pozivne funkcije) za vrijeme trajanja funkcije. Nakon što funkcija završi, temp nam više neće biti potreban. U liniji 23, temp varijabli je pridružena vrijednost na px. U liniji 24, vrijednost na px je pridružena vrijednosti na py. U liniji 25, vrijednost spremjena u temp (odnosno originalna vrijednost na px) se stavlja na py. Krajnji rezultat je da se vrijednosti u pozivnoj funkciji, koja je samo poslala adrese varijabli, zamijene.
Implementiranje swap() sa referencama Prethodni program radi, ali je sintaksa swap() funkcije nezgrapna zbog dva razloga. Prvo, ponovljena potreba za dereferenciranjem pokazivača unutar swap() funkcije čini ju podložnom greškama i nečitkom. Drugo, potreba za proslijeđivanjem adrese varijabli iz pozivne funkcije čini unutrašnje funkcioniranje previše očiglednim za korisnika funkcije. Cilj C++ jezika je omogućavanje korisniku da ne vodi brigu kako neka funkcija radi. Stoga je elegantnije napisati funkciju tako da se koristi referencama. Listing 9.7. swap() preuređen za reference. 1: //Listing 9.7 Demonstrates passing by reference 2: // using references! 3: 4: #include 5: 6: void swap(int &x, int &y); 7: 8: int main() 9: { 10: int x = 5, y = 10; 11: 12: cout << "Main. Before swap, x: " << x << " y: " << y << "\n"; 13: swap(x,y); 14: cout << "Main. After swap, x: " << x << " y: " << y << "\n"; 15: return 0; 16: } 17: 18: void swap (int &rx, int &ry) 19: { 20: int temp; 21: 22: cout << "Swap. Before swap, rx: " << rx << " ry: " << ry << "\n"; 23: 24: temp = rx; 25: rx = ry; 26: ry = temp; 27: 28: cout << "Swap. After swap, rx: " << rx << " ry: " << ry << "\n"; 29: 30: } Output: Main. Before swap, x:5 y: 10 Swap. Before swap, rx:5 ry:10
Swap. After swap, rx:10 ry:5 Main. After swap, x:10, y:5
Razumijevanje funkcijskih zaglavlja i prototipova Listing 9.6 pokazuje swap() koji koristi pokazivače, dok listing 9.7 koristi reference. Upotreba funkcije koja koristi reference je jednostavnija, i kod se lakše čita, ali kako funkcija koja poziva zna kada se parametri proslijeđuju po vrijednosti a kada po referenci? Kao korisnik swap() funkcije, programer mora biti siguran da će swap() uistinu promjeniti parametre. Tu dolazimo do još jedne uloge funkcijskih prototipova, koji su obično u datoteci zaglavlja. Promatrajući datoteku zaglavlja programer zna da su vrijednosti funkciji swap() pridruženi po referenci, i samim time pravilno zamijenjeni. Ako je swap() kojim slučajem bio funkcijski član deklaracije neke klase, to bismo također vidjeli u zaglavlju datoteke prilikom deklaracije klase. U C++ jeziku, korisnici klasa i funkcija se oslanjaju na header datoteku kako bi vidjeli što je sve potrebno; ona služi kao sučelje (engl. interface) prema klasi ili funkciji. Stvarna implementacija je skrivena samome klijentu. To omogućuje programeru da se usredotoči na problem i koristi klasu ili funkciju bez brige o tome kako ona radi.
Vraćanje višestrukih vrijednosti Kao što smo razmatrali, funkcije mogu vraćati samo jednu vrijednost. Što ako trebamo vratiti dvije vrijednosti iz funkcije? Jedno od rješenja je proslijeđivanje dvaju objekata funkciji, po referenci. Funkcija tada može dodijeliti objektima željene vrijednosti. Budući da proslijeđivanje po referenci omogućuje funkciji mijenjanje originalnih objekata, to je kao i da smo imali dvije povratne vrijednosti. Taj pristup zaobilazi povratnu vrijednost funkcije, koja tada može biti rezervirana za izvještaje o potencijalnim greškama. I još jednom, ovo možemo postići sa referencama ili pokazivačima. Listing 9.8 demonstrira funkciju koja vraća tri vrijednosti: dve kao parametri pokazivača ijednu kao povratnu vrijednost funkcije. Listing 9.8. Vraćanje vrijednosti sa pokazivačima. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28:
//Listing 9.8 // Returning multiple values from a function #include typedef unsigned short USHORT; short Factor(USHORT, USHORT*, USHORT*); int main() { USHORT number, squared, cubed; short error; cout << "Enter a number (0 - 20): "; cin >> number; error = Factor(number, &squared, &cubed); if (!error) { cout << "number: " << number << "\n"; cout << "square: " << squared << "\n"; cout << "cubed: " << cubed << "\n"; } else cout << "Error encountered!!\n"; return 0;
29: } 30: 31: short Factor(USHORT n, USHORT *pSquared, USHORT *pCubed) 32: { 33: short Value = 0; 34: if (n > 20) 35: Value = 1; 36: else 37: { 38: *pSquared = n*n; 39: *pCubed = n*n*n; 40: Value = 0; 41: } 42: return Value; 43: } Output: Enter a number (0-20): 3 number: 3 square: 9 cubed: 27
Vraćanje vrijednosti po referenci Iako listing 9.8 radi, možemo ga pojednostaviti uporabom referenci umjesto pokazivača. Listing 9.9 pokazuje isti program ponovno napisan kako bi koristio reference i uključio bolje određivanje error varijable preko enumeriranih tipova. Listing 9.9.Listing 9.8 sa referencama. 1: //Listing 9.9 2: // Returning multiple values from a function 3: // using references 4: 5: #include 6: 7: typedef unsigned short USHORT; 8: enum ERR_CODE { SUCCESS, ERROR }; 9: 10: ERR_CODE Factor(USHORT, USHORT&, USHORT&); 11: 12: int main() 13: { 14: USHORT number, squared, cubed; 15: ERR_CODE result; 16: 17: cout << "Enter a number (0 - 20): "; 18: cin >> number; 19: 20: result = Factor(number, squared, cubed); 21: 22: if (result == SUCCESS) 23: { 24: cout << "number: " << number << "\n"; 25: cout << "square: " << squared << "\n"; 26: cout << "cubed: " << cubed << "\n"; 27: } 28: else 29: cout << "Error encountered!!\n"; 30: return 0; 31: }
32: 33: ERR_CODE Factor(USHORT n, USHORT &rSquared, USHORT &rCubed) 34: { 35: if (n > 20) 36: return ERROR; // simple error code 37: else 38: { 39: rSquared = n*n; 40: rCubed = n*n*n; 41: return SUCCESS; 42: } 43: } Output: Enter a number (0 - 20): 3 number: 3 square: 9 cubed: 27
Proslijeđivanje po referencama za efikasnost Svaki put kad proslijedimo objekt u funkciju po njegovoj vrijednosti, stvara se kopija objekta. Svaki put kad vratite objekt iz funkcije (po vrijednosti), još jedna kopija je kreirana. Do sada smo već naučili da se ti objekti u stvari kopiraju u dijelu memorije koju zovemo stog. Za male objekte, kao što su cijeli brojevi, to je malena cijena. Ali sa velikim korisnički definiranim objektima, zauzeće memorije je znatno veća. Veličina takvog objekta je jednaka sumi svih varijabli—članova objekta. Taj postupak kopiranja objekata na stog i sa stoga za velike objekte može biti prilično zahtjevno i po pitanju performansi, po pitanju zauzeća memorije. Postoji još jedan trošak. Sa klasama koje kreirate svaka od tih privremenih kopija se kopira tako da kompajler poziva jedan specijalni konstruktor, tzv. konstruktor kopiranja. Ti stalni pozivi konstruktora (sjetite se, svaki objekt je na kraju potrebno i uništiti destruktorom) su također veliko opterećenje za procesor i memoriju. Kako bi vam to predočio, listing 9.10 stvara ogoljenu verziju objekta: SimpleCat. Pravi objekti su naravno još glomazniji, ali ovo je dovoljno za demonstraciju kako često se konstruktor i destruktor pozivaju. Listing 9.10 kreira SimpleCat objekt i tada poziva dvije funkcije. Prva i prima i vraća Cat po vrijednosti. Druga prihvaća pokazivač na objekt umjesto samog objekta, i vraća pokazivač na objekt. Listing 9.10. Proslijeđivanje objekata po referenci. 1: //Listing 9.10 2: // Passing pointers to objects 3: 4: #include 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat (); // constructor 10: SimpleCat(SimpleCat&); // copy constructor 11: ~SimpleCat(); // destructor 12: }; 13: 14: SimpleCat::SimpleCat() 15: { 16: cout << "Simple Cat Constructor...\n"; 17: } 18: 19: SimpleCat::SimpleCat(SimpleCat&) 20: { 21: cout << "Simple Cat Copy Constructor...\n"; 22: } 23:
24: SimpleCat::~SimpleCat() 25: { 26: cout << "Simple Cat Destructor...\n"; 27: } 28: 29: SimpleCat FunctionOne (SimpleCat theCat); 30: SimpleCat* FunctionTwo (SimpleCat *theCat); 31: 32: int main() 33: { 34: cout << "Making a cat...\n"; 35: SimpleCat Frisky; 36: cout << "Calling FunctionOne...\n"; 37: FunctionOne(Frisky); 38: cout << "Calling FunctionTwo...\n"; 39: FunctionTwo(&Frisky); 40: return 0; 41: } 42: 43: // FunctionOne, passes by value 44: SimpleCat FunctionOne(SimpleCat theCat) 45: { 46: cout << "Function One. Returning...\n"; 47: return theCat; 48: } 49: 50: // functionTwo, passes by reference 51: SimpleCat* FunctionTwo (SimpleCat *theCat) 52: { 53: cout << "Function Two. Returning...\n"; 54: return theCat; 55: } Output: 1: Making a cat... Simple Cat Constructor... Calling FunctionOne... Simple Cat Copy Constructor... Function One. Returning... Simple Cat Copy Constructor... Simple Cat Destructor... Simple Cat Destructor... Calling FunctionTwo... Function Two. Returning... Simple Cat Destructor...
Proslijeđivanje const pokazivača Iako proslijeđivanje pokazivača na FunctionTwo() definitovno zrokuje efikasniji kod, isto tako je i opasno. FunctionTwo() nema dozvolu za izmjene proslijeđenog SimpleCat objekta, iako jj je predana adresa samog objekta. To izlaže naš objekt većoj mogućnosti nesmotrenog mijenjanja i uklanja zaštite ponuđene prilikom proslijeđivanja po vrijednosti. Proslijeđivanje po vrijednosti je poput davanja reprodukcije vrhunske slike muzeju. Ako ju vandali oštete, nikakva šteta neće nastati na orginalu. Proslijeđivanje po referenci je poput slanja vlastite adrese muzeju i pozivanja gostiju da pogledaju pravu stvar. Izlaz je u proslijeđivanju const pokazivača u SimpleCat. Čineći to onemogućavamo pozivanje nekonstantnih metoda klase SimpleCat, i time štitimo objekt od promjena. Listing 9.11 demonstrira ideju. Listing 9.11. Proslijeđivanje const pokazivača. 1: //Listing 9.11
2: // Passing pointers to objects 3: 4: #include 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(); 10: SimpleCat(SimpleCat&); 11: ~SimpleCat(); 12: 13: int GetAge() const { return itsAge; } 14: void SetAge(int age) { itsAge = age; } 15: 16: private: 17: int itsAge; 18: }; 19: 20: SimpleCat::SimpleCat() 21: { 22: cout << "Simple Cat Constructor...\n"; 23: itsAge = 1; 24: } 25: 26: SimpleCat::SimpleCat(SimpleCat&) 27: { 28: cout << "Simple Cat Copy Constructor...\n"; 29: } 30: 31: SimpleCat::~SimpleCat() 32: { 33: cout << "Simple Cat Destructor...\n"; 34: } 35: 36:const SimpleCat * const FunctionTwo (const SimpleCat * const theCat); 37: 38: int main() 39: { 40: cout << "Making a cat...\n"; 41: SimpleCat Frisky; 42: cout << "Frisky is " ; 43 cout << Frisky.GetAge(); 44: cout << " years _old\n"; 45: int age = 5; 46: Frisky.SetAge(age); 47: cout << "Frisky is " ; 48 cout << Frisky.GetAge(); 49: cout << " years _old\n"; 50: cout << "Calling FunctionTwo...\n"; 51: FunctionTwo(&Frisky); 52: cout << "Frisky is " ; 53 cout << Frisky.GetAge(); 54: cout << " years _old\n"; 55: return 0; 56: } 57: 58: // functionTwo, passes a const pointer 59: const SimpleCat * const FunctionTwo (const SimpleCat * const theCat) 60: {
61: cout << "Function Two. Returning...\n"; 62: cout << "Frisky is now " << theCat->GetAge(); 63: cout << " years old \n"; 64: // theCat->SetAge(8); const! 65: return theCat; 66: } Output: Making a cat... Simple Cat constructor... Frisky is 1 years old Frisky is 5 years old Calling FunctionTwo... FunctionTwo. Returning... Frisky is now 5 years old Frisky is 5 years old Simple Cat Destructor...
Reference kao alternativa Listing 9.11 rješava problem pravljenja više kopija i time štedi vrijeme i prostor koje gubimo prilikom poziva konstruktora kopiranja i destruktora. On koristi konstantne pokazivače na konstantne objekte i time rješava problem funkcije koja mijenja objekt. I dalje je pomalo nezgodno to što objekte proslijeđujemo preko pokazivača. Kako znate da objekt nikada neće biti nul, bilo bi lakše raditi unutar funkcije ako umjesto pokazivača proslijedimo referencu. Listing 9.12. Proslijeđivanje referenci objektu. 1: //Listing 9.12 2: // Passing references to objects 3: 4: #include 5: 6: class SimpleCat 7: { 8: public: 9: SimpleCat(); 10: SimpleCat(SimpleCat&); 11: ~SimpleCat(); 12: 13: int GetAge() const { return itsAge; } 14: void SetAge(int age) { itsAge = age; } 15: 16: private: 17: int itsAge; 18: }; 19: 20: SimpleCat::SimpleCat() 21: { 22: cout << "Simple Cat Constructor...\n"; 23: itsAge = 1; 24: } 25: 26: SimpleCat::SimpleCat(SimpleCat&) 27: { 28: cout << "Simple Cat Copy Constructor...\n"; 29: } 30: 31: SimpleCat::~SimpleCat() 32: {
33: cout << "Simple Cat Destructor...\n"; 34: } 35: 36: const SimpleCat & FunctionTwo (const SimpleCat & theCat); 37: 38: int main() 39: { 40: cout << "Making a cat...\n"; 41: SimpleCat Frisky; 42: cout << "Frisky is " << Frisky.GetAge() << " years old\n"; 43: int age = 5; 44: Frisky.SetAge(age); 45: cout << "Frisky is " << Frisky.GetAge() << " years old\n"; 46: cout << "Calling FunctionTwo...\n"; 47: FunctionTwo(Frisky); 48: cout << "Frisky is " << Frisky.GetAge() << " years old\n"; 49: return 0; 50: } 51: 52: // functionTwo, passes a ref to a const object 53: const SimpleCat & FunctionTwo (const SimpleCat & theCat) 54: { 55: cout << "Function Two. Returning...\n"; 56: cout << "Frisky is now " << theCat.GetAge(); 57: cout << " years old \n"; 58: // theCat.SetAge(8); const! 59: return theCat; 60: } Output: Making a cat... Simple Cat constructor... Frisky is 1 years old Frisky is 5 years old Calling FunctionTwo... FunctionTwo. Returning... Frisky is now 5 years old Frisky is 5 years old Simple Cat Destructor...
const reference C++ programeri obično ne razlikuju "konstantnu referencu na SimpleCat objekt" i "referencu na konstantni SimpleCat objekt." Same reference i onako ne mogu biti ponovno pridružene na drugi objekt, pa su ionako uvijek konstantne. Ako ključnu riječ const primjenimo na referencu, to je kao da smo objekt proglasili konstantom.
Kada koristiti reference a kada pokazivače C++ programeri uvijek preferiraju reference nad pokazivačima. One su jasnije i jednostavnije za upotrebu, te bolje skrivaju informacije, kako smo vidjeli u prethodnim primjerima. Bilo kako bilo, one nemogu biti repridružene. Ako u programu trebate pokazivati prvo na jedan objekt a potom na drugi, morate koristiti pokazivač. Reference ne mogu biti nule, pa ako postoji ikakva šansa da vam objekt bude nula, ne smijezte koristiti referencu. Primjer dodatne brige je operator new. Ako new ne može alocirati memoriju na slobodnom spremniku, on vraća nul pokazivač. Budući da referenca ne može biti nul, ne smijete ju inicijalizirati na toj memoriji dok ne provjerite da li je možda nula. Slijedeći primjer nam ukazuje na moguće rješenje toga problema: int *pInt = new int; if (pInt != NULL) int &rInt = *pInt;
U ovom primjeru okazivač na int, pInt, je deklariran i inicijaliziran s memorijskom adresom koju nam vrati operator new. Adresa u pInt se testira i ako nije nula, pInt je dereferenciran. Rezultat dereferenciranja int varijable je int objekt, i rInt je inicijaliziran kao referenca na taj objekt. Time rInt postaje alias na int kojeg je vratio operator new.
Miješanje referenci i pokazivača Potpuno je legalno deklarirati i pokazivača i refeence u istoj listi parametara, skupa sa objektima proslijeđenim po vrijednosti.Evo primjera: CAT * SomeFunction (Person &theOwner, House *theHouse, int age); Ova deklaracija kaže da SomeFunction uzima tri parametra. Prvi je referenca na Person objekt, drugi je pokazivač na house objekt, a treći je cijeli broj. Funkcija vraća pokazivač na CAT objekt.
Ne vraćajte referencu na objekt koji nije u dosegu! Jednom kad programeri nauče proslijeđivati po referenci, imaju tendeciju ka potpunom divljanju. Ipa neumjerenost nikada nije dobra osobina. Zapamtite da je referenca uvijek alias na neki drugi objekt. Ako proslijedite referencu u ili iz funkcije, uvijek se zapitajte, "Što je objekt na kojeg se referenciram, i hoće li postojati svaki put kad kad ga koristim?" Listing 9.13 ilustrira opasnost vraćanja reference na objekt koji više ne postoji. 1: // Listing 9.13 2: // Returning a reference to an object 3: // which no longer exists 4: 5: #include 6: 7: class SimpleCat 8: { 9: public: 10: SimpleCat (int age, int weight); 11: ~SimpleCat() {} 12: int GetAge() { return itsAge; } 13: int GetWeight() { return itsWeight; } 14: private: 15: int itsAge; 16: int itsWeight; 17: }; 18: 19: SimpleCat::SimpleCat(int age, int weight): 20: itsAge(age), itsWeight(weight) {} 21: 22: SimpleCat &TheFunction(); 23: 24: int main() 25: { 26: SimpleCat &rCat = TheFunction(); 27: int age = rCat.GetAge(); 28: cout << "rCat is " << age << " years old!\n"; 29: return 0; 30: } 31: 32: SimpleCat &TheFunction() 33: { 34: SimpleCat Frisky(5,9); 35: return Frisky;
36: } Output: Compile error: Attempting to return a reference to a local object! UPOZORENJE: Ovaj se program neće kompajlirati na Borlandovom prevodiocu. Iako će ga Microsoftov C prevesti, treba zapamtiti da je ovo primjer lošeg programiranja, sa ponekad nepredvidljivim rezultatima.
Analiza: U linijima 7-17, SimpleCat je deklariran. U liniji 26, referenca na SimpleCat je inicijalizirana s rezultatima pozivanja TheFunction(), koja je deklarirana u liniji 22 da vraća referencu na SimpleCat. Tijelo funkcije TheFunction() deklarira lokalni objekt tipa SimpleCat i inicijalizira njegove godine i težinu. Tada vraća lokalni objekt preko reference. Neki kompajleri su dovoljno pametni da uhvate ovu grešku i ne daju vam da pokrenete program. Drugi će vam omogućiti kreiranje programa s nepredvidljivim ponašanjem. Kada TheFunction() završi, lokalni objekt, Frisky, će biti uništen (bezbolno, uvjeravam vas). Referenca koju funkcija vraća će biti alias na nepostojeći objekt, a to je ono što ne smijemo raditi.
Vraćanje reference na objekt u slobodnom spremištu Možda dođete u iskušenje da riješite problem sa listinga 9.13 tjerajući TheFunction() da kreira Frisky u slobodnom spremniku. Na taj način će Frisky postojati čak i kad izađemo iz funkcije. Problem sa tim pristupom je slijedeći: Što učiniti s memorijom alociranom za Frisky objekt kada nam više nije potreban? Listing 9.14 ilustrira problem. Listing 9.14. Curenje memorije. 1: // Listing 9.14 2: // Resolving memory leaks 3: #include 4: 5: class SimpleCat 6: { 7: public: 8: SimpleCat (int age, int weight); 9: ~SimpleCat() {} 10: int GetAge() { return itsAge; } 11: int GetWeight() { return itsWeight; } 12: 13 private: 14: int itsAge; 15: int itsWeight; 16: }; 17: 18: SimpleCat::SimpleCat(int age, int weight): 19: itsAge(age), itsWeight(weight) {} 20: 21: SimpleCat & TheFunction(); 22: 23: int main() 24: { 25: SimpleCat & rCat = TheFunction(); 26: int age = rCat.GetAge(); 27: cout << "rCat is " << age << " years old!\n"; 28: cout << "&rCat: " << &rCat << endl; 29: // How do you get rid of that memory? 30: SimpleCat * pCat = &rCat; 31: delete pCat; 32: // Uh oh, rCat now refers to ?? 33: return 0;
34: } 35: 36: SimpleCat &TheFunction() 37: { 38: SimpleCat * pFrisky = new SimpleCat(5,9); 39: cout << "pFrisky: " << pFrisky << endl; 40: return *pFrisky; 41: } Output: pFrisky: 0x2bf4 rCat is 5 years old! &rCat: 0x2bf4 UPOZORENJE: Ovaj se primjer kompajlira, linka i izgleda kao da radi. U stvari, to je vremenska bomba koja samo čeka da eksplodira.
Analiza: TheFunction() je promijenjen kako ne bi vraćao referencu na lokalnu varijablu. Memorija je alocirana u slobodnom spremištu i pridružena pokazivaču u liniji 38. Adresa koju pokazivač sadrži je ispisana, a potom pokazivač dereferenciran i SimpleCat objekt je vraćen po referenci. U liniji 25, povrat iz TheFunction() je pridružen referenci SimpleCat, i taj objekt nam služi za dobivanje starosti mačke, što ispisujemo u liniji 27. Kako bismo dokazali da referenca deklarirana u main() se odnosi na objekt u slob. spremniku iz TheFunction(), operator adrese je primjenjen na rCat. Sigurno će to biti ista adresa. Za sada, sve OK. Ali kako ćemo osloboditi memoriju? Ne možemo pozvati delete na referencu. Jedno pametno rješenje je kreiranje drugog pokazivača i njegovo inicijaliziranje na adresu dobivenu iz rCat. Time brišemo memoriju, i sprečavamo curenje memorije. Ima samo jedan mali problem: Na šta se rCat odnosi nakon linije 31? Referenca uvijek mora pokazivati na stvaran objekt. Ako je ona 0, program je nepravilan. U praksi postoje tri rješenja za ovaj problem. Prvo je deklariranje SimpleCat objektom u liniji 25, i zatim povrat cat iz TheFunction po vrijednosti. Drugo je da deklariramo SimpleCat u slobodnom spremniku unutar TheFunction(), ali da TheFunction() vraća pokazivač na tu memoriju. Nakon toga pozivajuća funkcija može obrisati pokazivač kada više nije potreban. Treće, i vjerojatno najelegantnije, rješenje je deklariranje objekta u pozivajućoj funkciji i njegovo proslijeđivanje u TheFunction() po referenci.
Kviz 1. U čemu je razlika između pokazivača i reference? 2. Kada koristimo pokazivač, a ne referencu? 3. Što vraća new ako nema dovoljno memorije za pravljenje novog objekta? 4. Kakva je to konstantna referenca?
Vježbe 1. Napišite program koji deklarira int, referencu na int, i pokazivač na int. Koristeći pokazivač i referencu, promijenite vrijednost u int. 2. Napišite program koji deklarira konstantan pokazivač na konstantan integer. Inicijalizirajte pokazivač na integer varijablu, varOne. Dodijelite 6 u varOne. Preko pokazivača, dodijelite 7 u varOne. Kreirajte novu varijablu, varTwo. Pridružite pokazivač novoj varijabli. 3. BUG BUSTERS: Što ne valja u ovom programu? 1: #include 2: 3: class CAT 4: { 5: public: 6: CAT(int age) { itsAge = age; } 7: ~CAT(){} 8: int GetAge() const { return itsAge;} 9: private: 10: int itsAge;
11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
}; CAT & MakeCat(int age); int main() { int age = 7; CAT Boots = MakeCat(age); cout << "Boots is " << Boots.GetAge() << " years old\n"; } CAT & MakeCat(int age) { CAT * pCat = new CAT(age); return *pCat; }
4. Popravite gornji program.
Lekcija 10
Napredne funkcije
U petoj lekciji naučili smo osnove rada sa funkcijama. Sada, kada znate kako pokazivači i reference rade, možemo naučiti još bolje koristiti funkcije. Danas ćete naučiti • Kako preopteretiti funkcijske članove. • Kako preopteretiti operatore. • Kako napisati funkcije koje podržavaju klase s dinamički alociranim varijablama.
Preopterećeni funkcijski članovi U petoj lekciji naučili smo kako implementirati funkcijski polimorfizam ili preopterećenje funkcija, pišući dvije ili više funkcija istog imena ali sa različitim parametrim. I funkcijski članovi mogu biti preopterećeni na približno sti način. Rectangle klasa, demonstrirana na listingu 10.1, ima dvije DrawShape() funkcije. Jedna, koja ne uzima parametre, crta pravokutnik ovisno o trenutnim vrijednostima klase. Druga uzima dvije vrijednosti, width i length, te crta pravokutnik baziran na tim vrijednostima, ignorirajući trenutne vrijednosti unutar klase. Listing 10.1. Preopterećenje funkcijskih članova. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
//Listing 10.1 Overloading class member functions #include typedef unsigned short int USHORT; enum BOOL { FALSE, TRUE}; // Rectangle class declaration class Rectangle { public: // constructors Rectangle(USHORT width, USHORT height); ~Rectangle(){} // overloaded class function DrawShape void DrawShape() const; void DrawShape(USHORT aWidth, USHORT aHeight) const;
19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: }
private: USHORT itsWidth; USHORT itsHeight; }; //Constructor implementation Rectangle::Rectangle(USHORT width, USHORT height) { itsWidth = width; itsHeight = height; } // Overloaded DrawShape - takes no values // Draws based on current class member values void Rectangle::DrawShape() const { DrawShape( itsWidth, itsHeight); } // overloaded DrawShape - takes two values // draws shape based on the parameters void Rectangle::DrawShape(USHORT width, USHORT height) const { for (USHORT i = 0; i
PAŽNJA: Ovaj listing proslijeđuje width i height vrijednosti u nekoliko funkcija. Primjetite da ponekad width proslijeđujemo prvog, a ponekad je i height proslijeđen prvi.
Output: DrawShape(): ****************************** ****************************** ****************************** ****************************** ******************************
DrawShape(40,2): ************************************************************ ************************************************************ Analiza: Listing 10.1 predstavlja ogljenu verziju jednog dijela vašeg prvog seminarskog rada. Važan dio koda se nalazi u linijama 16 i 17, gdje je DrawShape() preopterećen. Implementacija za te preopterećene funkcijskee članove je u linijama 32-52. Primjetite da verzija DrawShape() koja ne uzima nikakve parametre jednostavno poziva verziju koja uzima dva parametra, proslijeđujući trenutne vrijednosti podtkovnih članova. Uvijek pokušavajte izbjeći dupliranje koda u dvijema funkcijama. Inače ćete u slučaju izmjena morati paziti da ih sprovedete u obje funkcije, što je često potencijalni izvor pogreške.
Upotreba podrazumijevanih vrijednosti Baš kao što i funkcije koje ne pripadaju klasi mogu imati jednu ili više podrazumijevanih vrijednosti, tako ih može imati i funkcijski član neke klase. To nam prikazuje listing 10.2. Listing 10.2. Upotreba podrazumijevanih vrijednosti. 1: //Listing 10.2 Default values in member functions 2: #include 3: 4: typedef unsigned short int USHORT; 5: enum BOOL { FALSE, TRUE}; 6: 7: // Rectangle class declaration 8: class Rectangle 9: { 10: public: 11: // constructors 12: Rectangle(USHORT width, USHORT height); 13: ~Rectangle(){} 14: void DrawShape(USHORT aWidth, USHORT aHeight, BOOL UseCurrentVals = Â FALSE) const; 15: 16: private: 17: USHORT itsWidth; 18: USHORT itsHeight; 19: }; 20: 21: //Constructor implementation 22: Rectangle::Rectangle(USHORT width, USHORT height): 23: itsWidth(width), // initializations 24: itsHeight(height) 25: {} // empty body 26: 27: 28: // default values used for third parameter 29: void Rectangle::DrawShape( 30: USHORT width, 31: USHORT height, 32: BOOL UseCurrentValue 33: ) const 34: { 35: int printWidth; 36: int printHeight; 37: 38: if (UseCurrentValue == TRUE) 39: { 40: printWidth = itsWidth; // use current class values
41: printHeight = itsHeight; 42: } 43: else 44: { 45: printWidth = width; // use parameter values 46: printHeight = height; 47: } 48: 49: 50: for (int i = 0; i<printHeight; i++) 51: { 52: for (int j = 0; j< printWidth; j++) 53: { 54: cout << "*"; 55: } 56: cout << "\n"; 57: } 58: } 59: 60: // Driver program to demonstrate overloaded functions 61: int main() 62: { 63: // initialize a rectangle to 10,20 64: Rectangle theRect(30,5); 65: cout << "DrawShape(0,0,TRUE)...\n"; 66: theRect.DrawShape(0,0,TRUE); 67: cout <<"DrawShape(40,2)...\n"; 68: theRect.DrawShape(40,2); 69: return 0; 70: } Output: DrawShape(0,0,TRUE)... ****************************** ****************************** ****************************** ****************************** ****************************** DrawShape(40,2)... ************************************************************ ************************************************************ Analiza: Listing 10.2 zamjenjuje preopterećenu DrawShape() funkciju s jednom funkcijom koja ima podrazumijevane parametre. Funkcija je deklariran u liniji 14, i prima tri parametra. Prva dva, aWidth i aHeight, su USHORT, a treći, UseCurrentVals, je BOOL (istina ili laž) kojemu je podrazumijevana vrijednost FALSE. PAŽNJA: Boolean vrijednosti su one koje sadrže ili TRUE ili FALSE. C++ smatra 0 kao laž (false), a sve druge vrijednosti istinom (true). Implementacija ove pomalo neobične funkcije počinje u liniji 29. Treći parametar, UseCurrentValue, je ispitan. Ako je istina, podatkovni članovi itsWidth i itsHeight se koriste za postavljanje lokalnih varijabli printWidth i printHeight. Ako je UseCurrentValue laž, bilo po podrazumijevanoj vrijednosti, bilo postavljen na laž od strane korisnika, prva dva parametra nam služe za postavljanje printWidth i printHeight. Primjetite da u slučaju UseCurrentValue je istina, vrijednosti druga dva parametra funkcija u potpunosti ignorira.
Odabir između podrazumijevanih vrijednosti i preopterećenih funkcija Iako programi sa listinga 10.1 i 10.2 postižu istu stvar, preopterećene funkcije sa listinga 10,1 su jednostavnije za razumijevanje i, nekako, prirodniji izbor. Također, ako je potrebna i treća varijacija—na primjer korisnik
poželi unesti samo širinu ili sam visinu—lakše je proširiti preopterećene funkcije. Funkcija iz 10.2 će ubrzo postati neupotrebljivo kompleksna kako budemo dodavali nove varijacije. Kako odlučiti kada koristiti preopterećenje funkcija, a kada podrazumijevane vrijednosti? Evo nekoliko pravila: Koristite preopterećenje funkcija kada • Ne postoji razumna podrazumijevana vrijednost. • Trebate različite algoritme. • Trebate podršku za različite tupove parametara.
Podrazumijevani konstruktor Kao što smo diskutirali u šestoj lekciji ("Osnovne klase"), ako ekplicitno ne deklarirate podrazumijevani konstruktor za vašu klasu, podrazumijevani konstruktor se kreira koji ne prima nikakve parametre i ne čini ništa. Vi ste naravno slobodni kreirati i vlastiti konstruktor koji ne uzima nikakve argumente, ali "postavlja" vaš objekt na željenee vrijednosti. Samoinicijalizirani konstruktor se zove i podrazumijevani, "default" konstruktor, ali po konvenciji to je svaki konstruktor koji ne prima nikakve parametre.
Preopterećenje konstruktora Ideja konstruktora je da napravi objekt. Na primjer, Rectangle konstruktor pravi pravokutnik u memoriji. Prije nego li je konstruktor pokrenut, postoji samo područje memorije. Kad konstruktor završi, memorija se pretvara u kompletan rectangle objekt, spreman za uporabu. Konstruktori, kao i svi funkcijski članovi, mogu biti preopterećeni. Sposobnost preopterećenja konstruktora je vrlo moćna i fleksibilna. Na primjer, naš rectangle objekt bi mogao imati dva konstruktora: prvi koji uzima dužinu i širinu, te pravi pravokutnik željene veličine. Drugi ne uzima nikakve vrijednosti i pravi pravokutnik podrazumijevane veličine. Listing 10.3 je implementacija te ideje. Listing 10.3. Preopterećenje konstruktora. 1: // Listing 10.3 2: // Overloading constructors 3: 4: #include 5: 6: class Rectangle 7: { 8: public: 9: Rectangle(); 10: Rectangle(int width, int length); 11: ~Rectangle() {} 12: int GetWidth() const { return itsWidth; } 13: int GetLength() const { return itsLength; } 14: private: 15: int itsWidth; 16: int itsLength; 17: }; 18: 19: Rectangle::Rectangle() 20: { 21: itsWidth = 5; 22: itsLength = 10; 23: } 24: 25: Rectangle::Rectangle (int width, int length) 26: { 27: itsWidth = width; 28: itsLength = length; 29: }
30: 31: int main() 32: { 33: Rectangle Rect1; 34: cout << "Rect1 width: " << Rect1.GetWidth() << endl; 35: cout << "Rect1 length: " << Rect1.GetLength() << endl; 36: 37: int aWidth, aLength; 38: cout << "Enter a width: "; 39: cin >> aWidth; 40: cout << "\nEnter a length: "; 41: cin >> aLength; 42: 43: Rectangle Rect2(aWidth, aLength); 44: cout << "\nRect2 width: " << Rect2.GetWidth() << endl; 45: cout << "Rect2 length: " << Rect2.GetLength() << endl; 46: return 0; 47: } Output: Rect1 width: 5 Rect1 length: 10 Enter a width: 20 Enter a length: 50 Rect2 width: 20 Rect2 length: 50 Analiza: Rectangle klasa je deklarirana u linijama 6-17. Dva konstruktora su deklarirana: "default konstruktor" u liniji 9 i konstruktor koji uzima dve cjelobrojne varijable. U liniji33, pravokutnik se kreira koristeći podrazumijevani konstruktor, te su njegove vrijednosti ispisane u linijama 34-35. U linijama 37-41, korisnik unosi visinu i širinu, te pozivamo konstruktor koji uzima dva parametra u liniji 43. Konačno, širina i visina pravokutnika se ispisuje u linijama 44-45. Baš kao prilikom preopterećenja funkcija, prevodioc odabire pravi konstruktor, ovisno o broju i tipu parametara.
Inicijaliziranje objekta Do sada ste postavljali podatkovne članove objekta u tijelu konstruktora. Konstruktori se sprovode u dvije faze. Prilikom inicijalizacije i samoga tijela. Većina varijabli može biti postavljena u bilo kojoj od te dvije faze. "Čišće" je i često efikasnije inicijalizirati podatkovne članove u inicijalizacijskoj fazi. Slijedeći primjer nam pokazuje kako se iniijaliziraju podatkovni članovi: CAT(): // constructor name and parameters itsAge(5), // initialization list itsWeight(8) {} // body of constructor Nakon zatvorene zagrade u listi parametara samog konstruktora, stavljamo dvotočku. Nakon toga stavljamo ime podatkovnog člana i par zagrada. Unutar zagrada, pišemo izraz koji se koristi za inicijaliziranje podatkovnog člana. Ako je više od jedne inicijalizacije, nakon zatvaranja zagrade stavljamo zarez, te navodimo novi član. Listing 10.4 pokazuje definiciju konstruktora sa listinga 10.3, inicijaliziranjem podatkovnih članova umjesto pridruživanja. Listing 10.4. Djelić koda koji pokazuje inicijaliziranje podatkovnih članova. 1: 2: 3: 4: 5: 6: 7:
Rectangle::Rectangle(): itsWidth(5), itsLength(10) { }; Rectangle::Rectangle (int width, int length)
8: itsWidth(width), 9: itsLength(length) 10: 11: };
Konstruktor kopiranja Uz dodatak koji omogućuje podrazumijevani konstruktor i destruktor, kompajler nam pruža i podrazumijevani konstruktor kopiranja. On se poziva svaki put kad se stvara kopija objekta. Kada proslijeđujete objekt po vrijednosti, bilo u funkciju, bilo kao povratnu vrijednost funkcije, njegova privremena kopija se stvara. Ako je riječ o korisnički definiranom objektu, konstruktor kopiranja klase se poziva, kao što ste vidjeli u prethodnoj lekciji na listingu 9.6. Svi konstruktori kopiranja uzimaju jedan parametar, referencu na objekt iste klase. Dobra ideja je proglasiti ga konstantom, budući da konstruktor neće mijenjati proslijeđeni objekt. Na primjer: CAT(const CAT & theCat); Ovdje CAT konstruktor prima konstantnu referencu na postojeći CAT objekt. Cilj konstruktora kopiranja je da napravi kopiju od CAT. Podrazumijevani konstruktor kopiranja jednostavno kopira svaki podatkovni član iz proslijeđenog objekta u podatkovne članove novostvorenog objekta. Iako ovo funkcionira za podatkovne članove, brzo se "slomi" za podatkovne članove koji su pokazivači na objekte u slobodnom spremniku. Rješenje za taj problem je kreiranje vlastitog konstruktora kopiranja i alociranja potrebne memorije. Jednom kad je memorija rezervirana, stare vrijednosti mogu biti kopirane u novu memoriju. To se zove "duboka" (engl. deep) kopija, za razliku od podrazumijevane, "plitke" (engl. shallow) kopije. Listing 10.5 ilustrira kako to postići. Listing 10.5. Konstruktori kopiranja. 1: // Listing 10.5 2: // Copy constructors 3: 4: #include 5: 6: class CAT 7: { 8: public: 9: CAT(); // default constructor 10: CAT (const CAT &); // copy constructor 11: ~CAT(); // destructor 12: int GetAge() const { return *itsAge; } 13: int GetWeight() const { return *itsWeight; } 14: void SetAge(int age) { *itsAge = age; } 15: 16: private: 17: int *itsAge; 18: int *itsWeight; 19: }; 20: 21: CAT::CAT() 22: { 23: itsAge = new int; 24: itsWeight = new int; 25: *itsAge = 5; 26: *itsWeight = 9; 27: } 28: 29: CAT::CAT(const CAT & rhs) 30: { 31: itsAge = new int; 32: itsWeight = new int; 33: *itsAge = rhs.GetAge();
34: *itsWeight = rhs.GetWeight(); 35: } 36: 37: CAT::~CAT() 38: { 39: delete itsAge; 40: itsAge = 0; 41: delete itsWeight; 42: itsWeight = 0; 43: } 44: 45: int main() 46: { 47: CAT frisky; 48: cout << "frisky's age: " << frisky.GetAge() << endl; 49: cout << "Setting frisky to 6...\n"; 50: frisky.SetAge(6); 51: cout << "Creating boots from frisky\n"; 52: CAT boots(frisky); 53: cout << "frisky's age: " << frisky.GetAge() << endl; 54: cout << "boots' age: " << boots.GetAge() << endl; 55: cout << "setting frisky to 7...\n"; 56: frisky.SetAge(7); 57: cout << "frisky's age: " << frisky.GetAge() << endl; 58: cout << "boot's age: " << boots.GetAge() << endl; 59: return 0; 60: } Output: frisky's age: 5 Setting frisky to 6... Creating boots from frisky frisky's age: 6 boots' age: 6 setting frisky to 7... frisky's age: 7 boots' age: 6 Analiza: U linijama 6-19, CAT klasa je deklarirana. Primjetite da je u liniji 9 podrazumijevani konstruktor deklariran, a u liniji 10 je deklaracija konstruktora kopiranja. U linijama 17 i 18, dva podatkovna člana su deklarirana, svaki kao pokazivač na cijeli broj. U stvarnosti se rijetko ukazuje potreba za tim da klasa pohranjuje članove kao pokazivače, ali ovo smo napravili da demonstriramo kako upravljati podatkovnim članovima u slobodnom spremniku. Pdrazumijevani konstruktor, u linijama 21-27, alocira prostor u slobodnom spremniku za dve int varijable i potom im pridružuje vrijednost. Konstruktor kopiranja počinje u liniji 29. Primjetite da je parametar rhs. Često se parametru konstruktora kopiranja odnosimo kao rhs, što je skraćenica od engl. right-hand side. Kad proučite pridruživanja u linijama 33 i 34, vidjet ćete da su objekti proslijeđeni kao parametar s desne strane. Evo kako to radi. U linijama 31 i 32, memoriju alociramo u slobodnmo spremniku. Tada, u linijama 33 i 34, vrijednost nove memorijske lokacije je pridružena vrijednostima u postojećem CAT. Parametar rhs je CAT koji je proslijeđen u konstruktor kopiranja kao konstantna referenca. Funkcijski član rhs.GetAge() vraća vrijednost pohranjenu u memoriji na koju pokazuje podatkovni član itsAge. Kao CAT objekt, rhs sadrži sve podatkovne članove od bilo kojeg drugog objekta klase CAT. Kad je konstruktor kopiranja pozvan da kreira novi CAT, postojeći CAT je proslijeđen kao parametar. Novi CAT može raditi s vlastitim podatkovnim članovima direktno; ipak, on mora pristupati podatkovnim članovima rhs-a korištenjem javnih pristupnih metoda. Slika 10.1 demonstrira što se dogodilo ovdje. Vrijednosti na koje pokazuje postojeći CAT su iskopirane u memoriju zauzetu za novi CAT Slika 10.1 Ilustracija duboke kopije.
U liniji 47, CAT zvan frisky je stvoren. frisky-jeve godine su ispisane i potom njegove godine postavljene na 6 u liniji 50. U liniji 52, novi CAT boots je kreiran korištenjem konstruktora kopiranja i pridruživanjem frisky-ja. U linijama 53 i 54, godine obje mačke su ispisane. Očito, boots ima frisky-jeve godine, 6, a ne podrazumijavanu vrijednost 5. U liniji 56, frisky-jeve godine su postavljene na 7, i potom ponovno ispisane. Ovaj put frisky-jeve godine su 7, ali boots je još uvijek na 6, time demonstrirajući da su njihovi podaci spremljeni u različitim dijelovima memorije. Kad CAT objekti izađu iz dosega, njihovi destruktori se automatski pokreću. Implementacija destruktora klase CAT je prikazana u linijama 37-43. delete je pozvan na oba pokazivača, itsAge i itsWeight, vraćajući rezerviranu memoriju u slobodni spremnik. Također, sigurnosti radi, pokazivači su postavljeni na NULL.
Preopterećenje operatora C++ ima mnoštvo ugrađenih tipova, uključujući i int, real, char, itd. Svaki od njih ima mnoštvo predefiniranih operatora, poput zbrajanja (+) i množenja (*). C++ omogućuje dodavanje tih operatora i našoj klasi također. Kako biste u potpunosti istražili preopterećenje operatora, listing 10.6 stvara novu klasu, Counter. Counter objekt će biti korišten prilikom brojanja u petlji i ostalim aplikacijama gje broj mora biti inkrementiran, dekrementiran ili na neki drugi način praćen. Listing 10.6. Counter klasa. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27:
// Listing 10.6 // The Counter class typedef unsigned short USHORT; #include class Counter { public: Counter(); ~Counter(){} USHORT GetItsVal()const { return itsVal; } void SetItsVal(USHORT x) {itsVal = x; } private: USHORT itsVal; }; Counter::Counter(): itsVal(0) {}; int main() { Counter i; cout << "The value of i is " << i.GetItsVal() << endl;
28: return 0; 29: } Output: The value of i is 0 Analiza: Kao što vidite, ovo je prilični beskorisna klasa. Ona je definirana u linijama 7-18. Njezina jedina podatkovna varijabla je USHORT. Podrazumijevani konstruktor, koji je deklariran u liniji 10, a čija je implementacija u liniji 20, inicijalizira jedan podatkovni član, itsVal, na nulu.
Pisanje funkcije inkrementiranja Preopterećenje operatora vraća nam mnogo od funkcionalnosti koja je bila iščupana iz ove klase. Na primjer, postoje dva načina za dodavanje svojstva inkrementiranja našoj klasi. Prvi je da napišemo metodu inkrementiranja, kako je prikazano na listingu 10.7. Listing 10.7. Dodavanje operatora inkrementiranja. 1: // Listing 10.7 2: // The Counter class 3: 4: typedef unsigned short USHORT; 5: #include 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){} 12: USHORT GetItsVal()const { return itsVal; } 13: void SetItsVal(USHORT x) {itsVal = x; } 14: void Increment() { ++itsVal; } 15: 16: private: 17: USHORT itsVal; 18: 19: }; 20: 21: Counter::Counter(): 22: itsVal(0) 23: {}; 24: 25: int main() 26: { 27: Counter i; 28: cout << "The value of i is " << i.GetItsVal() << endl; 29: i.Increment(); 30: cout << "The value of i is " << i.GetItsVal() << endl; 31: return 0; 32: } Output: The value of i is 0 The value of i is 1 Analiza: Listing 10.7 dodaje funkciju Increment, definiranu u liniji 14. Iako ovo radi, prilično je nezgodno za upotrebu. Ovaj program upravo "plače" za svojstvom dodavanja ++ operatora, i to se, naravno, može napraviti.
Preopterećeni prefix operator Prefiks operatori mogu biti preopterećeni deklariranjem funkcije u obliku: returnType Operator op (parameters)
Ovdje je, op operator kojeg preopterećujemo. Prema tome, ++ operator može biti preopterećen slijedećom sintaksom: void operator++ () Listing 10.8 demonstrira nam ovu alternativu. Listing 10.8. Preopterećeni operator++. 1: // Listing 10.8 2: // The Counter class 3: 4: typedef unsigned short USHORT; 5: #include 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){} 12: USHORT GetItsVal()const { return itsVal; } 13: void SetItsVal(USHORT x) {itsVal = x; } 14: void Increment() { ++itsVal; } 15: void operator++ () { ++itsVal; } 16: 17: private: 18: USHORT itsVal; 19: 20: }; 21: 22: Counter::Counter(): 23: itsVal(0) 24: {}; 25: 26: int main() 27: { 28: Counter i; 29: cout << "The value of i is " << i.GetItsVal() << endl; 30: i.Increment(); 31: cout << "The value of i is " << i.GetItsVal() << endl; 32: ++i; 33: cout << "The value of i is " << i.GetItsVal() << endl; 34: return 0; 35: } Output: The value of i is 0 The value of i is 1 The value of i is 2 Analiza: U liniji 15, operator++ je preopterećen i upotrebljen u liniji32. Ovo je mnogo bliže sintaksi koju bismo očekivali uz Counter objekt. Prilikom ovoga možete razmisliti o stavljanju dodatnih sposobnosti zbog kojih je objekt i kreiran, kao na primjer detekcija kad Counter prevrši svoju maksimalnu veličinu. Ipak, postoji značajan defekt u načinu na koji je operator inkrementiranja napisan. Ako želite staviti Counter na lijevu stranu pridruživanja, neće vam uspjeti. Na primjer: Counter a = ++i; Ovaj kod pokušava napraviti novi Counter, a, i potom u njega pridružiti vrijednost od i nakon što je i inkrementiran. Ugrađeni konstruktor kopije će obraditi pridruživanje, ali trenutni operator inkrementa ne vraća Counter objekt. On nam vraća void. Vi naravno ne možete pridružiti void objekt u Counter objekt. (Odnosno, ne možete iz Ničega dobiti Nešto!)
Povratni tipovi u preopterećenim operatorskim funkcijama Očito, vi želite vratiti Counter objekt kako biste ga pridružili drugome Counter objektu. Koji bi objekt trebao biti vraćen? Jedan bi pristubp bio kreiranje trenutnog objekta i vraćanje istog. Listing 10.9 ilustrira taj pristup. Listing 10.9. Vraćanje privremenog objekta. 1: // Listing 10.9 2: // operator++ returns a temporary object 3: 4: typedef unsigned short USHORT; 5: #include 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){} 12: USHORT GetItsVal()const { return itsVal; } 13: void SetItsVal(USHORT x) {itsVal = x; } 14: void Increment() { ++itsVal; } 15: Counter operator++ (); 16: 17: private: 18: USHORT itsVal; 19: 20: }; 21: 22: Counter::Counter(): 23: itsVal(0) 24: {}; 25: 26: Counter Counter::operator++() 27: { 28: ++itsVal; 29: Counter temp; 30: temp.SetItsVal(itsVal); 31: return temp; 32: } 33: 34: int main() 35: { 36: Counter i; 37: cout << "The value of i is " << i.GetItsVal() << endl; 38: i.Increment(); 39: cout << "The value of i is " << i.GetItsVal() << endl; 40: ++i; 41: cout << "The value of i is " << i.GetItsVal() << endl; 42: Counter a = ++i; 43: cout << "The value of a: " << a.GetItsVal(); 44: cout << " and i: " << i.GetItsVal() << endl; 45: return 0; 46: } Output: The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3
Analiza: U ovoj verziji, operator++ je deklariran u liniji 15 da vraća Counter objekt. U linioji 29, trenutna varijabla, temp, je napravljena i njezina vrijednost postavljena da odgovara trenutnom objektu. Ta privremena varijabla je vraćena i trenutno pridržena u liniji 42.
Vraćanje bezimenih privremenih Uopće nema potrebe za imenovanjem privremenog objekta kreiranog u liniji 29. Ako je Counter imao konstruktor koji uzima vrijednost, jednostavno možete vratiti rezultat toga konstruktora kao povratnu vrijednost operatora inkrementiranja. Listing 10.10 ilustrira tu ideju. Listing 10.10. Vraćanje neimenovanog privremenog objekta. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47:
// Listing 10.10 // operator++ returns a nameless temporary object typedef unsigned short USHORT; #include class Counter { public: Counter(); Counter(USHORT val); ~Counter(){} USHORT GetItsVal()const { return itsVal; } void SetItsVal(USHORT x) {itsVal = x; } void Increment() { ++itsVal; } Counter operator++ (); private: USHORT itsVal; }; Counter::Counter(): itsVal(0) {} Counter::Counter(USHORT val): itsVal(val) {} Counter Counter::operator++() { ++itsVal; return Counter (itsVal); } int main() { Counter i; cout << "The value of i is " << i.GetItsVal() << endl; i.Increment(); cout << "The value of i is " << i.GetItsVal() << endl; ++i; cout << "The value of i is " << i.GetItsVal() << endl; Counter a = ++i; cout << "The value of a: " << a.GetItsVal(); cout << " and i: " << i.GetItsVal() << endl;
48: return 0; 49: } Output: The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3 Analiza: U liniji 11, novi konstruktor je deklariran koji prima USHORT. Implementacija je u linijama 27-29. On inicijalizira itsVal s proslijeđenom mu vrijednošću. Implementacija operator++ je sad pojednostavljena. U liniji 33, itsVal je inkrementiran. Potom, u liniji 34 kreiramo privremeni Counter objekt, te inicijaliziran na vrijednost iz itsVal, i potom vraćen kao rezultat od operator++. Ovo je u svakom slučaju elegantnije, ali povlači pitanje "Zašto uopće kreirati privremeni objekt?" Zapamtite da svaki privremeni objekt mora biti konstruiran i kasnije i uništen—dvije potiencijalno rastrošne operacije. Također, objekt i već postoji i ima željenu vrijednost, pa zašto ne vratiti njega? Riješiti ćemo taj problem uporabom this pokazivača.
Upotreba this pokazivača this pokazivač, o kojem smo diskutirali u prethodnoj lekciji, je proslijeđen u funkcijski član operator++ kao u sve funkcijske članove. this pokazivač pokazuje na i, i ako je dereferenciran, vratit će objekt i, koji već ima pravu vrijednost u svojom podatkovnom članu itsVal. Listing 10.11 ilustrira vraćanje dereferenciranog this pokazivača i izbjegavanja stvaranja nepotrebnog privremenog objekta. Listing 10.11. Vraćanje this pokazivača. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33:
// Listing 10.11 // Returning the dereferenced this pointer typedef unsigned short USHORT; #include class Counter { public: Counter(); ~Counter(){} USHORT GetItsVal()const { return itsVal; } void SetItsVal(USHORT x) {itsVal = x; } void Increment() { ++itsVal; } const Counter& operator++ (); private: USHORT itsVal; }; Counter::Counter(): itsVal(0) {}; const Counter& Counter::operator++() { ++itsVal; return *this; } int main() {
34: Counter i; 35: cout << "The value of i is " << i.GetItsVal() << endl; 36: i.Increment(); 37: cout << "The value of i is " << i.GetItsVal() << endl; 38: ++i; 39: cout << "The value of i is " << i.GetItsVal() << endl; 40: Counter a = ++i; 41: cout << "The value of a: " << a.GetItsVal(); 42: cout << " and i: " << i.GetItsVal() << endl; 48: return 0; 49: } Output: The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3 Analiza: Implementacija operator++, u linijama 26-30, je promijenjena kako bi dereferencirala this pokazivač i vratila trenutni objekt. To omogućuje pridruživanje Counter objekta u a. Kao što smo prethodno diskutirali, da je Counter objekt alocirao memoriju, bilo bi neophodno zaobići konstruktor kopiranja. U ovom slučaju podrazumjevani konstruktor kopije radi u redu. Primjetite da je povratna vrijednost referenca na Counter, te je time izbjegnuto kreiranje dodatnog privremenog objekta. To je const referenca stoga što vrijednost ne bi smjela biti promijenjena u funkciji koja koristi ovaj Counter.
Preopterećenje postfiks operatora Do sada ste propteretili prefiks operator. Što ako želita preopteretiti postfiks inkrement operator? Ovdje kompajler nailazi na problem: Kako razlikovati prefiks i postfiks? Po konvenciji, cjelobrojna varijabla je pružena kao parametar u deklaraciji operatora. Vrijednost parametra se ignrira; to je samo signal da je riječ o postfiks operatoru. Razlika između prefiksa i postfiksa Prije nego li napišemo postfiks operator, moramo razumjeti na koji se on način razlikuje od prefiks operatora. O tome smo detaljno raspravljali u Lekciji 4 (pogledajte listing 4.3). Za podsjećanje, prefiks kaže "inkrementiraj i pridruži" dok postfiks kaže "pridruži i inkrementiraj". Tako, dok prefiks operator može jednostavno inkrementirati vrijednost i potom vratiti sami objekt, postfiks mora vratiti vrijednost koja je bila prije inkrementiranja. Kako biste to napravili, morate kreirati privremeni objekt koji će zadržati originalnu vrijednost, potom inkrementirati vrijednost orginalnog objekta, a potom vratiti privremeni. Prođimo kroz to još jedanput. Proučite slijedeću liniju koda: a = x++; Ako je x bio 5, nakon ove naredbe varijabla a ima vrijednost 5, ali x je 6. Dakle, mi smo vratili vrijednost od x i pridružili ju u a, i potom povećali vrijednost x. Da je x objekt, njegov postfiks inkremnt operator mora proslijediti originalnu vrijednost (5) u privremeni objekt, povećati vrijednost x na 6, i potom vratiti privremeni objekt pridruživši njegovu vrijednost u a. Primjetite da budući je riječ o vraćanju privremenog objekta, moramo ga vratiti po vrijednosti a ne po referenci, jer će privremeni objekt izaći iz dosega čim izađemo iz funkcije. Listing 10.12 demonstrira korištenje obadva i prefiks i postfiks operatora. Listing 10.12. Prefiks i postfiks operatori. 1: 2: 3: 4: 5: 6: 7:
// Listing 10.12 // Returning the dereferenced this pointer typedef unsigned short USHORT; #include class Counter
8: { 9: public: 10: Counter(); 11: ~Counter(){} 12: USHORT GetItsVal()const { return itsVal; } 13: void SetItsVal(USHORT x) {itsVal = x; } 14: const Counter& operator++ (); // prefix 15: const Counter operator++ (int); // postfix 16: 17: private: 18: USHORT itsVal; 19: }; 20: 21: Counter::Counter(): 22: itsVal(0) 23: {} 24: 25: const Counter& Counter::operator++() 26: { 27: ++itsVal; 28: return *this; 29: } 30: 31: const Counter Counter::operator++(int) 32: { 33: Counter temp(*this); 34: ++itsVal; 35: return temp; 36: } 37: 38: int main() 39: { 40: Counter i; 41: cout << "The value of i is " << i.GetItsVal() << endl; 42: i++; 43: cout << "The value of i is " << i.GetItsVal() << endl; 44: ++i; 45: cout << "The value of i is " << i.GetItsVal() << endl; 46: Counter a = ++i; 47: cout << "The value of a: " << a.GetItsVal(); 48: cout << " and i: " << i.GetItsVal() << endl; 49: a = i++; 50: cout << "The value of a: " << a.GetItsVal(); 51: cout << " and i: " << i.GetItsVal() << endl; 52: return 0; 53: } Output: The value of i is 0 The value of i is 1 The value of i is 2 The value of a: 3 and i: 3 The value of a: 3 and i: 4 Analiza: Postfiks operator je deklariran u liniji 15 i implementiran u linijama 31-36. Primjetite da poziv prefiks operatora u liniji 14 ne uključuje signalni cijeli broj (x), ali se koristi svojom normalnom sintaksom. Postfiks operator koristi signalnu vrijednost (x) kako bi dao do znanja da je riječ o postfiksu a ne o prefiksu. Ta se vrijednost inače nikada ne koristi.
Operator zbrajanja Operator inkrementiranja je unarni operator. To znači da djeluje samo na jedan objekt. Operator zbrajanja (+) je binarni operator, u kojem su uključena dva objekta. Kako implementirati preopterećenje + operatora za Count? Cilj je da budemo u stanju deklarirati dvije Counter varjiable i potom ih zbrojiti, kao u slijedećem primjeru: Counter varOne, varTwo, varThree; VarThree = VarOne + VarTwo; Još jednom, mogli bismo početi pisanjem funkcije Add(), koja bi uzimala Counter kao svoj argument, zbrojila vrijednosti, te potom vratila Counter s rezultatom. Listing 10.13 ilustrira taj pristup. Listing 10.13. Add() funkcija. 1: // Listing 10.13 2: // Add function 3: 4: typedef unsigned short USHORT; 5: #include 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: Counter(USHORT initialValue); 12: ~Counter(){} 13: USHORT GetItsVal()const { return itsVal; } 14: void SetItsVal(USHORT x) {itsVal = x; } 15: Counter Add(const Counter &); 16: 17: private: 18: USHORT itsVal; 19: 20: }; 21: 22: Counter::Counter(USHORT initialValue): 23: itsVal(initialValue) 24: {} 25: 26: Counter::Counter(): 27: itsVal(0) 28: {} 29: 30: Counter Counter::Add(const Counter & rhs) 31: { 32: return Counter(itsVal+ rhs.GetItsVal()); 33: } 34: 35: int main() 36: { 37: Counter varOne(2), varTwo(4), varThree; 38: varThree = varOne.Add(varTwo); 39: cout << "varOne: " << varOne.GetItsVal()<< endl; 40: cout << "varTwo: " << varTwo.GetItsVal() << endl; 41: cout << "varThree: " << varThree.GetItsVal() << endl; 42: 43: return 0; 44: } Output: varOne: 2 varTwo: 4 varThree: 6
Analiza: Add()funkcija je deklarirana u liniji 15. Ona uzima konstantnu Counter referencu, što je u stvari broj kojeg pribrajamo trenutnom objektu. Ona veaća Counter objekt, što je rezultat koji se pridružuje lijevoj strani naredbe, kako je vidljivo u liniji 38. U tome slučaju, VarOne je objekt, varTwo je parametar u Add() funkciji, a rezultat se pridružuje u VarThree. Kako bismo kreirali varThree bez potrebe za incijaliziranjem njegove vrijednosti, potreban je podrazumijevani konstruktor. Podrazumijevani konstruktor inicijalizira itsVal na 0, kako je prikazano u linijama 26-28. Budući da varOne i varTwo moraju biti inicijalizirani na vrijednost različitu od nule, još jedan konstrzktor je kreiran, u linijama 22-24. Drugo rješenje za ovaj problem bilo bi stavljanje podrazumijevane vrijednosti 0 u konstrukto deklariran u liniji 11.
Preopterećeni operator + Sama Add() funckcija je prikazana u linijama 30-33. On radi, ali njezina upotreba je neprirodna. Preopterećenje + operatora bi omogućilo prirodniju upotrebu Counter klase. Listing 10.14 ilustrira ovo. Listing 10.14. Operator +. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40:
// Listing 10.14 //Overload operator plus (+) typedef unsigned short USHORT; #include class Counter { public: Counter(); Counter(USHORT initialValue); ~Counter(){} USHORT GetItsVal()const { return itsVal; } void SetItsVal(USHORT x) {itsVal = x; } Counter operator+ (const Counter &); private: USHORT itsVal; }; Counter::Counter(USHORT initialValue): itsVal(initialValue) {} Counter::Counter(): itsVal(0) {} Counter Counter::operator+ (const Counter & rhs) { return Counter(itsVal + rhs.GetItsVal()); } int main() { Counter varOne(2), varTwo(4), varThree; varThree = varOne + varTwo; cout << "varOne: " << varOne.GetItsVal()<< endl; cout << "varTwo: " << varTwo.GetItsVal() << endl; cout << "varThree: " << varThree.GetItsVal() << endl;
41: return 0; 42: } Output: varOne: 2 varTwo: 4 varThree: 6 Analiza: operator+ je deklariran u liniji 15 i definiran u linijama 28-31. Usporedite ih s deklaracijom i definicijom Add() funkcije u prethodnom listingu; oni su gotovo identični. Sintaksa za njihovu upotrebu je bitno drugačija. Puno je prirodnije napisati varThree = varOne + varTwo; nego napisati varThree = varOne.Add(varTwo); Nije velika promjena, ali dovoljna za pravljenje programa lakšim za upotrebu i razumijevanje. PAŽNJA: Tehnike opisane za preopterećeni operator++ mogu biti primjenjene i na druge unarne operatore, poput operator-.
Preopterećeni binarni operatori Binarni operatori su kreirani kao i unarni, osim što oni primaju parametar. Parametar je konstantna referenca na objekt istoga tipa. Primjer 1 Counter Counter::operator+ (const Counter & rhs); Primjer 2 Counter Counter::operator-(const Counter & rhs);
Operator pridruživanja Četvrta i konačna funkcija koja dolazi s prevoditeljem, ako je ne specificirate je operator pridruživanja (operator=()). Ovaj operator se poziva kad god nešto pridružite objektu. Na primjer: CAT catOne(5,7); CAT catTwo(3,4); // ... other code here catTwo = catOne; Ovdje, catOne je kreiran i inicijaliziran sa itsAge jednako 5 i itsWeight jednako 7. catTwo je tada kreiran i pridruženi su mu vrijednosti 3 i 4. Nakon nekog vremena, catTwo je pridružena vrijednost iz catOne. Ovdje se povlače dva pitanja: Što se događa ako je itsAge pokazivač, te što se događa sa originalnim vrijednostima iz catTwo? Upravljanje podatkovnim članovima kako bi spremali podatke u slobodnom spremniku je tema obrađena prilikom diskusije o konstruktoru kopije. Isto se pitanje povlači i ovdje. C++ programeri razlikuju shallow (plitku) kopiju s jedne strane i deep (duboku) kopiju s druge. Plitka kopija kopira samo članove, te na kraju oba objekta pokazuju na isto područje slobodnog spremnika. Duboka kopija alocira neophodnu memoriju. Dodatna komplikacija kod operatora pridruživanja je u tome što objekt catTwo već postoji i ima alociranu memoriju. Ta memorija mora biti obrisana ako ne želimo prouzročiti curenje memorije. Ali što se događa ako pridružimo catTwo samome sebi? catTwo = catTwo; Nitko ovo neće napraviti namjerno, ali program mora znati baratati i s takvim izrazom. Što je još važnije, moguće je da se ovo dogodi pogreškom kada referencirani i dereferencirani pokazivači sakriju činjenicu da pridružujemo objekt samom sebi. Ako ne pripazite na tu moguću situaciju, catTwo će izbrisati svoju memorijsku alokaciju. Tada će, kad je spreman kopirati u memoriju desnu stranu izraza, imati velik problem: memorija će biti izgubljena. Kako bi se zaštitili od toga, vaš operator pridruživanja mora provjeriti da li je desna strana operatora pridružvanja sam objekt. To radi ispitujući this pokazivač. Listing 10.15 pokazuje nam klasu s operatorom pridruživanja. Listing 10.15. Operator pridruživanja.
1: // Listing 10.15 2: // Copy constructors 3: 4: #include 5: 6: class CAT 7: { 8: public: 9: CAT(); // default constructor 10: // copy constructor and destructor elided! 11: int GetAge() const { return *itsAge; } 12: int GetWeight() const { return *itsWeight; } 13: void SetAge(int age) { *itsAge = age; } 14: CAT operator=(const CAT &); 15: 16: private: 17: int *itsAge; 18: int *itsWeight; 19: }; 20: 21: CAT::CAT() 22: { 23: itsAge = new int; 24: itsWeight = new int; 25: *itsAge = 5; 26: *itsWeight = 9; 27: } 28: 29: 30: CAT CAT::operator=(const CAT & rhs) 31: { 32: if (this == &rhs) 33: return *this; 34: delete itsAge; 35: delete itsWeight; 36: itsAge = new int; 37: itsWeight = new int; 38: *itsAge = rhs.GetAge(); 39: *itsWeight = rhs.GetWeight(); 40: return *this; 41: } 42: 43: 44: int main() 45: { 46: CAT frisky; 47: cout << "frisky's age: " << frisky.GetAge() << endl; 48: cout << "Setting frisky to 6...\n"; 49: frisky.SetAge(6); 50: CAT whiskers; 51: cout << "whiskers' age: " << whiskers.GetAge() << endl; 52: cout << "copying frisky to whiskers...\n"; 53: whiskers = frisky; 54: cout << "whiskers' age: " << whiskers.GetAge() << endl; 55: return 0; 56: } frisky's age: 5 Setting frisky to 6...
whiskers' age: 5 copying frisky to whiskers... whiskers' age: 6 Izlaz: Listing 10.15 nam vraća CAT klasu, i izostavlja konstruktor kopije i destuktor kako bi uštedjeli prostor. U liniji 14, deklariran je operator pridruživanja, te je definiran u linijama 30-41. Analiza: U liniji 32, trenutni objekt (CAT kojemu pridružujemo) se testira kako bi provjerili da li je jednak onome objektu kojega pridružujemo. To postižemo provjerom adrese rhs i usporešivanjem s adresom pohranjenom u this pokazivaču.
Operatori konverzije Što se događa kad pokušamo pridružiti varijablu ugrađenog tipa, poput int ili unsigned short, u objekt korisnički definirane klase? Listing 10.16 nam vraća Counter klasu, i pokušava pridružiti varijablu tipa USHORT to u Counter objekt. UPOZORENJE: Listing 10.16 se neće prevesti!
Listing 10.16. Pokušaj pridruživanja Counter objekta u USHORT 1: // Listing 10.16 2: // This code won't compile! 3: 4: typedef unsigned short USHORT; 5: #include 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: ~Counter(){} 12: USHORT GetItsVal()const { return itsVal; } 13: void SetItsVal(USHORT x) {itsVal = x; } 14: private: 15: USHORT itsVal; 16: 17: }; 18: 19: Counter::Counter(): 20: itsVal(0) 21: {} 22: 23: int main() 24: { 25: USHORT theShort = 5; 26: Counter theCtr = theShort; 27: cout << "theCtr: " << theCtr.GetItsVal() << endl; 28: return ;0 29: } Output: Compiler error! Unable to convert USHORT to Counter Analiza: Counter klasa deklarirana u linijama 7-17 ima samo podrazumijevani konstruktor. Ne deklarira posebnu metodu za pretvaranje USHORT u Counter objekt, pa linija 26 uzrokuje pogrešku prilikom prevođenja. Prevoditelj ne zna "skužiti" ukoliko mu ne kažete, da ako mu pridružite USHORT, treba pridružiti vrijednost podatkovnom članu itsVal.
Listing 10.17 popravlja ovo kreirajući operator konverzije: konstruktor koji uzima USHORT i proizvodi Counter objekt. Listing 10.17. Pretvaranje USHORT u Counter. 1: // Listing 10.17 2: // Constructor as conversion operator 3: 4: typedef unsigned short USHORT; 5: #include 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: Counter(USHORT val); 12: ~Counter(){} 13: USHORT GetItsVal()const { return itsVal; } 14: void SetItsVal(USHORT x) {itsVal = x; } 15: private: 16: USHORT itsVal; 17: 18: }; 19: 20: Counter::Counter(): 21: itsVal(0) 22: {} 23: 24: Counter::Counter(USHORT val): 25: itsVal(val) 26: {} 27: 28: 29: int main() 30: { 31: USHORT theShort = 5; 32: Counter theCtr = theShort; 33: cout << "theCtr: " << theCtr.GetItsVal() << endl; 34: return 0; 35: Output: theCtr: 5 Analiza: Važna promjena je u liniji 11, gdje je konstruktor preopterećen kako bi primio USHORT, i u linijama 24-26, gdje je konstruktor implementiran. Efekt ovog konstruktora je kreiranje Counter iz USHORT. Dajući ovo, kompajler je sposoban pozvati konstruktor koji uzima USHORT kao svoj argument. Što se događa, kad pokušamo izokrenuti pridruživanje pišući slijedeći kod? 1: Counter theCtr(5); 2: USHORT theShort = theCtr; 3: cout << "theShort : " << theShort << endl; Još jednom, generirati ćemo grešku kod prevođenja. Iako sada prevoditelj zna generirati Counter iz USHORT, on nezna izokrenuti proces.
Operatori konverzije Kako bi riješili taj i njemu slične probleme, C++ nam pruža operatore konverzije koji mogu biti umetnuti u klasu. To omogućuje specificiranje kako implicirati konverziju u ugrađene tipove. Listing 10.18 ilustrira ovo. Jedna napomena: Operatori konverzije ne specificiraju povratnu vrijednost, iako oni efektivno vraćaju zamijenjenu vrijednost.
Listing 10.18. Pretvaranje iz Counter u unsigned short(). 1: // Listing 10.18 2: // conversion operator 3: 4: typedef unsigned short USHORT; 5: #include 6: 7: class Counter 8: { 9: public: 10: Counter(); 11: Counter(USHORT val); 12: ~Counter(){} 13: USHORT GetItsVal()const { return itsVal; } 14: void SetItsVal(USHORT x) {itsVal = x; } 15: operator unsigned short(); 16: private: 17: USHORT itsVal; 18: 19: }; 20: 21: Counter::Counter(): 22: itsVal(0) 23: {} 24: 25: Counter::Counter(USHORT val): 26: itsVal(val) 27: {} 28: 29: Counter::operator unsigned short () 30: { 31: return ( USHORT (itsVal) ); 32: } 33: 34: int main() 35: { 36: Counter ctr(5); 37: USHORT theShort = ctr; 38: cout << "theShort: " << theShort << endl; 39: return 0; 40: Output: theShort: 5 Analiza: U liniji 15, operator konverzije je deklariran. Primjetite da on nema povratnu vrijednost. Implementacija te funkcije je u linijama 29-32. Linija 31 vraća vrijednost iz itsVal, pretvorenu u USHORT. Sada prevoditelj zna kako prebaciti USHORT u Counter objekte i ibrnuto, te oni mogu međusobno biti pridruživani kako želimo.
Kviz 1. Kad preopterećujete funkcijske članove, kako se oni trebaju razlikovati? 2. Koja je razlika između deklaracije i definicije? 3. Kada se poziva konstruktor kopije? 4. Kada se poziva destruktor? 5. Koja je razlika između konstruktora kopije i operatora pridruživanja (=)? 6. Što je this pokazivač? 7. Kako razlikujemo između preopterećenja prefiks i postfiks inkrement operatora?
8. Možete li preopteretiti operator+ za short integer vrijednosti? 9. Da li je legalno U C++ preopteretiti operator++ tako da on dekrementira vrijednost u vašoj klasi? 10. Kakvu povratnu vrijednost moraju imati operatorri konverzije u svojim deklaracijama? Vježbe 1. Napišite SimpleCircle deklaraciju klase s jednim podatkovnim članom: itsRadius. Uključite podrazumijevani konstruktor, destruktor, te pristupne metode za polumjer. 2. Koristeći klasu kreiranu u vježbi 1, napišite implementaciju podrazumijevanog konstruktora, inicijalizirajući itsRadius s vrijednošću 5. 3. Koristeći istu klasu dodajte i drugi konstruktor koji prima vrijednost kao svoj parametar i pridružuje ju u itsRadius. 4. Kreirajte prefiks i postfiks inkrement operator za vašu SimpleCircle klasu koji inkrementira itsRadius. 5. Promjenite SimpleCircle da pohrani itsRadius u slobodnom spremniku, te popravite postojeće metode. 6. Napravite konstruktor kopije za SimpleCircle. 7. Napravite operator pridruživanja za SimpleCircle. 8. Napišite program koji kreira dva SimpleCircle objeka. Koristeći podrazumijevani konstruktor na jednom i inicijaliziranjem drugog na vrijednost 9. Pozovite operator inkrementiranja za svaki i potom ispišite njihove vrijednosti. Konačno, pridružite drugi prvome i ispišite njihove vrijednosti. 9. BUG BUSTERS: Što ne valja s ovom implementacijom operatora pridruživanja? SQUARE SQUARE ::operator=(const SQUARE & rhs) { itsSide = new int; *itsSide = rhs.GetSide(); return *this; } 10. BUG BUSTERS: Što ne valja s implementacijom operatora zbrajanja? VeryShort VeryShort::operator+ (const VeryShort& rhs) { itsVal += rhs.GetItsVal(); return *this; }
Lekcija 11
Polja
U prethodnim lekcijama deklarirali smo jedinstvene int, char, ili nek druge objekte. Često je potrebno deklarirati grupu objekata, kao na primjer, 20 cijelih brojeva ili 12 objekata klase Machak. Danas ćete naučiti • Što su polja i kako ih deklarirati. • Što su stringovi i kako napraviti polja znakova da bi ih koristili. • Odnos među poljima i pokazivačima. • Kako koristiti aritmetiku pokazivača sa poljima.
Što je polje? Polje je kolekcija lokacija za pohranu podataka, od kojih svaka drži isti tip podatka. Svaka lokacija za pohranu se zove element polja. Polje deklarirate pišući tip, kojeg slijedi ime polja i subskript. Subskript je broj članova polja okružen uglatim zagradama. Na primjer, long LongArray[25]; deklarira polje od 25 long integera nazvanio LongArray. Kad kompajler vidi ovu deklaraciju, rezervira dovoljno memorije za pohranu svih 25 elemenata. Budući da svaki long integer zahtijeva 4 bytea, ta deklaracija rezervira 100 byteova memorije u nizu, kao što je ilustrirano na slici 11.1 Slika 11.1: Deklaracija polja.
Elementi polja Pojedinim elementima polja pristupamo navodeći ofset (redni broj) tog elementa. Elementi se broje od nule. Prema tome, prvi element gore spomenutog polja bio bi LongArray[0], drugi je LongArray[1] itd. Ovo zna biti pomalo zbunjujuće. Polje SomeArray[3] ima tri elementa. To su SomeArray[0], SomeArray[1], i SomeArray[2]. Uopćeno, SomeArray[n] ima n elemenata koji su pobrojani SomeArray[0] do SomeArray[n-1]. Prema tome, LongArray[25] ide od LongArray[0] do LongArray[24]. Listing 11.1 pokazuje kako deklarirati polje pet cijelih brojeva i ispuniti ih vrijednošću. Listing 11.1. Uotreba cjelobrojnog polja. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
//Listing 11.1 - Arrays #include int main() { int myArray[5]; int i; for ( i=0; i<5; i++) // 0-4 { cout << "Value for myArray[" << i << "]: "; cin >> myArray[i]; } for (i = 0; i<5; i++) cout << i << ": " << myArray[i] << "\n"; return 0;
16: } Output: Value for myArray[0]: 3 Value for myArray[1]: 6 Value for myArray[2]: 9 Value for myArray[3]: 12 Value for myArray[4]: 15 0: 3 1: 6 2: 9 3: 12 4: 15 Analiza: Linija 6 deklarira polje zvano myArray, koje drži 5 cjelobrojnih varijabli. Linija 8 stvaa petlju koja broji od 0 do 4, što je pravilan slijed ofseta za polje od 5 elemenata. Korisnik je upitan za vrijednost, i ona se pohranjuje u pravilnom ofsetu u polje. Prva vrijednost je pohranjena u myArray[0], druga u myArray[1], i tako dalje. Druga for petlja ispisuje sve vrijednosti na ekran. PAŽNJA: Polja broje od 0, a ne od 1. To uzrokuje mnoge bugove u programima koji su pisali C++ početnici. Kad god koristite polje, zapamtite da polje sa 10 elemenata broji od ArrayName[0] do ArrayName[9]. Ne postoji ArrayName[10].
Pisanje nakon kraja polja Kada upisujete vrijednost u element polja, kompajler računa gdje spremiti vrijednosti bazirane na veličini pojedinog elementa i indeksu polja. Recimo da zatražite upisivanje vrijednosti u LongArray[5], što je zapravo šesti element. Kompajler množi ofset (5) s veličinom svakog elementa—u ovom slučaju, 4. Tada skoči za toliko mjesta (20 byteova) od početka polja i upisuje vrijednost u tu lokaciju. Ako zahtjevate upisivanje na LongArray[50], kompajler ignorira činjenicu da ne postoji takav element. On računa koliko daleko od prvog elementa treba gledati (200 byteova) i zatim piše preko vrijednosti koja je pohranjena na toj lokaciji. To u praksi može biti bilo koji podatak, i upisivanje nove vrrijednosti može dati nepredvidljive rezultate. Ako imate sreće, vaš program će se trenutno srušiti. Ako nemate, dobiti ćete neobične rezultate mnogo kasnije u vašem programu, i teško ćete otkriti što je pošlo po zlu. Kompajler je poput slijepca koji mjeri razdaljinu od svoje kuće. On počne od prve kuće, MainStreet[0]. Ako mu kažete da dođe do šeste kuće, on će si reći, "Moram proći još pet kuća. Svaka kuća je četiri velika koraka. Znači, trebam dodatnih 20 koraka." Ako mu kažete da ide u MainStreet[100], a Main Street je samo 25 kuća dugačka, on će napraviti 400 koraka. Puno prije nego što dođe tamo, vjerojatno će izletiti pred autobus. Zato budite pažljivi kamo ga šaljete. Listing 11.2 pokazuje što se dogodi kad upisujemo "iza" zavšetka polja. UPOZORENJE: Ne pokrećite ovaj program; može vam srušiti računalo!
Listing 11.2. Upisivanje iza završetka polja. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
//Listing 11.2 // Demonstrates what happens when you write past the end // of an array #include int main() { // sentinels long sentinelOne[3]; long TargetArray[25]; // array to fill long sentinelTwo[3];
12: int i; 13: for (i=0; i<3; i++) 14: sentinelOne[i] = sentinelTwo[i] = 0; 15: 16: for (i=0; i<25; i++) 17: TargetArray[i] = 0; 18: 19: cout << "Test 1: \n"; // test current values (should be 0) 20: cout << "TargetArray[0]: " << TargetArray[0] << "\n"; 21: cout << "TargetArray[24]: " << TargetArray[24] << "\n\n"; 22: 23: for (i = 0; i<3; i++) 24: { 25: cout << "sentinelOne[" << i << "]: "; 26: cout << sentinelOne[i] << "\n"; 27: cout << "sentinelTwo[" << i << "]: "; 28: cout << sentinelTwo[i]<< "\n"; 29: } 30: 31: cout << "\nAssigning..."; 32: for (i = 0; i<=25; i++) 33: TargetArray[i] = 20; 34: 35: cout << "\nTest 2: \n"; 36: cout << "TargetArray[0]: " << TargetArray[0] << "\n"; 37: cout << "TargetArray[24]: " << TargetArray[24] << "\n"; 38: cout << "TargetArray[25]: " << TargetArray[25] << "\n\n"; 39: for (i = 0; i<3; i++) 40: { 41: cout << "sentinelOne[" << i << "]: "; 42: cout << sentinelOne[i]<< "\n"; 43: cout << "sentinelTwo[" << i << "]: "; 44: cout << sentinelTwo[i]<< "\n"; 45: } 46: 47: return 0; 48: } Output: Test 1: TargetArray[0]: 0 TargetArray[24]: 0 SentinelOne[0]: 0 SentinelTwo[0]: 0 SentinelOne[1]: 0 SentinelTwo[1]: 0 SentinelOne[2]: 0 SentinelTwo[2]: 0 Assigning... Test 2: TargetArray[0]: 20 TargetArray[24]: 20 TargetArray[25]: 20 SentinelOne[0]: 20 SentinelTwo[0]: 0 SentinelOne[1]: 0 SentinelTwo[1]: 0
SentinelOne[2]: 0 SentinelTwo[2]: 0
"Fence Post" pogreške Toliko je često upisivanje jednog člana iza završetka polja da ta pogreška ima i svoje ime. Zove se "fence post error". To se odnosi na broj stupića od ograde potrebnih za 10-metara ograde, ako zabijate stupiće svaki metar. Većina ljudi odgovara 10, ali vi ih u stvari trebate 11. Slika 11.2 to pojašnjava. Slika 11.2. Fence post error.
Inicijaliziranje polja Jednostavno polje ugrađenih tipova možete inicijalizirati prilikom deklaracije. Nakon imena polja, stavljamo znak jednakosti (=) i listu zarezom odvojenih vrijednosti u vitičastim zagradama. Na primjer, int IntegerArray[5] = { 10, 20, 30, 40, 50 }; deklarira IntegerArray kao polje 5 cijelih brojeva. U IntegerArray[0] pridružuje vrijednost 10, IntegerArray[1] je 20, i tako dalje. Ako ne navedete veličinu polja, polje dovoljno veliko za pohranjivanje inicijalizacije se kreira. Tako, ako napišete int IntegerArray[] = { 10, 20, 30, 40, 50 }; kreirati ćete potpuno isto polje kao ono iz prethodnog primjera. Ako trebate znati veličinu polja, možete tražiti od kompajlera da vam ju izračuna. Na primjer, const USHORT IntegerArrayLength; IntegerArrayLength = sizeof(IntegerArray)/sizeof(IntegerArray[0]); Računa ukupno zaueće memorije za cijelo polje i dijeli ga s zauzećem memorije jednog elementa polja, što nam daje broj članova polja. Ne možete inicijalizirati više elemenata nego što ste deklarirali polje. Prema tome, int IntegerArray[5] = { 10, 20, 30, 40, 50, 60}; generira grešku prilikom prevođenja. Međutim, legalno je napisati int IntegerArray[5] = { 10, 20}; Iako neinicijalizirani članovi polja nemaju garantiranu vrijednost, u stvari, će oni biti postavljeni na 0.
Deklaracija polja Polja mogu imati bilo koje legalno ime varijable, ali ne mogu imati isto ime kao druga varijabla ili poljee unutar njihova dosega. Prema tome ne možete imati polje myCats[5] i varijablu myCats u isto vrijeme. Možete deklarirati dimenzije polja i s konstantom ili enumeracijom. Listing 11.3 ilustrira ovo. Listing 11.3. Upotreba const i enum u polju. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:
// Listing 11.3 // Dimensioning arrays with consts and enumerations #include int main() { enum WeekDays { Sun, Mon, Tue, Wed, Thu, Fri, Sat, DaysInWeek }; int ArrayWeek[DaysInWeek] = { 10, 20, 30, 40, 50, 60, 70 }; cout << "The value at Tuesday is: " << ArrayWeek[Tue]; return 0;
13: } Output: The value at Tuesday is: 30 Analiza: Linija 7 stvara enumeraciju nazvanu WeekDays. Ona ima osam brojeva. Sunday je jednak 0, a DaysInWeek je jednak 7. Linija 11 koristi enumeriranu konstantu Tue kao indeks elementa polja. budući da je Tue jednak 2, treći element polja, DaysInWeek[2], je vraćen i ispisan u liniji 11.
Polja Za deklariranje niza pišemo tip pohranjenog objekta, kojeg slijedi ime niza i uglata zagrada s brojem elemenata polja. Primjeri: int MyIntegerArray[90]; long * ArrayOfPointersToLongs[100]; Za pristupanje pojedinim članovima polja koristimo njihove indekse. Primjeri: int theNinethInteger = MyIntegerArray[8]; long * pLong = ArrayOfPointersToLongs[8] Sva polja broje od 0. Polje od n elemenata je označeno od 0 do n-1.
Polja objekata Svaki objekt, bilo ugrađeni, bilo korisnički definiran, može biti pohranjen u polje. Prilikom deklaracije polja kažemo kompajleru tip objekta za pohranjivanje i broj objekata za koje treba rezervirati prostor. Kompajler zna koliko mjesta svaki objekt zahtjeva po njegovoj deklaraciji. Klasa kojoj pripada mora imati podrazumijevani konstruktor koji ne prima nikakve argumente kako bi objekti mogli biti kreirani prilikom definicije polja. Pristupanje podatkovnim članovima u polju objekata je dvostupanjski proces. Identificiramo element polja koristeći se indeks operatorom ([ ]), a tada pozivamo operator člana (member operator) (.) za pristup pojedinom podatkovnom članu. Listing 11.4 demonstrira kako biste kreirali polje od pet objekata klase CAT. Listing 11.4. Kreiranje niza objekata. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28:
// Listing 11.4 - An array of objects #include class CAT { public: CAT() { itsAge = 1; itsWeight=5; } ~CAT() {} int GetAge() const { return itsAge; } int GetWeight() const { return itsWeight; } void SetAge(int age) { itsAge = age; } private: int itsAge; int itsWeight; }; int main() { CAT Litter[5]; int i; for (i = 0; i < 5; i++) Litter[i].SetAge(2*i +1); for (i = 0; i < 5; i++) { cout << "Cat #" << i+1<< ": ";
29: cout << Litter[i].GetAge() << endl; 30: } 31: return 0; 32: } Output: cat #1: 1 cat #2: 3 cat #3: 5 cat #4: 7 cat #5: 9
Višedimenzionalna polja Moguće je imati i polja koja sadržavaju više dimenzija. Svaka dimenzija je reprezentirana u svojoj uglatoj zagradi prilikom deklaracije polja. Broj dimenzija nije ograničen, ali je malo vjerojatno da ćete koristiti nešto drugo osim jedno i dvodimenzionalnih polja. Dobar primjer dvodimenzionalnog polja je šahovska ploča. Jedna dimenzija predstavlja osam redaka; druga dimenzija predstavlja osam stupaca. Slika 11.3 ilustrira ideju. Pretpostavimo da imate klasu nazvanu SQUARE. Deklaracija polja imena Board koji ju predstavlja glasila bi SQUARE Board[8][8]; Iste podatke možete predstaviti i jednodimenzionalnim poljem od 64 elementa, na primjer, SQUARE Board[64] Međutim, prva deklaracija je puno bliža stvarnom svijetu. Kad igra počinje, kralj je lociran na četvrtu poziciju prvoga reda, Brojeći od nule, pozicija odfgovara Board[0][3]; Pretpostavljajući da prva zagrada predstavlja retke, a druga stupce, prikaz za cijelu ploču bio bi ilustriran na slici 11.3. Slika 11.3. Šahovska ploča i dvodimenzionalno polje.
Inicijaliziranje višedimenzionalnog polja Inicijalizirati možete i višedimenzionalno polje. Dodjeljujete listu vrijednosti niza u redosljedu, s promjenjivim zadnjim indeksom polja, dok drugi miruju. Tako na primjer, ako imate polje int theArray[5][3] prva tri elementa idu u theArray[0]; slijedeća tri idu u theArray[1]; i tako dalje. To polje inicijalizirate pišući int theArray[5][3] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 } Kako bi pojednostavili čitljivost, možete grupirati inicijalizacije u vitičaste zagrade. Na primjer, int theArray[5][3] = { {1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}, {13,14,15} }; Svaka vrijednost mora biti odvojena zarezom, bez obzira na zagrade, jer ih kompajler ignorira. Listing 11.5 stvara dvodimenzionalno polje. Prva dimenzija je slijed brojeva od 0 do 5. Druga dimenzija se sastoji od duplirane svake vrijednosti u prvoj dimenziji.
Listing 11.5. Stvaranje višedimenzionalnog polja. 1: #include 2: int main() 3: { 4: int SomeArray[5][2] = { {0,0}, {1,2}, {2,4}, {3,6}, {4,8}}; 5: for (int i = 0; i<5; i++) 6: for (int j=0; j<2; j++) 7: { 8: cout << "SomeArray[" << i << "][" << j << "]: "; 9: cout << SomeArray[i][j]<< endl; 10: } 11: 12: return 0; 13: } Output: SomeArray[0][0]: 0 SomeArray[0][1]: 0 SomeArray[1][0]: 1 SomeArray[1][1]: 2 SomeArray[2][0]: 2 SomeArray[2][1]: 4 SomeArray[3][0]: 3 SomeArray[3][1]: 6 SomeArray[4][0]: 4 SomeArray[4][1]: 8 Analiza: Linija 4 deklarira SomeArray kao dvodimenzionalno polje. Prva dimenzija se sastoji od pet integera; druga se sastoji od dva integera. Ovo kreira mrežu 5x2, kao što prikazuje slika 11.4 Slika 11.4. polje 5x2.
Vrijednosti inicijaliziramo u parovima, iako bi mogle biti i izračunate. Linije 5 i 6 kreiraju ugnježdenu for petlju. Vanjska for petlja se mijenja za svakog člana prve dimenzije. Za svakog člana u toj dimenziji, unutrašnja for petlja se izvrti kroz sve članove druge dimenzije. Tako će SomeArray[0][0] biti prvi element, nakon njega ići će SomeArray[0][1]. Kad se izvrte svi članovi druge dimenzije, prva će se povećati za jedan i ponovo će unutrašnja petlja krenuti od 0.
Riječ o memoriji Prilikom deklaracije polja vi kažete kompajleru točno koliko očekujete pohranjenih objekata u njemu. Kompajler rezervira memoriju za sve objekte, čak i za one koje nikad ne koristite. To nam je neprihvatljivo kod velikih polja unaprijed nepoznate veličine, jer bi time morali rezervirati ogromne količine memorije. U tom slučaju maramo koristiti napredne strukture podataka. Mi ćemo se pozabaviti nizovima pokazivača, nizovima kreiranim u slobodnom spremniku i raznim drugim metodama.
Nizovi pokazivača Nizovi o kojima smo do sada diskutirali smještaju sve svoje članove u stog. Obično je memorijski prostor stoga ograničen, dok je slobodni spremnik i veći i predviđen upravo za smještanje velike količine podataka. Moguće je deklarirati svaki objekt u slobodnom spremniku a potom pohraniti samo pokazivače na objekte unutar polja. Time dramatično smanjujemo količinu zauzetog stoga. Listing 11.6 je prepisano polje sa listinga 11.4, ali sprema sve objekte u slobodnom spremniku. Kao pokazatelj koliko više memorije smo si time priskrbili, polje je prošireno sa 5 na 500 i promijenjeno je ime polja.
Listing 11.6. Spremanje polja u slobodnom spremniku. 1: // Listing 11.6 - An array of pointers to objects 2: 3: #include 4: 5: class CAT 6: { 7: public: 8: CAT() { itsAge = 1; itsWeight=5; } 9: ~CAT() {} // destructor 10: int GetAge() const { return itsAge; } 11: int GetWeight() const { return itsWeight; } 12: void SetAge(int age) { itsAge = age; } 13: 14: private: 15: int itsAge; 16: int itsWeight; 17: }; 18: 19: int main() 20: { 21: CAT * Family[500]; 22: int i; 23: CAT * pCat; 24: for (i = 0; i < 500; i++) 25: { 26: pCat = new CAT; 27: pCat->SetAge(2*i +1); 28: Family[i] = pCat; 29: } 30: 31: for (i = 0; i < 500; i++) 32: { 33: cout << "Cat #" << i+1 << ": "; 34: cout << Family[i]->GetAge() << endl; 35: } 36: return 0; 37: } Output: Cat #1: 1 Cat #2: 3 Cat #3: 5 ... Cat #499: 997 Cat #500: 999 Analiza: CAT objekt deklariran u linijama 5-17 je potpuno jednak CAT objektu deklariranom u listingu 11.4. Ovaj put je, polje u liniji 27 nazvano Family, i deklarirano je da drži 500 pokazivača na CAT objekte. U inicijalnoj petlji (linije 24-29) novi CAT objekti se stvaraju u slobodnom spremniku, i svakome je godina postavljena na dvostruku vrijednost indeksa plus 1. Tako će prvi CAT imati vrijednost 1, drugi – 3, treći – 5 itd. Konačno, i pokazivač je dodan polju. Budući da polje deklarirano da sadrži pokazivače, sam pokazivač—za razliku od dereferencirane vrijednosti pokazivača—je dodan u polje. Druga petlja (linije 31 i 32) ispisuje svaku od vrijednosti. Pokazivaču se pristupa koristeći indeks, Family[i]. Ta adresa se koristi za pristup GetAge() metodi. U ovom primjeru, polje Family i svi njezini pokazivači su pohranjeni na stogu, ali svih 500 CAT objekata je spremljeno u slobodnom spremniku.
Deklariranje polja u slobodnom spremniku Moguće je staviti i cijelo polje u slobodni spremnik, također poznat i pod nazivom "heap". To činimo pozivom new i korištenjem subskript operatora. Rezultat je pokazivač na područje slobodnog spremnika koji sadrži polje. Na primjer, CAT *Family = new CAT[500]; deklarira Family kao pokazivač ma prvi u nizu od 500 CAT objekata. Drugim rječima, , Family pokazuje na— odnosno ima adresu od--Family[0]. Prednost ovog načina upotrebe je mogućnost korištenja aritmetike pokazivača za pristupanje pojedinim članovima od Family. Na primjer, ako napišete CAT *Family = new CAT[500]; CAT *pCat = Family; //pCat points to Family[0] pCat->SetAge(10); // set Family[0] to 10 pCat++; // advance to Family[1] pCat->SetAge(20); // set Family[1] to 20 Time deklarirate novo polje od 500 CAT objekata i pokazivač koji pokazuje na početak toga polja. Koristeći taj pokzivač, prva CAT SetAge() funkcija je pozvana s vrijednosti 10. Pokazivač se tada inkrementira na mjesto idućeg objekta, i druga Cat SetAge() metoda je tada pozvana.
Pokazivač na polje protiv polja pokazivača Proučite slijedeće deklaracije: 1: Cat FamilyOne[500] 2: CAT * FamilyTwo[500]; 3: CAT * FamilyThree = new CAT[500]; FamilyOne je polje od 500 CAT objekata. FamilyTwo je polje od 500 pokazivača na objekte klase CAT. FamilyThree je pokazivač na polje od 500 objekata. Razlike među ovim linijama dramatično utječu na to kakao polja funkcioniraju. Što je još čudnije, FamilyThree je varijanta od FamilyOne, ali je vrlo različit od FamilyTwo. Ovo povlači (bolno) pitanje u kakvom su odnosu pokazivači i polja. U trećem slučaju, FamilyThree je pokazivač na polje. Što znači da je adresa u FamilyThree iadresa prvog člana polja. Isti je slučaj i sa FamilyOne.
Pokazivači i imena polja U C++ jeziku, naziv polja je konstantan pokazivač na prvi element toga polja. Prema tome, prilikom deklaracije CAT Family[50]; Family je pokazivač na &Family[0], što je adresa prvog elementa polja Family. Legelno je koristiti imena polja kao konstantne pokazivače i obrnuto, Na primjer, Family + 4 je legalan način za pristup podacima u Family[4]. Kompajler obavlja svu aritmetiku kada dodajete, inkrementirate i dekrementirate pokazivače. Adresa kojoj pristupate kad napišete Family + 4 nije 4 bytea iza adrese od Family—već je četiri objekta. Ako je svaki objekt dug 4 bytea, Family + 4 je 16 bytea. Ako je svaki objekt CAT koji sadrži četiri long podatkovna člana od 4 bytea svaki i dva short podatkovna člana od 2 bytea svaki, objekt klase CAT zauzima 20 byteova, a Family + 4 ide 80 byteova iza početka polja. Listing 11.7 ilustrira deklariranje i upotrebu polja u slobodnom spremniku. Listing 11.7. Stvaranje polja korištenjem new. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
// Listing 11.7 - An array on the free store #include class CAT { public: CAT() { itsAge = 1; itsWeight=5; } ~CAT(); int GetAge() const { return itsAge; }
11: int GetWeight() const { return itsWeight; } 12: void SetAge(int age) { itsAge = age; } 13: 14: private: 15: int itsAge; 16: int itsWeight; 17: }; 18: 19: CAT :: ~CAT() 20: { 21: // cout << "Destructor called!\n"; 22: } 23: 24: int main() 25: { 26: CAT * Family = new CAT[500]; 27: int i; 28: CAT * pCat; 29: for (i = 0; i < 500; i++) 30: { 31: pCat = new CAT; 32: pCat->SetAge(2*i +1); 33: Family[i] = *pCat; 34: delete pCat; 35: } 36: 37: for (i = 0; i < 500; i++) 38: { 38: cout << "Cat #" << i+1 << ": "; 39: cout << Family[i].GetAge() << endl; 40: } 41: 42: delete [] Family; 43: 44: return 0; 45: } Output: Cat #1: 1 Cat #2: 3 Cat #3: 5 ... Cat #499: 997 Cat #500: 999 Analiza: Linija 26 deklarira polje Family, koje sadrži 500 CAT objekata. Cijelo polje je kreirano u slobodnom spremniku s pozivom new CAT[500]. Svaki CAT objekt umetnut u polje je također napravljen u slobodnom spremniku (linij 31). Primjetite, da sada ne dodajemo pokazivač u polje nego sam objekt. Ovo nije polje pokazivača na objekte, već je to polje objekata.
Brisanje polja u slobodnom spremniku Family je pokazivač na polje u slobodnom spremniku. Kada u liniji 33 dereferenciramo pokazivač pCat, CAT objekt je spremljen u polju. Ali pCat ponovno koristimo u slijedećoj iteraciji petlje. Nije li opasno sada ostaviti prethodni objekt bez pokazivača i nećemo l itako uzrokovati curenje memorije? To bi bio povelik problem, kada brisanje Family ne bi oslobodilo svu zauzetu memoriju. Kompajler je dovoljno pametan za to. Kako biste to provjerili , promjeite veličinu polja sa 500 na 10 u linijama 26, 29, i 37. Potom odkomentirajte cout naredbu u liniji 21. Kada dosegnemo liniju 40, i dođe do uništavanja polja, svaki CAT objekt destruktor je pozvan.
Kada god stvaramo objekt u slobodnom spremniku koristeći new, moramo ga i obristai sa delete. Slično tome, kad god stvorimo niz koristeći new [size], brišete to polje i oslobađate memoriju sa delete[]. Uglate zagrade signaliziraju kompajleru da se briše cijelo polje. Izostavite li ih, samo će prvi element polja biti obrisan. To si možete dokazati mičući zagrade u liniji 40. U slučaju dekomentirane linije 21, kako bi se destruktor ispisao, vidjet ćete da je samo jedan CAT objekt uništen. Čestitam! Upravo ste prouzročili curenje memorije.
polja znakova String je niz znakova. Jedini stringovi koje ste doasad koristili bili su oni neimenovani, upotrebljeni u cout naredbama, poput cout << "hello world.\n"; U C++ string je niz char elemenata koji završava null karakterom. Možete deklarirati i inicijalizirati string baš kao što bi inicijalizirali bilo koje drugo polje. Na primjer, char Greeting[] = { `H', `e', `l', `l', `o', ` `, `W','o','r','l','d', `\0' }; Posljednji znak, `\0', je null znak, kojeg mnoge C++ funkcije prepoznaju kao terminator stringa. Zbog male nezgrapnosti ovog načina pisanja, C++ nam omogućuje i skraćivanje prethodnog koda, npr: char Greeting[] = "Hello World"; Trebate primjetiti dvije stvari o sintaksi: • Umjesto jednostrukih navodnika, koristimo dvostruke, te su nam nepotrebni zarezi i vitičaste zagrade. • Ne morate dodavati null znak jer ga kompajler doda umjesto vas. String Hello World je dug 12 byteova. Hello je 5 byteova, razmak je 1, World 5, i nul znak je 1. Također možete kreirati i neinicijalizirane nizove znakova. Kao i uvijek sa poljima, treba paziti da ne unesemo više znakova u spremnik nego li ima mjesta. Listing 11.8 demonstrira upotrebu neinicijaliziranog spremnika. Listing 11.8. Popunjavanje niza. 1: //Listing 11.8 char array buffers 2: 3: #include 4: 5: int main() 6: { 7: char buffer[80]; 8: cout << "Enter the string: "; 9: cin >> buffer; 10: cout << "Here's the buffer: " << buffer << endl; 11: return 0; 12: } Output: Enter the string: Hello World Here's the buffer: Hello Analiza: U liniji 7, buffer je deklariran da drži 80 znakova. To je dovoljno veliko za držanje 79 znakova i završnog null znaka. U liniji 8, Korisnika tražimo da unese string, koji se sprema u buffer u liniji 9. Sam cin se brine o upisivanju null karaktera kad završimo s tekstom. Postoje dva problema s tim programom. Prvo, ne može spremiti više od 79 znakova. Drugo, ako korisnik utipka razmak, cin misli da je došlo do kraja stringa, te prestaje zapisivati u buffer. Kako bismo riješili te probleme, morate pozvati specijalnu metodu na cin: get(). cin.get() uzima tri parametra: Bafer u za popunjavanje Maksimalan broj znakova Delimiter koji terminira upis Podrazumijevani delimiter je newline. Listing 11.9 ilustrira njegovu upotrebu. Listing 11.9. Ispunjavanje polja. 1:
//Listing 11.9 using cin.get()
2: 3: #include 4: 5: int main() 6: { 7: char buffer[80]; 8: cout << "Enter the string: "; 9: cin.get(buffer, 79); // get up to 79 or newline 10: cout << "Here's the buffer: " << buffer << endl; 11: return 0; 12: } Output: Enter the string: Hello World Here's the buffer: Hello World Analiza: Linija 9 poziva metodu get() od cin. Bafer deklariran u liniji 7 se prosljeđuje kao prvi argument. Drugi argument je maksimalan broj znakova. U našem slučaju to mora biti 79 kako bi ostalo mjesta za terminirajući null znak. Ne postoji potreba za pružanjem terminirajućeg znaka budući da je podrazumijevana vrijednost newline zadovoljavajuća. cin i sve njegove varijacije će biti još detaljnije objašnjeni prilikom diskutiranja o tokovima.
strcpy() i strncpy() C++ nasljeđuje iz C bibilioteke funkcije za baratanje stringovima. Među mnoštva funkcija su i dvije za kopiranje jednog stringa u drugi: strcpy() i strncpy(). strcpy() kopira cijeli sadržaj nekog stringa u redviđeni bafer. Listing 11.10 demonstrira upotrebu strcpy(). Listing 11.10. Upotreba strcpy(). 1: #include 2: #include <string.h> 3: int main() 4: { 5: char String1[] = "No man is an island"; 6: char String2[80]; 7: 8: strcpy(String2,String1); 9: 10: cout << "String1: " << String1 << endl; 11: cout << "String2: " << String2 << endl; 12: return 0; 13: } Output: String1: No man is an island String2: No man is an island Analiza: Datoteka zaglavlja, string.h je uključena u liniji 2. Ta datoteka sadrži prototip strcpy() funkcije. strcpy() uzima dva niza znakova, odredišni i početni, te kopira početni u odredišni. Ako je početni veći od odredišnog, strcpy() bi nastavio pisati iza kraj bafera. Kako bi se zaštitili od toga, standardna biblioteka također sadrži i strncpy(). Ova varijanta uzima i maksimalan broj znakova za kopiranje u obzir. Kopiranje će ići ili do prvog null znaka na kojeg naleti ili do maksimalnog broja znakova deklariranog za odredišni bafer. Listing 11.11 ilustrira upotrebu strncpy().
Listing 11.11. Upotreba strncpy().
1: #include 2: #include <string.h> 3: int main() 4: { 5: const int MaxLength = 80; 6: char String1[] = "No man is an island"; 7: char String2[MaxLength+1]; 8: 9: 10: strncpy(String2,String1,MaxLength); 11: 12: cout << "String1: " << String1 << endl; 13: cout << "String2: " << String2 << endl; 14: return 0; 15: } Output: String1: No man is an island String2: No man is an island
String klase Većina C++ kompajlera dolazi s bibliotekom klasa koja uključuje i velik skup klasa za manipulaciju podacima. Standardna komponenta biblioteke klasa je i String klasa. C++ je naslijedio null-terminirani string i biblioteku funkcija koja uključuje i strcpy() iz C-a, ali te funkcije nisu integrirane u objektno-orjentirani sustav. String klasa nam pruža enkapsulirani set podataka i funkcija za njihovo manipuliranje, kao i pristupne funkcije kako bi sami podaci bili skriveni od klijenata, korisnika String klase. Ako vaš kompajler ne dolazi s napravljenom String klasom—a možda i ako dolazi—možemo ju napisati sami. Ostatak ovog poglavlja diskutira o dizajnu i djelomičnoj implementaciji String klase. Kao minimum, String klasa bi trebala nadilaziti osnovna ograničenja polja znakova. Kao i sva polja, polja znakova su statična. Vi definirate koliko su velika. Oni uvijek zauzimaju deklariranu količinu memorije, čak i ako ju ne koristite. Pisanje iza kraja polja uzrokuje katastrofalne bagove. Dobra String klasa alocira samo onoliko memorije koliko joj je stvarno potrebno, i uvijek dovoljno da drži što god joj damo. Ako nije u stanju alocirati dovoljnu količinu memorije, treba nam to dojaviti. Listing 11.12 pruža brzu aproksimaciju String klase. Listing 11.12. Upotreba string klase. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22:
//Listing 11.12 #include #include <string.h> // Rudimentary string class class String { public: // constructors String(); String(const char *const); String(const String &); ~String(); // overloaded operators char & operator[](unsigned short offset); char operator[](unsigned short offset) const; String operator+(const String&); void operator+=(const String&); String & operator= (const String &);
23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81:
// General accessors unsigned short GetLen()const { return itsLen; } const char * GetString() const { return itsString; } private: String (unsigned short); char * itsString; unsigned short itsLen;
// private constructor
}; // default constructor creates string of 0 bytes String::String() { itsString = new char[1]; itsString[0] = `\0'; itsLen=0; } // private (helper) constructor, used only by // class methods for creating a new string of // required size. Null filled. String::String(unsigned short len) { itsString = new char[len+1]; for (unsigned short i = 0; i<=len; i++) itsString[i] = `\0'; itsLen=len; } // Converts a character array to a String String::String(const char * const cString) { itsLen = strlen(cString); itsString = new char[itsLen+1]; for (unsigned short i = 0; i
82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140:
{ if (this == &rhs) return *this; delete [] itsString; itsLen=rhs.GetLen(); itsString = new char[itsLen+1]; for (unsigned short i = 0; i itsLen) return itsString[itsLen-1]; else return itsString[offset]; } // constant offset operator for use // on const objects (see copy constructor!) char String::operator[](unsigned short offset) const { if (offset > itsLen) return itsString[itsLen-1]; else return itsString[offset]; } // creates a new string by adding current // string to rhs String String::operator+(const String& rhs) { unsigned short totalLen = itsLen + rhs.GetLen(); String temp(totalLen); for (unsigned short i = 0; i
141: } 142: 143: int main() 144: { 145: String s1("initial test"); 146: cout << "S1:\t" << s1.GetString() << endl; 147: 148: char * temp = "Hello World"; 149: s1 = temp; 150: cout << "S1:\t" << s1.GetString() << endl; 151: 152: char tempTwo[20]; 153: strcpy(tempTwo,"; nice to be here!"); 154: s1 += tempTwo; 155: cout << "tempTwo:\t" << tempTwo << endl; 156: cout << "S1:\t" << s1.GetString() << endl; 157: 158: cout << "S1[4]:\t" << s1[4] << endl; 159: s1[4]='x'; 160: cout << "S1:\t" << s1.GetString() << endl; 161: 162: cout << "S1[999]:\t" << s1[999] << endl; 163: 164: String s2(" Another string"); 165: String s3; 166: s3 = s1+s2; 167: cout << "S3:\t" << s3.GetString() << endl; 168: 169: String s4; 170: s4 = "Why does this work?"; 171: cout << "S4:\t" << s4.GetString() << endl; 172: return 0; 173: } Output: S1: initial test S1: Hello world tempTwo: ; nice to be here! S1: Hello world; nice to be here! S1[4]: o S1: Hellx World; nice to be here! S1[999]: ! S3: Hellx World; nice to be here! Another string S4: Why does this work? Analiza: Linije 7-31 su deklaracije jednostavne String klase. Linije 11-13 sadrže tri konstruktora: pdrazumijevani konstruktor, konstruktor kopije, i konstruktor koji prima postojeći nul-terminirani string. Ova String klasa preopterećuje offset operator ([ ]), operator plus (+), i operator plus-jednako (+=). Ofset operator je propterećen dvaputa: jednom kao konstantna funkcija koja vraća char i ponovo kao nekonstantna funkcija koja vraća referencu na char. Nekonstantna verzija se koristi u naredbama poput SomeString[4]='x'; u liniji 159. To nam omogućuje direktan pristup svakog znakovnog karaktera u nizu. Referenca na znak se vraća kako bi ju pozivajuća funkcija mogla manipulirati. Konstantna verzija se koristi kada pristupamo konstantnom String objektu, kao što je implementacija konstruktora kopijre (linija 63). Primjetite da se pristupa rhs[i], a ipak je rhs is deklariran kao const String &. U slučaju da je objekt kojeg vraćamo velik, možda bismo željeli deklarirati konstantnu referencu kao povratnu vrijednost. Budući da je char samo jedan byte, to u ovom slučaju ne bi imalo naročitog smisla. Podrazumijevani konstruktor je implementiran u linijama 33-39. On kreira string čija je dužina 0. Konvencija je String klase da vraća svoju veličinu ne računajući terminirajuću nulu.
konstruktor kopije je implementiran u linijama 63-70. On postavlja novu veličinu stringa na onu postojećeg stringa—plus1 za treminirajuću nulu. On kopira svaki znak iz postojećeg stringa u novi string i null-terminira ga na kraju. Linije 53-60 implementiraju konstruktor koji uzima postojeći string C-stila. Ovaj konstruktor je sličan konstruktoru kopije. Dužina postojećeg stringa je postignuta pozivanjem standardne String bibliotečne funkcije strlen(). U liniji 28, još jedan konstruktor, String(unsigned short), je deklarairan kao privatni funkcijsk član. Cilj je dizajnera ove klase da niti jedna korisnička klasa nikad ne kreira String proizvoljne duljine. Ovaj konstruktor postoji samo kako bi pomogao u internom kreiranju String objekata po potrebi, na primjer s operator+=, u liniji 130. String(unsigned short) koonstruktor ispunjava svaki član svog polja sa NULL. Zbog toga, for petlja provjerava do i<=len umjesto i
Vezane liste i ostale strukture Polja su poput kontejnera. Odlična su spremišta, ali fiksne veličine. Ako odaberete prevelik kontejner, gubite prostor u vašoj ostavi. Ako odaberete premali, sve će vam se prosipati i imat ćete veliki nered. Jedan način za rješenje tog problema su vezane liste.Vezana lista je takva struktura podataka koja se sastoji od malih kontejnera koji se po potrebi međusobno mogu povezivati. Ideja je da se napiše klasa koja sadrži jedan objekt vaših podataka—poput jednog CAT ili jednog Rectangle—i da pokazuje na slijedeći kontejner. Vi kreirate jedan konteiner za svaki objekt koji trebate pohraniti, i ulančavate ih po potrebi. Kontejnere nazivamo nodovima (engl. nodes). Prvi nod u listi se zove glava, a zadnji rep. Liste dolaze u tri osnovna oblika. Od najjednostavnijih do najkompleksnijih, to su • Jednostruko povezane • Dvostruko povezane • Stabla U jednostruko povezanoj listi, svaki nod pokazuje naprijed na slijedeći, ali ne i nazad. Za traženje određenog noda, krećemo od vrha i idemo od noda do noda. Duplo vezana lista omogućuje nam kretanje unaprijed i unazad kroz lanac. Stablo je kompleksna struktura izgrađena iz nodova, od kojih svaki može pokazivati na dva ili tri mjesta. Listing 11.13 pokazuje nam kako kreirati i koristiti jednostavne vezane liste. Listing 11.13. Implementiranje vezane liste. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
// Listing 11.13 // Linked list simple implementation #include // object to add to list class CAT { public: CAT() { itsAge = 1;} CAT(int age):itsAge(age){} ~CAT(){}; int GetAge() const { return itsAge; } private: int itsAge; };
18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76:
// manages list, orders by cat's age! class Node { public: Node (CAT*); ~Node(); void SetNext(Node * node) { itsNext = node; } Node * GetNext() const { return itsNext; } CAT * GetCat() const { return itsCat; } void Insert(Node *); void Display(); private: CAT *itsCat; Node * itsNext; }; Node::Node(CAT* pCat): itsCat(pCat), itsNext(0) {} Node::~Node() { cout << "Deleting node...\n"; delete itsCat; itsCat = 0; delete itsNext; itsNext = 0; } // ************************************ // Insert // Orders cats based on their ages // Algorithim: If you are last in line, add the cat // Otherwise, if the new cat is older than you // and also younger than next in line, insert it after // this one. Otherwise call insert on the next in line // ************************************ void Node::Insert(Node* newNode) { if (!itsNext) itsNext = newNode; else { int NextCatsAge = itsNext->GetCat()->GetAge(); int NewAge = newNode->GetCat()->GetAge(); int ThisNodeAge = itsCat->GetAge(); if ( NewAge >= ThisNodeAge && NewAge < NextCatsAge ) { newNode->SetNext(itsNext); itsNext = newNode; } else itsNext->Insert(newNode); } }
77: void Node::Display() 78: { 79: if (itsCat->GetAge() > 0) 80: { 81: cout << "My cat is "; 82: cout << itsCat->GetAge() << " years old\n"; 83: } 84: if (itsNext) 85: itsNext->Display(); 86: } 87: 88: int main() 89: { 90: 91: Node *pNode = 0; 92: CAT * pCat = new CAT(0); 93: int age; 94: 95: Node *pHead = new Node(pCat); 96: 97: while (1) 98: { 99: cout << "New Cat's age? (0 to quit): "; 100: cin >> age; 101: if (!age) 102: break; 103: pCat = new CAT(age); 104: pNode = new Node(pCat); 105: pHead->Insert(pNode); 106: } 107: pHead->Display(); 108: delete pHead; 109: cout << "Exiting...\n\n"; 110: return 0; 111: } Output: New Cat's age? (0 to quit): 1 New Cat's age? (0 to quit): 9 New Cat's age? (0 to quit): 3 New Cat's age? (0 to quit): 7 New Cat's age? (0 to quit): 2 New Cat's age? (0 to quit): 5 New Cat's age? (0 to quit): 0 My cat is 1 years old My cat is 2 years old My cat is 3 years old My cat is 5 years old My cat is 7 years old My cat is 9 years old Deleting node... Deleting node... Deleting node... Deleting node... Deleting node... Deleting node... Deleting node... Exiting...
Kviz 1. Koji su prvi i zadnji elementi u SomeArray[25]? 2. Kako deklarirati neko višedimenzionalno polje? 3. Inicijalizirajte članove polja iz drugog pitanja. 4. Koliko elemenata se nalazi u SomeArray[10][5][20]? 5. Koji je najveći broj elemenata koje možete dodati u vezane liste? 6. Koji je zadnji znak stringa "Josip je dobar momak "?
Vježbe 1.
Deklarirajte dvodimenzionalno polje koje predstavlja križić-kružić poligon za igru. 2. Napišite kod koji inicijalizira sve elemente u kreiranom polju iz vj.1 na vrijednost 0. 3. Napišite deklaraciju Node klase koja drži unsigned short integere. class Node { public: Node (); Node (int); ~Node(); void SetNext(Node * node) { itsNext = node; } Node * GetNext() const { return itsNext; } int GetVal() const { return itsVal; } void Insert(Node *); void Display(); private: int itsVal; Node * itsNext; };
4. BUG BUSTERS: Što ne valja u slijedećem kodu? unsigned short SomeArray[5][4]; for (int i = 0; i<4; i++) for (int j = 0; j<5; j++) SomeArray[i][j] = i+j; 5. BUG BUSTERS: Što ne valja u slijedećem kodu? unsigned short SomeArray[5][4]; for (int i = 0; i<=5; i++) for (int j = 0; j<=4; j++) SomeArray[i][j] = 0;
Lekcija 12
Naslijeđivanje
Danas ćete naučiti • Što je naslijeđivanje • Kako izvesti jednu klasu iz druge. • Što je zaštićeni pristup i kako ga koristiti. • Što su virtualne funkcije.
Što je naslijeđivanje? Toyota je marka automobila, koji je prevozno sredstvo. Tiramisu je vrsta deserta koji spada u širu nazivnu skupinu hrana. Što time mislimo reći? Mi mislimo reći da je to oblikk specijalizacije. Npr., automobil je specijalna vrsta prevoznih sredstava.
Naslijeđivanje i derivacija Pas automatski nasljeđuje svojstva sisavaca. Budući da je sisavac, znamo da se miče i da diše zrak —svi sisavci to rade po definiciji. Ali u definiciju psa ide i pojam lajanja, mahanja repom, itd. Dalje možemo podijeliti pse na lovačke pse i terijere, a terijere dijelimo na Jorkširske terijere, Dendi terijere, Bulterijere, itd. Hijerarija je predstavljena na slici 12.1. Slika 12.1.Hijerarhija životinja Životinja Sisavac Konj
Reptil Pas
Ovčar
Terijer Jorkšir
Pit Bull
C++ pokušava predstaviti ove relacije omogućavajući nam definiranje klase koja se izvodi iz druge klase. Izvođenje (derivacija) je način izražavanja odnosa među klasama. Vi derivirate novu klasu, Pas, iz klase Sisavac. Ne morate ekplicitno navesti da se pas miče, jer je to svojstvo naslijedio od sisavaca.
Životinjsko carstvo Zamislite sa ste bili upitani da dizajnirate dječiju igru—simulaciju farme.
S vremenom ćete razviti čitav niz životinja na farmi, uključujući konje, krave, pse, mačke, ovce, itd. Kreirat ćete metode za ove klase tako da one djeluju na očekivani način, ali zasad ćemo se zadovoljiti s jednostavnom print naredbom. Osjećajte se slobodnim da proširite minimalni kod kado bi omogučili životinjama realističnije ponašanje. Sintaksa derivacije Kad deklarirate klasu, možete indicirati iz koje je klase izvedena tipkajući dvotočku iza njenog imena, tip derivacije (public ili neki drugi) i klasu iz koje deriviramo. Slijedi primjer: class Dog : public Mammal Tip derivacije će biti tema nešto kasnije diskusije. Za sada, uvijek koristite public. Klasa iz koje derivirate morala je biti ranije deklarirana, ili će van kompajler javiti grešku. Listing 12.1 ilustrira kako deklarirati kalsu Dog koja je izvedena iz Mammal klase. Listing 12.1. Jednostavno naslijeđivanje. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42:
//Listing 12.1 Simple inheritance #include enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; class Mammal { public: // constructors Mammal(); ~Mammal(); //accessors int GetAge()const; void SetAge(int); int GetWeight() const; void SetWeight(); //Other methods void Speak(); void Sleep(); protected: int itsAge; int itsWeight; }; class Dog : public Mammal { public: // Constructors Dog(); ~Dog(); // Accessors BREED GetBreed() const; void SetBreed(BREED); // Other methods // WagTail();
43: // BegForFood(); 44: 45: protected: 46: BREED itsBreed; 47: }; Ovaj program nema nikakav output budući da je riječ samo odeklaraciji klase bez njihove implementacije. Ipak, imamo dosta za pogledati ovdje.
Privatne protiv zaštićenih Možda ste primjetili da je nova ključna riječ, protected, upotrebljena u linijama 24 i 25 listinga 12.1. Do nedavn su podaci u klasi uvijek bili deklarirani kao private. Ali private članovi nisu dostupni izvedenoj, odn. deriviranoj klasi. Mogli bi ih naravno proglasiti i javnima, ali to nije preporučljivo. Ne želite da druge klase pristupaju ovim članovima direktno. Listing 12.2 demonstrira kako kreirati objekte tipa Dog i pristupati podacima i funkcijama toga tipa. Listing 12.2. Upotreba deriviranog objekta. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40:
//Listing 12.2 Using a derived object #include enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; class Mammal { public: // constructors Mammal():itsAge(2), itsWeight(5){} ~Mammal(){} //accessors int GetAge()const { return itsAge; } void SetAge(int age) { itsAge = age; } int GetWeight() const { return itsWeight; } void SetWeight(int weight) { itsWeight = weight; } //Other methods void Speak()const { cout << "Mammal sound!\n"; } void Sleep()const { cout << "shhh. I'm sleeping.\n"; } protected: int itsAge; int itsWeight; }; class Dog : public Mammal { public: // Constructors Dog():itsBreed(YORKIE){} ~Dog(){} // Accessors BREED GetBreed() const { return itsBreed; } void SetBreed(BREED breed) { itsBreed = breed; }
41: // Other methods 42: void WagTail() { cout << "Tail wagging...\n"; } 43: void BegForFood() { cout << "Begging for food...\n"; } 44: 45: private: 46: BREED itsBreed; 47: }; 48: 49: int main() 50: { 51: Dog fido; 52: fido.Speak(); 53: fido.WagTail(); 54: cout << "Fido is " << fido.GetAge() << " years old\n"; 55: return 0; 56: } Output: Mammal sound! Tail wagging... Fido is 2 years old
Konstruktori i destruktori Dog objekti su Mammal objekti. Kada se stvara Fido, njegov glavni konstruktor se prvo poziva, kreirajući Mammal. Potom se poziva Dog konstruktor, kompletirajući stvaranje Dog objekta. Budući da u Fido nismo proslijedili nikakve parametre, podrazumijevani konstruktor se poziva u svakom slučaju. Fido ne postoji dok nije kompletno konstruiran, što znači da i njegov Mammal dio i njegov Dog dio moraju biti konstruirani. Samime time moraju biti pozvana i oba konstruktora. Kada je Fido uništen, prvo se poziva Dog destruktor, a potom i destruktor za Mammal dio od Fida. Svaki destruktor počisti za svojim dijelom Fida. Upamtite da morate čistiti za Vašim Psom! Listing 12.3 demonstrira ideju. Listing 12.3. Pozivi konstruktora i destruktora. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
//Listing 12.3 Constructors and destructors called. #include enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; class Mammal { public: // constructors Mammal(); ~Mammal(); //accessors int GetAge() const { return itsAge; } void SetAge(int age) { itsAge = age; } int GetWeight() const { return itsWeight; } void SetWeight(int weight) { itsWeight = weight; } //Other methods void Speak() const { cout << "Mammal sound!\n"; } void Sleep() const { cout << "shhh. I'm sleeping.\n"; } protected: int itsAge;
26: int itsWeight; 27: }; 28: 29: class Dog : public Mammal 30: { 31: public: 32: 33: // Constructors 34: Dog(); 35: ~Dog(); 36: 37: // Accessors 38: BREED GetBreed() const { return itsBreed; } 39: void SetBreed(BREED breed) { itsBreed = breed; } 40: 41: // Other methods 42: void WagTail() { cout << "Tail wagging...\n"; } 43: void BegForFood() { cout << "Begging for food...\n"; } 44: 45: private: 46: BREED itsBreed; 47: }; 48: 49: Mammal::Mammal(): 50: itsAge(1), 51: itsWeight(5) 52: { 53: cout << "Mammal constructor...\n"; 54: } 55: 56: Mammal::~Mammal() 57: { 58: cout << "Mammal destructor...\n"; 59: } 60: 61: Dog::Dog(): 62: itsBreed(YORKIE) 63: { 64: cout << "Dog constructor...\n"; 65: } 66: 67: Dog::~Dog() 68: { 69: cout << "Dog destructor...\n"; 70: } 71: int main() 72: { 73: Dog fido; 74: fido.Speak(); 75: fido.WagTail(); 76: cout << "Fido is " << fido.GetAge() << " years old\n"; 77: return 0; 78: } Output: Mammal constructor... Dog constructor... Mammal sound! Tail wagging... Fido is 1 years old Dog destructor...
Mammal destructor... Analiza: Listing 12.3 je poput Listinga 12.2, osim što konstruktori i destruktori sada ispisuju na ekran kada su pozvani. Mammal konstruktor se prvo poziva, a tek zatim Dog. U tom trenutku Dog postoji u cjelosti, i njegove metode možemo koristiti. Kad fido izađe iz dosega, Dog destruktor je pozvan, iza kojeg slijedi i poziv Mammal destruktora.
Proslijeđivanje argumenata baznim konstruktorima Moguće je da čete željeti preopteretiti konstruktor od Mammal da prima određenu starost, a da preopterećeni Dog construktor prima pasminu. Kako postižemo da se ti parametri pravilo proslijede u prikladne konstruktore u Mammal? Što ako Dog želi inicijalizirati težinu ali Mammals ne želi? Osnovna inicijalizacija klase može biti izvedena za vrijeme inicijalizacije klase upisivanjem imena osnovne klase i pripadajućim parametrima. Listing 12.4 demonstrira ovo. Listing 12.4. Preopterećenje konstruktora u izvedenim klasama. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42:
//Listing 12.4 Overloading constructors in derived classes #include enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; class Mammal { public: // constructors Mammal(); Mammal(int age); ~Mammal(); //accessors int GetAge() const { return itsAge; } void SetAge(int age) { itsAge = age; } int GetWeight() const { return itsWeight; } void SetWeight(int weight) { itsWeight = weight; } //Other methods void Speak() const { cout << "Mammal sound!\n"; } void Sleep() const { cout << "shhh. I'm sleeping.\n"; } protected: int itsAge; int itsWeight; }; class Dog : public Mammal { public: // Constructors Dog(); Dog(int age); Dog(int age, int weight); Dog(int age, BREED breed); Dog(int age, int weight, BREED breed); ~Dog(); // Accessors
43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101:
BREED GetBreed() const { return itsBreed; } void SetBreed(BREED breed) { itsBreed = breed; } // Other methods void WagTail() { cout << "Tail wagging...\n"; } void BegForFood() { cout << "Begging for food...\n"; } private: BREED itsBreed; }; Mammal::Mammal(): itsAge(1), itsWeight(5) { cout << "Mammal constructor...\n"; } Mammal::Mammal(int age): itsAge(age), itsWeight(5) { cout << "Mammal(int) constructor...\n"; } Mammal::~Mammal() { cout << "Mammal destructor...\n"; } Dog::Dog(): Mammal(), itsBreed(YORKIE) { cout << "Dog constructor...\n"; } Dog::Dog(int age): Mammal(age), itsBreed(YORKIE) { cout << "Dog(int) constructor...\n"; } Dog::Dog(int age, int weight): Mammal(age), itsBreed(YORKIE) { itsWeight = weight; cout << "Dog(int, int) constructor...\n"; } Dog::Dog(int age, int weight, BREED breed): Mammal(age), itsBreed(breed) { itsWeight = weight; cout << "Dog(int, int, BREED) constructor...\n"; }
102: 103: Dog::Dog(int age, BREED breed): 104: Mammal(age), 105: itsBreed(breed) 106: { 107: cout << "Dog(int, BREED) constructor...\n"; 108: } 109: 110: Dog::~Dog() 111: { 112: cout << "Dog destructor...\n"; 113: } 114: int main() 115: { 116: Dog fido; 117: Dog rover(5); 118: Dog buster(6,8); 119: Dog yorkie (3,YORKIE); 120: Dog dobbie (4,20,DOBERMAN); 121: fido.Sp Speak(); 122: rover.WagTail(); 123: cout << "Yorkie is " << yorkie.GetAge() << " years old\n"; 124: cout << "Dobbie weighs "; 125: cout << dobbie.GetWeight() << " pounds\n"; 126: return 0; 127: } Pažnja: Ispis je pobrojan kako bi se mogli referencirati na pojedine linije tjekom analize. Output: 1: Mammal constructor... 2: Dog constructor... 3: Mammal(int) constructor... 4: Dog(int) constructor... 5: Mammal(int) constructor... 6: Dog(int, int) constructor... 7: Mammal(int) constructor... 8: Dog(int, BREED) constructor.... 9: Mammal(int) constructor... 10: Dog(int, int, BREED) constructor... 11: Mammal sound! 12: Tail wagging... 13: Yorkie is 3 years old. 14: Dobbie weighs 20 pounds. 15: Dog destructor. . . 16: Mammal destructor... 17: Dog destructor... 18: Mammal destructor... 19: Dog destructor... 20: Mammal destructor... 21: Dog destructor... 22: Mammal destructor... 23: Dog destructor... 24: Mammal destructor... Analiza: U Listingu 12.4, Mammal konstruktor je preopterećen u liniji 11 kako bi uzimao cijeli broj, Mammal age. Implementacija u linijama 61-66 inicijalizira itsAge s vrijednošću proslijeđenoj u konstruktor i inicijalizira itsWeight s vrijednošću 5.
Dog ima pet preopterećenih konstruktora, u linijama 35-39. Prvi je podrazumijevani konstruktor. Drugi prima godine, što je isti parametar koji uzima i Mammal konstruktor. Treći konstruktor prima i godine i težinu, četvrti godine i pasminu, a peti godine, težinu i pasminu. Primjetite da u liniji 74 podrazumijevani konstruktor od Dog poziva Mammal podrazumijevani konstruktor. Iako nije striktno neophodno to učiniti, služi nam kao dokumentacija da smo željeli pozvati bazni konstruktor bez parametara. Bazni konstruktor bi bio svakako pozvan, ali čineći to naše namjere su razumljivije. Sama implementacija Dog konstruktora, koji prima cijeli broj, je u linijama 80-85. U svojoj inicijalizacijskoj fazi (linije 81-82), Dog inicijalizira svoju baznu klasu, proslijeđujući parametar, a potom inicijalizira svoju pasminu, its breed. Drugi Dog konstruktor je u linijama 87-93. Obvaj prima dva parametra. Još jednom on inicijalizira svoju baznu klasu s pozivom odgovarajućeg konstruktora, ali ovaj put također pridružuje weight u varijablu bazne klase itsWeight. Primjetite da ne možete pridruživati u baznu klasu u inicijalizacijskoj fazi. Budući da Mammal nema konstruktor koji prima taj parametar, to morate učiniti unutar tijela Dog konstruktora.
Zaobilaženje funkcija Dog objekt ima pristup do svih funkcijskih članova klase Mammal, kao i do bilo koje vlastite funkcije, poput WagTail(). On također može i zaobilaziti funkcije bazne klase. Zaobilaženje funkcije znači mijenjanje implementacije funkcije iz bazne klase u deriviranoj klasi. Kada napravite objekt derivirane klase, prava funkcija je pozvana. Novi izraz: Kada derivirana klasa stvori funkciju istog povratnog tipa i potpisa kao i funkcijski član bazne klase, ali sa novom implementacijom, kažemo da ona zaobilazi tu metodu. Kad zaobilazite funkciju. moraju se podudarati povratni tipovi i potpisi samih funkcija. Potpiss je funkcijski prototip osim povratnog tipa, odnosno :ime funkcije, lista parametara, te riječ const ako je upotrebljena. Listing 12.5 ilustrira što se događa sa Dog klasom koja zaobilazi Speak() metodu u Mammal. Za uštedu prostora, pristupne funkcije su izbačene iz ovih klasa. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29:
//Listing 12.5 Overriding a base class method in a derived class #include enum BREED { YORKIE, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB }; class Mammal { public: // constructors Mammal() { cout << "Mammal constructor...\n"; } ~Mammal() { cout << "Mammal destructor...\n"; } //Other methods void Speak()const { cout << "Mammal sound!\n"; } void Sleep()const { cout << "shhh. I'm sleeping.\n"; } protected: int itsAge; int itsWeight; }; class Dog : public Mammal { public: // Constructors Dog(){ cout << "Dog constructor...\n"; } ~Dog(){ cout << "Dog destructor...\n"; }
30: 31: // Other methods 32: void WagTail() { cout << "Tail wagging...\n"; } 33: void BegForFood() { cout << "Begging for food...\n"; } 34: void Speak()const { cout << "Woof!\n"; } 35: 36: private: 37: BREED itsBreed; 38: }; 39: 40: int main() 41: { 42: Mammal bigAnimal; 43: Dog fido; 44: bigAnimal.Speak(); 45: fido.Speak(); 46: return 0; 47: } Output: Mammal constructor... Mammal constructor... Dog constructor... Mammal sound! Woof! Dog destructor... Mammal destructor... Mammal destructor...
Skrivanje metoda osnovne klase U prethodnom listingu, Speak() metoda Dog klase sakriva metodu bazne klase. To je upravo ono što smo i željeli, ali može imati neočekivane rezultate. Ako npr., Mammal ima preopterećenu metodu, Move(),a Dog zaobilazi tu metodu, Dog metoda će sakriti sve Mammal metode sa tim imenom. Ako Mammal preopterećuje Move() sa tri metode—jednom bez parametara, jednom koja prima integer, te jednom koja prima integer i smjer—a Dog zaobilazi samo Move() metodu koja ne prima parametre, neće biti jednostavno pristupiti preostalim dvjema metodama koristeći Dog objekt. Listing 12.6 ilustrira taj problem. Listing 12.6. Skrivanje metoda. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
//Listing 12.6 Hiding methods #include class Mammal { public: void Move() const { cout << "Mammal move one step\n"; } void Move(int distance) const { cout << "Mammal move "; cout << distance <<" _steps.\n"; } protected: int itsAge; int itsWeight; }; class Dog : public Mammal {
21: public: 22: // You may receive a warning that you are hiding a function! 23: void Move() const { cout << "Dog move 5 steps.\n"; } 24: }; 25: 26: int main() 27: { 28: Mammal bigAnimal; 29: Dog fido; 30: bigAnimal.Move(); 31: bigAnimal.Move(2); 32: fido.Move(); 33: // fido.Move(10); 34: return 0; 35: } Output: Mammal move one step Mammal move 2 steps. Dog move 5 steps.
Zaobilaženje protiv skrivanja U slijedećem odlomku bit će opisane virtualne metode. Zaobilaženje virtualnih metoda podržava polimorfizam —njihovo skrivanje ga "potkopava". Uskoro ćete naučiti više o tome.
Pozivanje bazne metode Ako ste zaobišli baznu metodu, i dalje ju je moguće pozvati potpunim navođenjem imena metode. To postižete pisanjem imena baze, dvije dvotočke i potom imena metode. Na primjer, Mammal::Move(). Moguće je prepraviti liniju 28 u listingu 12.6 kako bi bio kompletan, pišući 28: fido.Mammal::Move(10); Time ekplicitno pozivamo metodu klase Mammal. Listing 12.7 ilustrira tu ideju.
Listing 12.7. Pozivanje bazne metode iz zaobiđene metode. 1: //Listing 12.7 Calling base method from overridden method. 2: 3: #include 4: 5: class Mammal 6: { 7: public: 8: void Move() const { cout << "Mammal move one step\n"; } 9: void Move(int distance) const 10: { 11: cout << "Mammal move " << distance; 12: cout << " steps.\n"; 13: } 14: 15: protected: 16: int itsAge; 17: int itsWeight; 18: }; 19: 20: class Dog : public Mammal 21: { 22: public: 23: void Move()const; 24: 25: }; 26: 27: void Dog::Move() const 28: { 29: cout << "In dog move...\n"; 30: Mammal::Move(3); 31: } 32: 33: int main() 34: { 35: Mammal bigAnimal; 36: Dog fido; 37: bigAnimal.Move(2); 38: fido.Mammal::Move(6); 39: return 0; 40: } Output: Mammal move 2 steps. Mammal move 6 steps. Analiza: U liniji 35, Mammal, bigAnimal, je stvoren, a u linijii 36 i Dog, fido. Poziv metode u liniji 37 poziva Move() metodu od Mammal, koja uzima int. Programer je želio pozvati Move(int) za Dog objekt, ali ima problem. Dog zaobilazi Move() metodu, ali ju ne preopterećuje i ne pruža verziju koja prima int. To je riješeno eksplicitnom pozivom metode iz bazne klase Move(int) u liniji 33.
Virtualne metode Ova lekcija je naglasila činjenicu da je Dog objekt ujedno i Mammal objekt. Do sada je to značilo da je Dog objekt naslijedio sve atribute (podatke) i sposobnosti (metode) bazne klase. U C++ jezuku njihova je relacija i mnogo dublja od toga. C++ proširuje svoj polimorfizam kako bi omogućio pokazivačima na bazne klase da budu pridruženi izvedenim objektima. Tako možete pisati
Mammal* pMammal = new Dog; Ovime kreiramo novi Dog objekt u slobodnom spremniku i vraćamo pokazivač na taj objekt, kojeg potom pridružujemo pokazivaču na Mammal. To je u redu, budući da je pas i sisavac. PAŽNJA: Ovo je bit polimorfizma. Na primjer, možete kreirati mnoštvo različitih tipova prozora, uključujući i dijaloške okvire, srolajuće prozore, i liste, te im svima dodjeliti virtualnu draw() metodu. Kreirajući pokazivač na prozor i pridružujući dijaloške okvire i ostale derivirane tipove na taj pokazivač, vi možete zvati draw() bez obzira na trenutni tip objekta na kojeg pokazujemo. Ispravna draw() funkcija će biti pozvana. Potom možete koristiti taj pokazivač za pokretanje bilo koje Mammal metode. Ono što biste željeli je da one metode koje su zaobiđene u Dog() pozovu pravilnu funkciju. Virtualne funkcije omogućuju nam upravo to. Listing 12.8 ilustrira kako to radi, te što se događa sa ne-virtualnim metodama. Listing 12.8. Upotreba virtualnih metoda. 1: //Listing 12.8 Using virtual methods 2: 3: #include 4: 5: class Mammal 6: { 7: public: 8: Mammal():itsAge(1) { cout << "Mammal constructor...\n"; } 9: ~Mammal() { cout << "Mammal destructor...\n"; } 10: void Move() const { cout << "Mammal move one step\n"; } 11: virtual void Speak() const { cout << "Mammal speak!\n"; } 12: protected: 13: int itsAge; 14: 15: }; 16: 17: class Dog : public Mammal 18: { 19: public: 20: Dog() { cout << "Dog Constructor...\n"; } 21: ~Dog() { cout << "Dog destructor...\n"; } 22: void WagTail() { cout << "Wagging Tail...\n"; } 23: void Speak()const { cout << "Woof!\n"; } 24: void Move()const { cout << "Dog moves 5 steps...\n"; } 25: }; 26: 27: int main() 28: { 29: 30: Mammal *pDog = new Dog; 31: pDog->Move(); 32: pDog->Speak(); 33: 34: return 0; 35: } Output: Mammal constructor... Dog Constructor... Mammal move one step Woof! Analiza: U liniji 11, Mammal ima virtualnu metodu--speak(). Programer ove klase time signalizira da očekuje kako će ova klasa eventualno postati bazna klasa neke druge klase. Izvedena klasa će vjerojatno željeti zaobići ovu funkciju.
U liniji 30, pokazivač na Mammal je kreiran (pDog), ali mu je pridružena adresa novoga Dog objekta. Budući da je pas sisavac, ovo je legalno pridruživanje. Pointer nam potom služi za pozivanje Move() funkcije. Budući da kompajler zna samo da je pDog Mammal, on u Mammal objektu traži Move() metodu. U liniji 32, pokazivač potom poziva Speak() metodu. Budući da je Speak() virtualan, zaobiđena Speak() metoda u Dog se pokreće. Ovo je gotovo magično. Koliko je pozivna funkcija znala, imala je Mammal pokazivač. ali je ipak pozvana metoda iz Dog klase. U stvari, da ste imali poloje pokazivača na Mammal, od kojih svaki pokazuje na podklasu od Mammal, mogli bi pozivati svakog od njih i prava funkcija bi bila pozvana. Listing 12.9 ilustrira tu ideju. Listing 12.9. Višestruke virtualne funkcije naizmjenično pozivane. 1: //Listing 12.9 Multiple virtual functions called in turn 2: 3: #include 4: 5: class Mammal 6: { 7: public: 8: Mammal():itsAge(1) { } 9: ~Mammal() { } 10: virtual void Speak() const { cout << "Mammal speak!\n"; } 11: protected: 12: int itsAge; 13: }; 14: 15: class Dog : public Mammal 16: { 17: public: 18: void Speak()const { cout << "Woof!\n"; } 19: }; 20: 21: 22: class Cat : public Mammal 23: { 24: public: 25: void Speak()const { cout << "Meow!\n"; } 26: }; 27: 28: 29: class Horse : public Mammal 30: { 31: public: 32: void Speak()const { cout << "Winnie!\n"; } 33: }; 34: 35: class Pig : public Mammal 36: { 37: public: 38: void Speak()const { cout << "Oink!\n"; } 39: }; 40: 41: int main() 42: { 43: Mammal* theArray[5]; 44: Mammal* ptr; 45: int choice, i; 46: for ( i = 0; i<5; i++) 47: { 48: cout << "(1)dog (2)cat (3)horse (4)pig: ";
49: cin >> choice; 50: switch (choice) 51: { 52: case 1: ptr = new Dog; 53: break; 54: case 2: ptr = new Cat; 55: break; 56: case 3: ptr = new Horse; 57: break; 58: case 4: ptr = new Pig; 59: break; 60: default: ptr = new Mammal; 61: break; 62: } 63: theArray[i] = ptr; 64: } 65: for (i=0;i<5;i++) 66: theArray[i]->Speak(); 67: return 0; 68: } Output: (1)dog (2)cat (3)horse (4)pig: 1 (1)dog (2)cat (3)horse (4)pig: 2 (1)dog (2)cat (3)horse (4)pig: 3 (1)dog (2)cat (3)horse (4)pig: 4 (1)dog (2)cat (3)horse (4)pig: 5 Woof! Meow! Winnie! Oink! Mammal speak!
Kako virtualne funkcije rade Kada se izvedeni objekt, poput Dog, kreira, prvo se poziva konstruktor bazne klase a potom konstruktor derivirane klase. Slika 12.2 pokazuje kako Dog objekt izgleda nakon to je kreiran. Primjetite da je Mammal dio objekta u memorijskom bloku zajedno s Dog dijelom. Slika 12.2. Dog objekt nakon kreiranja.
Kad je virtualna funkcija kreirana u objektu, objekt mora biti svjestan njenog postojanja. Mnogi kompajleri grade tablicu virtualnih funkcija, zvanu v-table. Jedna tablica se kreira za svaki tip, i svaki objekt tog tipa sadrži pokazivač na virtualnu tablicu (zvan vptr ili v-pointer), koji pokazuje na tu tablicu. Dok implementacije variraju, svi kompajleri moraju postići istu stvar, pa ova definicija nije "previše pogrešna". Slika 12.3. v-table od Mammal.
vptr svakog objekta pokazuje na v-tablicu koja, zauzvrat, ima pokazivač na svaku od virtualnih funkcija.Kada je Mammal dio od Dog kreiran, vptr je inicijaliziran da pokazuje na korektan dio v-tablice, kako je prikazano na slici 12.3.
Slika 12.4. v-table od Dog.
Kada pozovemo Dog konstruktor, i Dog dio tog objekta se dodaje, vptr se prilagođava kako bi pokazivao na zaobiđene virtualne funkcije (ako ih ima) u Dog objektu (vidi sl. 12.4) . Kada koristimo pokazivač na Mammal, vptr nastavlja pokazivati na pravu funkciju, ovisno o tipu objekta, Prema tome, kada pozovemo Speak(), prava funkcija se pokreće.
Ne možete stići tamo odavde Da je Dog objekt imao metodu, WagTail(), koja se ne nalazi u Mammal, ne biste mogli koristiti pokazivač na Mammal kako biste pristupili toj metodi (osim ako ju proglasite pokazivačem na Dog). Budući da WagTail() nije virtualna funkcija, i budući da nije Mammal objekt, ne možete joj pristupiti bez bilo Dog objekta bilo Dog pokazivača. Iako možete transformirati Mammal pokazivač u Dog pokazivač, obično postoje mnogo bolji i sigurniji načini za poziv WagTail() metode.
Rezanje Primjetite da magija virtualnih funkcija djeluje samo na pokazivače i reference. Proslijeđivanje objekta po vrijednosti neće omogućiti pokretanje virtualne funkcije. Listing 12.10 ilustrira taj problem. Listing 12.10. Rezanje podataka kad se proslijeđuju po vrijednosti. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28 29: 30: 31:
//Listing 12.10 Data slicing with passing by value #include enum BOOL { FALSE, TRUE }; class Mammal { public: Mammal():itsAge(1) { } ~Mammal() { } virtual void Speak() const { cout << "Mammal speak!\n"; } protected: int itsAge; }; class Dog : public Mammal { public: void Speak()const { cout << "Woof!\n"; } }; class Cat : public Mammal { public: void Speak()const { cout << "Meow!\n"; } }; void ValueFunction (Mammal); void PtrFunction (Mammal*); void RefFunction (Mammal&); int main()
32: { 33: Mammal* ptr=0; 34: int choice; 35: while (1) 36: { 37: BOOL fQuit = FALSE; 38: cout << "(1)dog (2)cat (0)Quit: "; 39: cin >> choice; 40: switch (choice) 41: { 42: case 0: fQuit = TRUE; 43: break; 44: case 1: ptr = new Dog; 45: break; 46: case 2: ptr = new Cat; 47: break; 48: default: ptr = new Mammal; 49: break; 50: } 51: if (fQuit) 52: break; 53: PtrFunction(ptr); 54: RefFunction(*ptr); 55: ValueFunction(*ptr); 56: } 57: return 0; 58: } 59: 60: void ValueFunction (Mammal MammalValue) 61: { 62: MammalValue.Speak(); 63: } 64: 65: void PtrFunction (Mammal * pMammal) 66: { 67: pMammal->Speak(); 68: } 69: 70: void RefFunction (Mammal & rMammal) 71: { 72: rMammal.Speak(); 73: } Output: (1)dog (2)cat (0)Quit: 1 Woof Woof Mammal Speak! (1)dog (2)cat (0)Quit: 2 Meow! Meow! Mammal Speak! (1)dog (2)cat (0)Quit: 0
Virtualni destruktori Legalno je i prilično često proslijediti pokazivač na izvedeni objekt kada očekujemo pokazivač na bazni objekt. Što se događa kada obrišemo takav pokazivač na izvedeni objekt? Ako je destruktor virtualan, kako bi i trebao biti, dogodit će se "prava stvar"—bit će pozvan destruktor derivirane klase. Budući da će destruktor izvedene klase automatski pozvati i bazni destruktor, cijeli objekt će biti pravilno izbrisan. Pravilo glasi: Ako je bilo koja funkcija u klasi virtualna, trebao bi biti i destruktor.
Virtualni konstruktori kopije Kao što smo prethodno rekli, niti jedan konstruktor ne može biti virtualan. Ipak, ponekad nam je neophodna mogućnost proslijeđivanja pokazivača na bazni objekt i držanja kopije pravilno izvedenog objekta. Često rješenje toga problema je kreiranje Clone() metode u baznoj klasi i proglašavanje je virtualnom. Clone() metoda kreira kopiju novog objekta trenutne klase, te vraća taj objekt. Budući da svaka derivirana klasa zaobilazi Clone() metodu, kopija derivirane klase se kreira. Listing 12.11 ilustrira kako se to koristi. Listing 12.11. Virtualni konstruktor kopije. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47:
//Listing 12.11 Virtual copy constructor #include class Mammal { public: Mammal():itsAge(1) { cout << "Mammal constructor...\n"; } ~Mammal() { cout << "Mammal destructor...\n"; } Mammal (const Mammal & rhs); virtual void Speak() const { cout << "Mammal speak!\n"; } virtual Mammal* Clone() { return new Mammal(*this); } int GetAge()const { return itsAge; } protected: int itsAge; }; Mammal::Mammal (const Mammal & rhs):itsAge(rhs.GetAge()) { cout << "Mammal Copy Constructor...\n"; } class Dog : public Mammal { public: Dog() { cout << "Dog constructor...\n"; } ~Dog() { cout << "Dog destructor...\n"; } Dog (const Dog & rhs); void Speak()const { cout << "Woof!\n"; } virtual Mammal* Clone() { return new Dog(*this); } }; Dog::Dog(const Dog & rhs): Mammal(rhs) { cout << "Dog copy constructor...\n"; } class Cat : public Mammal { public: Cat() { cout << "Cat constructor...\n"; } ~Cat() { cout << "Cat destructor...\n"; } Cat (const Cat &); void Speak()const { cout << "Meow!\n"; } virtual Mammal* Clone() { return new Cat(*this); } };
48: 49: Cat::Cat(const Cat & rhs): 50: Mammal(rhs) 51: { 52: cout << "Cat copy constructor...\n"; 53: } 54: 55: enum ANIMALS { MAMMAL, DOG, CAT}; 56: const int NumAnimalTypes = 3; 57: int main() 58: { 59: Mammal *theArray[NumAnimalTypes]; 60: Mammal* ptr; 61: int choice, i; 62: for ( i = 0; i> choice; 66: switch (choice) 67: { 68: case DOG: ptr = new Dog; 69: break; 70: case CAT: ptr = new Cat; 71: break; 72: default: ptr = new Mammal; 73: break; 74: } 75: theArray[i] = ptr; 76: } 77: Mammal *OtherArray[NumAnimalTypes]; 78: for (i=0;iSpeak(); 81: OtherArray[i] = theArray[i]->Clone(); 82: } 83: for (i=0;iSpeak(); 25: return 0; 86: } 1: (1)dog (2)cat (3)Mammal: 1 2: Mammal constructor... 3: Dog constructor... 4: (1)dog (2)cat (3)Mammal: 2 5: Mammal constructor... 6: Cat constructor... 7: (1)dog (2)cat (3)Mammal: 3 8: Mammal constructor... 9: Woof! 10: Mammal Copy Constructor... 11: Dog copy constructor... 12: Meow! 13: Mammal Copy Constructor... 14: Cat copy constructor... 15: Mammal speak! 16: Mammal Copy Constructor... 17: Woof! 18: Meow! 19: Mammal speak!
Kviz 1. Što je v-table? 2. Što je virtualni destruktor? 3. Kako pokazujemo deklaraciju virtualnog konstruktora? 4. Kako kreiramo virtualni konstruktor kopije? 5. Kako pozivamo funkciju bazne klase iz derivirane klase u kojoj ste zaobišli tu funkciju? 6. Kako pozivamo baznu funkciju iz izvedene klase u kojoj nismo zaobilazili funkciju? 7. Ako bazna klasa deklarira funkciju virtualnom, a izvedena klasa ne koristi izraz virtual kad zaobilazi klasu, da li je ona još uvije kvirtualna ako ju naslijedi treća gemneracija klase? 8. Kada koristimo protected ključnu riječ?
Vježbe 1. Pokaži deklaraciju virtualne funkcije koja uzima cjelobrojni parametar i vraća void. 2. Pokažite deklaraciju klase Square, koja se izvodi iz Rectangle, kojii se izvodi iz Shape. 3. Ako, u vj.2, Shape ne uzima parametre, Rectangle uzima dva (length i width), ali Square uzima samo jedan (length), pokažite inicijalizaciju konstruktora za Square. 4. Napišite virtualni konstruktor kopije za klasu Square (vj. 3). 5. BUG BUSTERS: Što ne valja? void SomeFunction (Shape); Shape * pRect = new Rectangle; SomeFunction(*pRect); 6. BUG BUSTERS: Što ne valja? class Shape() { public: Shape(); virtual ~Shape(); virtual Shape(const Shape&); };
Lekcija 13
Polimorfizam
U prošloj ste lekciji naučili kako pisati virtualne funkcije u izvedenim klasama. To je osnovni građevni blok polimorfizma: sposobnost vezanja specifičnih izvedenih objekata na pokazivače bazne klase. Danas ćemo vidjeti što je to višestruko naslijeđivanje i kako se koristi. • Što je virtualno naslijeđivanje. • Što su apstraktni tipovi podataka (ADT engl. Abstract Data Types). • Što su čiste virtualne funkcije.
Problemi s jednostrukim naslijeđivanjem Pretpostavimo da ste radili sa vašim klasama životinja neko vrijeme, te ste podijelili hijerarhiju klasa na Birds i Mammals. Bird klasa sadrži funkcijski član Fly(). Mammal klasa je podijeljena na čitav niz podtipova od Mammal, uključujući Horse. Horse klasa sadrži funkcijske članove Whinny() i Gallop(). Iznenada shvatite da vam je potreban Pegasus objekt: mješanac između konja i ptice. Pegasus zna Fly(), ali i Whinny(), i zna Gallop(). S jednostrukim naslijeđivanjem sada ste u priličnoj "gabuli". Možete proglasiti Pegasus kao Bird, ali onda neće znati ni Whinny() ni Gallop(). Možete ga načiniti kao Horse, ali tada neće znati Fly(). Prvo rješenje bilo bi kopiranje Fly() metode u Pegasus klasu i izvođenje Pegasus iz Horse. Ovo radi, ali pod cijenu imanja dvije istovjetene Fly() metode na dva mjesta (Bird i Pegasus). Ako promjenite jednu, morate se sjetiti promjeniti i drugu. Naravno, programer koji dođe raditi mjesecima nakon vas mora također znati da se promjene vrše na dva mjesta. Uskoro dolazimo i do novog problema. Vi želite napraviti listu svih Horse objekata i listu svih Bird objekata. Željeli biste staviti svoj Pegasus objekt na obje liste, ali ako je Pegasus horse, ne možete ga staviti na listu od birds. Postoji nekoliko potencijalnih rješenja. Možete preimenovati Horse metodu Gallop() u Move(), i potom zaobići Move() u svom Pegasus objektu da radi posao od Fly(). Potom biste zaobišli Move() u drugim konjima da radi posao od Gallop(). Možda bi čak Pegasus mogao biti dovoljno pametan da galopira na kraćim relacijama, a leti na dužim. Pegasus::Move(long distance) { if (distance > veryFar) fly(distance); else gallop(distance); } Ovo nas pomalo ograničava. Možda će jednoga dana naš Pegasus zaželjeti letjeti i na kraćoj relaciji ili galopirati na duljoj. Slijedeće rješenje bilo bi da pomaknemo Fly() u Horse, kao na listingu 13.1. Problem je što većina konja ipak ne leti, pa morate natjerati tu metodu da ništa ne radi osim ako je riječ o Pegasus. Listing 13.1. Kad bi konji letjeli... 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
// Listing 13.1. If horses could fly... // Percolating Fly() up into Horse #include class Horse { public: void Gallop(){ cout << "Galloping...\n"; } virtual void Fly() { cout << "Horses can't fly.\n" ; } private: int itsAge; };
15: class Pegasus : public Horse 16: { 17: public: 18: virtual void Fly() { cout << "I can fly! I can fly! I can fly!\n"; } 19: }; 20: 21: const int NumberHorses = 5; 22: int main() 23: { 24: Horse* Ranch[NumberHorses]; 25: Horse* pHorse; 26: int choice,i; 27: for (i=0; i> choice; 31: if (choice == 2) 32: pHorse = new Pegasus; 33: else 34: pHorse = new Horse; 35: Ranch[i] = pHorse; 36: } 37: cout << "\n"; 38: for (i=0; iFly(); 41: delete Ranch[i]; 42: } 43: return 0; 44: } Output: (1)Horse (2)Pegasus: 1 (1)Horse (2)Pegasus: 2 (1)Horse (2)Pegasus: 1 (1)Horse (2)Pegasus: 2 (1)Horse (2)Pegasus: 1 Horses can't fly. I can fly! I can fly! I can fly! Horses can't fly. I can fly! I can fly! I can fly! Horses can't fly.
Višestruko naslijeđivanje Moguće je izvesti novu klasu iz više od jedne bazne klase. Ovo se zove višestruko naslijeđivanje. Kako bi izveli iz više baznih klasa, svaku baznu klasu navodimo u opsiu klase odvojenu zarezom. Listing 13.3 ilustrira kako deklarirati Pegasus tako da se izvodi i iz Horses i Birds. Program potom dodaje Pegasus objekte na liste oba tipa. Listing 13.3. Višestruko naslijeđivanje. 1: 2: 3: 4: 5: 6: 7: 8:
// Listing 13.3. Multiple inheritance. // Multiple Inheritance #include class Horse { public:
9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67:
Horse() { cout << "Horse constructor... "; } virtual ~Horse() { cout << "Horse destructor... "; } virtual void Whinny() const { cout << "Whinny!... "; } private: int itsAge; }; class Bird { public: Bird() { cout << "Bird constructor... "; } virtual ~Bird() { cout << "Bird destructor... "; } virtual void Chirp() const { cout << "Chirp... "; } virtual void Fly() const { cout << "I can fly! I can fly! I can fly! "; } private: int itsWeight; }; class Pegasus : public Horse, public Bird { public: void Chirp() const { Whinny(); } Pegasus() { cout << "Pegasus constructor... "; } ~Pegasus() { cout << "Pegasus destructor... "; } }; const int MagicNumber = 2; int main() { Horse* Ranch[MagicNumber]; Bird* Aviary[MagicNumber]; Horse * pHorse; Bird * pBird; int choice,i; for (i=0; i<MagicNumber; i++) { cout << "\n(1)Horse (2)Pegasus: "; cin >> choice; if (choice == 2) pHorse = new Pegasus; else pHorse = new Horse; Ranch[i] = pHorse; } for (i=0; i<MagicNumber; i++) { cout << "\n(1)Bird (2)Pegasus: "; cin >> choice; if (choice == 2) pBird = new Pegasus; else pBird = new Bird; Aviary[i] = pBird; } cout << "\n";
68: for (i=0; i<MagicNumber; i++) 69: { 70: cout << "\nRanch[" << i << "]: " ; 71: Ranch[i]->Whinny(); 72: delete Ranch[i]; 73: } 74: 75: for (i=0; i<MagicNumber; i++) 76: { 77: cout << "\nAviary[" << i << "]: " ; 78: Aviary[i]->Chirp(); 79: Aviary[i]->Fly(); 80: delete Aviary[i]; 81: } 82: return 0; 83: } Output: (1)Horse (2)Pegasus: 1 Horse constructor... (1)Horse (2)Pegasus: 2 Horse constructor... Bird constructor... Pegasus constructor... (1)Bird (2)Pegasus: 1 Bird constructor... (1)Bird (2)Pegasus: 2 Horse constructor... Bird constructor... Pegasus constructor... Ranch[0]: Whinny!... Horse destructor... Ranch[1]: Whinny!... Pegasus destructor... Bird destructor... Horse destructor... Aviary[0]: Chirp... I can fly! I can fly! I can fly! Bird destructor... Aviary[1]: Whinny!... I can fly! I can fly! I can fly! Pegasus destructor... Bird destructor... Horse destructor... Aviary[0]: Chirp... I can fly! I can fly! I can fly! Bird destructor... Aviary[1]: Whinny!... I can fly! I can fly! I can fly! Pegasus destructor.. Bird destructor... Horse destructor... Analiza: U linijama 6-14, Horse klasa je deklarirana. Konstruktor i destruktor ispisuju poruku, a Whinny() metoda ispisuje poruku Whinny! U linijama 16-25, Bird klasa je deklarirana. Uz dodatak konstruktoru i destruktoru, ova klasa ima dvije metode: Chirp() i Fly(). Konačno, u linijama 30-36, klasa Pegasus je deklarirana. Ona izvodi i iz Horse i Bird. Pegasus klasa zaobilazi Chirp() metodu pozivanjem Whinny() metode, koju nasljeđuje iz Horse. Dvije liste su kreirane, Ranch s pokazivačima na Horse u liniji 41, te Aviary s pokazivačima na Bird u liniji 42. U linijama 46-55, Horse i Pegasus objekti su dodani u Ranch. U linijama 56-65, Bird i Pegasus objekti su dodani u Aviary. Pozivanje virtualnih metoda i na Bird pokazivače i na Horse pokazivače čini prave stvari za Pegasus objekte. Na primjer, u liniji 78 članovi Aviary niza pozivaju Chirp() na pokazane objekte. Bird klasa deklarira ovo virtualnom metodom, pa se prava funkcija poziva za svaki objekt. Primjetite da za svaki kreirani Pegasus objekt, izlaz reflektira da se i Bird dio i Horse dio Pegasus objekta također kreira. Kada je Pegasus objekt uništen, Bird i Horse dijelovi se također uništavaju, zahvaljujući virtualnim destruktorima.
Dijelovi višestruko naslijeđenog objekta Kada je Pegasus objekt kreiran u memoriji, obje osnovne klase formiraju dio Pegasus objekta, kao što je prikazano na slici 13.1. Slika 13.1. Višestruko naslijeđeni objekti.
Mnogo se pitanja postavlja kod objekata izvedenih iz više baznih klasa. Na primjer, što se događa ako dvije bazne klase imaju isto ime neke virtualne funkcije ili nekog podatka? Kako se inicijaliziraju konstruktori za ovako izvedene klase? Što ako višestruke bazne klase potječu iz iste klase?
Konstruktori u višestruko naslijeđenim objektima Ako Pegasus derivira i iz Horse i Bird, a svaka od baznih klasa ima konstruktore koji primaju parametre, Pegasus klasa inicijalizira te konstruktore naizmjenično. Listing 13.4 ilustrira kako se to radi. Listing 13.4. Pozivi višestrukih konstruktora. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41:
// Listing 13.4 // Calling multiple constructors #include typedef int HANDS; enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ; enum BOOL { FALSE, TRUE }; class Horse { public: Horse(COLOR color, HANDS height); virtual ~Horse() { cout << "Horse destructor...\n"; } virtual void Whinny()const { cout << "Whinny!... "; } virtual HANDS GetHeight() const { return itsHeight; } virtual COLOR GetColor() const { return itsColor; } private: HANDS itsHeight; COLOR itsColor; }; Horse::Horse(COLOR color, HANDS height): itsColor(color),itsHeight(height) { cout << "Horse constructor...\n"; } class Bird { public: Bird(COLOR color, BOOL migrates); virtual ~Bird() {cout << "Bird destructor...\n"; } virtual void Chirp()const { cout << "Chirp... "; } virtual void Fly()const { cout << "I can fly! I can fly! I can fly! "; } virtual COLOR GetColor()const { return itsColor; } virtual BOOL GetMigration() const { return itsMigration; } private: COLOR itsColor;
42: BOOL itsMigration; 43: }; 44: 45: Bird::Bird(COLOR color, BOOL migrates): 46: itsColor(color), itsMigration(migrates) 47: { 48: cout << "Bird constructor...\n"; 49: } 50: 51: class Pegasus : public Horse, public Bird 52: { 53: public: 54: void Chirp()const { Whinny(); } 55: Pegasus(COLOR, HANDS, BOOL,long); 56: ~Pegasus() {cout << "Pegasus destructor...\n";} 57: virtual long GetNumberBelievers() const 58: { 59: return itsNumberBelievers; 60: } 61: 62: private: 63: long itsNumberBelievers; 64: }; 65: 66: Pegasus::Pegasus( 67: COLOR aColor, 68: HANDS height, 69: BOOL migrates, 70: long NumBelieve): 71: Horse(aColor, height), 72: Bird(aColor, migrates), 73: itsNumberBelievers(NumBelieve) 74: { 75: cout << "Pegasus constructor...\n"; 76: } 77: 78: int main() 79: { 80: Pegasus *pPeg = new Pegasus(Red, 5, TRUE, 10); 81: pPeg->Fly(); 82: pPeg->Whinny(); 83: cout << "\nYour Pegasus is " << pPeg->GetHeight(); 84: cout << " hands tall and "; 85: if (pPeg->GetMigration()) 86: cout << "it does migrate."; 87: else 88: cout << "it does not migrate."; 89: cout << "\nA total of " << pPeg->GetNumberBelievers(); 90: cout << " people believe it exists.\n"; 91: delete pPeg; 92: return 0; 93: } Output: Horse constructor... Bird constructor... Pegasus constructor... I can fly! I can fly! I can fly! Whinny!... Your Pegasus is 5 hands tall and it does migrate. A total of 10 people believe it exists. Pegasus destructor...
Bird destructor... Horse destructor...
Problem dvosmislenosti U listingu 13.4, i Horse klasa i Bird klasa imaju metodu GetColor(). Vi, naravno, možete te metode koristiti i a Pegasus objektu, ali tu nastupa problem: Pegasus klasa naslijeđuje i iz Bird i Horse. Obe klase imaju color, i njihove metode za dobivanje boje imaju ista imena i potpis. To stvara problem dvosmislenosti kojeg morate rješiti. Ako napišete COLOR currentColor = pPeg->GetColor(); dobiti ćete grešku prilikom prevođenja: Member is ambiguous: `Horse::GetColor' and `Bird::GetColor' Tu dvosmislenost (engl. ambiguity) rješavamo ekplicitnim navođenjem funkcije koju želimo pozvati: COLOR currentColor = pPeg->Horse::GetColor(); Primjetite da ako Pegasus zaobilazi tu funkciju, problem bi bio izbjegnut: virtual COLOR GetColor()const { return Horse::itsColor; } Korisnik bi i dalje mogao forsirati poziv pišući: COLOR currentColor = pPeg->Bird::GetColor();
Naslijeđivanje iz dijeljene bazne klase Što se događa ako i Bird i Horse potječu iz zajedničke bazne klase, kao npr. Animal? Slika 13.2 ilustrira kako to izgleda. Kao što možete vidjeti na slici 13.2, dva bazna objekta postoje i potvo dolazi do problema dvosmislenosti, ilustriranog u Listingu 13.5. Slika 13.2.
Listing 13.5. Zajedničke bazne klase. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
// Listing 13.5 // Common base classes #include typedef int HANDS; enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ; enum BOOL { FALSE, TRUE }; class Animal // common base to both horse and bird { public: Animal(int); virtual ~Animal() { cout << "Animal destructor...\n"; } virtual int GetAge() const { return itsAge; } virtual void SetAge(int age) { itsAge = age; }
16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74:
private: int itsAge; }; Animal::Animal(int age): itsAge(age) { cout << "Animal constructor...\n"; } class Horse : public Animal { public: Horse(COLOR color, HANDS height, int age); virtual ~Horse() { cout << "Horse destructor...\n"; } virtual void Whinny()const { cout << "Whinny!... "; } virtual HANDS GetHeight() const { return itsHeight; } virtual COLOR GetColor() const { return itsColor; } protected: HANDS itsHeight; COLOR itsColor; }; Horse::Horse(COLOR color, HANDS height, int age): Animal(age), itsColor(color),itsHeight(height) { cout << "Horse constructor...\n"; } class Bird : public Animal { public: Bird(COLOR color, BOOL migrates, int age); virtual ~Bird() {cout << "Bird destructor...\n"; } virtual void Chirp()const { cout << "Chirp... "; } virtual void Fly()const { cout << "I can fly! I can fly! I can fly! "; } virtual COLOR GetColor()const { return itsColor; } virtual BOOL GetMigration() const { return itsMigration; } protected: COLOR itsColor; BOOL itsMigration; }; Bird::Bird(COLOR color, BOOL migrates, int age): Animal(age), itsColor(color), itsMigration(migrates) { cout << "Bird constructor...\n"; } class Pegasus : public Horse, public Bird { public: void Chirp()const { Whinny(); } Pegasus(COLOR, HANDS, BOOL, long, int); ~Pegasus() {cout << "Pegasus destructor...\n";} virtual long GetNumberBelievers() const
75: { return itsNumberBelievers; } 76: virtual COLOR GetColor()const { return Horse::itsColor; } 77: virtual int GetAge() const { return Horse::GetAge(); } 78: private: 79: long itsNumberBelievers; 80: }; 81: 82: Pegasus::Pegasus( 83: COLOR aColor, 84: HANDS height, 85: BOOL migrates, 86: long NumBelieve, 87: int age): 88: Horse(aColor, height,age), 89: Bird(aColor, migrates,age), 90: itsNumberBelievers(NumBelieve) 91: { 92: cout << "Pegasus constructor...\n"; 93: } 94: 95: int main() 96: { 97: Pegasus *pPeg = new Pegasus(Red, 5, TRUE, 10, 2); 98: int age = pPeg->GetAge(); 99: cout << "This pegasus is " << age << " years old.\n"; 100: delete pPeg; 101: return 0; 102: } Output: Animal constructor... Horse constructor... Animal constructor... Bird constructor... Pegasus constructor... This pegasus is 2 years old. Pegasus destructor... Bird destructor... Animal destructor... Horse destructor... Animal destructor... Analiza: Čitav je niz zanimljivosti u ovom listingu. Animal klasa je deklarirana u linijama 9-18. Animal dodaje jedan podatkovni član, itsAge i pristupnu funkciju SetAge(). U liniji 26, Horse klasa je deklarirana kao izvedenica iz Animal. Horse konstruktor sad ima i treći parametar, age, kojeg proslijeđuje u baznu klasu, Animal. Primjetite da Horse klasa ne zaobilazi GetAge(), već ga jednostavno naslijeđuje. U liniji 46, Bird klasa je deklarirana kao izvedenica iz Animal. Pegasus naslijeđuje i Bird i Horse, i time ima dvije Animal klase u svom lancu naslijeđivanja. Ako pozovete GetAge() na Pegasus objekt, morali bi prvo razrješiti čiju metodu koristimo da Pegasus ne zaobilazi tu metodu. To je razrješeno u liniji 77 kad Pegasus objekt zaobilazi GetAge() i to običnim ulančavanjem—odnosno pozivanjem iste metode iz bazne klase. Pegasus konstruktor prima 5 parametara: boju, visinu, da li migrira, koliko ih vjeruje u njega, i svoje godine.
Virtualno naslijeđivanje U listingu 13.5, Pegasus klasa prolazi kroz dosta muka da se izbjegne dvosmislenost poziva baznih klasa. Neke stvari možemo poboljšati upotrebom virtualnog naslijeđivanja. Slika 13.3. "Dijamantno" naslijeđivanje
Listing 13.6. Ilustracija upotrbe virtualnog naslijeđivanja. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37:
// Listing 13.6 // Virtual inheritance #include typedef int HANDS; enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ; enum BOOL { FALSE, TRUE }; class Animal // common base to both horse and bird { public: Animal(int); virtual ~Animal() { cout << "Animal destructor...\n"; } virtual int GetAge() const { return itsAge; } virtual void SetAge(int age) { itsAge = age; } private: int itsAge; }; Animal::Animal(int age): itsAge(age) { cout << "Animal constructor...\n"; } class Horse : virtual public Animal { public: Horse(COLOR color, HANDS height, int age); virtual ~Horse() { cout << "Horse destructor...\n"; } virtual void Whinny()const { cout << "Whinny!... "; } virtual HANDS GetHeight() const { return itsHeight; } virtual COLOR GetColor() const { return itsColor; } protected: HANDS itsHeight; COLOR itsColor; };
38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96:
Horse::Horse(COLOR color, HANDS height, int age): Animal(age), itsColor(color),itsHeight(height) { cout << "Horse constructor...\n"; } class Bird : virtual public Animal { public: Bird(COLOR color, BOOL migrates, int age); virtual ~Bird() {cout << "Bird destructor...\n"; } virtual void Chirp()const { cout << "Chirp... "; } virtual void Fly()const { cout << "I can fly! I can fly! I can fly! "; } virtual COLOR GetColor()const { return itsColor; } virtual BOOL GetMigration() const { return itsMigration; } protected: COLOR itsColor; BOOL itsMigration; }; Bird::Bird(COLOR color, BOOL migrates, int age): Animal(age), itsColor(color), itsMigration(migrates) { cout << "Bird constructor...\n"; } class Pegasus : public Horse, public Bird { public: void Chirp()const { Whinny(); } Pegasus(COLOR, HANDS, BOOL, long, int); ~Pegasus() {cout << "Pegasus destructor...\n";} virtual long GetNumberBelievers() const { return itsNumberBelievers; } virtual COLOR GetColor()const { return Horse::itsColor; } private: long itsNumberBelievers; }; Pegasus::Pegasus( COLOR aColor, HANDS height, BOOL migrates, long NumBelieve, int age): Horse(aColor, height,age), Bird(aColor, migrates,age), Animal(age*2), itsNumberBelievers(NumBelieve) { cout << "Pegasus constructor...\n"; } int main() {
97: Pegasus *pPeg = new Pegasus(Red, 5, TRUE, 10, 2); 98: int age = pPeg->GetAge(); 99: cout << "This pegasus is " << age << " years old.\n"; 100: delete pPeg; 101: return 0; 102: } Output: Animal constructor... Horse constructor... Bird constructor... Pegasus constructor... This pegasus is 4 years old. Pegasus destructor... Bird destructor... Horse destructor... Animal destructor...
Deklariranje klasa za virtualno naslijeđivanje Kako biste osigurali da derivirane klase imaju samo jednu instancu osnovnih baznih klasa, deklariramo međuklase koje virtualno nasljeđuju iz osnovne klase. Primjer 1: class Horse : virtual public Animal class Bird : virtual public Animal class Pegasus : public Horse, public Bird Primjer 2: class Schnauzer : virtual public Dog class Poodle : virtual public Dog class Schnoodle : public Schnauzer, public Poodle
Abstraktni tipovi podataka (Abstract Data Types-ADT) Često ćete kreirati zajeedničku hijerarhiju klasa. Na primjer, možete kreirati Shape klasu i iz nje izvesti klase Rectangle i Circle. Iz Rectangle, potom možete izvesti Square, kao poseban slučaj od Rectangle. Svaka od izvedenih klasa će zaobilaziti Draw() metodu, GetArea() metodu, i tako dalje. Listing 13.7 ilustrira kostur implementacije Shape klase i njeoj izvedenih Circle i Rectangle klasa. Listing 13.7. "Shape" klase. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
//Listing 13.7. Shape classes. #include enum BOOL { FALSE, TRUE }; class Shape { public: Shape(){} ~Shape(){} virtual long GetArea() { return -1; } // error virtual long GetPerim() { return -1; } virtual void Draw() {} private: }; class Circle : public Shape { public: Circle(int radius):itsRadius(radius){}
22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80:
~Circle(){} long GetArea() { return 3 * itsRadius * itsRadius; } long GetPerim() { return 9 * itsRadius; } void Draw(); private: int itsRadius; int itsCircumference; }; void Circle::Draw() { cout << "Circle drawing routine here!\n"; } class Rectangle : public Shape { public: Rectangle(int len, int width): itsLength(len), itsWidth(width){} ~Rectangle(){} virtual long GetArea() { return itsLength * itsWidth; } virtual long GetPerim() {return 2*itsLength + 2*itsWidth; } virtual int GetLength() { return itsLength; } virtual int GetWidth() { return itsWidth; } virtual void Draw(); private: int itsWidth; int itsLength; }; void Rectangle::Draw() { for (int i = 0; i
81: if (GetLength() != GetWidth()) 82: cout << "Error, not a square... a Rectangle??\n"; 83: } 84: 85: int main() 86: { 87: int choice; 88: BOOL fQuit = FALSE; 89: Shape * sp; 90: 91: while (1) 92: { 93: cout << "(1)Circle (2)Rectangle (3)Square (0)Quit: "; 94: cin >> choice; 95: 96: switch (choice) 97: { 98: case 1: sp = new Circle(5); 99: break; 100: case 2: sp = new Rectangle(4,6); 101: break; 102: case 3: sp = new Square(5); 103: break; 104: default: fQuit = TRUE; 105: break; 106: } 107: if (fQuit) 108: break; 109: 110: sp->Draw(); 111: cout << "\n"; 112: } 113: return 0; 114: } Output: (1)Circle (2)Rectangle (3)Square (0)Quit: 2 xxxxxx xxxxxx xxxxxx xxxxxx (1)Circle (2)Rectangle (3)Square (0)Quit:3 xxxxx xxxxx xxxxx xxxxx xxxxx (1)Circle (2)Rectangle (3)Square (0)Quit:0 Analiza: U linijama 7-16, Shape klasa je deklarirana. GetArea() i GetPerim() methode vraćaju poruku o greški, a Draw() ne radi ništa. Na kraju krajeva, što to znači "Nacrtaj oblik"? Samo tiopvi oblika (kružnice, pravokutnici, itd.) mogu biti nacrtani, oblici kao apstraktan pojam ne mogu. Circle izvodi iz Shape i zaobilazi tri virtualne metode. Primjetite da nije potrebno dodoavati riječ "virtual", jer je riječ o njihovom naslijeđu. Ali niti ne smeta ako dodamo, kao što vidimo u linijama 43, 44 i 47, kod Rectangle klase. Dobra je ideja uključiti izraz virtual, kao podsjetnik za kasnije lakše čitanje programa. Square izvodi iz Rectangle, i on također zaobilazi GetPerim() metodu, naslijeđujući ostale metode definirane u Rectangle. Potencijalni je problem ako iz glavnog programa kreiramo Shape objekt, i poželjno je da to onemogućimo. Shape klasa postoji samo za omogućavanje sučelja za njoj izveden klase; kao takvu, nazivamo ju apstraktnim tipom podatka, engl. Abstract Data Type, ili ADT.
Novi izraz: Abstract Data Type predstavlja koncept (kao oblik, shape klasa u prethodnom primjeru) za razliku od objekta (krug). U C++, ADT he uvijek bazna klasa za druge klase, i nije moguće napraviti instancu od ADT-a.
Čiste virtualne funkcije C++ omogućuje kreiranje abstraktnih tipova podataka s čistim virtualnim funkcijama. Virtualna funkcija je proglašena čistom kad ju inicijaliziramo s nulom, kao u virtual void Draw() = 0; Svaka klasa s jednom ili više čistih virtualnih funkcija je ADT, i ilegalno je instancirati objekt klase koja je ADT. Pokušavanje toga će dati grešku prilikom prevođenja. Umetanjem takve funkcije u klasu signaliziramo dvije stvari korisniku klase: • Ne pravite objekt takve klase, izvedite iz nje. • Ne zaboravite zaobići čiste virtualne funkcije. Svaka klas koja se izvodi iz ADT-a. naslijeđuje i čiste virtualne funkcije kao takve, te ih mora zaobići želi li instancirati objekt. Prema tome, ako Rectangle naslijeđuje od Shape, a Shape ima tri čiste virtualne funkcije, Rectangle mora zaobići sve tri inače će i on biti ADT. Listing 13.8 mijenja Shape klasu kako bi bila abstraktni tip podatka. Listing 13.8. Apstraktni tipovi podataka. 1: class Shape 2: { 3: public: 4: Shape(){} 5: ~Shape(){} 6: virtual long GetArea() = 0; // error 7: virtual long GetPerim()= 0; 8: virtual void Draw() = 0; 9: private: 10: }; Output: (1)Circle (2)Rectangle (3)Square (0)Quit: 2 xxxxxx xxxxxx xxxxxx xxxxxx (1)Circle (2)Rectangle (3)Square (0)Quit: 3 xxxxx xxxxx xxxxx xxxxx xxxxx (1)Circle (2)Rectangle (3)Square (0)Quit: 0
Implementiranje čistih virtualnih funkcija Obično se čiste virtualne funkcije ne implementiraju, budući da ionako iz ADT-a ne možemo instancirati objekte. Ipak je moguće i pružiti implementaciju čiste virtualne funkcije. Funkcija tada može biti pozvana iz objekata deriviranih iz ADT-a, možda za pružanje funkcionalnosti svim zaobiđenim funkcijama. Listing 13.9 je prepravljeni listing 13.7, ovaj put sa Shape kao ADT i s implementacijom čiste virtualne funkcije Draw().Circle klasa zaobilazi Draw(), kako i treba, ali se potom ulančava s funkcijom bazne klase za dodatnu funkcionalnost. Listing 13.9. Implementiranje čistih virtualnih funkcija.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59:
//Implementing pure virtual functions #include enum BOOL { FALSE, TRUE }; class Shape { public: Shape(){} ~Shape(){} virtual long GetArea() = 0; // error virtual long GetPerim()= 0; virtual void Draw() = 0; private: }; void Shape::Draw() { cout << "Abstract drawing mechanism!\n"; } class Circle : public Shape { public: Circle(int radius):itsRadius(radius){} ~Circle(){} long GetArea() { return 3 * itsRadius * itsRadius; } long GetPerim() { return 9 * itsRadius; } void Draw(); private: int itsRadius; int itsCircumference; }; void Circle::Draw() { cout << "Circle drawing routine here!\n"; Shape::Draw(); } class Rectangle : public Shape { public: Rectangle(int len, int width): itsLength(len), itsWidth(width){} ~Rectangle(){} long GetArea() { return itsLength * itsWidth; } long GetPerim() {return 2*itsLength + 2*itsWidth; } virtual int GetLength() { return itsLength; } virtual int GetWidth() { return itsWidth; } void Draw(); private: int itsWidth; int itsLength; }; void Rectangle::Draw()
60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118:
{ for (int i = 0; i> choice; switch (choice) { case 1: sp = new Circle(5); break; case 2: sp = new Rectangle(4,6); break; case 3: sp = new Square (5); break; default: fQuit = TRUE; break; } if (fQuit) break; sp->Draw();
119: cout << "\n"; 120: } 121: return 0; 122: } Output: (1)Circle (2)Rectangle (3)Square (0)Quit: 2 xxxxxx xxxxxx xxxxxx xxxxxx Abstract drawing mechanism! (1)Circle (2)Rectangle (3)Square (0)Quit: 3 xxxxx xxxxx xxxxx xxxxx xxxxx Abstract drawing mechanism! (1)Circle (2)Rectangle (3)Square (0)Quit: 0
Kviz 1. Što je v-ptr? 2. Ako zaobljeni pravokutnik ima ravne linije i oble rubove, a vaša RoundRect klasa naslijeđuje i iz Rectangle i Circle, a oni su naslijednici iz Shape, koliko Shapes se kreira kad kreirate RoundRect? 3. Ako Horse i Bird naslijeđuju iz Animal koristeći javno virtualno naslijeđivanje, da li njihovi konstruktori inicijaliziraju Animal konstruktor? Ako Pegasus naslijeđuje i iz Horse i Bird, kako on inicijalizira Animal konstruktor? 4. Deklarirajte klasu vehicle, i proglasite ju ADT-om. 5. Ako je bazna klasa ADT, i ima tri čiste virtualne funkcije, koliko od njih mora biti zaobiđeno u izvedenim klasama?
Vježbe 1. Prikažite deklaraciju klase JetPlane, koja naslijeđuje od Rocket i Airplane. 2. Pokažite deklaraciju od 747, koji naslijeđuje iz JetPlane klase prethodne vježbe. 3. Napišite program koji izvodi Car i Bus iz klase Vehicle. Proglasite Vehicle ADT-om s dvije čiste virtualne funkcije. Napravite da Car i Bus nisu ADT-i.
Lekcija 14
Specijalne klase i funkcije
C++ nudi nam različite načine određivanja dosega i djelovanja varijabli i pokazivača. Do sada ste naučili kako kreirati globalne varijable, lokalne funkcijeske varijable, pokazivače na varijable, i podatkovne članove klasa. Danas ćete naučiti • Što su statički podatkovni članovi i statički funkcijski članov i kako ih koristiti. • Kako kreirati i manipulirati pokazivačima na funkcije i pokazivačima na funkcijske članove. • Kako raditi s poljima pokazivača na funkcije.
Statički podatkovni članovi Do sada ste vjerojatno smatrali podatke unutar nekog objekta jedinstvenim samo za taj objekt i nedjeljivim među objektima iste kase. Na primjer, ako imate pet Cat objekata, svaki od njih ima vlastitu starost, težinu i ostale podatke. Starost jednoga ne utječe na starost drugoga. Postoje trenuci kad ćete željeti kontrolirati skupom podataka. Npr., možda ćete željeti znati koliko je ukupno objekata neke klase kreirano u vašem programu, te koliko ih još postoji. Statički podatkovni članovi se dijele između svih instanci određene klase. Oni su kompromis između globalnih podataka, koji su dostupni svim dijelovima programa, te podatkovnih članova koji su obično dostupni unutar jednog objekta. Možete misliti o statičkim članovima kao o pripadnicima klase, a ne objekta. Listing 14.1 deklarira Cat objekt sa statičkim članom, HowManyCats. Ova varijabla vodi račun o tome koliko Cat objekata je kreirano. Ovo postižemo inkrementiranjem statičke varijable, HowManyCats, prilikom svakog kreiranja i dekrementiranjem prilikom svakog uništavanja. Listing 14.1. Statički podatkovni članovi. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30:
//Listing 14.1 static data members #include class Cat { public: Cat(int age):itsAge(age){HowManyCats++; } virtual ~Cat() { HowManyCats--; } virtual int GetAge() { return itsAge; } virtual void SetAge(int age) { itsAge = age; } static int HowManyCats; private: int itsAge; }; int Cat::HowManyCats = 0; int main() { const int MaxCats = 5; int i; Cat *CatHouse[MaxCats]; for (i = 0; i<MaxCats; i++) CatHouse[i] = new Cat(i); for (i = 0; i<MaxCats; i++) { cout << "There are ";
31: cout << Cat::HowManyCats; 32: cout << " cats left!\n"; 33: cout << "Deleting the one which is "; 34: cout << CatHouse[i]->GetAge(); 35: cout << " years old\n"; 36: delete CatHouse[i]; 37: CatHouse[i] = 0; 38: } 39: return 0; 40: } Output: There are 5 cats left! Deleting the one which is 0 years old There are 4 cats left! Deleting the one which is 1 years old There are 3 cats left! Deleting the one which is 2 years old There are 2 cats left! Deleting the one which is 3 years old There are 1 cats left! Deleting the one which is 4 years old Analiza: U linijama 5 do 17 je deklarirana pojednostavljena klasa Cat. U liniji 12, HowManyCats je deklariran kao statički podatkovni član tipa int. Deklaracija HowManyCats ne definira nam kolika je vrijednost unutra, niti razervira memoriju za tu varijablu. Stoga je u liniji 19 varijabla definirana i inicijalizirana. Česta je pogreška zaboraviti definirati statičke podatkovne članove. Neka se to ne dogodi i vama! Vi to naravno ne trebate raditi i za itsAge, budući da je to ne-statički podatkovni član, pa je definiran prilikom stvaranja Cat objekta, ovdje to radimo u liniji 26. Konstruktor za Cat inkrementira statičku varijablu u liniji 8. Destruktor dekrementira istu u liniji 9. Stoga varijabla HowManyCats u svakom trenutku ima točan podatak o tome koliko je Cat objekata kreirano, ali još nije uništeno. Program dalje kreira pet Cat objekata i stavlja ih u polje (linije 21-40). Time se poziva pet konstruktora, pa se i HowManyCats inkrementira pet puta sa svoje inicijalne vrijednosti 0. Primjetite da je HowManyCats public i da mu pristupamo direktno iz main(). Ne postoji razlog za takvo izlaganje podatkovnog člana. Preferirano je rješenje proglasiti ju privatnom zajedno s ostalim podatkovnim članovima i omogućiti javnu pristupnu metodu, i time omogućiti pristup preko bilo kojeg objekta. Želite li toj varijabli pristupati direktno, bez uvjetovanog Cat objekta, imate dvije opcije: ostavite ju javnom, kako je prikazano na listingu 14.2, ili omogučite statički funkcijski član, no o tome nešto kasnije.
Listing 14.2. Pristupanje statičkim članovima bez objekta. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
//Listing 14.2 static data members #include class Cat { public: Cat(int age):itsAge(age){HowManyCats++; } virtual ~Cat() { HowManyCats--; } virtual int GetAge() { return itsAge; } virtual void SetAge(int age) { itsAge = age; } static int HowManyCats; private:
15: int itsAge; 16: 17: }; 18: 19: int Cat::HowManyCats = 0; 20: 21: void TelepathicFunction(); 22: 23: int main() 24: { 25: const int MaxCats = 5; int i; 26: Cat *CatHouse[MaxCats]; 27: for (i = 0; i<MaxCats; i++) 28: { 29: CatHouse[i] = new Cat(i); 30: TelepathicFunction(); 31: } 32: 33: for ( i = 0; i<MaxCats; i++) 34: { 35: delete CatHouse[i]; 36: TelepathicFunction(); 37: } 38: return 0; 39: } 40: 41: void TelepathicFunction() 42: { 43: cout << "There are "; 44: cout << Cat::HowManyCats << " cats alive!\n"; 45: } Output: There are 1 cats alive! There are 2 cats alive! There are 3 cats alive! There are 4 cats alive! There are 5 cats alive! There are 4 cats alive! There are 3 cats alive! There are 2 cats alive! There are 1 cats alive! There are 0 cats alive! Analiza: Listing 14.2 je sličan prethodnom osim dodatka nove funkcije, TelepathicFunction(). Ova funkcija ne kreira Cat objekt, niti uzima Cat objekt kao parametar, pa ipak može pristupiti HowManyCats varijabli. Još jednom vrijedi naglasiti da se ova varijabla ne nalazi niti u jednom objektu, već u klasi, i, akoje javna, može joj se pristupiti iz bilo koje funkcije u programu. Alternativa je proglašavanje varijable privatnom i pristupanje preko pristupne funkcije. U tom slučaju joj možete pristupiti samo ako imate dostupan i objekt. Listing 14.3 predstavlja takav pristup. Alternativa, statički funkcijski članovi slijedi odmah nakon listinga 14.3. Listing 14.3. Pristupanje statičkim članovima preko ne-statičkih funkcijskih članova. 1: 2: 3: 4: 5: 6: 7:
//Listing 14.3 private static data members #include class Cat { public:
8: Cat(int age):itsAge(age){HowManyCats++; } 9: virtual ~Cat() { HowManyCats--; } 10: virtual int GetAge() { return itsAge; } 11: virtual void SetAge(int age) { itsAge = age; } 12: virtual int GetHowMany() { return HowManyCats; } 13: 14: 15: private: 16: int itsAge; 17: static int HowManyCats; 18: }; 19: 20: int Cat::HowManyCats = 0; 21: 22: int main() 23: { 24: const int MaxCats = 5; int i; 25: Cat *CatHouse[MaxCats]; 26: for (i = 0; i<MaxCats; i++) 27: CatHouse[i] = new Cat(i); 28: 29: for (i = 0; i<MaxCats; i++) 30: { 31: cout << "There are "; 32: cout << CatHouse[i]->GetHowMany(); 33: cout << " cats left!\n"; 34: cout << "Deleting the one which is "; 35: cout << CatHouse[i]->GetAge()+2; 36: cout << " years old\n"; 37: delete CatHouse[i]; 38: CatHouse[i] = 0; 39: } 40: return 0; 41: } Output: There are 5 cats left! Deleting the one which is 2 years old There are 4 cats left! Deleting the one which is 3 years old There are 3 cats left! Deleting the one which is 4 years old There are 2 cats left! Deleting the one which is 5 years old There are 1 cats left! Deleting the one which is 6 years old Analiza: U liniji 17, HowManyCats je deklarirana sa privatnim pristupom. Sada više ne bi mogli pristupati varijabli iz ne-članski funkcija, poput TelepathicFunction sa pretodnog listinga. Iako je HowManyCats statička varijabla, još uvijek je u dosegu klase. Svaka funkcija klase, kao GetHowMany(), joj može pristupati, baš kao što i funkcijski članovi mogu pristupati bilo kojim podatkovnim članovima. Ipak, da bismo pozvali GetHowMany(), moramo imati objek na kojem ćemo pozvati tu funkciju.
Statički funkcijski članovi Statički funkcijski članovi su poput statičkih podatkovnih članova: postoje ne u objektu nego unutar dosegaa klase. Tako mogu biti pozvane bez postojanja objekta te klase, kao što je prikazano u listingu 14.4. Listing 14.4. Statički funkcijski članovi. 1:
//Listing 14.4 static data members
2: 3: #include 4: 5: class Cat 6: { 7: public: 8: Cat(int age):itsAge(age){HowManyCats++; } 9: virtual ~Cat() { HowManyCats--; } 10: virtual int GetAge() { return itsAge; } 11: virtual void SetAge(int age) { itsAge = age; } 12: static int GetHowMany() { return HowManyCats; } 13: private: 14: int itsAge; 15: static int HowManyCats; 16: }; 17: 18: int Cat::HowManyCats = 0; 19: 20: void TelepathicFunction(); 21: 22: int main() 23: { 24: const int MaxCats = 5; 25: Cat *CatHouse[MaxCats]; int i; 26: for (i = 0; i<MaxCats; i++) 27: { 28: CatHouse[i] = new Cat(i); 29: TelepathicFunction(); 30: } 31: 32: for ( i = 0; i<MaxCats; i++) 33: { 34: delete CatHouse[i]; 35: TelepathicFunction(); 36: } 37: return 0; 38: } 39: 40: void TelepathicFunction() 41: { 42: cout << "There are " << Cat::GetHowMany() << " cats alive!\n"; 43: } Output: There are 1 cats alive! There are 2 cats alive! There are 3 cats alive! There are 4 cats alive! There are 5 cats alive! There are 4 cats alive! There are 3 cats alive! There are 2 cats alive! There are 1 cats alive! There are 0 cats alive! Analiza: Statički podatkovni član HowManyCats je deklariran sa privatnim pristupom u liniji 15. Javna pristupna funkcija, GetHowMany(), je deklarirana i kao public i kao static ou liniji 12. Budući da je GetHowMany() public, može joj se pristupiti iz bilo koje funkcije, a kako je i static nema potrebe za stvaranjem objekta tog tipa kako bismo joj pristupili. Zato je u liniji 42, funkcija TelepathicFunction() sposobna pristupiti javnoj statičnoj pristupnoj funkciji, iako nema pristup da Cat objekta. Naravno, mogli ste pozvati GetHowMany() i na Cat objekte dostupne u main(), baš kao i bilo koju drugu pristupnu funkciju.
PAŽNJA: Statički funkcijski članovi nemaju this pokazivač. Tako ne mogu biti deklarirani kao const. Također, budući da se podatkovnim članovima u funkcijskim članovima pristupa preko this pokazivača, statički funkcijski članovi ne mogu pristupati bilo kojoj ne-statičkoj varijabli!
Statički funkcijski članovi Možete pristupati statičkim funkcijskim članovima pozivajući ih preko objekta te klase kao i svaki drugi funkcijski član, ili ih možete pozivati i bez objekta navodeći ime klase. Primjer class Cat { public: static int GetHowMany() { return HowManyCats; } private: static int HowManyCats; }; int Cat::HowManyCats = 0; int main() { int howMany; Cat theCat; // define a cat howMany = theCat.GetHowMany(); // access through an object howMany = Cat::GetHowMany(); // access without an object }
Pokazivači na funkcije Baš kao što je ime polja konstantan pokazivač na prvi element polja, ime funkcije je konstantan pokazivač na funkciju. Moguće je deklarirati pokazivačku varijablu koja pokazuje na funkciju, i pozvati funkciju koristeći taj pokazivač. To može biti vrlo korisno; omogućuje nam kreiranje programa koji pozivaju određenu funkciju ovisno o korisnikovom inputu. Jedini "tricky" dio u razumijevanju funkcijskih pokazivača je tip objekta na kojeg pokazujemo. Pokazivač na int pokazuje na cjelobrojnu varijablu, a pokazivač na funkciju mora pokazivati na funkciju odgovarajućeg povratnog tipa i potpisa. U deklaraciji long (* funcPtr) (int); funcPtr je deklariran kao pokazivač (primjetite * ispred imena) koji pokazuje na funkciju koja prima integer parametar i vraća long. Proučite ove dvije deklaracije: long * Function (int); long (* funcPtr) (int); Prva, Function (), je funkcija koja prima integer i vraća pokazivač na varijablu tipa long. Druga, funcPtr, je pokazivač na funkciju koja prima cijeli broj i vraća varijablu tipa long. Listing 14.5. Pokazivači na funkcije. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10:
// Listing 14.5 Using function pointers #include void Square (int&,int&); void Cube (int&, int&); void Swap (int&, int &); void GetVals(int&, int&); void PrintVals(int, int); enum BOOL { FALSE, TRUE };
11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69:
int main() { void (* pFunc) (int &, int &); BOOL fQuit = FALSE; int valOne=1, valTwo=2; int choice; while (fQuit == FALSE) { cout << "(0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: "; cin >> choice; switch (choice) { case 1: pFunc = GetVals; break; case 2: pFunc = Square; break; case 3: pFunc = Cube; break; case 4: pFunc = Swap; break; default : fQuit = TRUE; break; } if (fQuit) break; PrintVals(valOne, valTwo); pFunc(valOne, valTwo); PrintVals(valOne, valTwo); } return 0; } void PrintVals(int x, int y) { cout << "x: " << x << " y: " << y << endl; } void Square (int & rX, int & rY) { rX *= rX; rY *= rY; } void Cube (int & rX, int & rY) { int tmp; tmp = rX; rX *= rX; rX = rX * tmp; tmp = rY; rY *= rY; rY = rY * tmp; } void Swap(int & rX, int & rY) { int temp; temp = rX;
70: rX = rY; 71: rY = temp; 72: } 73: 74: void GetVals (int & rValOne, int & rValTwo) 75: { 76: cout << "New value for ValOne: "; 77: cin >> rValOne; 78: cout << "New value for ValTwo: "; 79: cin >> rValTwo; 80: } Output: (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 1 x: 1 y: 2 New value for ValOne: 2 New value for ValTwo: 3 x: 2 y: 3 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 y: 3 x: 8 y: 27 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 2 x: 8 y: 27 x: 64 y: 729 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x: 64 y: 729 x: 729 y: 64 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 0 Analiza: U linijama 5-8, četiri funkcije su deklarirane, svaka s istim povratnim tipom i potpisom, sve vraćaju void i primaju dvije reference na cijele brojeve. U liniji 14, pFunc je deklariran kao pokazivač na funkciju koja vraća void i prima dvije reference na cijele brojeve. Svaka od deklariranih funkcija može biti pokazana od pFunc. Korisniku se učestalo nudi odabir poziva funkcije, i prema tome se pridružuje pFunc. U linijama 35-36, trenutna vrijednost dva cijela broja se ispisuje, trenutno pridružena funkcija se poziva, i vrijednosti se ponovno ispisuju.
Zašto koristiti pokazivače na funkcije? Sigurno biste mogli napisati program sa listinga 14.5 bez funkcijskih pokazivača, ali upotreba pokazivača čini upotrebu i rad programa jasnijim: odaberite funkciju s liste i potom ju pozovite. Listing 14.6 koristi funkcijske prototipe i definicije s Listinga 14.5, ali tijelo programa ne koristi funkcijski pokazivač. Pročite razlike između dva listinga. PAŽNJA: Za prevođenje ovoga programa, stavite linije 41-80 iz listinga 14.5 odmah nakon linije 56.
Listing 14.6. Ponovljeni listing 14.5 bez pokazivača na funkcije. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
// Listing 14.6 Without function pointers #include void Square (int&,int&); void Cube (int&, int&); void Swap (int&, int &); void GetVals(int&, int&); void PrintVals(int, int); enum BOOL { FALSE, TRUE };
12: int main() 13: { 14: BOOL fQuit = FALSE; 15: int valOne=1, valTwo=2; 16: int choice; 17: while (fQuit == FALSE) 18: { 19: cout << "(0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: "; 20: cin >> choice; 21: switch (choice) 22: { 23: case 1: 24: PrintVals(valOne, valTwo); 25: GetVals(valOne, valTwo); 26: PrintVals(valOne, valTwo); 27: break; 28: 29: case 2: 30: PrintVals(valOne, valTwo); 31: Square(valOne,valTwo); 32: PrintVals(valOne, valTwo); 33: break; 34: 35: case 3: 36: PrintVals(valOne, valTwo); 37: Cube(valOne, valTwo); 38: PrintVals(valOne, valTwo); 39: break; 40: 41: case 4: 42: PrintVals(valOne, valTwo); 43: Swap(valOne, valTwo); 44: PrintVals(valOne, valTwo); 45: break; 46: 47: default : 48: fQuit = TRUE; 49: break; 50: } 51: 52: if (fQuit) 53: break; 54: } 55: return 0; 56: } Output: (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 1 x: 1 y: 2 New value for ValOne: 2 New value for ValTwo: 3 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 y: 3 x: 8 y: 27 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 2 x: 8 y: 27 x: 64 y: 729 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x: 64 y: 729 x: 729 y: 64 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 0
Polja pokazivača na funkcije Baš kao što deklarirate polje pokazivača na integere, možete deklarirati i polja pokazivača na funkcije (koje naravno moraju biti specifičnoga povratnog tipa i potpisa). Listing 14.7 ponovo koristi listing 14.5 ovaj put rabeći polje za pokretanje svih izbora odjedanput. PAŽNJA: Za prevođenje ovoga programa, umetnite linije 41-80 listinga 14.5 odmah nakon linije 39. Listing 14.7. Demonstracija upotrebe polja pokazivača na funkcije. 1: // Listing 14.7 demonstrates use of an array of pointers to functions 2: 3: #include 4: 5: void Square (int&,int&); 6: void Cube (int&, int&); 7: void Swap (int&, int &); 8: void GetVals(int&, int&); 9: void PrintVals(int, int); 10: enum BOOL { FALSE, TRUE }; 11: 12: int main() 13: { 14: int valOne=1, valTwo=2; 15: int choice, i; 16: const MaxArray = 5; 17: void (*pFuncArray[MaxArray])(int&, int&); // polje pointera na funkcije 18: 19: for (i=0;i<MaxArray;i++) 20: { 21: cout << "(1)Change Values (2)Square (3)Cube (4)Swap: "; 22: cin >> choice; 23: switch (choice) 24: { 25: case 1:pFuncArray[i] = GetVals; break; 26: case 2:pFuncArray[i] = Square; break; 27: case 3:pFuncArray[i] = Cube; break; 28: case 4:pFuncArray[i] = Swap; break; 29: default:pFuncArray[i] = 0; 30: } 31: } 32: 33: for (i=0;i<MaxArray; i++) 34: { 35: pFuncArray[i](valOne,valTwo); 36: PrintVals(valOne,valTwo); 37: } 38: return 0; 39: } Output: (1)Change Values (2)Square (3)Cube (4)Swap: 1 (1)Change Values (2)Square (3)Cube (4)Swap: 2 (1)Change Values (2)Square (3)Cube (4)Swap: 3 (1)Change Values (2)Square (3)Cube (4)Swap: 4 (1)Change Values (2)Square (3)Cube (4)Swap: 2 New Value for ValOne: 2 New Value for ValTwo: 3 x: 2 y: 3
x: 4 y: 9 x: 64 y: 729 x: 729 y: 64 x: 7153 y:4096
Proslijeđivanje pokazivača na funkcije u druge funkcije Pokazivač na funkciju (kao i polje pokazivača na funkcije) može biti proslijeđeno u druge funkcije, koje mogu potom poduzeti akciju pozivom odgovarajuće funkcije korištenjem pokazivača. Na primjer, možemo poboljšati listing 14.5 proslijeđujući odabrani pokazivač na funkciju u drugu funkciju (izvan main()), koja potom ispisuje vrijednosti, poziva funkciju i ponovno ispisuje vrijednosti. Listing 14.8 ilustrira tu varijaciju. PAŽNJA: Za prevođenje programa, umetnite linije 46-80 listinga 14.5 odmah nakon linije 45. Listing 14.8. Slanje pokazivača na funkcije kao funkcijskih argumenata. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41:
// Listing 14.8 Without function pointers #include void Square (int&,int&); void Cube (int&, int&); void Swap (int&, int &); void GetVals(int&, int&); void PrintVals(void (*)(int&, int&),int&, int&); // deklaracija enum BOOL { FALSE, TRUE }; int main() { int valOne=1, valTwo=2; int choice; BOOL fQuit = FALSE; void (*pFunc)(int&, int&); dekl. Pokaz. Na funkciju while (fQuit == FALSE) { cout << "(0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: "; cin >> choice; switch (choice) { case 1:pFunc = GetVals; break; case 2:pFunc = Square; break; case 3:pFunc = Cube; break; case 4:pFunc = Swap; break; default:fQuit = TRUE; break; } if (fQuit == TRUE) break; PrintVals ( pFunc, valOne, valTwo); // poziv } return 0; } void PrintVals( void (*pFunc)(int&, int&),int& x, int& y) {
42: cout << "x: " << x << " y: " << y << endl; 43: pFunc(x,y); 44: cout << "x: " << x << " y: " << y << endl; 45: } Output: (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 1 x: 1 y: 2 New value for ValOne: 2 New value for ValTwo: 3 x: 2 y: 3 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 y: 3 x: 8 y: 27 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 2 x: 8 y: 27 x: 64 y: 729 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x: 64 y: 729 x: 729 y:64 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 0 Zadatak: Pronađite nekog C++ programera i pitajte ga što znači slijedeća deklaracija: void PrintVals(void (*)(int&, int&),int&, int&); Ovoje vrlo rijetko korištenje i vjerojatno ćete svaki put kad naletite na takvo što morati gledati u neku C++ knjigu, ali vam može spasiti program u rijetkim situacijama kada vam je takva konstrukcija neophodna.
Upotreba typedef s pokazivačima na funkcije Konstrukcija void (*)(int&, int&) je, blago rečeno, nečitka. Možete koristiti typedef kako biste to pojednostavili deklarirajući tip VPF kao pokazivač na funkciju koja vraća void i prima dve reference na cijele brojeve. Listing 14.9 je prepisani listing 14.8 koji koristi typedef naredbu. PAŽNJA: Za prevođenje programa, stavite linije 46-80 u listing 14.5 odmah nakon linije 45. Listing 14.9. Upotreba typedef za pravljenje pokazivača na funkcije čitljivijima. 1: // Listing 14.9. Using typedef to make pointers to functions more _readable 2: 3: #include 4: 5: void Square (int&,int&); 6: void Cube (int&, int&); 7: void Swap (int&, int &); 8: void GetVals(int&, int&); 9: typedef void (*VPF) (int&, int&) ; 10: void PrintVals(VPF,int&, int&); 11: enum BOOL { FALSE, TRUE }; 12: 13: int main() 14: { 15: int valOne=1, valTwo=2; 16: int choice; 17: BOOL fQuit = FALSE; 18: 19: VPF pFunc; 20: 21: while (fQuit == FALSE) 22: { 23: cout << "(0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: ";
24: cin >> choice; 25: switch (choice) 26: { 27: case 1:pFunc = GetVals; break; 28: case 2:pFunc = Square; break; 29: case 3:pFunc = Cube; break; 30: case 4:pFunc = Swap; break; 31: default:fQuit = TRUE; break; 32: } 33: if (fQuit == TRUE) 34: break; 35: PrintVals ( pFunc, valOne, valTwo); 36: } 37: return 0; 38: } 39: 40: void PrintVals( VPF pFunc,int& x, int& y) 41: { 42: cout << "x: " << x << " y: " << y << endl; 43: pFunc(x,y); 44: cout << "x: " << x << " y: " << y << endl; 45: } Output: (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 1 x: 1 y: 2 New value for ValOne: 2 New value for ValTwo: 3 x: 2 y: 3 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 3 x: 2 y: 3 x: 8 y: 27 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 2 x: 8 y: 27 x: 64 y: 729 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 4 x: 64 y: 729 x: 729 y: 64 (0)Quit (1)Change Values (2)Square (3)Cube (4)Swap: 0
Pokazivači na funkcijske članove Do sada su svi pokazivači na funkcije bili za opće ne-klasne funkcije. Također je moguće kreirati i pokazivače na funkcijske članove neke klase. Za kreiranje pokazivača na funkcijski član, koristite istu sintaksu kao i s pokazivačima na funkciju, ali uključite ime klase i scoping operator (::). Tako, ako pFunc pokazuje na funkcijski član klase Shape, koja prima dva integera i vraća void, deklaracija za pFunc je slijedeća: void (Shape::*pFunc) (int, int); Pokazivači na funkcijske članove se koriste isto kao i pokazivači na funkcije, osim što zahtijevaju objekt odgovarajuće klase kako bi ih mogli pozvati. Listing 14.10 ilustrira upotrebu pokazivača na funkcijske članove. Listing 14.10. Pokazivači na funkcijske članove. 1: 2: 3: 4: 5: 6: 7: 8:
//Listing 14.10 Pointers to member functions using virtual methods #include enum BOOL {FALSE, TRUE}; class Mammal { public:
9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67:
Mammal():itsAge(1) { } ~Mammal() { } virtual void Speak() const = 0; virtual void Move() const = 0; protected: int itsAge; }; class Dog : public Mammal { public: void Speak()const { cout << "Woof!\n"; } void Move() const { cout << "Walking to heel...\n"; } }; class Cat : public Mammal { public: void Speak()const { cout << "Meow!\n"; } void Move() const { cout << "slinking...\n"; } }; class Horse : public Mammal { public: void Speak()const { cout << "Winnie!\n"; } void Move() const { cout << "Galloping...\n"; } }; int main() { void (Mammal::*pFunc)() const =0; Mammal* ptr =0; int Animal; int Method; BOOL fQuit = FALSE; while (fQuit == FALSE) { cout << "(0)Quit (1)dog (2)cat (3)horse: "; cin >> Animal; switch (Animal) { case 1: ptr = new Dog; break; case 2: ptr = new Cat; break; case 3: ptr = new Horse; break; default: fQuit = TRUE; break; } if (fQuit) break; cout << "(1)Speak (2)Move: "; cin >> Method; switch (Method) { case 1: pFunc = Mammal::Speak; break;
68: default: pFunc = Mammal::Move; break; 69: } 70: 71: (ptr->*pFunc)(); 72: delete ptr; 73: } 74: return 0; 75: } Output: (0)Quit (1)dog (2)cat (3)horse: 1 (1)Speak (2)Move: 1 Woof! (0)Quit (1)dog (2)cat (3)horse: 2 (1)Speak (2)Move: 1 Meow! (0)Quit (1)dog (2)cat (3)horse: 3 (1)Speak (2)Move: 2 Galloping (0)Quit (1)dog (2)cat (3)horse: 0 Analiza: U linijama 6-15, abstraktni tip podatka Mammal je deklariran s dvije čiste virtualne metode, Speak() i Move(). Mammal je subklasiran u Dog, Cat, i Horse, od kojih svaki zaobilazi Speak() i Move(). Glavni dio programa unutar main() pita korisnika za odabir kojeg tipa životinje kreirati, i potom nova podklasa od Animal se kreira u slobodnom spremniku i dodjeluje u ptr u linijama 55-57. Korisnik je potom upitan koju metodu pozvati, te se potom ta metoda pridružuje u pokazivač pFunc. U liniji 71, odabrana metoda se poziva preko kreiranog objekta, koristeći pokazivač ptr za pristup objektu i pFunc za pristup funkciji. Konačno, u liniji 72, delete je pozvan na pokazivač ptr kako bi oslobodili zauzetu memoriju. Primjetite da nema potrebe zvati delete za pFunc budući da je to pokazivač na kod, a ne na objekt u slobodnom spremniku. U stvari pokušate li napraviti takvo što, izazvat ćete grešku priliko kompajliranja.
Polja pokazivača na funkcijske članove Kao sa pokazivačima na funkcije, pokazivači na funkcijske članove mogu biti pohranjeni u polju. Polje može biti inicijalizirano s adresama raznih funkcijskih članova, i oni mogu biti pozvani preko indeksa polja. Listing 14.11 ilustrira ovu tehniku. Listing 14.11. Polja pokazivača na funkcijske članove. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
//Listing 14.11 Array of pointers to member functions #include enum BOOL {FALSE, TRUE}; class Dog { public: void Speak()const { cout << "Woof!\n"; } void Move() const { cout << "Walking to heel...\n"; } void Eat() const { cout << "Gobbling food...\n"; } void Growl() const { cout << "Grrrrr\n"; } void Whimper() const { cout << "Whining noises...\n"; } void RollOver() const { cout << "Rolling over...\n"; } void PlayDead() const { cout << "Is this the end of Little Caeser?\n"; } }; typedef void (Dog::*PDF)()const ; int main() {
22: const int MaxFuncs = 7; 23: PDF DogFunctions[MaxFuncs] = 24: { Dog::Speak, 25: Dog::Move, 26: Dog::Eat, 27: Dog::Growl, 28: Dog::Whimper, 29: Dog::RollOver, 30: Dog::PlayDead }; 31: 32: Dog* pDog =0; 33: int Method; 34: BOOL fQuit = FALSE; 35: 36: while (!fQuit) 37: { 38: cout << "(0)Quit (1)Speak (2)Move (3)Eat (4)Growl"; 39: cout << " (5)Whimper (6)Roll Over (7)Play Dead: "; 40: cin >> Method; 41: if (Method == 0) 42: { 43: fQuit = TRUE; 44: break; 45: } 46: else 47: { 48: pDog = new Dog; 49: (pDog->*DogFunctions[Method-1])(); 50: delete pDog; 51: } 52: } 53: return 0; 54: } Output: (0)Quit (1)Speak (2)Move (3)Eat (4)Growl (5)Whimper (6)Roll Over (7)Play Dead: 1 Woof! (0)Quit (1)Speak (2)Move (3)Eat (4)Growl (5)Whimper (6)Roll Over (7)Play Dead: 4 Grrr (0)Quit (1)Speak (2)Move (3)Eat (4)Growl (5)Whimper (6)Roll Over (7)Play Dead: 7 Is this the end of Little Caeser? (0)Quit (1)Speak (2)Move (3)Eat (4)Growl (5)Whimper (6)Roll Over (7)Play Dead: 0 Analiza: U linijama 7-17, klasa Dog je krirana, sa 7 funkcijskih članova istog povratnog tipa i potpisa. U liniji 19, typedef deklarira PDF kao pokazivač na funkcijski član od Dog koji ne prima parametre i ne vraća vrijednost, i koji je const: potpis 7 funkcijskih članova od Dog. U linijama 23-30, polje DogFunctions je deklarirano da drži 7 takvih funkcijskih članova, i inicijalizirano s adresama tih funkcija. U linijama 38 i 39, korisnik je upitan da odabere metodu. Ukoliko ne odabere Quit, novi Dog je kreiran u slobodnom spremniku, i potom se korektna metoda poziva iz polja u liniji 49. Evo još jedne dobre linije za testiranje nekog C++ programera: (pDog->*DogFunctions[Method-1])(); Iako pomalo ezoterična, kad vam zatreba tablicu izgrađenu iz funkcijskih članova, može učiniti vaš program mnogo razumljivijim.
Kviz 1. Mogu li statički podatkovni članovi biti privatni? 2. Pokažite deklaraciju statičkog podatkovnog člana. 3. Pokažite deklaraciju statičkog funkcijskog pokazivača. 4. Pokažite deklariciju za pokazivač na funkciju koji vraća long i prima integer parametar.
5. Promijenite pokazivač iz četvrtog pitanja da pokazuje na funkcijski član klase Car. 6. Pokažite deklaraciju za polje 10 pokazivača difiniranih u pitanju 5.
Vježbe 1. Napišite kratak program deklariranjem klase s jednim podatkovnim članom i jednim statičkim podatkovnim članom. Neka konstruktor inicijalizira podatkovni član i inkrementira statični podatkovni član. Neka destruktor dekrementira podatkovni član. 2. Koristeći program sa vježbe 1, napišite kratki program koji kreira tri objekta i potom prikazuje njihove podatkovne članove i statički podatkovni član. Potom uništite svaki objekt i pokažite kako to djeluje na statički podatkovni član. 3. Promijenite program sa vježbe 2 da koristi statički funkcijski član kako bi pristupio statičkom podatkovnom članu. Proglasite statički podatkovni član privatnim. 4. Napišite pokazivač na funkcijski član za pristup ne-statičkim podatkovnim članovima u programu sa vježbe 3, i koristite taj pokazivač za ispis vrijednosti tog podatka. 5. Dodajte još dva podatkovna člana u klasu iz prethodnog pitanja. Dodajte pristupne funkcije za dobijanje vrijednosti tih članova, i dajte svim funkcijskim članovima iste povratne tipove i potpise. Koristeći pokazivač na funkcijske članove pristupite tim funkcijama.
Lekcija 15
Tokovi
Do sada ste koristili cout za pisanje na ekran i cin za čitanje s tastature, bez potpunog razumijevanja kako zapravo rade. Danas ćete naučiti • Što su tokovi i kako se koriste • Kako upravljati ulazom i izlazom koristeći tokove • Kako pisati i čitati iz datoteka koristeći tokove
Osnovno o tokovima C++ ne definira kao dio jezika, kako se podaci zapisuju na ekran ili u datoteku, niti kako se podaci učitavaju u program. Ovo su očito esencijalni dijelovi svakog ozbiljnijeg C++ programa, te stoga standardna C++ bibilioteka uključuje iostream biblioteku, koja omogućuje ulazno/izlazne funkcije našem programu (engl. input/output -- I/O). Prednost držanja ulaza i izlaza izvan samog jezika i uključenog u biblioteke je u tome što time naši programi postaju neovisni o platformi na kojoj se izvršavaju.
Enkapsulacija iostream klase promatraju tok podataka iz vašeg programa ka ekranu kao tok podataka, u kojem jedan byte slijedi drugog. Ako je destinacija toka datoteka ili ekran, izvor je obično neki dio našeg programa. Ako je tok obrnut, podaci mogu doći sa tastature ili iz datoteke na disku te se potom "pretočiti" u podatkovne varijable. Osnovni cilj tokova je enkapsulacija problema vezanih uz dobijanje/proslijeđivanje podataka. Jednom kad je tok kreiran, vaš program radi s njim, i sam tok sakriva detalje. Slika 15.1 ilustrira osnovnu ideju. Slika 15.1 Enkapsulacija kroz tokove.
Buffering Zapisivanje na disk (i u manjem obimu na ekran) je vrlo "skupo". Potrebo je puno vremena (relativno govoreći) za pisanje i čitanje podataka sa diska, a izvršenje programa je općenito blokirano sa tim pisanjima i čitanjima. Za rješenje tog problema, tokovi nam omogućuju "buffering." Podaci se zapisuju na tok, ali se on ne zapisuje trenutno na disk. Umjesto toga, spremnik (engl. buffer) toka se puni, i jednom kad je pun zapisuje svoj kompletan sadržaj na disk odjednom.
Tokovi i bufferi Kao što se može i očekivati, C++ implmentira tokove i spremnike na objektno-orjentirani način. • streambuf klasa upravlja bufferom, i njoj pripadne funkcije omogućuju punjenje, pražnjenje, te ostala manipuliranja s bufferom. • ios klasa je osnovna klasa za ulazno izlazne tokove. ios klasa ima streambuf objekt kao podatkovni član. • istream i ostream klase izvode se iz ios klase i specijalizirane su za ponašanje toka prilikom ulaza, odnosno izlaza.
• •
iostream klasa je derivirana iz istream i ostream klasa i pruža nam metode ulaza i izlaza za pisanje na ekran. fstream klase omogućuju input i output iz datoteka.
Standardni I/O objekti Kada se C++ program koji uključuje iostream klase pokrene, četiri objekta se kreiraju i inicijaliziraju: • cin barata ulazom za standardni input, tastaturom. • cout barata outputom za standardni output, ekran. • cerr barata nebufferirani output na standardni uređaj za baratanje greškama, ekran. Budući da je nebuferriran, sve se odmah ispisuje na ekran, bez popunjavanja ili pražnjenja. • clog upravlja bufferiranim porukama o greškama, koje se obično preusmjeravaju u log datoteku.
Input upotrebom cin Globalni objekt cin je odgovoran za unos i dostpan je našem programu kad uključimo iostream.h. U dosadašnjim primjerima, koristeći preopterećeni operator ekstrakcije (>>) umetali smo podatke u programske varijable. Kako to radi? Sintakse je, kako se zasigurno sjećate, slijedeća: int someVariable; cout << "Enter a number: "; cin >> someVariable; O globalnom objektu nešto kasnije; za sada se usredotočimo na treću liniju, cin >> someVariable;. Što zaključujete o cin? Očito je riječ o globalnom objektu, budući da nije definiran u našem kodu. S prijašnjim iskustvom o operatorima također znate da je cin preopteretio operator (>>) i da je efekt naredbe da se bilo koji podaci koji se nalaze u bufferu prenose u lokalnu varijablu, someVariable. Ono što odmah ne upada u oči jest da je cin preopteretio operator ekstrakcije za veliki raspon parametara, među njima int&, short&, long&, double&, float&, char&, char*, i tako dalje. Kada utipkate cin >> someVariable;, tipu od someVariable se pristupa. U gornjem primjeru, someVariable je cijeli broj, pa je slijedeća funkcija pozvana: istream & operator>> (int &) Primjetite da je stoga što parametar proslijeđujemo po referenci, moguće djelovati na originalnu varijablu. Listing 15.1 ilustrira ideju upotrebe cin. Listing 15.1. cin barata različitim tipovima podataka. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22:
//Listing 15.1 -- character strings and cin #include int main() { int myInt; long myLong; double myDouble; float myFloat; unsigned int myUnsigned; cout << "int: "; cin >> myInt; cout << "Long: "; cin >> myLong; cout << "Double: "; cin >> myDouble; cout << "Float: "; cin >> myFloat; cout << "Unsigned: "; cin >> myUnsigned;
23: 24: cout << "\n\nInt:\t" << myInt << endl; 25: cout << "Long:\t" << myLong << endl; 26: cout << "Double:\t" << myDouble << endl; 27: cout << "Float:\t" << myFloat << endl; 28: cout << "Unsigned:\t" << myUnsigned << endl; 29: return 0; 30: } Output: int: 2 Long: 70000 Double: 987654321 Float: 3.33 Unsigned: 25 Int: 2 Long: 70000 Double: 9.87654e+08 Float: 3.33 Unsigned: 25
Stringovi cin također barata i pokazivačima na znakove (char*); tako možete kreirati i spremnik znakova i koristiti cin za njegovo ispunjavanje. Na primjern možete napisati : char YourName[50] cout << "Enter your name: "; cin >> YourName; Ako unesete Jesse, varijabla YourName će biti ispunjena znakovima J, e, s, s, e, \0. Zadnji znak je null; cin automatski završava string sa null znakom, i morate imati dovoljno mjesta u bufferu za spremanje cijelog stringa i null znaka. Taj znak signalizira kraj stringa standardnoj biblioteci funkcija.
Problemi sa stringovima cin vjeruje da je i razmak separator. Kad vidi razmak ili novu liniju, pretpostavi da je upis avršen, i u slučaju stringa dodaje null znak upravo na to mjesto. Listing 15.2 ilustrira problem. Listing 15.2. Pokušaj upisivanja više riječi u cin. 1: //Listing 15.2 -- character strings and cin 2: 3: #include 4: 5: int main() 6: { 7: char YourName[50]; 8: cout << "Your first name: "; 9: cin >> YourName; 10: cout << "Here it is: " << YourName << endl; 11: cout << "Your entire name: "; 12: cin >> YourName; 13: cout << "Here it is: " << YourName << endl; 14: return 0; 15: } Output: Your first name: Jesse Here it is: Jesse Your entire name: Jesse Liberty Here it is: Jesse Za razumijevanje ovoga problema pogledajte listing 15.3, koji pokazuje input više polja.
Listing 15.3. Višestruki input. 1: //Listing 15.3 - character strings and cin 2: 3: #include 4: 5: int main() 6: { 7: int myInt; 8: long myLong; 9: double myDouble; 10: float myFloat; 11: unsigned int myUnsigned; 12: char myWord[50]; 13: 14: cout << "int: "; 15: cin >> myInt; 16: cout << "Long: "; 17: cin >> myLong; 18: cout << "Double: "; 19: cin >> myDouble; 20: cout << "Float: "; 21: cin >> myFloat; 22: cout << "Word: "; 23: cin >> myWord; 24: cout << "Unsigned: "; 25: cin >> myUnsigned; 26: 27: cout << "\n\nInt:\t" << myInt << endl; 28: cout << "Long:\t" << myLong << endl; 29: cout << "Double:\t" << myDouble << endl; 30: cout << "Float:\t" << myFloat << endl; 31: cout << "Word: \t" << myWord << endl; 32: cout << "Unsigned:\t" << myUnsigned << endl; 33: 34: cout << "\n\nInt, Long, Double, Float, Word, Unsigned: "; 35: cin >> myInt >> myLong >> myDouble; 36: cin >> myFloat >> myWord >> myUnsigned; 37: cout << "\n\nInt:\t" << myInt << endl; 38: cout << "Long:\t" << myLong << endl; 39: cout << "Double:\t" << myDouble << endl; 40: cout << "Float:\t" << myFloat << endl; 41: cout << "Word: \t" << myWord << endl; 42: cout << "Unsigned:\t" << myUnsigned << endl; 43: 44: 45: return 0; 46: } Output: Int: 2 Long: 30303 Double: 393939397834 Float: 3.33 Word: Hello Unsigned: 85 Int: 2 Long: 30303 Double: 3.93939e+11 Float: 3.33
Word: Hello Unsigned: 85 Int, Long, Double, Float, Word, Unsigned: 3 304938 393847473 6.66 bye -2 Int: 3 Long: 304938 Double: 3.93847e+08 Float: 6.66 Word: bye Unsigned:
65534
Ostali funkcijski članovi cin Uz preopterećeni operator >>, cin ima i neke korisne funkcijske članove. Oni se koriste kad je preciznija kontrola inputa potrebna.
Jednostuki znakovni input Funkcijski član get() nam služi za učitavanje samo jednog znaka, i to može raditi na dva načina. get() može biti korišten bez parametara, u tom slučaju se koristi povratna vrijednost, ili može biti upotrebljen s referencom na znak. Listing 15.4. ilustrira upotrebu get() bez parametara. Listing 15.4. Upotreba get() bez parametara. 1: // Listing 15.4 - Using get() with no parameters 2: #include 3: 4: int main() 5: { 6: char ch; 7: while ( (ch = cin.get()) != EOF) 8: { 9: cout << "ch: " << ch << endl; 10: } 11: cout << "\nDone!\n"; 12: return 0; 13: } PAŽNJA: Za izlazak iz program morate poslati znak EOF (eng. End Of File) sa tastature. Na DOS računalima to je Ctrl+Z; UNIX koristi Ctrl+D. Output: Hello ch: H ch: e ch: l ch: l ch: o ch: World ch: W ch: o ch: r ch: l ch: d ch:
(ctrl-z) Done!
Primjetite da svaka implementacija istream ne podržava verziju get() prikazanu na slijedećem listingu. Listing 15.5 Upotreba get() sa parametrima. 1: // Listing 15.5 - Using get() with parameters 2: #include 3: 4: int main() 5: { 6: char a, b, c; 7: 8: cout << "Enter three letters: "; 9: 10: cin.get(a).get(b).get(c); 11: 12: cout << "a: " << a << "\nb: " << b << "\nc: " << c << endl; 13: return 0; 14: } Output: Enter three letters: one a: o b: n c: e
Dobijanje stringova iz standardnog inputa Operator ekstrakcije (>>) se može koristiti za ispunjavanje polja znakova, ka što mogu i funkcijski članovi get() i getline(). Konačni oblik od get() prima tri parametra. Prvi parametar je pokazivač na polje znakova, drugi je maksimalan broj znakova plus jedan, i treći je znak terminacije. Ako unesete 20 za drugi parametar, get() će pročitati 19 znakova i potom null-terminirati string. Treći parametar, znak terminacije ima podrazumijevanu vrijednost "newline" (`\n'). On se koristi ako dođemo do kraja prije ispunjavanja cijelog polja. Listing 15.6 ilustrira ovakvu upotrebu get(). Listing 15.6. Upotreba get() s poljem znakova. 1: // Listing 15.6 - Using get() with a character array 2: #include 3: 4: int main() 5: { 6: char stringOne[256]; 7: char stringTwo[256]; 8: 9: cout << "Enter string one: "; 10: cin.get(stringOne,256); 11: cout << "stringOne: " << stringOne << endl; 12: 13: cout << "Enter string two: "; 14: cin >> stringTwo; 15: cout << "StringTwo: " << stringTwo << endl; 16: return 0; 17: } Output: Enter string one: Now is the time stringOne: Now is the time
Enter string two: For all good StringTwo: For Drugi problem za rješenje problema učitavanja stringova je upotreba getline(), kao što je ilustrirano u listingu 15.7. Listing 15.7. Upotreba getline(). 1: // Listing 15.7 - Using getline() 2: #include 3: 4: int main() 5: { 6: char stringOne[256]; 7: char stringTwo[256]; 8: char stringThree[256]; 9: 10: cout << "Enter string one: "; 11: cin.getline(stringOne,256); 12: cout << "stringOne: " << stringOne << endl; 13: 14: cout << "Enter string two: "; 15: cin >> stringTwo; 16: cout << "stringTwo: " << stringTwo << endl; 17: 18: cout << "Enter string three: "; 19: cin.getline(stringThree,256); 20: cout << "stringThree: " << stringThree << endl; 21: return 0; 22: } Output: Enter string one: one two three stringOne: one two three Enter string two: four five six stringTwo: four Enter string three: stringThree: five six
Upotreba cin.ignore() Ponekad želite ignorirati preostale znakove u liniji dok ne dođete ili na kraj linije (EOL) ili kraj datoteke (EOF). Funkcijski član ignore() služi toj svrsi . ignore() prima dva parametra, maksimalan broj znakova koje će ignorirati i znak terminacije. Ako utipkate ignore(80,'\n'), do 80 znakova će biti odbačeno dok ne nađemo znak nove linije. Potom se newline odbacuje i ignore() naredba završava. Listing 15.8 ilustrira upotrebu ignore(). Listing 15.8. Upotreba ignore(). 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
// Listing 15.8 - Using ignore() #include int main() { char stringOne[255]; char stringTwo[255]; cout << "Enter string one:"; cin.get(stringOne,255); cout << "String one" << stringOne << endl; cout << "Enter string two: "; cin.getline(stringTwo,255);
15: cout << "String two: " << stringTwo << endl; 16: 17: cout << "\n\nNow try again...\n"; 18: 19: cout << "Enter string one: "; 20: cin.get(stringOne,255); 21: cout << "String one: " << stringOne<< endl; 22: 23: cin.ignore(255,'\n'); 24: 25: cout << "Enter string two: "; 26: cin.getline(stringTwo,255); 27: cout << "String Two: " << stringTwo<< endl; 28: return 0; 29: } Output: Enter string one:once upon a time String oneonce upon a time Enter string two: String two: Now try again... Enter string one: once upon a time String one: once upon a time Enter string two: there was a String Two: there was a
peek() i putback() Input objekt cin ima dvije dodatne metode koje mogu biti prilično korisne: peek(), koja gleda ali ne izvlači slijedeći znak, te putback(), koji unosi znak u ulazni tok. Listing 15.9 ilustrira kako se oni mogu koristiti. Listing 15.9. Upotreba peek() i putback(). 1: // Listing 15.9 - Using peek() and putback() 2: #include 3: 4: int main() 5: { 6: char ch; 7: cout << "enter a phrase: "; 8: while ( cin.get(ch) ) 9: { 10: if (ch == `!') 11: cin.putback(`$'); 12: else 13: cout << ch; 14: while (cin.peek() == `#') 15: cin.ignore(1,'#'); 16: } 17: return 0; 18: } Output: enter a phrase: Now!is#the!time#for!fun#! Now$isthe$timefor$fun$
Pražnjenje izlaza Već ste vidjeli kako će endl isprazniti izlazni buffer. endl poziva cout funkcijski član flush(), koji ispisuje sve podatke iz buffera. Možete pozivati flush() metodu direktno, ili pozivanjem flush() funkcijskog člana pišući slijedeće: cout << flush
Ovo je prikladno kada trebamo osigurati da je izlazni buffer prazan i da je njegov sadržaj ispisan na ekran.
Povezane funkcije Baš kao što operator ekstrakcije može biti nadopunjen s get() i getline(), operator umetanja može biti nadopunjen sa put() i write(). Funkcija put() se koristi za pisanje jednoga znaka na izlazni uređaj. Budući da put() vraća ostream referencu, i budući da je cout također ostream objekt, možete ulančavati put() baš kao i operator umetanja Listing 15.10 ilustrira tu ideju. Listing 15.10. Upotreba put(). 1: // Listing 15.10 - Using put() 2: #include 3: 4: int main() 5: { 6: cout.put(`H').put(`e').put(`l').put(`l').put(`o').put(`\n'); 7: return 0; 8: } Output: Hello Funkcija write() radi kaoi operator umetanja (<<), osim što prima i parametar koji kazuje funkciji maksimalan broj znakova za pisanje. Listing 15.11 pokazuje njezinu upotrebu. Listing 15.11. Upotreba write(). 1: // Listing 15.11 - Using write() 2: #include 3: #include <string.h> 4: 5: int main() 6: { 7: char One[] = "One if by land"; 8: 9: 10: 11: int fullLength = strlen(One); 12: int tooShort = fullLength -4; 13: int tooLong = fullLength + 6; 14: 15: cout.write(One,fullLength) << "\n"; 16: cout.write(One,tooShort) << "\n"; 17: cout.write(One,tooLong) << "\n"; 18: return 0; 19: } Output: One if by land One if by One if by land i?!
Upotreba cout.width() Podrazumijevana širina vašega izlaza će biti upravo onolika koliko je potrebno za ispis zadanog znaka, broja, stringa, odn. izlaznog toka. To možete i promijeniti koristeći width(). Budući da je width() funkcijski član, on se mora pozvati preko cout objekta. On samo mijenja širinu slijedećeg izlaznog polja i potom se odmah vraća na default vrijednost. Listing 15.12. ilustrira njegovu upotrebu.
Listing 15.12. Podešavanje širine izlaza. 1: // Listing 15.12 - Adjusting the width of output 2: #include 3: 4: int main() 5: { 6: cout << "Start >"; 7: cout.width(25); 8: cout << 123 << "< End\n"; 9: 10: cout << "Start >"; 11: cout.width(25); 12: cout << 123<< "< Next >"; 13: cout << 456 << "< End\n"; 14: 15: cout << "Start >"; 16: cout.width(4); 17: cout << 123456 << "< End\n"; 18: 19: return 0; 20: } Output: Start > 123< End Start > 123< Next >456< End Start >123456< End
Postavljanje Fill znakova Normalno cout spunjava prazna polja nastala pozivanjem width() s razmacima. Ponekad ćete takve praznine željeti ispuniti drugim znakovima, na primjer, asteriksima (*). Za to možemo koristiti fill() i kao parametar proslijediti znak koji želimo koristiti kao fill znak. Listing 15.13 ilustrira ovo. Listing 15.13. Upotreba fill(). 1: // Listing 15.3 - fill() 2: 3: #include 4: 5: int main() 6: { 7: cout << "Start >"; 8: cout.width(25); 9: cout << 123 << "< End\n"; 10: 11: 12: cout << "Start >"; 13: cout.width(25); 14: cout.fill(`*'); 15: cout << 123 << "< End\n"; 16: return 0; 17: } Output: Start > 123< End Start >******************123< End
Postavljanje zastavica iostream objekti vode računa o svom stanju korištenjem zastavica (eng. flag). Vi možete postavljati te zastavice pozivanjem funkcije setf() i proslijeđivanjem jedne ili druge predefinirane enumerirane konstante.
Novi izraz: Za objekte kažemo da imaju stanje kad neki ili svi njihovi podaci predstavljaju uvjet koji se može mijenjati tjekom izvršavanja programa. Na primjer, možete odlučiti prikazati ili ne nule iza decimalnog zareza. Za uključivanje nula, pozovemo setf(ios::showpoint). Enumerirane konstante su u dosegu iostream klase (ios) i prema tome se mogu punopravno pozivati preko ios::flagname, kao npr. ios::showpoint. Možete uključiti znak za pozitivne brojeve (+) sa ios::showpos. Možete promijeniti poravnavanje sa ios::left, ios::right, ili ios::internal. Konačno, možete odabrati brojevni sustav za prikazivanje brojeva sa ios::dec (decimal), ios::oct (octal--baze osam), ili ios::hex (hexadecimal--baze šesnaest). Listing 15.14 ilustrira te postavke. Kao bonus, Listing 15.14 također pokazuje setw manipulator, koji određuje širinu ali također može biti ulančan s operatorom umetanja. Listing 15.14. Upotreba setf. 1: // Listing 15.14 - Using setf 2: #include 3: #include 4: 5: int main() 6: { 7: const int number = 185; 8: cout << "The number is " << number << endl; 9: 10: cout << "The number is " << hex << number << endl; 11: 12: cout.setf(ios::showbase); 13: cout << "The number is " << hex << number << endl; 14: 15: cout << "The number is " ; 16: cout.width(10); 17: cout << hex << number << endl; 18: 19: cout << "The number is " ; 20: cout.width(10); 21: cout.setf(ios::left); 22: cout << hex << number << endl; 23: 24: cout << "The number is " ; 25: cout.width(10); 26: cout.setf(ios::internal); 27: cout << hex << number << endl; 28: 29: cout << "The number is:" << setw(10) << hex << number << endl; 30: return 0; 31: } Output: The number is 185 The number is b9 The number is 0xb9 The number is 0xb9 The number is 0xb9 The number is 0x b9 The number is:0x b9
Tokovi protiv printf() funkcije Printf funkcija je zaostavština iz C jezika i nije ju preporučljivo koristiti zbog njenog nepodržavanaj klasa i općenite nesigurnosti vezane uz ispisne rezultate krivo definiranih tipova podataka. Ipak, mnogi ju programi i
dalje vole koristiti stoga što vrlo jednostavno omogućuje formatiranje teksta. Stoga vam dajemo primjer da se podsjetite kako se ona koristi. Prije upotrebe provjerite dali ste uključili stdio.h biblioteku čiji je printf funkcija član. Listing 15.15. Ispis sa printf(). 1: #include <stdio.h> 2: int main() 3: { 4: printf("%s","hello world\n"); 5: 6: char *phrase = "Hello again!\n"; 7: printf("%s",phrase); 8: 9: int x = 5; 10: printf("%d\n",x); 11: 12: char *phraseTwo = "Here's some values: "; 13: char *phraseThree = " and also these: "; 14: int y = 7, z = 35; 15: long longVar = 98456; 16: float floatVar = 8.8; 17: 18: printf("%s %d %d %s %ld %f\n",phraseTwo,y,z,phraseThree,longVar,floatVar); 19: 20: char *phraseFour = "Formatted: "; 21: printf("%s %5d %10d %10.5f\n",phraseFour,y,z,floatVar); 22: return 0; 23: } Output: hello world Hello again! 5 Here's some values: 7 35 and also these: 98456 8.800000 Formatted: 7 35 8.800000
Datotečni ulaz i izlaz Tokovi omogućuju uniforman način aratanja podacima bez obzira dolaze li iz hard diska ili s tastature i odlaze li na ekran ili hard disk. Za otvaranje i zatvaranje datoteka, kreirate ifstream i ofstream objekte kako je objašnjeno u slijedećih nekoliko stranica.
ofstream Određeni objekti koji se koriste za čitanje ili pisanje u datoteke zovu se ofstream objekti. Oni su izvedeni iz iostream objekata koje ste dosad koristili. Kako biste zapisali nešto u datoteku, morate prvo kreirati ofstream objekt, i potom povezati taj objekt sa određenom datotekom na vašem disku. Za upotrebu ofstream objekata, morate uključiti fstream.h u vaš program. PAŽNJA: Budući da fstream.h uključuje iostream.h, nema potrebe da eskplicitno uvodite iostream.
Uvjetna stanja iostream objekti održavaju zastavice koje izvještavaju o stanju vašeg ulaza i izlaza. Svaku od zastavica možete provjeriti sa Boolean funkcijama eof(), bad(), fail(), i good(). Funkcija eof() vraća istinu ako je iostream objekt naišao na EOF, kraj datotekee. Funkcija bad() vraća TRUE ako pokušate nedozvoljenu operaciju. Funkcija fail() vraća TRUE uvijek kad je bad() istinit ili operacija propadne. Konačno, funkcija good() vraća TRUE svaki put kad su preostale tri funkcije FALSE.
Otvaranje datoteka za ulaz/izlaz Za otvaranje datoteke myfile.cpp sa ofstream objektom, deklarirajte instancu od ofstream objekta i proslijedite ime datoteke kao parametar: ofstream fout("myfile.cpp"); Otvaranje te datoteke za ulaz radi na potpuno isti način, osim što koristi ifstream objekt: ifstream fin("myfile.cpp"); Primjetite da su fout i fin imena koja vi dodjeljujete. Jedna važna funkcija datotečnog toka koju ćete uskoro trebati je close(). Svaki objekt dat. toka koji kreirate otvara datoteku za ulaz, izlaz ili oboje. Važno je da zatvorite (close()) datoteku nakon što ste završili sa čitanjem ili pisanjem; time osiguravate da datoteka neće biti oštećena i da će svi podaci iz buffera biti snimljeni. Jednom kad su objekti povezani s datotekama, mogu se upotrebljavati kao i svaki objekt toka. Listing 15.16 ilustrira ovo. Listing 15.16. Otvaranje datoteka za čitanje i pisanje. 1: #include 2: int main() 3: { 4: char fileName[80]; 5: char buffer[255]; // for user input 6: cout << "File name: "; 7: cin >> fileName; 8: 9: ofstream fout(fileName); // open for writing 10: fout << "This line written directly to the file...\n"; 11: cout << "Enter text for the file: "; 12: cin.ignore(1,'\n'); // eat the newline after the file name 13: cin.getline(buffer,255); // get the user's input 14: fout << buffer << "\n"; // and write it to the file 15: fout.close(); // close the file, ready for reopen 16: 17: ifstream fin(fileName); // reopen for reading 18: cout << "Here's the contents of the file:\n"; 19: char ch; 20: while (fin.get(ch)) 21: cout << ch; 22: 23: cout << "\n***End of file contents.***\n"; 24: 25: fin.close(); // always pays to be tidy 26: return 0; 27: } Output: File name: test1 Enter text for the file: This text is written to the file! Here's the contents of the file: This line written directly to the file... This text is written to the file! ***End of file contents.***
Promjena podrazumijevanog ponašanja za ofstream Podrazumijevano ponašanje prilikom otvaranja datoteke je kreiranje datoteke ako ne postoji, i potom brisanje njezinog sadržaja. Ako ne želite to podrazumijevano ponašanje, možete eksplicitno navesti drugi argument prilikom konstrukcije vašeg ofstream objekta.
Valjani argumenti su: • ios::app—Dodaje na kraj postojećih datoteka umjesto brisanja. • ios::at—Stavlja vas na kraj datoteke, ali možete upisivati podatke bilo gdje. • ios::trun—Podrazumijevani. Uzrokuje pražnjenje postojećih datoteka. • ios::nocreat—Ako datoteka ne postoji, otvaranje propada. • ios::noreplac—Ako datoteka već postoji, otvaranje propada. Listing 15.17 ilustrira dodavanje na ponovno otvorenu datoteku s listinga 15.16 Listing 15.17. Dodavanja na kraj datoteke. 1: #include 2: int main() // returns 1 on error 3: { 4: char fileName[80]; 5: char buffer[255]; 6: cout << "Please re-enter the file name: "; 7: cin >> fileName; 8: 9: ifstream fin(fileName); 10: if (fin) // already exists? 11: { 12: cout << "Current file contents:\n"; 13: char ch; 14: while (fin.get(ch)) 15: cout << ch; 16: cout << "\n***End of file contents.***\n"; 17: } 18: fin.close(); 19: 20: cout << "\nOpening " << fileName << " in append mode...\n"; 21: 22: ofstream fout(fileName,ios::app); 23: if (!fout) 24: { 25: cout << "Unable to open " << fileName << " for appending.\n"; 26: return(1); 27: } 28: 29: cout << "\nEnter text for the file: "; 30: cin.ignore(1,'\n'); 31: cin.getline(buffer,255); 32: fout << buffer << "\n"; 33: fout.close(); 34: 35: fin.open(fileName); // reassign existing fin object! 36: if (!fin) 37: { 38: cout << "Unable to open " << fileName << " for reading.\n"; 39: return(1); 40: } 41: cout << "\nHere's the contents of the file:\n"; 42: char ch; 43: while (fin.get(ch)) 44: cout << ch; 45: cout << "\n***End of file contents.***\n"; 46: fin.close(); 47: return 0; 48: }
Output: Please re-enter the file name: test1 Current file contents: This line written directly to the file... This text is written to the file! ***End of file contents.*** Opening test1 in append mode... Enter text for the file: More text for the file! Here's the contents of the file: This line written directly to the file... This text is written to the file! More text for the file! ***End of file contents.*** Napomena: if(fin) = (fin.good()). if(!fout) = (fout.fail()).
Binarni protiv tekst datoteka Za razlikovanje između tekstualnih i binarnih datoteka, C++ podržava ios::binary zastavicu. Binarne datotek mogu spremiti ne samo integere i stringove, nego cijelu strukturu podataka. Svi podatke možete pisati odjednom koristeći write() metodu iz fstream. Ako koristite write(), podatke možete povratiti sa read(). Svaka od tih funkcija očekuje pokazivač na znak. Drugi argument tih funkcija je broj znakova za pisanje, kojeg možemo odrediti sa sizeof(). Listing 15.18 ilustrira pisanje sadržaja klase u datoteku. Listing 15.18. Pisanje klase u datoteku. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33:
#include class Animal { public: Animal(int weight, long days):itsWeight(weight),itsNumberDaysAlive(days){} ~Animal(){} int GetWeight()const { return itsWeight; } void SetWeight(int weight) { itsWeight = weight; } long GetDaysAlive()const { return itsNumberDaysAlive; } void SetDaysAlive(long days) { itsNumberDaysAlive = days; } private: int itsWeight; long itsNumberDaysAlive; }; int main() // returns 1 on error { char fileName[80]; char buffer[255]; cout << "Please enter the file name: "; cin >> fileName; ofstream fout(fileName,ios::binary); if (!fout) { cout << "Unable to open " << fileName << " for writing.\n"; return(1); }
34: Animal Bear(50,100); 35: fout.write((char*) &Bear,sizeof Bear); 36: 37: fout.close(); 38: 39: ifstream fin(fileName,ios::binary); 40: if (!fin) 41: { 42: cout << "Unable to open " << fileName << " for reading.\n"; 43: return(1); 44: } 45: 46: Animal BearTwo(1,1); 47: 48: cout << "BearTwo weight: " << BearTwo.GetWeight() << endl; 49: cout << "BearTwo days: " << BearTwo.GetDaysAlive() << endl; 50: 51: fin.read((char*) &BearTwo, sizeof BearTwo); 52: 53: cout << "BearTwo weight: " << BearTwo.GetWeight() << endl; 54: cout << "BearTwo days: " << BearTwo.GetDaysAlive() << endl; 55: fin.close(); 56: return 0; 57: } Output: Please enter the file name: Animals BearTwo weight: 1 BearTwo days: 1 BearTwo weight: 50 BearTwo days: 100
Obrada komandne linije Mnogi OS-i, poput DOS-a ili UNIX-a, omogućuju korisniku da proslijedi parametre u program prilikom njegovog pokretanja. To se nazivaju komandno linijske opcije i obično se odvajaju razmacina u komandnoj liniji. Na prmjer: SomeProgram Param1 Param2 Param3 Ovi se parametri ne prenose u main() direktno. U svaku main() funkciju se šalju dva parametra. Prvi je broj argumenata u komandnoj liniji (broji se i ime programa, pa svaki program ima bar jedan parametar). Slijedeći parametar je polje pokazivača na znakovne stringove. Prvi argument se zove argc (argument count), ali vi ga možete nazvati kako god hoćete, to je samo stvar konvencije. Drugi se obično naziva argv (argument vector). Listing 15.19 ilustrira kako koristiti komandno linijske argumente. Listing 15.19. Upotreba komandno-linijskih argumenata. 1: #include 2: int main(int argc, char **argv) 3: { 4: cout << "Received " << argc << " arguments...\n"; 5: for (int i=0; i<argc; i++) 6: cout << "argument " << i << ": " << argv[i] << endl; 7: return 0; 8: } Output: TestProgram Teach Yourself C++ In 21 Days Received 7 arguments... argumnet 0: TestProgram.exe argument 1: Teach argument 2: Yourself
argument 3: C++ argument 4: In argument 5: 21 argument 6: Days Češća upotreba je prikazana modifikacijom listinga 15.18 da prima ime datoteke kao komandno linijski argument. Listing ne sadrži deklaraciju klase, koja je nepromijenjena. Listing 15.20. Upotreba komandno linijskih argumenata. 1: #include 2: int main(int argc, char *argv[]) // returns 1 on error 3: { 4: if (argc != 2) 5: { 6: cout << "Usage: " << argv[0] << " " << endl; 7: return(1); 8: } 9: 10: ofstream fout(argv[1],ios::binary); 11: if (!fout) 12: { 13: cout << "Unable to open " << argv[1] << " for writing.\n"; 14: return(1); 15: } 16: 17: Animal Bear(50,100); 18: fout.write((char*) &Bear,sizeof Bear); 19: 20: fout.close(); 21: 22: ifstream fin(argv[1],ios::binary); 23: if (!fin) 24: { 25: cout << "Unable to open " << argv[1] << " for reading.\n"; 26: return(1); 27: } 28: 29: Animal BearTwo(1,1); 30: 31: cout << "BearTwo weight: " << BearTwo.GetWeight() << endl; 32: cout << "BearTwo days: " << BearTwo.GetDaysAlive() << endl; 33: 34: fin.read((char*) &BearTwo, sizeof BearTwo); 35: 36: cout << "BearTwo weight: " << BearTwo.GetWeight() << endl; 37: cout << "BearTwo days: " << BearTwo.GetDaysAlive() << endl; 38: fin.close(); 39: return 0; 40: } Output: BearTwo weight: 1 BearTwo days: 1 BearTwo weight: 50 BearTwo days: 100
. Kviz 1. Što je operator umetanja i što on radi? 2. Što je operator ekstrakcije i što on radi?
3. Koja su tri oblika cin.get(), i u čemu je njihova razlika? 4. U čemu je razlika između cin.read() i cin.getline()? 5. Koja je podrazumijevana širina za izlaz long integera korištenjem operatora umetanja? 6. Koja je povratna vrijednost operatora umetanja? 7. Koje parametre konstruktor na ofstream objekt uzima? 8. Što radi ios::ate argument ?
Vježbe 1. Napišite program koji piše u četri standardna iostream objekta: cin, cout, cerr, i clog. 2. Napišite program za upis cijelog imena i zatim ga ispišite na ekran. 3. Napišite ponoov listing 15.9, ali da ne koristi putback() ili ignore(). 4. Napišite program koji prima ime datoteke kao parametar i otvara datoteku za čitanje. Pročitajte svaki znak iz datoteke i prikažite samo slova i znakove na ekran, ignorrirajući sve specijalne znakove. Zatvorite datoteku i izađite. 5. Napišite program koji prikazuje svoje komandno linijske argumente u obrnutom redoslijedu i ne prikazuje ime programa.
Lekcija 16
Predlošci
Predlošci nam dozvoljavaju da naučimo prevoditelj kako da napravi listu bilo kojeg tipa, umjesto kreiranja specifičnih tipova lista—PartsList je lista dijelova, CatList je lista mačaka. jedino u čemu se oni razlikuju je tip podatka unutar liste. S predlošcima, tip podatka na listi postaje parametar u definiciji klase. Uobičajena komponenta gotovo svake C++ bibilioteke je klasa niz. Kao što smo vidjeli sa Lists, zamorno je i neefikasno kreirati jednu klasu polje za integere, drugu za brojeve duple preciznosti, i još jednu za Animals. Predlošci nam omogućuju deklariranje parametriziranog polja klase i specificiranje tipa objekta kojeg će svaka instanca polja sadržavati. Novi Izraz: Instancijacija je djelo stvaranja specifičnog tipa iz predloška. Pojedine klase nazivamo instancama predloška. Parameterizirani predlošci pružaju nam mogućnost stvaranja općih klasa, te proslijeđivanje tipova kao parametara toj klasi kako bi se izgradila specifična instanca.
Definicija predloška Parametrizirani Array objekt (predložak za niz) definiramo pisanjem: 1: template // declare the template and the parameter 2: class Array // the class being parameterized 3: { 4: public: 5: Array(); 6: // full class declaration here 7: }; Ključna riječ template se koristi na početku svake deklaracije i definicije klase predloška. Parametri predloška dolaze nakon ključne riječi template. Parametri se mijenjaju sa svakom instancom. U ovom primjeru, ključnu riječ slijedi identifikator T. Identifikator koristimo kroz ostatak definicije predloška kako bi označili parametrizirani tip. Jedna instanca te klase će zamijeniti int tamo gdje sa pojavi T appears, a druga će zamijeniti Cat. Za deklariranje int i Cat instance parameterizirane Array klase, trebali biste napisati Array anIntArray; Array aCatArray; Listing 16.1 prikazuje punu deklaraciju ovog ogoljelog Array predloška. Pažnja: Listing 16.1 nije kompletan program!
Listing 16.1. Predložak Array klase. 1: Listing 16.1 A template of an array class 2: #include 3: const int DefaultSize = 10; 4: 5: template // declare the template and the parameter 6: class Array // the class being parameterized 7: { 8: public: 9: // constructors 10: Array(int itsSize = DefaultSize); 11: Array(const Array &rhs); 12: ~Array() { delete [] pType; } 13: 14: // operators
15: Array& operator=(const Array&); 16: T& operator[](int offSet) { return pType[offSet]; } 17: 18: // accessors 19: int getSize() { return itsSize; } 20: 21: private: 22: T *pType; 23: int itsSize; 24: }; Izlaz: Nema nikakvog ispisa. Ovo je nekompletan program. Analiza: Definicija predloška počinje u liniji 5, s ključnom riječi template koju slijedi parametar. U ovom slučaju, parametar je identificiran kao tip ključne riječi class, a identifikator T služi za predstavljanje parametriziranog tipa. Od linijie 6 do kraja predloška u liniji 24, ostatak deklaracije je poput svake druge deklaracije. Jedina razlika je u tome što se umjesto uobučajenog tipa objekta, ovdje pojavljuje identifikator T.
Implementiranje predloška Puna implementacija Array predloška zahtjeva implementiranje konstruktora kopije, operatora =, itd. Listing 16.2 je primjer pogonitelja ovog predloška. Listing 16.2. Implementacija predloška niz. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32:
#include const int DefaultSize = 10; // declare a simple Animal class so that we can // create an array of animals class Animal { public: Animal(int); Animal(); ~Animal() {} int GetWeight() const { return itsWeight; } void Display() const { cout << itsWeight; } private: int itsWeight; }; Animal::Animal(int weight): itsWeight(weight) {} Animal::Animal(): itsWeight(0) {} template // declare the template and the parameter class Array // the class being parameterized { public:
33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91:
// constructors Array(int itsSize = DefaultSize); Array(const Array &rhs); ~Array() { delete [] pType; } // operators Array& operator=(const Array&); T& operator[](int offSet) { return pType[offSet]; } const T& operator[](int offSet) const { return pType[offSet]; } // accessors int GetSize() const { return itsSize; } private: T *pType; int itsSize; }; // implementations follow... // implement the Constructor template Array::Array(int size = DefaultSize): itsSize(size) { pType = new T[size]; for (int i = 0; i<size; i++) pType[i] = 0; } // copy constructor template Array::Array(const Array &rhs) { itsSize = rhs.GetSize(); pType = new T[itsSize]; for (int i = 0; i Array& Array::operator=(const Array &rhs) { if (this == &rhs) return *this; delete [] pType; itsSize = rhs.GetSize(); pType = new T[itsSize]; for (int i = 0; i theArray; // an array of integers Array theZoo; // an array of Animals
92: Animal *pAnimal; 93: 94: // fill the arrays 95: for (int i = 0; i < theArray.GetSize(); i++) 96: { 97: theArray[i] = i*2; 98: pAnimal = new Animal(i*3); 99: theZoo[i] = *pAnimal; 100: delete pAnimal; 101: } 102: // print the contents of the arrays 103: for (int j = 0; j < theArray.GetSize(); j++) 104: { 105: cout << "theArray[" << j << "]:\t"; 106: cout << theArray[j] << "\t\t"; 107: cout << "theZoo[" << j << "]:\t"; 108: theZoo[j].Display(); 109: cout << endl; 110: } 111: 112: for (int k = 0; k < theArray.GetSize(); k++) 113: delete &theZoo[j]; 114: return 0; 115: } Output: theArray[0]: 0 theZoo[0]: 0 theArray[1]: 2 theZoo[1]: 3 theArray[2]: 4 theZoo[2]: 6 theArray[3]: 6 theZoo[3]: 9 theArray[4]: 8 theZoo[4]: 12 theArray[5]: 10 theZoo[5]: 15 theArray[6]: 12 theZoo[6]: 18 theArray[7]: 14 theZoo[7]: 21 theArray[8]: 16 theZoo[8]: 24 theArray[9]: 18 theZoo[9]: 27
Funkcijski predlošci Ako želite proslijediti objekt polje u funkciju, morate proslijediti određenu instancu pulja, a ne predložak. Prema tome, prima li SomeFunction() cijeli broj kao parametar, morate pisati void SomeFunction(Array&); // ok a ne void SomeFunction(Array&); // error! jer se nezna što T& predstavlja. Također ne smijete pisati void SomeFunction(Array &); // error! budući da nema klase Array—samo predložak i instance. Za postizanje općenitijeg pristupa, morate deklarirati funkcijski predložak. template void MyTemplateFunction(Array&); // ok Ovdje je funkcija MyTemplateFunction() deklarirana kao funkcijski predložak.
Predlošci i prijatelji Predlošci mogu deklarirati tri tipova prijatelja: • Prijateljska klasa ili funkcija koja nije predložak. • Opći prijateljski predložak ili funkcijski predložak. • Prijateljski predložak ili funkcijski predložak specifičnoga tipa.
Prijateljska klasa ili funkcija koja nije predložak Moguće je deklarirati bilo koju klasu ili funkciju kao prijatelja vašeg predloška. Listing 16.3 dodaje trivijalnu prijateljsku funkciju Intrude(), u definiciju predloška Array klase, i glavni program poziva Intrude(). Budući je riječ o prijatelju, Intrude() potom može pristupati privatnim podacima od Array. Kako nije riječ o funkcijskom predlošku, može se pozivati samo za polja cijelih brojeva. PAŽNJA: Za uporebu listinga 16.3, kopirajte linije 1-26 listing 16.2 nakon linije 1 ovoga listinga i potom linje 51-86 listinga 16.2 nakon linije 37 ovoga listinga. Listing 16.3. Prijateljska funkcija ne-predložak. 1: // Listing 16.3 - Type specific friend functions in templates 2: 3: template // declare the template and the parameter 4: class Array // the class being parameterized 5: { 6: public: 7: // constructors 8: Array(int itsSize = DefaultSize); 9: Array(const Array &rhs); 10: ~Array() { delete [] pType; } 11: 12: // operators 13: Array& operator=(const Array&); 14: T& operator[](int offSet) { return pType[offSet]; } 15: const T& operator[](int offSet) const 16: { return pType[offSet]; } 17: // accessors 18: int GetSize() const { return itsSize; } 19: 20: // friend function 21: friend void Intrude(Array); 22: 23: private: 24: T *pType; 25: int itsSize; 26: }; 27: 28: // friend function. Not a template, can only be used 29: // with int arrays! Intrudes into private data. 30: void Intrude(Array theArray) 31: { 32: cout << "\n*** Intrude ***\n"; 33: for (int i = 0; i < theArray.itsSize; i++) 34: cout << "i: " << theArray.pType[i] << endl; 35: cout << "\n"; 36: } 37: 38: // driver program 39: int main() 40: { 41: Array theArray; // an array of integers 42: Array theZoo; // an array of Animals 43: Animal *pAnimal; 44: 45: // fill the arrays 46: for (int i = 0; i < theArray.GetSize(); i++)
47: { 48: theArray[i] = i*2; 49: pAnimal = new Animal(i*3); 50: theZoo[i] = *pAnimal; 51: } 52: 53: int j, k; 54: for (j = 0; j < theArray.GetSize(); j++) 55: { 56: cout << "theZoo[" << j << "]:\t"; 57: theZoo[j].Display(); 58: cout << endl; 59: } 60: cout << "Now use the friend function to "; 61: cout << "find the members of Array"; 62: Intrude(theArray); 63: 63: // return the allocated memory before the arrays are destroyed. 64: for (k = 0; k < theArray.GetSize(); k++) 65: delete &theZoo[j]; 66: 67: cout << "\n\nDone.\n"; 68: return 0; 69: } Output: theZoo[0]: 0 theZoo[1]: 3 theZoo[2]: 6 theZoo[3]: 9 theZoo[4]: 12 theZoo[5]: 15 theZoo[6]: 18 theZoo[7]: 21 theZoo[8]: 24 theZoo[9]: 27 Now use the friend function to find the members of Array *** Intrude *** i: 0 i: 2 i: 4 i: 6 i: 8 i: 10 i: 12 i: 14 i: 16 i: 18 Done.
Opći prijateljski predložak ili funkcijski predložak Bilo bi korisno dodoati operator prikazivanja u Array klasu. Jedan pristup bio bi deklariranje operatora prikaza za svaki mogući tip od Array, ali bi time skroz igubili prednost proglašavanja Array predloškom. Ono što nam treba jest operator umetanja koji radi za bilo koji tup od Array. ostream& operator<< (ostream& Array&); Kako bi ovo radilo, moramo proglasiti operator<< funkcijskim predloškom. template ostream& operator<< (ostream&, Array&) Sad kad je operator<< funkcijski predložak, trebamo samo implementaciju. Listing 16.4 pokazuje Array predložak proširen za ovu deklaraciju i ima implementiran operator<<.
PAŽNJA: Za prevođenje ovog listinga, iskopirajte linije 8-26 listinga 16.2 i umetnite ih između linija 3 i 4. Također iskopirajte linije 51-86 listinga 16.2 i umetnite ih između linija 37 i 38.
Listing 16.4. Upotreba operatora ostream. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 47: 48: 49: 50: 51:
#include const int DefaultSize = 10; template // declare the template and the parameter class Array // the class being parameterized { public: // constructors Array(int itsSize = DefaultSize); Array(const Array &rhs); ~Array() { delete [] pType; } // operators Array& operator=(const Array&); T& operator[](int offSet) { return pType[offSet]; } const T& operator[](int offSet) const { return pType[offSet]; } // accessors int GetSize() const { return itsSize; } friend ostream& operator<< (ostream&, Array&); private: T *pType; int itsSize; }; template ostream& operator<< (ostream& output, Array& theArray) { for (int i = 0; i theArray;
// flag for looping
while (!Stop) { cout << "Enter an offset (0-9) "; cout << "and a value. (-1 to stop): " ; cin >> offset >> value; if (offset < 0) break;
52: if (offset > 9) 53: { 54: cout << "***Please use values between 0 and 9.***\n"; 55: continue; 56: } 57: 58: theArray[offset] = value; 59: } 60: 61: cout << "\nHere's the entire array:\n"; 62: cout << theArray << endl; 63: return 0; 64: } Output: Enter an offset (0-9) and a value. (-1 to stop): 1 10 Enter an offset (0-9) and a value. (-1 to stop): 2 20 Enter an offset (0-9) and a value. (-1 to stop): 3 30 Enter an offset (0-9) and a value. (-1 to stop): 4 40 Enter an offset (0-9) and a value. (-1 to stop): 5 50 Enter an offset (0-9) and a value. (-1 to stop): 6 60 Enter an offset (0-9) and a value. (-1 to stop): 7 70 Enter an offset (0-9) and a value. (-1 to stop): 8 80 Enter an offset (0-9) and a value. (-1 to stop): 9 90 Enter an offset (0-9) and a value. (-1 to stop): 10 10 ***Please use values between 0 and 9.*** Enter an offset (0-9) and a value. (-1 to stop): -1 -1 Here's the entire array: [0] 0 [1] 10 [2] 20 [3] 30 [4] 40 [5] 50 [6] 60 [7] 70 [8] 80 [9] 90
Prijateljski predložak ili funkcijski predložak specifičnoga tipa Iako operator umetanja prikazan na 16.4 radi, još uvijek nije u potpunosti ono što nam je potrebno. Budući da deklaracija friend operatora u linij 29 dekarira predložak, raditi će za svaku instancu Array i svaki operator inservije prima niz bilo kojeg tipa. Za postizanje cilja, modificirati liniju 29 uklanjanjem riječi template . Potom, linij 30 treba biti friend ostream& operator<< (ostream&, Array&); To će koristiti tip (T) declariran u predlošku Array. Time će operator<< za cijeli broj samo raditi s nizon cijelih brojeva, itd.
Upotreba predlošaka Predloške možemo tretirati kao bilo koji tip podatka. Možemo ih proslijeđivati kao parametre po vrijdenosti i referenci, te ih možemo vraćati kao povratne tipove. Listing 16.5 demonstrira kako proslijeđivati objekte predloške. Listing 16.5. Proslijeđivanje predložaka u i iz funkcija. 1: 2:
#include
3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61:
const int DefaultSize = 10; // A trivial class for adding to arrays class Animal { public: // constructors Animal(int); Animal(); ~Animal(); // accessors int GetWeight() const { return itsWeight; } void SetWeight(int theWeight) { itsWeight = theWeight; } // friend operators friend ostream& operator<< (ostream&, const Animal&); private: int itsWeight; }; // extraction operator for printing animals ostream& operator<< (ostream& theStream, const Animal& theAnimal) { theStream << theAnimal.GetWeight(); return theStream; } Animal::Animal(int weight): itsWeight(weight) { // cout << "Animal(int)\n"; } Animal::Animal(): itsWeight(0) { // cout << "Animal()\n"; } Animal::~Animal() { // cout << "Destroyed an animal...\n"; } template // declare the template and the parameter class Array // the class being parameterized { public: Array(int itsSize = DefaultSize); Array(const Array &rhs); ~Array() { delete [] pType; } Array& operator=(const Array&); T& operator[](int offSet) { return pType[offSet]; } const T& operator[](int offSet) const { return pType[offSet]; }
62: 63 64: 65: 66: 67: 68: 69: 70: 71: 70: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 84: 85: 86: 87: 88: 87: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119:
int GetSize() const { return itsSize; } // friend function friend ostream& operator<< (ostream&, const Array&); private: T *pType; int itsSize; }; template ostream& operator<< (ostream& output, const Array& theArray) { for (int i = 0; i& theArray); void AnimalFillFunction(Array& theArray); enum BOOL {FALSE, TRUE}; int main() { Array intArray; Array animalArray; IntFillFunction(intArray); AnimalFillFunction(animalArray); cout << "intArray...\n" << intArray; cout << "\nanimalArray...\n" << animalArray << endl; return 0; } void IntFillFunction(Array& theArray) { BOOL Stop = FALSE; int offset, value; while (!Stop) { cout << "Enter an offset (0-9) "; cout << "and a value. (-1 to stop): " ; cin >> offset >> value; if (offset < 0) break; if (offset > 9) { cout << "***Please use values between 0 and 9.***\n"; continue; } theArray[offset] = value; } } void AnimalFillFunction(Array& theArray) { Animal * pAnimal; for (int i = 0; i
120: pAnimal = new Animal; 121: pAnimal->SetWeight(i*100); 122: theArray[i] = *pAnimal; 123: delete pAnimal; // a copy was put in the array 124: } 125: } Output: Enter an offset (0-9) and a value. (-1 to stop): 1 10 Enter an offset (0-9) and a value. (-1 to stop): 2 20 Enter an offset (0-9) and a value. (-1 to stop): 3 30 Enter an offset (0-9) and a value. (-1 to stop): 4 40 Enter an offset (0-9) and a value. (-1 to stop): 5 50 Enter an offset (0-9) and a value. (-1 to stop): 6 60 Enter an offset (0-9) and a value. (-1 to stop): 7 70 Enter an offset (0-9) and a value. (-1 to stop): 8 80 Enter an offset (0-9) and a value. (-1 to stop): 9 90 Enter an offset (0-9) and a value. (-1 to stop): 10 10 ***Please use values between 0 and 9.*** Enter an offset (0-9) and a value. (-1 to stop): -1 -1 intArray:... [0] 0 [1] 10 [2] 20 [3] 30 [4] 40 [5] 50 [6] 60 [7] 70 [8] 80 [9] 90 animalArray:... [0] 0 [1] 100 [2] 200 [3] 300 [4] 400 [5] 500 [6] 600 [7] 700 [8] 800 [9] 900
Standardna biblioteka predložaka Svi glavni proizvođači kompajlera uz njih sada isporučuju i standardnu bibiloteku predložaka, Standard Template Library (STL). Cilj STL-a je davanje alternative ponovnom otkrivanju "tople vode" za česte zahtjeve. STL je testiran, debuggiran, nudi visoke performanse, i još je besplatan. Najvažnije, STL je reiskoristiv; jednom kad naučite kako koristiti STL container, možete ga koristiti u svim svojim programima.
Kviz 1. U čemu je razlika između parametra u predlošku i funkciji? 2. Koja je razlika između prijateljskog predloška specifičnog tipa i općeg prijateljskog predloška? 3. Može li se omogućiti specijalno ponašanje za jednu instancu predloška, ali ne i za druge?
Vježbe 1. Kreirajte predložak baziran na List klasi: class List { private: public: List():head(0),tail(0),theCount(0) {} virtual ~List(); void insert( int value ); void append( int value ); int is_present( int value ) const; int is_empty() const { return head == 0; } int count() const { return theCount; } private: class ListCell { public: ListCell(int value, ListCell *cell = ):val(value),next(cell){} int val; ListCell *next; }; ListCell *head; ListCell *tail; int theCount; }; 2. Napišite implementaciju List klases (bez predloška). 3. Napišite verziju kao predložak. 4. Deklarirajte 3 list objekta: lista Strings, lista Cats, i lista ints. 5. BUG BUSTERS: Što ne valja sa slijedećim kodom? List Cat_List; Cat Felix; CatList.append( Felix ); cout << "Felix is " << ( Cat_List.is_present( Felix ) ) ? "" : "not " << "present\n";
Lekcija 17
Iznimke i baratanje pogreškama
Sav kod koji smo dosad obradili bio je kreiran u demonstracijske svrhe. Nije baratao sa pogreškama, kako se nebismo odmicali osnovnih problema koje smo obrađivali. Programi u stvarnom svijetu moraju raznmatrati i mogućnost pojavljivanja pogrešaka.
Bugovi, pogreške, zablude i truli kod Svi programi imaju bugove. Što je veći program, više je i bugova. To što je gornja tvrdnja istinita ne znači da je i u redu praviti bugovite programe. Pravljenje robusnih, nebugovitih programa je prioritet broj jedan za svakoga tko se misli ozbiljno baviti programiranjem. Bitno je razlikovati bugove, koji nastaju uslijed ogičke i sintaktičke pogreške, te iznimke, koje nastaju uslijed neobičnih ali predvidivih problema poput nestajanja resursa (memorije ili prostora na disku).
Iznimke Vaš korisnik će s vremena na vrijeme ostati bez memorije i pitanje je što ćete vi učiniti. Vaše mogućnosti su: • Srušiti program. • Obavijestiti korisnika i dostojanstveno se povući. • Obavijestiti korisnika i dozvoliti mu da pokuša osloboditi potrebne resurse i nastavi s radom. • Preuzeti akciju popravljanja i nastaviti bez ometanja korisnika. C++ baratanje iznimkama pruža nam sigurnu metodu za obradu predvidljivih ali neobičnih stanja koja mogu nastati prilikom pokretanja programa.
Što su iznimke U C++, iznimka je objekt koji se prenosi iz dijela koda gdje je problem nastao u dio koda koji će se baviti problemom. Tip iznimke određuje koji će dio koda obrađivati problem. Osnovna ideja iza iznimaka je vrlo jednostavna: • Stvarno zauzimanje resursa (na primjer, rezerviranje memorije ili zaključavanje datoteke) se obično obavlja na niskom nivou unutar programa. • Logika baratanja problemom, odn. što uraditi kad operacija propadne, je obično visoko u programu, u dijelu koda za interakciju s korisnikom. • Iznimke omogućuju trenutan prelazak iz prvog u drugi dio koda.
Kako se iznimke koriste try blokovi se kreiraju kako bi opkolili područja koda u kojima se možda nalazi problem. Na primjer: try { SomeDangerousFunction(); } catch blokovi barataju iznimkom koju nam javi try blok. Na primjer: try { SomeDangerousFunction(); } catch(OutOfMemory) { // take some actions } catch(FileNotFound) { // take other action } PAŽNJA: Neki stariji kompajleri ne podržavaju iznimke. Listing 17.1. Buđenje iznimke. 0: 1: 2: 3: 4: 5: 6: 7: 8: 9:
#include const int DefaultSize = 10; class Array { public: // constructors Array(int itsSize = DefaultSize); Array(const Array &rhs);
10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68:
~Array() { delete [] pType;} // operators Array& operator=(const Array&); int& operator[](int offSet); const int& operator[](int offSet) const; // accessors int GetitsSize() const { return itsSize; } // friend function friend ostream& operator<< (ostream&, const Array&); class xBoundary {}; // define the exception class private: int *pType; int itsSize; }; Array::Array(int size): itsSize(size) { pType = new int[size]; for (int i = 0; i<size; i++) pType[i] = 0; } Array& Array::operator=(const Array &rhs) { if (this == &rhs) return *this; delete [] pType; itsSize = rhs.GetitsSize(); pType = new int[itsSize]; for (int i = 0; i= 0 && offSet < GetitsSize()) return pType[offSet]; throw xBoundary(); return pType[0]; // appease MSC }
69: 70: const int& Array::operator[](int offSet) const 71: { 72: int mysize = GetitsSize(); 73: if (offSet >= 0 && offSet < GetitsSize()) 74: return pType[offSet]; 75: throw xBoundary(); 76: return pType[0]; // appease MSC 77: } 78: 79: ostream& operator<< (ostream& output, const Array& theArray) 80: { 81: for (int i = 0; i
Specificiranje više od jednog catch Moguće je da više uvjeta uzrokuje iznimku. U tom slučaju, catch naredbe mogu biti poredane jedna iza druge, poput uvjeta u switch naredbi. Ekvivalent za default naredbu je "catch everything" naredba, indicirana sa catch(...). Listing 17.2 ilustrira višestruke uvjete iznimaka. Listing 17.2. Višestruke iznimke. 0: 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49:
#include const int DefaultSize = 10; class Array { public: // constructors Array(int itsSize = DefaultSize); Array(const Array &rhs); ~Array() { delete [] pType;} // operators Array& operator=(const Array&); int& operator[](int offSet); const int& operator[](int offSet) const; // accessors int GetitsSize() const { return itsSize; } // friend function friend ostream& operator<< (ostream&, const Array&); // define the exception classes class xBoundary {}; class xTooBig {}; class xTooSmall{}; class xZero {}; class xNegative {}; private: int *pType; int itsSize; }; int& Array::operator[](int offSet) { int size = GetitsSize(); if (offSet >= 0 && offSet < GetitsSize()) return pType[offSet]; throw xBoundary(); return pType[0]; // appease MFC } const int& Array::operator[](int offSet) const { int mysize = GetitsSize(); if (offSet >= 0 && offSet < GetitsSize()) return pType[offSet]; throw xBoundary();
50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108:
return pType[0]; return pType[0]; // appease MFC } Array::Array(int size): itsSize(size) { if (size == 0) throw xZero(); if (size < 10) throw xTooSmall(); if (size > 30000) throw xTooBig(); if (size < 1) throw xNegative(); pType = new int[size]; for (int i = 0; i<size; i++) pType[i] = 0; }
int main() { try { Array intArray(0); for (int j = 0; j< 100; j++) { intArray[j] = j; cout << "intArray[" << j << "] okay...\n"; } } catch (Array::xBoundary) { cout << "Unable to process your input!\n"; } catch (Array::xTooBig) { cout << "This array is too big...\n"; } catch (Array::xTooSmall) { cout << "This array is too small...\n"; } catch (Array::xZero) { cout << "You asked for an array"; cout << " of zero objects!\n"; } catch (...) { cout << "Something went wrong!\n"; } cout << "Done.\n"; return 0;
109: } Output: You asked for an array of zero objects! Done.
Bugovi i debugiranje Svaka moderna razvojna okolina sadrži jedan ili više moćnih debuggera. Osnovna ideja prilikom korištenja debuggera je ova: Pokrenete debugger, koji učita vaš source , te potom pokrenete vaš program iz debuggera. Ovime možete vidjeti svaku instrukcije tjekom samog izvođenja programa, te da provjeravate varijable kako se mijenjaju tjekom izvršenja vašega programa. Svi prevoditelji omogućuju kompajliranje sa i bez simbola. Prevođenje sa simbolima govori kompajleru da kreira poseban sistem označavanja source i njegovog odnosa spram generiranog programa.
Kviz 1. Što je iznimka? 2. Što je try blok? 3. Što je catch naredba? Vježbe 1. Napravite try blok, catch naredbu i jednostavnu iznimku. 2. BUG BUSTERS: Što ne valja u ovom kodu? class xOutOfMemory { public: xOutOfMemory( const String& message ) : itsMsg( message ){} ~xOutOfMemory(){} virtual const String& Message(){ return itsMsg}; private: String itsMsg; } main() { try { char *var = new char; if ( var == 0 ) throw xOutOfMemory(); } catch( xOutOfMemory& theException ) { cout << theException.Message() << "\n"; } }
Lekcija1 Kviz 1. Koja je razlika između interpretera i kompajlera? Interpreteri prolaze kroz kod i prevode program direktno pretvarajući programske instrukcije u akcije. Kompajleri prevode source code u izvršni program koji se potom može pokrenuti.
2. Kako kompajlirate izvršni kod sa svojim kompajlerom? Svaki prevoditelj je drugačiji. Provjerite prateću dokumentaciju.
3. Što čini linker? Posao linkera je međusobno povezivanje vašeg kompajliranog koda sa pratećim bibiliotekama funkcija i ostalim source kodovima. Linker nam omogućuje izgradnju našeg programa u dijelićima i potom povezivanje dijelova u jedan veliki program.
4. Koji su koraci u razvojnom ciklusu? Editiranje source koda, prevođenje, linkanje, testiranje, ponavljanje.
Vježbe 1. Pogledajte u slijedeći program i pokušajte ustanoviti što on radi bez njegova pokretanja. 1: #include 2: int main() 3: { 4: int x = 5; 5: int y = 7; 6: cout "\n"; 7: cout << x + y << " " << x * y; 8: cout "\n"; 9:return 0; 10: } Inicijalizira dvije cjelobrojne varijable i potom ispisuje njihovu sumu i produkt.
2. Unesite program sa 1. vježbe, kompajlirajte ga i povežite. Što on čini ? Da li radi ono što ste mislili? 3. Unesite slijedeći program i kompajlirajte ga. Koja će se greška pojaviti? 1: include 2: int main() 3: { 4: cout << "Hello World\n"; 5: return 0; 6: } Morate staviti # ispred riječi include u prvoj liniji.
4. Popravite grešku u 3. vježbi, rekompajlirajte , linkajte i pokrenite program. Što on radi? Ovaj program ispisuje poruku Hello World na ekran, te potom prebacuje pokazivač u novu liniju (carriage return).
Lekcija 2 Kviz 1. Koja je razlika između kompajlera i pretprocesora? Prilikom svakog pokretanja kompajlera, pretprocesor se pokreće prvi. On "čita" naš program, te umeće sve navedene datoteke, te izvodi ostale potrebne djelatnosti.
2. Zašto je funkcija main() posebna? main() se poziva automatski, svaki put kad pokrenemo naš program.
3. Koja su dva tipa komentara i po čemu se razlikuju? C++ komentari su dva "slash" znaka (//), i oni komentiraju sav tekst do kraja linije. C komentari dolaze u paru (/* */), i sve unutar pripadajućih parova je komentar.
4. Mogu li komentari biti duži od jedne linije? C komentari mogu. Ako želite proširiti C++ komentare na drugu liniju, morate staviti drugi blok "slash" znakova (//).
Vježbe 1. Napišite program koji ispisuje "Ja volim C++" na ekran. 1: #include 2: 3: int main() 4: { 5: cout << "I love C++\n"; 6: return 0; 7: }
2. Napišite najmanji program koji se može prevesti, linkati i pokrenuti. int main(){}
3. Unesite slijedeći program i kompajlirajte ga. Gdje je greška? Možete li ju popraviti? 1: #include 2: void main() 3: { 4: cout << Is there a bug here?"; 5: } U liniji 4 nedostaje otvoreni navodnik za string.
4. Popravite pogrešku sa 3. vježbe i pokrenite program.
Lekcija 3 Kviz 1. Koja je razlika između integer i floating point varijable? Integeri spremaju cijele brojeve; floating-point varijable su realni brojevi koji mogu biti prikazani pomoću mantise i eksponenta.
2. Koje su razlike među unsigned short int i long int? Ključna riječ unsigned označuje da taj integer pamti samo pozitivne brojeve. Na većini računala, short integeri zauzimaju 2 bytea, a long integeri 4.
3. Što je prednost kod upotrebe const ključne riječi u odnosu na #define? Const varijable "preživljavaju" pretprocesor, pa kompajler može provjeriti dali se pravilno upotrebljavaju.
4. Kako razlikujemo dobro od lošeg imena varijable? Dobro imenovana varijabla nam govori o samoj svrsi varijable, npr. myAge i PeopleOnTheBus su dobra imena varijabli, a xjk i prndl su vjerojatno manje korisni.
5. Uz slijedeći enum, koja će biti vrijednost za BLUE? enum COLOR { WHITE, BLACK = 100, RED, BLUE, GREEN = 300 }; BLUE = 102
6. Koje od slijedećih varijabli su dobre, koje loše, a koje nelegalne? a. b. c. d. e.
Age !ex R79J TotalIncome __Invalid
Dobra Nedozvoljeno Dozvoljeno, ali loš odabir imena Dobra Dozvoljeno, ali loš odabir
Vježbe 1. Koji bi bio pravi tip varijable za pohranu slijedećih informacija? a. Vaše godine. b. Poštanski broj vašega grada. c. Broj zvijezda u galaksiji. d. Prosječna količina padalina za siječanj.
Unsigned short integer Unsigned long integer or unsigned float Unsigned double Unsigned short integer
Izmislite dobra imena varijabli za te informacije. a. myAge b. backYardArea c. StarsInGalaxy d. averageRainFall
2. Deklarirajte konstantu za pi kao 3.14159. const float PI = 3.14159;
3. Deklarirajte float varijablu i inicijalizirajte ju koristeći pi konstantu. float myPi = PI;
Lekcija 4 Kviz 1. Što je izraz? Bilo koja naredba koja vraća vrijednost.
2. Da li je x = 5 + 7 izraz? Koja mu je vrijednost? Da. 12
3. Kolika je vrijednost od 201 / 4? 50
4. Kolika je vrijednost od 201 % 4? 1
5. Ako su myAge, a, i b svi int varijable, koje su njihove vrijednosti nakon: myAge = 39; a = myAge++; b = ++myAge; myAge: 41, a: 39, b: 41
6. Kolika je vrijednost od 8+2*3? 14
7. Koja je razlika između x = 3 i x == 3? Prva je pridruživanje, a druga provjera jednakosti.
8. Da li slijedeće vrijednosti poprimaju TRUE ili FALSE? a. 0 b. 1 c. -1 d. x = 0 e. x == 0 // pretpostavimo da x ima vrijednost 0
FALSE TRUE TRUE FALSE TRUE
Vježbe 1. Napišite jednu naredbu koja ispituje dve cjelobrojne vrijednosti i mijenja veću u manju, koristeći samo jedan else uvjet. if (x > y) x = y; else y = x;
// y > x || y == x
2. Proučite slijedeći program. Zamislite unošenje tri broja, i napišite kakav izlaz očekujete. 1: #include 2: int main() 3: { 4: int a, b, c; 5: cout << "Please enter three numbers\n"; 6: cout << "a: "; 7: cin >> a; 8: cout << "\nb: "; 9: cin >> b; 10: cout << "\nc: "; 11: cin >> c; 12: 13: if (c = (a-b)) 14: {cout << "a: "; 15: cout << a; 16: cout << "minus b: "; 17: cout << b; 18: cout << "equals c: "; 19: cout << c << endl;} 20: else 21: cout << "a-b does not equal c: " << endl; 22: return 0; 23: }
3. Upišite program iz vježbe 2, te kreirajte exe datoteku. Unesite brojeve 20, 10 i 50 . Jeste li dobili ono što ste očekivali? Zašto niste? Ulaz 20, 10, 50. Izlaz a: 20 b: 30 c: 10. Linija 13 pridružuje, ne provjeravajući jednakost.
4. Pogledajte program i anticipirajte njegov izlaz: 1: #include 2: int main() 3: { 4: int a = 1, b = 1, c; 5: if (c = (a-b)) 6: cout << "The value of c is: " << c; 7: return 0; 8: }
5. Unesite, kompajlirajte, povežite i pokrenite program sa vježbe 4. Što je izlaz i zašto? Budući da linija 5 pridružuje vrijednost od a-b u c, vrijednost pridruživanja je a (1) minus b (1), ili 0. Kako 0 prozivamo kao FALSE, if propada i ništa se ne ispisuje.
Lekcija 5 Kviz 1. Koja je razlika između prototipa funkcije i definicije funkcije? Funkcijski prototip deklarira funkciju; definicija ju definira. Prototip završava sa; definicija ne. Deklaracija može sadržati riječ inline i podrazumijevane vrijednosti za parametre; definicija ne može. Deklaracija ne treba imati imena parametara; definicija mora.
2. Moraju li se imena parametara podudarati u prototipu, definiciji i pozivu funkcije? Ne. Parametre identificiramo po položaju, a ne po imenu.
3. Ako funkcije ne vraća nikakvu vrijednost, kako ćemo ju deklarirati? Kao void.
4. Ako ne deklariramo povratni tip funkcije, koji tip će biti podrazumijevan? Svaka funkcija koja eksplicitno ne deklarira povratni tip je cjelobrojna.
5. Što je lokalna varijabla? Lokalna varijabla je varijabla proslijeđena ili deklarirana u bloku, obično funkcijskom. Vidljiva je samo u bloku.
6. Što je doseg (engl. scope) varijable? Doseg se odnosi na vidljivost i životni vijek lokalnih i globalnih varijabli.
7. Što je rekurzija? Rekurzija se obično odnosi na sposobnost funkcija da poziva samu sebe.
8. Kad koristimo globalne varijable? Globalne varijable obično koristimo kad mnogo funkcija treba pristupati istim podacima. Globalne varijable su rijetkost u C++.
9. Što je preopterećenje funkcija? Preopterećenje funkcija je sposobnost pisanja više funkcija sa istim imenom, a funkcije se razlikuju po broju i tipu parametara.
10. Što je polimorfizam? Polimorfizam je svojstvo tretiranja mnogih objekata različitih ali sličnih tipova bez obzira na njihove razlike. U C++, polimorfizam se postiže uporabom izvedenih klasa i virtualnih funkcija.
Vježbe 1. Napišite prototip za funkciju imena Perimeter(), koja vraća unsigned long int i ima dva parametra, oba unsigned short int. unsigned long int Perimeter(unsigned short int, unsigned short int);
2. Napišite definiciju za funkciju Perimeter() iz vježbe 1. Dva parametra predstavljaju dužinu i širinu pravokutnika. Neka funkcija vraća perimetar (dvostruka dužina pomnožena dvostrukom širinom). unsigned long int Perimeter(unsigned short int length, unsigned short int width) { return 2*length + 2*width; }
3. BUG BUSTER: Što ne valja s funkcijom u slijedećem kodu? #include void myFunc(unsigned short int x); int main() { unsigned short int x, y; y = myFunc(int); cout << "x: " << x << " y: " << y << "\n"; } void myFunc(unsigned short int x) { return (4*x); } Funkcija je deklarirana kao void i ne može vraćati vrijednost.
4. BUG BUSTER: Što ne valja s funkcijom u slijedećem kodu? #include int myFunc(unsigned short int x); int main() { unsigned short int x, y; y = myFunc(x); cout << "x: " << x << " y: " << y << "\n"; } int myFunc(unsigned short int x); { return (4*x); } Ova funkcija je u redu, ali postoji točka-zarez u zaglavlju definicije funkcije.
5. Napišite funkciju koja uzima dva unsigned short integer argumenta i vraća rezultat dijeljenja prvog sa drugim. Ako je drugi broj nula, ne radi dijeljenje nego vrati –1. short int Divider(unsigned short int valOne, unsigned short int valTwo) { if (valTwo == 0) return -1; else return valOne / valTwo; }
6. Napišite program koji traži korisnika da unese dva broja i zove funkciju iz prethodne vježbe. Neka ispiše rješenje, odnosno poruku o grešci ako dobije –1. #include typedef unsigned short int USHORT; typedef unsigned long int ULONG; short int Divider( unsigned short int valone, unsigned short int valtwo); int main() { USHORT one, two; short int answer; cout << "Enter two numbers.\n Number one: "; cin >> one; cout << "Number two: "; cin >> two; answer = Divider(one, two); if (answer > -1) cout << "Answer: " << answer; else cout << "Error, can't divide by zero!"; return 0; }
7. Napišite program koji traži unos broja i potencije. Napišite rekurzivnu funkciju koja računa potenciju zadanog broja. Npr., ako je broj 2, a potencija 4, funkcija treba vratiti 16. #include typedef unsigned short USHORT; typedef unsigned long ULONG; ULONG GetPower(USHORT n, USHORT power); int main() {
USHORT number, power; ULONG answer; cout << "Enter a number: "; cin >> number; cout << "To what power? "; cin >> power; answer = GetPower(number,power); cout << number << " to the " << power << "th power is " << answer << endl; return 0; } ULONG GetPower(USHORT n, USHORT power) { if(power == 1) return n; else return (n * GetPower(n,power-1)); }
Lekcija 6 Kviz 1. Što je to dot operator i zašto ga koristimo? Dot operator je točka (.). Služi nam za pristupanje članovima klase.
2. Što rezervira memoriju—deklaracija ili definicija? Definicije varijabli rezerviraju memoriju. Deklaracije klasa ne zauzimaju memoriju.
3. Je li deklaracija klase njezino sučelje ili njezina implementacija? Deklaracija klase je njezino sučelje (interface); ona govori korisnicima klase kako komunicirati s njom. Sama implementacija klase je set funkcijskih članova, obično spremljenih u odvojenoj CPP datoteci.
4. Koja je razlika između privatnih i javnih podatkovnih članova? Javni podatkovni članovi su dostupni korisnicima klase. Privatni podatkovni članovi su dostupni samo funkcijskim članovima klase.
5. Mogu li funkcijski članovi biti privatni? Da. I funkcijski članovi i funkcijski podaci mogu biti privatni.
6. Mogu li podatkovni članovi biti javni? Iako podatkovni članovi mogu biti javni, dobra je programerska navika proglašavati ih privatnima i omogućiti im pristup preko javni pristupnih funkcijskih članova.
7. Ako deklarirate dva Cat objekta, mogu li oni imati različite vrijednosti unutar itsAge podatkovnog člana? Da. Svaki objekt izveden iz klase ima vlastite podatkovne članove.
8. Da li deklaracija klase završava sa ";" ? A definicija metode ? Deklaracije završavaju sa točka-zarezom nakon zatvorene zagrade; definicija funkcije ne završava.
9. Kako bi zaglavlje za Cat funkciju imena Meow izgledalo, pod pretpostavkom da ne uzima nikakve parametre i vraća void? void Cat::Meow()
10. Koju funkciju pozivamo za inicijalizaciju klase? Konstruktor.
Vježbe 1. Napišite kod koji deklarira klasu zvanu Employee sa slijedećim podatkovnim članovima: age, yearsOfService, and Salary. class Employee { int Age; int YearsOfService; int Salary; };
2. Promjenite Employee klasu tako da joj podatkovni članovi budu privatni, a pružite javne pristupne metode za dobivanje i postavljanje svakog od podatkovnih članova. class Employee { public: int GetAge() const; void SetAge(int age); int GetYearsOfService()const; void SetYearsOfService(int years); int GetSalary()const; void SetSalary(int salary); private: int Age; int YearsOfService; int Salary; };
3. Napišite program koji na osnovi gornje klase stvara dva zaposlena, postavlja njihove godine, godine rada i plaću, te ispisuje njihove vrijednosti. main() { Employee John; Employee Sally; John.SetAge(30); John.SetYearsOfService(5); John.SetSalary(50000); Sally.SetAge(32); Sally.SetYearsOfService(8); Sally.SetSalary(40000); cout << "At AcmeSexist company, John and Sally have the same job.\n"; cout << "John is " << John.GetAge() << " years old and he has been with"; cout << "the firm for " << John.GetYearsOfService << " years.\n"; cout << "John earns $" << John.GetSalary << " dollars per year.\n\n"; cout << "Sally, on the other hand is " << Sally.GetAge() << "years old and has"; cout << "been with the company " << Sally.GetYearsOfService; cout << " years. Yet Sally only makes $" << Sally.GetSalary(); cout << " dollars per year! Something here is unfair."; }
4. Nastavljajući vježbu 3, pružite metodu za Employee koja izvještava koliko tisuća dolara zarađuje, zaokruženo na najbližu 1000. float Employee:GetRoundedThousands()const { return Salary / 1000; }
5. Promjenite Emplyee klasu tako da možete inicijalizirati godine, godine službe i plaću kad kreirate uposlenika. class Employee { public: Employee(int age, int yearsOfService, int salary); int GetAge()const; void SetAge(int age); int GetYearsOfService()const; void SetYearsOfService(int years); int GetSalary()const; void SetSalary(int salary); private: int Age; int YearsOfService; int Salary; };
6. BUG BUSTERS: Šta ne valja sa slijedećom deklaracijom? class Square { public: int Side; } Deklaracija klase mora završiti sa točka-zarezom.
7. BUG BUSTERS: Zašto je slijedeća deklaracija klase beskorisna? class Cat { int GetAge()const; private: int itsAge; }; Pristupna funkcija GetAge() je privatna. Zapamtite: Svi članovi klase su privatni ukoliko ne kažete drugačije.
8. BUG BUSTERS: Koja će tri buga u ovome kodu kompajler pronaći? class TV { public: void SetStation(int Station); int GetStation() const; private: int itsStation; }; main() { TV myTV; myTV.itsStation = 9; TV.SetStation(10); TV myOtherTv(2); } Ne možete pristupati itsStation direktno. To je privatna varijabla. Ne možete pozvati SetStation() za klasu. Možete pozvati SetStation() samo za objekte. Ne možete inicijalizirati itsStation budući da nema sukladnog konstruktora.
Lekcija 7 Kviz 1. Kako inicijaliziramo više od jedne varijeble u for petlji? Odvojimo inicijalizaciju sa zarezima, na primjer for (x = 0, y = 10; x < 100; x++, y++)
2. Zašto izbjegavamo goto naredbu? goto skače u bilo kome smjeru na proizvoljnu liniju koda. Time generiramo izvorni kod koji je teško razumljiv, a samim tim težak za održavanje.
3. Je li moguće napisati for petlju s tijelom koje se nikad ne izvršava? Da, ako je uvijet FALSE nakon inicijalizacije, tijelo for petlje se nikada neće izvršiti. Evo primjera: for (int x = 100; x < 100; x++)
4. Je li moguće umetniti for petlju unutar druge for petlje? Da. Bilo koja petlja može biti ugniježdena unutar druge petlje.
5. Je li moguće napraviti petlju koja nikad ne završava? Dajte primjer. Da. Slijede primjeri i za for petlju i za while petlju: for(;;) { // This for loop never ends! } while(1) { // This while loop never ends! }
6. Što se događa kad kreirate petlju koja nikad ne završava? Program se "sruši", te obično moramo resetirati računalo.
Vježbe 1. Što je vrijednost od x naredbe kad petlja dođe do kraja? for (int x = 0; x < 100; x++) 100
2. Napišite ugnježdenu for petlju koja ispisuje uzorak 0 na površini 10*10 znakova. for (int i = 0; i< 10; i++) { for ( int j = 0; j< 10; j++) cout << "0"; cout << "\n"; }
3. Napišite for naredbu koja će brojati od 100 do 200 za 2. for (int x = 100; x<=200; x+=2)
4. Napišite while petlju koja će brojati od 100 do 200 za 2. int x = 100; while (x <= 200) x+= 2;
5. Napišite do...while petlju koja bi brojala od 100 to 200 za 2. int x = 100; do { x+=2; } while (x <= 200);
6. BUG BUSTERS: Što ne valja s ovim kodom? int counter = 0 while (counter < 10) { cout << "counter: " << counter; } brojač se nikada ne povećava i while petlja nikada ne završi
7. BUG BUSTERS: Što ne valja s ovim kodom? for (int counter = 0; counter < 10; counter++); cout << counter << " "; Točka-zarez nakon petlje uzrokuje to da petlja zapravo ne čini ništa.
8. BUG BUSTERS: Što ne valja s ovim kodom? int counter = 100; while (counter < 10) { cout << "counter now: " << counter; counter--; } counter je inicijaliziran na 100, ali test uvjet je da bude manji od 10. Stoga tijelo petlje nikada neće biti izvršeno. Da je u liniji 1 counter postavljen na 5, petlja se nebi zaustavila dok ne dosegne najmanji mogući int. Budući da je int podrazumjevan kao označen, dobili bismo nepredviđen rezultat.
9. BUG BUSTERS: Što ne valja s ovim kodom? cout << "Enter a number between 0 and 5: "; cin >> theNumber; switch (theNumber) { case 0: doZero(); case 1: // fall through case 2: // fall through case 3: // fall through case 4: // fall through case 5: doOneToFive(); break; default: doDefault(); break; } Case 0 vjerojatno treba break naredbu. Ako ne treba, trebalo bi dokumentirati to nekakvim komentarom.
Lekcija 8 Kviz 1. Koji operator koristimo za određivanje adrese od varijable? Address of operator (&) se koristi za određivanje adresa bilo koje varijable.
2. Koji operator koristimo za pronalaženje vrijednosti pohranjene na adresi na koju pokazuje pokazivač? Operator dereferenciranja (*) se koristi za pristup vrijednostima na adresi u pokazivaču.
3. Što je pokazivač? Pokazivač je varijabla koja čuva adresu druge varijable.
4. Koja je razlika između adrese pohranjene u pokazivaču i vrijednosti na toj adresi? Adresa pohranjena u pokazivaču je adresa druge varijable. Vrijednost pohranjena u pokazivaču je adresa druge varijable. Vrijednost pohranjena na toj adresi je bilo kakva vrijednost pohranjena u bilo kakvoj varijabli. Operator indirekcije (*) vraća vrijednost pohranjenu na toj adresi, koja je i sama pohranjena u pokazivaču.
5. Koja je razlika između operatora indirekcije i adress of operatora? Operator indirekcije vraća vrijednost na adresi pohranjenoj u pokazivaču. Address of operator (&) vraća memorijsku adresu varijable.
6. Koja je razlika između const int * ptrOne i int * const ptrTwo? const int * ptrOne deklarira ptrOne kao pokazivač na konstantni integer. Sam integer se ne može promijeniti koristeći taj pokazivač. int * const ptrTwo deklarira ptrTwo kao konstantan pokazivač na integer. Jednom inicijaliziran, tah se pokazivač ne može ponovno pridružiti nekoj drugoj varijabli.
Vježbe 1. Što ove deklaracije rade? a. int * pOne; b. int vTwo; c. int * pThree = &vTwo;
deklarira pokazivač na integer deklarira integer varijablu deklarira pokazivač na integer i inicijalizira ga s adresom druge varijable
2. Ako imate unsigned short varijablu imena yourAge, kako biste deklarirali pokazivač za manipuliranje sa yourAge? unsigned short *pAge = &yourAge;
3. Pridružite vrijednost 50 varijabli yourAge koristeći pokazivač koji ste deklarirali u vježbi 2. *pAge = 50;
4. BUG BUSTERS: Što ne valja s slijedećim kodom? #include int main() { int *pInt; *pInt = 9; cout << "The value at pInt: " << *pInt; return 0; } pInt je trebao biti inicijaliziran. Budući da nije inicijaliziran, i nije pridružen nekoj određenoj memorijskoj adresi, on pokazuje na slučajno mjesto u memoriji. Dodjelivši 9 na to slučajno mjesto je opasan bug.
6. BUG BUSTERS: Što ne valja sa slijedećim kodom? int main() { int SomeVariable = 5; cout << "SomeVariable: " << SomeVariable << "\n"; int *pVar = & SomeVariable; pVar = 9; cout << "SomeVariable: " << *pVar << "\n"; return 0; } Pretpostavljamo da je programer želio dodjeliti 9 kao vrijednost na koju pokazuje pVar. Na nesreću, 9 je pridružen kao vrijednost od pVar jer je izostavljen operator indirekcije (*).
Lekcija 9 Kviz 1. U čemu je razlika između pokazivača i reference? Referenca je alias, a pokazivač je varijabla koja sadrži drugu adresu. Reference ne mogu biti null i ne mogu se pridružiti.
2. Kada koristimo pokazivač, a ne referencu? Kada trebamo ponovni pridružiti na ono što pokazuje, ili kada pokazivač može biti null.
3. Što vraća new ako nema dovoljno memorije za pravljenje novog objekta? Nul pokazivač (0).
4. Kakva je to konstantna referenca? To je skraćenica od govora "referenca na konsantan objekt." Vježbe 1. Napišite program koji deklarira int, referencu na int, i pokazivač na int. Koristeći pokazivač i referencu, promijenite vrijednost u int. int main() { int varOne; int& rVar = varOne; int* pVar = &varOne; rVar = 5; *pVar = 7; return 0; }
2. Napišite program koji deklarira konstantan pokazivač na konstantan integer. Inicijalizirajte pokazivač na integer varijablu, varOne. Dodijelite 6 u varOne. Preko pokazivača, dodijelite 7 u varOne. Kreirajte novu varijablu, varTwo. Pridružite pokazivač novoj varijabli. int main() { int varOne; const int * const pVar = &varOne; *pVar = 7; int varTwo; pVar = &varTwo; return 0; }
3. BUG BUSTERS: Što ne valja u ovom programu? #include class CAT { public: CAT(int age) { itsAge = age; } ~CAT(){} int GetAge() const { return itsAge;} private: int itsAge; }; CAT & MakeCat(int age); int main() { int age = 7; CAT Boots = MakeCat(age); cout << "Boots is " << Boots.GetAge() << " years old\n"; } CAT & MakeCat(int age) { CAT * pCat = new CAT(age); return *pCat; } MakeCat vraća referencu na CAT kreiran u slobodnom spremniku. Nema načina da oslobodimo tu memoriju i to proizvodi curenje memorije.
4. Popravite gornji program. #include class CAT { public: CAT(int age) { itsAge = age; } ~CAT(){} int GetAge() const { return itsAge;} private: int itsAge; }; CAT * MakeCat(int age); int main() { int age = 7; CAT * Boots = MakeCat(age); cout << "Boots is " << Boots->GetAge() << " years old\n"; delete Boots; return 0; } CAT * MakeCat(int age) { return new CAT(age); }
Lekcija 10 Kviz 1. Kad preopterećujete funkcijske članove, kako se oni trebaju razlikovati? Preopterećeni funkcijski članovi su funkcije u klasi koje dijele isto ime ali se razlikuju u broju i tipu njihovih parametara.
2. Koja je razlika između deklaracije i definicije? Definicija rezervira memoriju, a ne deklaracija. Gotovo sve deklaracije su definicije; glavne iznimke su deklaracije klasa, funkcijski prototipi i typedef naredbe.
3. Kada se poziva konstruktor kopije? Uvijek kada kreiramo privremenu kopiju objekta. To se događa svaki put kad objekt proslijeđujemo po vrijednosti.
4. Kada se poziva destruktor? Destruktor se poziva svaki put kad je objekt uništen, bilo da ode izvan dosega ili da pokrenete delete na pokazivaču koji pokazuje na njega.
5. Koja je razlika između konstruktora kopije i operatora pridruživanja (=)? Operator pridruživanja djeluje na postojeći objekt, dok konstruktor kopije kreira novi.
6. Što je this pokazivač? This pokazivač je skriveni parameter svakog funkcijskog člana koji pokazuje na sam objekt.
7. Kako razlikujemo između preopterećenja prefiks i postfiks inkrement operatora? Prefiks operator ne prima parametre. Postfiks operator prima jedan int parametar, koji se koristi kao signal kompajleru da je riječ o postfiks varijanti.
8. Možete li preopteretiti operator+ za short integer vrijednosti? Ne, ne možete preopterećivati operatore za ugrađene tipove podataka.
9. Da li je legalno U C++ preopteretiti operator++ tako da on dekrementira vrijednost u vašoj klasi? Legalno je, ali je to loša ideja. Operatori bi trebali biti preopterećeni na takav način koji će biti razumljiv svakome čitaću vašega koda.
10. Kakvu povratnu vrijednost moraju imati operatori konverzije u svojim deklaracijama? Nikakvu. Poput konstruktora i destruktora, oni nemaju povratne vrijednosti.
Vježbe 1. Napišite SimpleCircle deklaraciju klase s jednim podatkovnim članom: itsRadius. Uključite podrazumijevani konstruktor, destruktor, te pristupne metode za polumjer. class SimpleCircle { public: SimpleCircle(); ~SimpleCircle(); void SetRadius(int); int GetRadius(); private: int itsRadius; };
2. Koristeći klasu kreiranu u vježbi 1, napišite implementaciju podrazumijevanog konstruktora, inicijalizirajući itsRadius s vrijednošću 5. SimpleCircle::SimpleCircle(): itsRadius(5) {}
3. Koristeći istu klasu dodajte i drugi konstruktor koji prima vrijednost kao svoj parametar i pridružuje ju u itsRadius. SimpleCircle::SimpleCircle(int radius): itsRadius(radius) {}
4. Kreirajte prefiks i postfiks inkrement operator za vašu SimpleCircle klasu koji inkrementira itsRadius. const SimpleCircle& SimpleCircle::operator++() { ++(itsRadius); return *this; } // Operator ++(int) postfix. // Fetch then increment const SimpleCircle SimpleCircle::operator++ (int) { // declare local SimpleCircle and initialize to value of *this SimpleCircle temp(*this); ++(itsRadius); return temp; }
5. Promjenite SimpleCircle da pohrani itsRadius u slobodnom spremniku, te popravite postojeće metode. class SimpleCircle { public: SimpleCircle(); SimpleCircle(int); ~SimpleCircle(); void SetRadius(int); int GetRadius(); const SimpleCircle& operator++(); const SimpleCircle operator++(int); private: int *itsRadius; }; SimpleCircle::SimpleCircle() {itsRadius = new int(5);} SimpleCircle::SimpleCircle(int radius) {itsRadius = new int(radius);} const SimpleCircle& SimpleCircle::operator++() { ++(itsRadius); return *this; } // Operator ++(int) postfix. // Fetch then increment const SimpleCircle SimpleCircle::operator++ (int) { // declare local SimpleCircle and initialize to value of *this SimpleCircle temp(*this); ++(itsRadius); return temp; } 6. Napravite konstruktor kopije za SimpleCircle. SimpleCircle::SimpleCircle(const SimpleCircle & rhs) { int val = rhs.GetRadius(); itsRadius = new int(val); }
7. Napravite operator pridruživanja za SimpleCircle. SimpleCircle& SimpleCircle::operator=(const SimpleCircle & rhs) { if (this == &rhs) return *this; delete itsRadius; itsRadius = new int; *itsRadius = rhs.GetRadius(); return *this; }
8. Napišite program koji kreira dva SimpleCircle objeka. Koristeći podrazumijevani konstruktor na jednom i inicijaliziranjem drugog na vrijednost 9. Pozovite operator inkrementiranja za svaki i potom ispišite njihove vrijednosti. Konačno, pridružite drugi prvome i ispišite njihove vrijednosti. #include class SimpleCircle { public: // constructors SimpleCircle(); SimpleCircle(int); SimpleCircle(const SimpleCircle &); ~SimpleCircle() {} // accessor functions void SetRadius(int); int GetRadius()const; // operators const SimpleCircle& operator++(); const SimpleCircle operator++(int); SimpleCircle& operator=(const SimpleCircle &); private: int *itsRadius; }; SimpleCircle::SimpleCircle() {itsRadius = new int(5);} SimpleCircle::SimpleCircle(int radius) {itsRadius = new int(radius);} SimpleCircle::SimpleCircle(const SimpleCircle & rhs) { int val = rhs.GetRadius(); itsRadius = new int(val); }
SimpleCircle& SimpleCircle::operator=(const SimpleCircle & rhs) { if (this == &rhs) return *this; *itsRadius = rhs.GetRadius(); return *this; } const SimpleCircle& SimpleCircle::operator++() { ++(itsRadius); return *this; } // Operator ++(int) postfix. // Fetch then increment const SimpleCircle SimpleCircle::operator++ (int) { // declare local SimpleCircle and initialize to value of *this SimpleCircle temp(*this); ++(itsRadius); return temp; } int SimpleCircle::GetRadius() const { return *itsRadius; } int main() { SimpleCircle CircleOne, CircleTwo(9); CircleOne++; ++CircleTwo; cout << "CircleOne: " << CircleOne.GetRadius() << endl; cout << "CircleTwo: " << CircleTwo.GetRadius() << endl; CircleOne = CircleTwo; cout << "CircleOne: " << CircleOne.GetRadius() << endl; cout << "CircleTwo: " << CircleTwo.GetRadius() << endl; return 0; }
9. BUG BUSTERS: Što ne valja s ovom implementacijom operatora pridruživanja? SQUARE SQUARE ::operator=(const SQUARE & rhs) { itsSide = new int; *itsSide = rhs.GetSide(); return *this; } Morate provjeriti da li je rhs jednak this, ili će poziv iz a = a srušiti vaš program.
10. BUG BUSTERS: Što ne valja s implementacijom operatora zbrajanja? VeryShort VeryShort::operator+ (const VeryShort& rhs) { itsVal += rhs.GetItsVal(); return *this; } Ovaj operator + mijenja vrijednost u jednom od operanda, ne stvarajući novi VeryShort objekt sa sum. Pravilan način bio bi: VeryShort VeryShort::operator+ (const VeryShort& rhs) { return VeryShort(itsVal + rhs.GetItsVal()); }
Lekcija 11 Kviz 1. Koji su prvi i zadnji elementi u SomeArray[25]? SomeArray[0], SomeArray[24]
2. Kako deklarirati neko višedimenzionalno polje? Napišite grupu uglatih zagrada za svaku dimenziju. Na primjer, SomeArray[2][3][2] je trodimenzionalno polje. Prva dimenzija ima dva elementa, druga tri, i treća dva.
3. Inicijalizirajte članove polja iz drugog pitanja. SomeArray[2][3][2] = { { {1,2},{3,4},{5,6} } , { {7,8},{9,10},{11,12} } };
4. Koliko elemenata se nalazi u SomeArray[10][5][20]? 10x5x20=1,000
5. Koji je najveći broj elemenata koje možete dodati u vezane liste? Ne postoji fiksni maksimum. Ovisi o količini slobodne memorije.
6. Koji je zadnji znak stringa "Josip je dobar momak "? Nul znak.
Vježbe 1. Deklarirajte dvodimenzionalno polje koje predstavlja križić-kružić poligon za igru. int GameBoard[3][3];
2. Napišite kod koji inicijalizira sve elemente u kreiranom polju iz vj.1 na vrijednost 0. int GameBoard[3][3] = { {0,0,0},{0,0,0},{0,0,0} }
3. Napišite deklaraciju Node klase koja drži unsigned short integere. class Node { public: Node (); Node (int); ~Node(); void SetNext(Node * node) { itsNext = node; } Node * GetNext() const { return itsNext; } int GetVal() const { return itsVal; } void Insert(Node *); void Display(); private: int itsVal; Node * itsNext; };
4. BUG BUSTERS: Što ne valja u slijedećem kodu? unsigned short SomeArray[5][4]; for (int i = 0; i<4; i++) for (int j = 0; j<5; j++) SomeArray[i][j] = i+j; Polje ima 5 x 4 elementa, a kod inicijalizira 4 x 5. 5. BUG BUSTERS: Što ne valja u slijedećem kodu? unsigned short SomeArray[5][4]; for (int i = 0; i<=5; i++) for (int j = 0; j<=4; j++) SomeArray[i][j] = 0; Trebalo je pisati i<5, a ne i<=5. Ista stvar vrijedi i za j budući da ne postoji element SomeArray[5][4].
Lekcija 12 Kviz 1. Što je v-table? V-table, ili tablica virtualnih funkcija jest uobičajen način kojim kompajleri upravljaju virtualnim funkcijama u C++u. Tablica sadrži listu adresa svih virtualnih funkcija i, ovisno o tipu na koji objekt pokazuje, poziva pravu funkciju.
2. Što je virtualni destruktor? Destruktor za bilo koju klasu može biti deklariran kao virtualan. Kada je pokazivač izbrisan, izvršni tip objekta će biti proučen i pravilno deriviran destuktor će se pokrenuti.
3. Kako pokazujemo deklaraciju virtualnog konstruktora? Ne postoje virutalni konstruktori.
4. Kako kreiramo virtualni konstruktor kopije? Kreiranjem virtualne metode u klasi, koja sama poziva konstruktor kopije.
5. Kako pozivamo funkciju bazne klase iz derivirane klase u kojoj ste zaobišli tu funkciju? Base::FunctionName();
6. Kako pozivamo baznu funkciju iz izvedene klase u kojoj nismo zaobilazili funkciju? FunctionName();
7. Ako bazna klasa deklarira funkciju virtualnom, a izvedena klasa ne koristi izraz virtual kad zaobilazi klasu, da li je ona još uvijek virtualna ako ju naslijedi treća generacija klase? Da, virtualnost se naslijeđuje i ne može biti isključena.
8. Kada koristimo protected ključnu riječ? protected članovi su dostupni funkcijskim članovima deriviranih objekata.
Vježbe 1. Pokaži deklaraciju virtualne funkcije koja uzima cjelobrojni parametar i vraća void. virtual void SomeFunction(int);
2. Pokažite deklaraciju klase Square, koja se izvodi iz Rectangle, kojii se izvodi iz Shape. class Square : public Rectangle {};
3. Ako, u vj.2, Shape ne uzima parametre, Rectangle uzima dva (length i width), ali Square uzima samo jedan (length), pokažite inicijalizaciju konstruktora za Square. Square::Square(int length): Rectangle(length, length){}
4. Napišite virtualni konstruktor kopije za klasu Square (vj. 3). class Square { public: // ... virtual Square * clone() const { return new Square(*this); } // ... };
5. BUG BUSTERS: Što ne valja? void SomeFunction (Shape); Shape * pRect = new Rectangle; SomeFunction(*pRect); Možda ništa. SomeFunction očekuje Shape objekt.Vi ste mu poslali Rectangle izveden iz Shape. Dok god ne trebate niti jedan dio specfičan za Rectangle, ovo će biti u redu. Ukoliko trebate Rectangle dijelove, morate promijeniti SomeFunction da uzima pokazivač ili referencu na Shape.
6. BUG BUSTERS: Što ne valja? class Shape() { public: Shape(); virtual ~Shape(); virtual Shape(const Shape&); }; Nije moguće deklarirati virtualan konstruktor kopije.
Lekcija 13 Kviz 1. Što je v-ptr? V-ptr, ili virtual-function pointer – pokazivač na virtualnu funkciju, jest implementacijski detalj virtualnih funkcija. Svaki objekt u klasi sa virtualnim funkcijama ima v-ptr, koji pokazuje na tablicu virtualnih funkcija za tu klasu.
2. Ako zaobljeni pravokutnik ima ravne linije i oble rubove, a vaša RoundRect klasa naslijeđuje i iz Rectangle i Circle, a oni su naslijednici iz Shape, koliko Shapes se kreira kad kreirate RoundRect? Ako niti jedna klasa ne naslijeđuje koristeći ključnu riječ virtual, dva Shape objekta se kreiraju: Jedan za Rectangle i jedan za Shape. Ako se ključna riječ virtual koristi za obje klase, samo jedan dijeljeni Shape se kreira.
3. Ako Horse i Bird naslijeđuju iz Animal koristeći javno virtualno naslijeđivanje, da li njihovi konstruktori inicijaliziraju Animal konstruktor? Ako Pegasus naslijeđuje i iz Horse i Bird, kako on inicijalizira Animal konstruktor? I Horse i Bird inicijaliziraju svoju baznu klasu, Animal, u svojim konstruktorima. Pegasus čini isto, a kad se Pegasus kreira, Horse i Bird inicijalizacije od Animal se ignoriraju.
4. Deklarirajte klasu vehicle, i proglasite ju ADT-om. class Vehicle { virtual void Move() = 0; }
5. Ako je bazna klasa ADT, i ima tri čiste virtualne funkcije, koliko od njih mora biti zaobiđeno u izvedenim klasama? Niti jedna ne mora biti zaobiđena ukoliko ne želite načiniti klasu ne-abstraktnom. U tom slučaju sve tri moraju biti zaobiđene.
Vježbe 1. Prikažite deklaraciju klase JetPlane, koja naslijeđuje od Rocket i Airplane. class JetPlane : public Rocket, public Airplane
2. Pokažite deklaraciju od 747, koji naslijeđuje iz JetPlane klase prethodne vježbe. class 747 : public JetPlane
3. Napišite program koji izvodi Car i Bus iz klase Vehicle. Proglasite Vehicle ADT-om s dvije čiste virtualne funkcije. Napravite da Car i Bus nisu ADT-i. class Vehicle { virtual void Move() = 0; virtual void Haul() = 0; }; class Car : public Vehicle { virtual void Move(); virtual void Haul(); }; class Bus : public Vehicle { virtual void Move(); virtual void Haul(); };
Lekcija 14 Kviz 1. Mogu li statički podatkovni članovi biti privatni? Da. To su podatkovni člaovi, i pristum njima može biti kontroliran. U slučaju da su privatni, može im se pristupati samo korištenjem funkcijskih članova, ili, što je češće, statičkih funkcijskih članova.
2. Pokažite deklaraciju statičkog podatkovnog člana. static int itsStatic;
3. Pokažite deklaraciju statičkog funkcijskog pokazivača. static int SomeFunction();
4. Pokažite deklariciju za pokazivač na funkciju koji vraća long i prima integer parametar. long (* function)(int);
5. Promijenite pokazivač iz četvrtog pitanja da pokazuje na funkcijski član klase Car. long ( Car::*function)(int);
6. Pokažite deklaraciju za polje 10 pokazivača difiniranih u pitanju 5. (long ( Car::*function)(int) theArray [10];
Vježbe 1. Napišite kratak program deklariranjem klase s jednim podatkovnim članom i jednim statičkim podatkovnim članom. Neka konstruktor inicijalizira podatkovni član i inkrementira statični podatkovni član. Neka destruktor dekrementira podatkovni član. class myClass { public: myClass(); ~myClass(); private: int itsMember; static int itsStatic; }; myClass::myClass(): itsMember(1) { itsStatic++; } myClass::~myClass() { itsStatic--; } int myClass::itsStatic = 0; int main() {}
2. Koristeći program sa vježbe 1, napišite kratki program koji kreira tri objekta i potom prikazuje njihove podatkovne članove i statički podatkovni član. Potom uništite svaki objekt i pokažite kako to djeluje na statički podatkovni član. #include class myClass { public: myClass(); ~myClass(); void ShowMember(); void ShowStatic(); private: int itsMember; static int itsStatic; }; myClass::myClass(): itsMember(1) { itsStatic++; } myClass::~myClass() { itsStatic--; cout << "In destructor. ItsStatic: " << itsStatic << endl; } void myClass::ShowMember() { cout << "itsMember: " << itsMember << endl; } void myClass::ShowStatic() { cout << "itsStatic: " << itsStatic << endl; } int myClass::itsStatic = 0; int main() { myClass obj1; obj1.ShowMember(); obj1.ShowStatic(); myClass obj2; obj2.ShowMember(); obj2.ShowStatic(); myClss obj3; obj3.ShowMember(); obj3.ShowStatic(); return 0; }
3. Promijenite program sa vježbe 2 da koristi statički funkcijski član kako bi pristupio statičkom podatkovnom članu. Proglasite statički podatkovni član privatnim. #include class myClass { public: myClass(); ~myClass(); void ShowMember(); static int GetStatic(); private: int itsMember; static int itsStatic; }; myClass::myClass(): itsMember(1) { itsStatic++; } myClass::~myClass() { itsStatic--; cout << "In destructor. ItsStatic: " << itsStatic << endl; } void myClass::ShowMember() { cout << "itsMember: " << itsMember << endl; } int myClass::itsStatic = 0; void myClass::GetStatic() { return itsStatic; } int main() { myClass obj1; obj1.ShowMember(); cout << "Static: " << myClass::GetStatic() << endl; myClass obj2; obj2.ShowMember(); cout << "Static: " << myClass::GetStatic() << endl; myClass obj3; obj3.ShowMember(); cout << "Static: " << myClass::GetStatic() << endl; return 0; }
4. Napišite pokazivač na funkcijski član za pristup ne-statičkim podatkovnim članovima u programu sa vježbe 3, i koristite taj pokazivač za ispis vrijednosti tog podatka. #include class myClass { public: myClass(); ~myClass(); void ShowMember(); static int GetStatic(); private: int itsMember; static int itsStatic; }; myClass::myClass(): itsMember(1) { itsStatic++; } myClass::~myClass() { itsStatic--; cout << "In destructor. ItsStatic: " << itsStatic << endl; } void myClass::ShowMember() { cout << "itsMember: " << itsMember << endl; } int myClass::itsStatic = 0; int myClass::GetStatic() { return itsStatic; } int main() { void (myClass::*PMF) (); PMF=myClass::ShowMember; myClass obj1; (obj1.*PMF)(); cout << "Static: " << myClass::GetStatic() << endl; myClass obj2; (obj2.*PMF)(); cout << "Static: " << myClass::GetStatic() << endl; myClass obj3; (obj3.*PMF)(); cout << "Static: " << myClass::GetStatic() << endl; return 0; }
5. Dodajte još dva podatkovna člana u klasu iz prethodnog pitanja. Dodajte pristupne funkcije za dobijanje vrijednosti tih članova, i dajte svim funkcijskim članovima iste povratne tipove i potpise. Koristeći pokazivač na funkcijske članove pristupite tim funkcijama. #include class myClass { public: myClass(); ~myClass(); void ShowMember(); void ShowSecond(); void ShowThird(); static int GetStatic(); private: int itsMember; int itsSecond; int itsThird; static int itsStatic; }; myClass::myClass(): itsMember(1), itsSecond(2), itsThird(3) { itsStatic++; } myClass::~myClass() { itsStatic--; cout << "In destructor. ItsStatic: " << itsStatic << endl; } void myClass::ShowMember() { cout << "itsMember: " << itsMember << endl; } void myClass::ShowSecond() { cout << "itsSecond: " << itsSecond << endl; } void myClass::ShowThird() { cout << "itsThird: " << itsThird << endl; } int myClass::itsStatic = 0; int myClass::GetStatic() { return itsStatic; } int main() { void (myClass::*PMF) ();
myClass obj1; PMF=myClass::ShowMember; (obj1.*PMF)(); PMF=myClass::ShowSecond; (obj1.*PMF)(); PMF=myClass::ShowThird; (obj1.*PMF)(); cout << "Static: " << myClass::GetStatic() << endl; myClass obj2; PMF=myClass::ShowMember; (obj2.*PMF)(); PMF=myClass::ShowSecond; (obj2.*PMF)(); PMF=myClass::ShowThird; (obj2.*PMF)(); cout << "Static: " << myClass::GetStatic() << endl; myClass obj3; PMF=myClass::ShowMember; (obj3.*PMF)(); PMF=myClass::ShowSecond; (obj3.*PMF)(); PMF=myClass::ShowThird; (obj3.*PMF)(); cout << "Static: " << myClass::GetStatic() << endl; return 0; }
Lekcija 15 Kviz 1. Što je operator umetanja i što on radi? Operator umetanja (<<) je operator-član ostream objekta i koristi se za pisanje na izlazni uređaj.
2. Što je operator ekstrakcije i što on radi? Operator ekstrakcije (>>) je član-operator istream objekta i koristi se za upisivanje u programske varijable.
3. Koja su tri oblika cin.get(), i u čemu je njihova razlika? Prvi oblik get() je bez parametara. On vraća vrijednost nađenog znaka, a kada dođe do kraja datoteke vratiti će EOF (end of file). Drugi oblik cin.get() uzima znakovnu referencu kao svoj parametar; taj znak se puni sa slijedećim znakom input toka. Povratna vrijednost je iostream objekt. Treći oblik cin.get() prima polje, maksimalan broj znakova i terminirajući znak. Taj oblik get() ispunjava polje sa maksimalnim brojem znakova –1 ukoliko prije ne dođe do terminirajućeg znaka. U tom slučaju trenutno upisuje null znak i ostavlja terminirajući znak u bufferu.
4. U čemu je razlika između cin.read() i cin.getline()? cin.read() se koristi za čitanje binarnih struktura podataka. getline() se koristi za čitanje iz istream spremnika.
5. Koja je podrazumijevana širina za izlaz long integera korištenjem operatora umetanja? Onoliko koliko je potrebno za ispisivanje cijelog broja.
6. Koji je povratni tip operatora umetanja? Referenca na istream objekt.
7. Koje parametre konstruktor na ofstream objekt uzima? Ime datoteke koju treba otvoriti.
8. Što radi ios::ate argument ? ios::ate nas stavlja na kraj datoteke, ali omogućuje pisanje u datoteci na bilo kojem mjestu.
Vježbe 1. Napišite program koji piše u četri standardna iostream objekta: cin, cout, cerr, i clog. #include int main() { int x; cout << "Enter a number: "; cin >> x; cout << "You entered: " << x << endl; cerr << "Uh oh, this to cerr!" << endl; clog << "Uh oh, this to clog!" << endl; return 0; }
2. Napišite program za upis cijelog imena i zatim ga ispišite na ekran. #include int main() { char name[80]; cout << "Enter your full name: "; cin.getline(name,80); cout << "\nYou entered: " << name << endl; return 0; }
3. Napišite ponovo listing 15.9, ali da ne koristi putback() ili ignore(). // Listing #include int main() { char ch; cout << "enter a phrase: "; while ( cin.get(ch) ) { switch (ch) { case `!': cout << `$'; break; case `#': break; default: cout << ch; break; } } return 0; }