 |
| |
Davor Đošan
Prenošenje podataka po referenci |
|
U programskom jeziku C sve se prenosi
po vrednosti. I C++ kao naslednik jezika
C uzima da se argumenti prenose po vrednosti, ali se kao novina
uvodi i prenošenje po referenci. Prenošenje po vrednosti znači da
će, ukoliko se drugačije ne naglasi, svi parametri funkcija biti
inicijalizovani kopijama stvarnih argumenata. Takođe, ukoliko funkcija
vraća vrednost, iz funkcije se vraća kopija objekta kreiranog unutar
funkcije.
Prenošenje po vrednosti znači da će pri pozivu funkcije biti pozivani
konstruktori kopije za objekte koji se prosleđuju kao argumenti
funkcije. Ovo znači da prosleđivanje po vrednosti može da bude izuzetno
“skupo”. Navesćemo jedan primer. Zamislimo da imamo definisane dve
klase Osnovna i iz nje izvedena klasa Izvedena i da su one definisane
kao (definicija klasa je maksimalno uprošćena):
class Osnovna {
public:
Osnovna(); //Konstruktor klase Osnovna
~Osnovna(); //Destruktor klase Osnovna
private:
string string1, string2; //Clanovi klase
}
class Izvedena : public Osnovna {
public:
Izvedena(); //Konstruktor klase Izvedena
~Izvedena(); //Destruktor klase Izvedena
…………….
private:
string string1, string2; //Clanovi klase
}
Zamislimo da u izvedenoj klasi imamo definisanu prostu funkciju
vratiIzvedena kojoj se kao argument po vrednosti prosleđuje objekat
tipa Izvedena i da funkcija po vrednosti vraća objekat tipa Izvedena:
Izvedena vratiIzvedena(Izvedena izv)
{
return izv;
}
Šta bi se desilo pri pozivu ove funkcije?
Prosto objašnjenje je sledeće: najpre bi bio pozvan konstruktor
kopije klase Izvedena da bi se inicijalizovao argument izv. Zatim
se poziva konstruktor kopije klase Izvedena da bi se inicijalizovao
objekat koji funkcija vraća. Posle se poziva destruktor klase Izvedena
da bi se uništio objekat izv i na kraju bi bio pozvan destruktor
klase Izvedena da bi se uništio objekat koji vraća funkcija. To
znači da bi smo pri pozivu ove funkcije koja praktično ne radi ništa
imali pozivanje dva puta konstruktora kopije za klasu Izvedena i
dva destruktora za tu klasu.
Ali tu još uvek nije kraj. Ako pogledamo definiciju klase Izvedena
vidimo da unutar nje imamo definisana dva stringa string1 i string2
tako da pri pozivu konstruktora moramo takođe da pozovemo i kontruktore
za stringove. Klasa Izvedena nesleđuje klasu Osnovna. To znači da
se prilikom pozivanja konstruktora klase Izvedena poziva i konstruktor
klase Osnovna. Unutar klase Osnovna takođe imamo definisana dva
stringa tako da pri njenom kreiranju kreiramo i te stringove.
Iz ovog svega vidimo da mi samo pri prenosu argumenta funkcije imamo
za ovaj konkretan primer pozivanje sledećih funkcija: jedan konstruktora
kopije za klasu Izvedena, jedan konstruktor kopije klase Osnovna
i četiri poziva konstruktora kopije za stringove. Ne zaboravimo
da to isto imamo i za objekat koji funkcija vraća. Ista priča sledi
i za destruktore. Pri uništavanju ovih objekata pozivaju se destruktori
za klase Izvedena i Osnovna, kao i za stringove. Znači ukupna “cena”
koju plaćamo pri pozivu ove funkcije samo za prenošenje argumenta
po vrednosti je poziv šest konstruktora i šest destruktora. Isto
to imamo i za objekat koji funkcija vraća tako da je ukupna “cena”
jednog poziva ove funkcije pozivanje dvanaest konstruktora i dvanaest
destruktora.
Ovo sve smo mogli da izbegnemo da smo radili sa referencama. Zamislimo
da je funkcija vratiIzvedena definisana kao:
const Izvedena& vratiIzvedena(Izvedena& izv)
{
return izv;
}
Sa ovakvom definicijom funkcije izbegli bismo sve nepotrebne pozive
konstruktora i destruktora koje smo imali sa ranijom definicijom
funkcije.
Međutim, ovo nije jedina prednost prenošenja po referenci u odnosu
na prenošenje po vrednosti. Zamislimo situaciju da u funkciju na
mestu argumenta neke osnovne klase prosleđujemo objekat izvedene
klase. U slučaju prenošenja po vrednosti bio bi kreiran objekat
osnovne klase i time bismo izgubili sve ono sto smo definisali unutar
izvedene klase (zamislimo da postoji neki polimorfizma za ove klase)
a to nam svakako ne odgovara. U slučaju prenošenja po referenci
unutar funkcije bismo radili sa konkretnim objektom izvedene klase.
Međutim ne bi bilo pošteno a da se ne pomenu i neke nedostaci koje
ima ovaj tip prenošenja argumenata. Tipičan primer za to bi bio
aliasing, tj. postojanje više imena za jedan isti objekat. Navešćemo
jedan primer.
Uzmimo da imamo klasu String čija bi uprosćena definicija imala
sledeći oblik:
class String {
public:
String(cnst char* value);
~String();
.............................
private:
char* data;
}
Neka nam je operator dodele vrednosti unutar ove klase:
String& String::operator=(const String& rhs)
{
delete [] data; //brisemo stare podatke
//alociramo novu memoriju i kopiramo podatke iz rhs stringa
data = new char[strlen(rhs.getData()) + 1];
strcpy(data, rhs.getData());
return *this;
}
Uzmimo sada sledeći slučaj:
String a = “Zdravo”;
a = a;
U ovom slučaju pri pozivu operatora dodele vrednosti *this i rhs
su nam u stvari različita imena za isti objekat. Problem nastaje
kada se uradi naredba delete za *this objekat. Brisanjem ovih podataka
takođe se brišu i podaci u rhs objektu jer je reč o istom objektu.
Rezultat pozivanja strlen funkcije na rhs.data bi nam u ovom slučaju
bio nedefinisan.
Takođe, prenošenje po referenci obično u stvarnosti znači prenošenje
pointera. Tako u slučaju da radimo sa nekim malim objektima (na
primer int) može da bude mnogo efektinije da prenosimo po vrednosti
nego po referenci.
Naveo bih još i jednu opasnost koja se može javiti prilikom prenošenja
po referenci. Jednom kada se uvidi slaba efikasnost kod prenošenja
po vrednosti može doći do velikih grešaka prilikom pokušaja da se
prenošenje po vrednosti izbaci na svim mogućim mestima. Tipičan
primer bio bi prenošenje reference objekata koji ne postoje, što
svakako nije dobra stvar. Navešćemo i jedan primer za ovaj slučaj.
Zamislimo klasu koja predstavlja racionalne brojeve i unutar nje
i friend funkciju za množenje dva racionalna broja:
class Rational {
public:
Rational (int numerator = 0, int denominator = 0);
..................
private:
int n, d;
}
friend const Rational operator* (const Rational& lhs, const
Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
Vidimo da operator množenja vraća rezultat po vrednosti. Ono o čemu
možemo da počnemo da razmišljamo na osnovu prethodnog što smo pročitali
bi bilo o nepotrebnim pozivima konstruktora i destruktora za ovaj
slučaj. To bismo mogli da izbegnemo ukoliko bismo vraćali rezultat
po referenci. Ali ono što ne treba zaboraviti je da referenca nije
ništa drugo do samo ime za neki objekat koji već postoji. Znači,
ukoliko hoćemo da funkcija vrati vrednost po referenci to bi morao
da bude neki objekat koji već postoji i sadrži rezultat množenja
dva racionalna broja. Možda bi jedan od načina mogao da bude:
friend const Rational& operator* (const Rational& lhs, const
Rational& rhs)
{
Rational result(lhs.r * rhs.r, lhs.i * rhs.i);
return result;
}
U ovom slučaju vidimo da će objekat koji predstavlja rezultat biti
kreiran kao i objekat u prethodnom slučaju, a mi smo hteli da izbegnemo
nepotreban poziv konstruktora. Ono što nam se samo pojavilo kao
veliki problem ovde je da vraćamo referencu lokalnog objekta a to
može da izazove velike probleme.
Možemo da navedemo još neke primere koji će da nam pokažu da je
razmišljanje o vraćanju rezultata po referenci iz funkcija kao što
je množenje samo nepotrebno gubljenje vremena.
Ono što možemo da zaključimo je sledeće: Prenošenje po referenci
u C++-u je odlična stvar. Mađutim treba biti veoma oprezan prilikom
odlučivanja da li prenositi po referenci ili po vrednosti. Albert
Ajnštajn je jednom rekao: Treba uprostiti stvari što je moguće više,
ali ne i uproštavati više od toga. Kada bismo to preveli u C++ terminologiju
to bi otprilike bilo: Učinimo nešto da radi što efikasnije, ali
ne i efikasnije od toga. Treba napomenuti da i kompajleri vrše neke
optimizacije koda i da je moguće da će sam kompajler izbaciti neke
nepotrebne pozive konstruktora kopije. Znači ono što je na programeru
prilikom odlučivanja da li da prenosi po vrednosti ili referenci
je da napravi pravi izbor i da to bude izbor koji će pravilno da
radi, a način na koji će to biti urađeno “najjeftinije” moguće treba
da prepusti kompajleru.
Verzija
za štampu
|