Fujitsu-Siemens
 
M A G A Z I N
 
PROGRAMIRANJE 
  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

 

VRH STRANE

(c) 2003 OMEGA - sva prava zadržana