OPERATOR SUPRAINCARCAT
Acest capitol descrie mecanismul pentru operatorul de supraincarcare furnizat de C++. Un programator poate defini un sens pentru operatori cind se aplica la obiectele unei clase specifice; in plus se pot defini fata de operatiile aritmetice, logice si relationale, apelul () si indexarea [] si atit initializarea cit si asignarea pot fi redefinite. Se pot defini conversii de tip implicite si explicite intre cele definite de utilizator si tipurile de baza. Se arata cum se defineste o clasa pentru care un obiect nu poate fi copiat sau distrus exceptind functiile specifice definite de utilizator.
6.1 Introducere
Programele adesea manipuleaza obiecte care sint reprezentari concrete ale conceptelor abstracte. De exemplu, datele de tip int din C++, impreuna cu operatorii +, -, *, /, etc., furnizeaza o implementare (restrictiva) a conceptului matematic de intregi. Astfel de concepte de obicei includ un set de operatori care reprezinta operatiile de baza asupra obiectelor intr-un mod concis, convenabil si conventional. Din nefericire, numai foarte putine astfel de concepte pot fi suportate direct prin limbajul de programare. De exemplu, ideile de aritmetica complexa, algebra matricilor, semnale logice si sirurile receptionate nu au un suport direct in C++. Clasele furnizeaza o facilitate pentru a specifica o reprezentare a obiectelor neprimitive in C++ impre- una cu un set de operatii care pot fi efectuate cu astfel de obiecte. Definind operatori care sa opereze asupra obiectelor unei clase, uneori se permite unui programator sa furnizeze o notatie mai conventionala si mai convenabila pentru a manipula obiectele unei clase, decit s-ar putea realiza utilizind numai notatia functionala de baza. De exemplu:
class complex
friend complex operator+(complex, complex);
friend complex operator*(complex, complex);
};
defineste o implementare simpla a conceptului de numere comlexe, unde un numar este reprezentat printr-o pereche de numere flotante in dubla precizie manipulate (exclusiv) prin operatorii + si *. Programatorul furnizeaza un inteles pentru + si * definind functiile denumite operator+ si operator*. De exemplu, dind b si c de tip complex, b+c inseamna (prin definitie) operator+(b, c). Este posibil acum sa se aproximeze interpretarea conventionala a expresiilor complexe. De exemplu:
void f()
6.2 Functiile operator
Functiile care definesc intelesul pentru operatorii urmatori pot fi declarate:
+ - * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=
/= << >> >>= <<= == != <= >= &&
|| ++ -- [] () new delete
Ultimii patru sint pentru indexare (&6.7), apel de functie (&6.8), alocare de memorie libera si dealocare de memorie libera (&3.2.6). Nu este posibil sa se schimbe precedenta acestor operatori si nici sintaxa expresiei nu poate fi schimbata. De exemplu, nu este posibil sa se defineasca un operator unar % sau unul binar !. Nu este posibil sa se defineasca operatori noi, dar noi putem utiliza notatia de apel de functie cind acest set de operatori nu este adecvat. De exemplu, vom utiliza pow() si nu **. Aceste restrictii s-ar parea sa fie suparatoare, dar reguli mai flexibile pot foarte usor sa conduca la ambiguitati. De exemplu, definind un operator ** care sa insemne exponentiala, expresia a**p se poate interpreta atit ca a*(*p) cit si (a)**(p).
Numele unei functii operator este cuvintul cheie operator urmat de operatorul insusi (de exemplu operator<<). O functie operator se declara si poate fi apelata ca orice alta functie; utilizarea unui operator este numai o prescurtare pentru un apel explicit a functiei operator. De exemplu:
void f(complex a, complex b)
6.2.1 Operatori binari si unari
Un operator binar poate fi definit sau printr-o functie membru care are un argument sau printr-o functie prieten care are doua argumente. Astfel pentru orice operator binar @, aa@bb poate fi interpretat sau ca 656s183g aa.operator@(bb) sau ca operator@(aa, bb). Daca ambii sint definiti, aa@bb este o eroare. Un operator unar, prefix sau postfix, poate fi definit fie ca o functie membru fara argumente, fie ca o functie prieten cu un argument. Astfel pentru un operator unar @, atit aa@ cit si @aa pot fi interpretate sau ca aa.operator@() sau ca operator@(aa). Daca ambele sint definite, aa@ si @aa sint erori. Consideram exemplele:
class X;
Cind sint supraincarcati operatorii ++ si --, nu este posibil sa se faca distinctie intre aplicatia postfix si cea prefix.
6.2.2 Sensul predefinit al operatorilor
Nu se face nici o presupunere despre sensul unui operator definit de utilizator. In particular, intrucit supraincarcarea lui = nu se presupune ca implementeaza atribuirea la primul operand al lui; nu se face nici un test pentru a asigura ca acel operand este o lvalue (&r6). Sensurile unor operatori predefiniti se definesc astfel incit sa fie echivalente cu anumite combinatii de alti operatori asupra acelorasi argumente. De exemplu, daca a este un intreg, ++a inseamna a+=1, care la rindul ei inseamna a=a+1. Astfel de relatii nu au loc pentru operatorii definiti de utilizator, numai daca se intimpla ca utilizatorul sa le defineasca in acel fel. De exemplu, definitia lui operator++() pentru un tip complex nu poate fi dedusa din definitiile complex::operator+() si complex::operator=().
Din cauza unui accident istoric, operatorii = si & au sensuri predefinite cind se aplica la obiectele unei clase. Nu exista un mod elegant de a "nedefini" acesti doi operatori. Ei pot totusi sa fie dezactivati pentru o clasa X. Se poate, de exemplu, declara X::operator&() fara a furniza o definitie pentru el. Daca undeva se ia adresa unui obiect al clasei X, linkerul va detecta o lipsa de definitie. Pe anumite sisteme, linkerul este atit de "destept" incit el se descurca cind o fun- ctie neutilizata nu este definita. Pe astfel de sisteme aceasta tehnica nu poate fi utilizata. O alta alternativa este de a defini X::operator&() asa ca sa dea la executie o eroare.
6.2.3 Operatori si Tipuri definite de utilizatori
O functie operator trebuie sau sa fie un membru sau sa aiba cel putin un argument obiect al unei clase (functiile care redefinesc operatorii new si delete nu sint necesare). Aceasta regula asigura ca un utilizator sa nu poata schimba sensul oricarei expresii care nu implica un tip de data definit de utilizator. In particular, nu este posibil sa se defineasca o functie operator care sa opereze exclusiv asupra pointerilor.
O functie operator care intentioneaza sa accepte un tip de baza ca primul sau operand nu poate fi o functie membru. De exemplu, sa consideram adaugarea unei variabile complexe aa la intregul 2: aa+2 poate cu o functie membru corespunzatoare sa fie interpretata ca aa.operator+(2), dar 2+aa nu poate fi, intrucit nu exista nici o clasa int pentru care sa se defineasca + ca sa insemne 2.operator+(aa). Chiar daca ar fi, ar fi necesare doua functii membru diferite care sa trateze 2+aa si aa+2. Deoarece compilatorul nu cunoaste intelesul lui + definit de utilizator, el nu poate presupune ca el este comutativ si sa interpreteze 2+aa ca aa+2. Acest exemplu se trateaza trivial cind se utilizeaza functii friends.
Toate functiile operator sint prin definitie supraincarcate. O functie operator furnizeaza un inteles nou pentru un operator in plus fata de definitia predefinita si pot fi functii operator diferite cu acelasi nume atita timp cit ele difera suficient prin tipul argumentelor lor (&4.6.7).
6.3 Conversia de tip definita de utilizator
Implementarea numerelor complexe prezentata in introducere este prea restrictiva ca sa placa cuiva, asa ca ea trebuie extinsa. Aceasta este mai mult o repetitie triviala a tehnicilor prezentate anterior.
class complex
friend complex operator+(complex, complex);
friend complex operator+(complex, double);
friend complex operator+(double, complex);
friend complex operator-(complex, complex);
friend complex operator-(complex, double);
friend complex operator-(double, complex);
complex operator-(); //unar -
friend complex operator*(complex, complex);
friend complex operator*(complex, double);
friend complex operator*(double, complex);
// ...
};
Acum, cu aceasta declaratie a lui complex noi putem scrie:
void f()
Totusi, scrierea unei functii pentru fiecare combinatie dintre complex si double ca si pentru operator*() de mai sus, este o tendinta de nesuportat. Mai mult decit atit, o facilitate realista pentru aritmetica complexa trebuie sa furnizeze cel putin o duzina de astfel de functii; vezi de exemplu, tipul complex asa cum este el declarat in <complex.h>.
6.3.1 Constructori
O varianta de a utiliza diferite functii supraincarcate este de a declara un constructor care dindu-i-se un double creaza un complex. De exemplu:
class complex
};
Un constructor care cere un singur argument nu poate fi apelat explicit:
complex z1 = complex(23);
complex z2 = 23;
Atit z1 cit si z2 vor fi initializate apelind complex(23, 0).
Un constructor este o prescriptie pentru a crea o valoare a unui tip dat. Cind se asteapta o valoare de un tip si cind o astfel de valoare poate fi creata printr-un constructor dind va- loarea de asignat, se poate utiliza constructorul. De exemplu, clasa complex ar putea fi declarata astfel:
class complex
friend complex operator+(complex, complex);
friend complex operator*(complex, complex);
};
iar operatiile care implica variabilele complexe si constantele intregi vor fi legale. O constanta intreaga va fi interpretata ca un complex cu partea imaginara zero. De exemplu, a=b*2 inseamna:
a = operator*(b, complex(double(2), double(0)))
O conversie definita de utilizator se aplica implicit numai daca ea este unica (&6.3.3).
Un obiect construit prin utilizarea implicita sau explicita a unui constructor este automatic si va fi distrus la prima ocazie; de obicei imediat dupa instructiunea care l-a creat.
6.3.2 Operatori de conversie
Utilizarea unui constructor care sa specifice conversia de tip este convenabil, dar are implicatii care pot fi nedorite:
[1] Nu pot fi conversii implicite de la un tip definit de utilizator spre un tip de baza (intrucit tipurile de baza nu sint clase);
[2] Nu este posibil sa se specifice o conversie de la un tip nou la unul vechi fara a modifica declaratia pentru cel vechi.
[3] Nu este posibil sa avem un constructor cu un singur argument fara a avea de asemenea o conversie.
Ultima restrictie se pare ca nu este o problema serioasa si primele doua probleme pot fi acoperite definind un operator de conversie pentru tipul sursa. O functie membru X::operatorT(), unde T este un nume de tip, defineste o conversie de la X la T. De exemplu, se poate defini un tip tiny care are valori in dome- niul 0..63, dar care se poate utiliza combinat cu intregi in operatiile aritmetice:
class tiny
public:
tiny(int i)
tiny(tiny& t)
int operator=(tiny& t)
int operator=(int i)
operator int()
};
Domeniul este verificat ori de cite ori este initializat un tiny printr-un int si ori de cite ori un int este asignat la un tiny. Un tiny poate fi asignat la un altul fara a verifica domeniul. Pentru a permite operatiile uzuale cu intregi asupra variabilelor tiny, tiny::operator int(), defineste conversii implicite de la tiny spre int. Ori de cite ori apare un tiny unde este necesar un int se utilizeaza int-ul potrivit. De exemplu:
void main(void)
Un vector de tip tiny pare sa fie mai util intrucit el de asemenea salveaza spatiu; operatorul de indexare [] poate fi folosit sa faca, ca un astfel de tip sa fie util.
O alta utilizare a operatorilor de conversie definiti de utilizator sint tipurile ce furnizeaza reprezentari nestandard de numere (aritmetica in baza 100, aritmetica in virgula fixa, reprezentare BCD, etc.);acestea de obicei vor implica redefinirea
operatorilor + si *.
Functiile de conversie par sa fie mai utile mai ales pentru tratarea structurilor de date cind citirea este triviala (implementate printr-un operator de conversie), in timp ce atribuirea si initializarea sint mai putin triviale.
Tipurile istream si ostream sint legate de o functie de conversie care sa faca posibile instructiuni de forma:
while(cin >> x)
cout << x;
Operatia de intrare de mai sus returneaza un istream&. Aceasta valoare se converteste implicit spre o valoare care indica starea lui cin si apoi aceasta valoare poate fi testata de while (&8.4.2). Totusi, nu este o idee buna sa se defineasca o conversie implicita de la un tip la altul astfel incit sa se piarda informatie prin conversie.
6.3.3 Ambiguitati
O asignare (sau initializare) la un obiect al unei clase X este legala daca, sau valoarea care se asigneaza este un X sau exista o conversie unica a valorii asignate spre tipul X.
Intr-un astfel de caz, o valoare a tipului cerut poate fi construita prin utilizarea repetata a constructorilor sau a operatorilor de conversie.Aceasta trebuie sa fie tratata printr-o utilizare explicita; numai un nivel de conversie implicita definita de utilizator este legal! In anumite cazuri, o valoare a tipului cerut poate fi construita in mai mult decit un mod. Astfel de cazuri sint ilegale. De exemplu:
class x;
class y;
class z;
overload f;
x f(x);
y f(y);
z g(z);
f(1); //ilegal: este ambiguu f(x(1)) sau f(y(1))
f(x(1));
f(y(1));
g("asdf"); //ilegal: g(z(x("asdf")))
g(z("asdf"));
Conversiile definite de utilizator sint considerate numai daca un apel nu se rezolva fara ele. De exemplu:
class x;
overload h(double), h(x);
h(1);
Apelul ar putea fi interpretat ca h(double(1)) sau h(x(1)) si va apare ilegal potrivit regulii de unicitate. Cu toate acestea, prima interpretare utilizeaza numai o conversie standard si va fi aleasa conform regulii prezentate in &4.6.7.
Regulile pentru conversie nu sint nici cel mai simplu de implementat si nici cel mai simplu de documentat. Sa consideram cerinta ca o conversie trebuie sa fie unica pentru a fi legala. O conceptie mai simpla ar admite ca,compilatorul sa utilizeze orice conversie pe care el o poate gasi; astfel nu ar fi necesar sa consideram toate conversiile posibile inainte de a declara o expresie legala. Din nefericire, aceasta ar insemna ca sensul unui program depinde de conversiile care au fost gasite. De fapt, sensul unui program ar fi intr-un anumit mod dependent de ordinea declararii conversiilor. Intrucit acestea adesea vor rezida in fisiere sursa diferite (scrise de diferiti programatori), sensul unui program ar depinde de ordinea in care partile lui s-ar interclasa. Alternativ, conversiile implicite ar fi nepermise. Nimic nu ar putea fi mai simplu, dar aceasta regula conduce sau la o utilizare neeleganta a interfetelor sau la o explozie a functiilor supraincarcate asa cum se vede in clasa complex din sectiunea precedenta.
O conceptie mai generala ar fi luarea in considerare a intregii informatii de tip disponibile si considerarea tuturor conversiilor posibile. De exemplu, utilizind declaratiile precedente, aa=f(1) ar putea fi tratata din cauza ca tipul lui aa determina o interpretare unica. Daca aa este un x, f(x(1)) este singurul care produce pe x necesar in asignari; daca aa este un y, va fi folosit in schimb f(y(1)). Cea mai generala conceptie ar acoperi de asemenea pe g("asdf") deoarece g(z(x("asdf"))) este o interpretare unica. Problema cu aceasta conceptie este ca ea cere o analiza extensiva a unei expresii complete pentru a determina interpretarea fiecarui operator si apel de functie. Aceasta conduce spre o compilare inceata si de asemenea spre interpretari si mesaje de eroare surprinzatoare deoarece compilatorul consi- dera conversiile definite in biblioteci, etc. Cu aceasta conceptie compilatorul tine seama de mai multa informatie decit se asteapta programatorul ca sa cunoasca.
6.4 Constante
Nu este posibil sa se defineasca constante de tip clasa in sensul ca 1.2 si 12e3 sint constante de tip double. Totusi constantele de tip predefinit pot fi utilizate daca in schimb, functiile membru ale unei clase se utilizeaza ca sa furnizeze o interpretare pentru ele. Constructorii care au un singur argument furnizeaza un mecanism general pentru acest lucru. Cind constructorii sint simpli si se substituie inline, este cit se poate de rezonabil sa interpretam apelurile constructorului ca si constante. De exemplu, dindu-se declaratia de clasa complex in <complex.h>, expresia zz1*3+zz2*complex(1,2) va apela doua functii si nu cinci. Cele doua operatii * vor apela functii, dar operatia + si constructorul apelat pentru a crea complex(3) si complex(1,2) vor fi expandate inline.
6.5 Obiecte mari
Pentru orice utilizare a unui operator binar complex declarat in prealabil, se transfera o copie a fiecarui operand la functia care implementeaza operatorul. Pentru copierea a doua double acest lucru este acceptabil. Din nefericire, nu toate clasele au o reprezentare convenabil de mica. Pentru a elimina copierea excesiva, se pot declara functii care sa aiba ca argumente referinte. De exemplu:
class matrix;
Referintele permit utilizarea expresiilor care implica operatori aritmetici uzuali pentru obiecte mari fara a face copieri excesive. Pointerii nu pot fi utilizati deoarece nu este posibil sa se redefineasca sensul unui operator cind el se aplica la un pointer. Operatorul + ar putea fi definit astfel:
matrix operator+(matrix& arg1, matrix& arg2)
Acest operator+() are acces la operanzii lui + prin referinte, dar returneaza o valoare obiect. Returnarea unei referinte pare sa fie mai eficienta:
class matrix;
Aceasta este legal, dar provoaca probleme de alocare a memoriei. Intrucit o referinta la rezultat va iesi in afara functiei ca referinta la valoarea returnata, ea nu poate fi variabila automatica. Intrucit un operator este adesea utilizat mai mult decit o data intr-o expresie rezultatul el nu poate fi o variabila locala statica. Ea va fi alocata de obicei in memoria libera. Copierea valorii returnate este adesea mai ieftina (in executie de timp, spatiu de cod si spatiu de data ) si mai simplu de programat.
6.6 Asignare si Initializare
Sa consideram o clasa sir foarte simpla:
struct string
~string()
};
Un sir este o data structurata care consta dintr-un pointer spre un vector de caractere si din dimensiunea acelui vector. Vectorul este creat printr-un constructor si sters printr-un destructor. Cu toate acestea, asa cum se arata in &5.10 aceasta poate sa creeze probleme. De exemplu:
void f()
va aloca doi vectori de caractere, dar asignarea s1=s2 va distruge pointerul spre unul din ei si va duplica pe celalalt. Destructorul va fi apelat pentru s1 si s2 la iesirea din f() si atunci va sterge acelasi vector de doua ori cu rezultate dezastruoase. Solutia la aceasta problema este de a defini asignarea de obiecte in mod corespunzator.
struct string
~string()
void operator=(string&);
};
void string::operator=(string& a)
Aceasta definitie a lui string va asigura ca exemplul precedent sa functioneze asa cum s-a intentionat. Cu toate acestea, o mica modificare a lui f() va face ca problema sa reapara intr-o forma diferita.
void f()
Acum numai un sir este construit, iar doua sint distruse. Un operator de asignare definit de utilizator nu poate fi aplicat la un obiect neinitializat. O privire rapida la string::operator=() arata de ce acesta este nerezonabil: pointerul p ar contine o valoare aleatoare nedefinita. Un operator de atribuire adesea se bazeaza pe faptul ca argumentele lui sint initializate. Pentru o initializare ca cea precedenta, aceasta prin definitie nu este asa. In consecinta, trebuie sa se defineasca o functie care sa se ocupe cu initializarea:
struct string
~string()
void operator=(string&);
string(string&);
};
void string::string(string& a)
Pentru un tip X, constructorul X(X&) are grija de initializare pentru un obiect de acelasi tip cu X. Nu se poate suprautiliza caci asignarea si initializarea sint operatii diferite. Aceast lucru este important mai ales atunci cind se declara un destructor. Daca o clasa X are un destructor care realizeaza o sarcina netriviala, cum ar fi dealocare in memoria libera, este foarte probabil ca el necesita complementul complet al functiilor pentru eliminarea completa a copierii pe biti a obiectelor:
class X;
Exista inca doua cazuri cind se copiaza un obiect: ca un argument de functie si ca o valoare returnata de functie. Cind un argument este pasat, o variabila neinitializata numita argument formal se initializeaza. Semantica este identica cu cea a altor initializari. Acelasi lucru este valabil pentru functii cu return, desi acestea sint mai putin evidente. In ambele cazuri, X(X&) va fi aplicat daca este definit:
string g(string arg)
main()
Evident, valoarea lui s se cade sa fie "asdf" dupa apelul lui g(). Luarea unei copii a valorii lui s in argumentul arg nu este dificil; se face un apel a lui string(string&). Luarea unei copii a acestei valori ca iesire a lui g() face un alt apel la string(string&); de data aceasta, variabila initializata este una temporara, care apoi este atribuita lui s. Aceasta variabila temporara este desigur distrusa folosind cit de repede posibil string::~string().
6.7 Indexare
O functie operator[] poate fi utilizata pentru a da indici- lor un inteles pentru obiectele unei clase. Argumentul al doilea (indicele) al unei functii operator[] poate fi de orice tip. Aceasta face posibil sa se defineasca tablouri asociative, etc.. Ca un exemplu, sa recodificam exemplul din &2.3.10 in care un tablou asociativ se foloseste pentru a scrie un program mic pentru calculul numarului de aparitii al cuvintelor dintr-un fisier. Aici se defineste un tip de tablou asociativ:
struct pair;
class assoc;
Un assoc pastreaza un vector de perechi de dimensiune maxima. Indexul primului element al vectorului neutilizat este pastrat in free. Constructorul arata astfel:
assoc::assoc(int s)
Implementarea utilizeaza aceeasi metoda ineficienta ca si cea utilizata in &2.3.10. Cu toate acestea, un assoc poate creste usor cind se produce depasire:
#include <string.h>
int& assoc::operator[](char* p)
/* mentine un set de perechi "pair"; cautarea sirului spre care pointeaza p, returneaza o referinta spre partea intreaga a lui "pair" si se construieste o pereche "pair" noua daca "p" nu a fost gasit */
pp = &vec[free++];
pp->name = new char[strlen(p)+1];
strcpy(pp->name, p);
pp->val = 0; //valoare initiala: 0
return pp->val;
}
Intrucit reprezentarea unui assoc este ascunsa, noi avem nevoie de o cale de al afisa. Sectiunea urmatoare va arata cum poate fi definit un iterator propriu. Aici noi vom utiliza o functie simpla de imprimare:
void assoc::print_all()
In final putem scrie programul principal:
main() //numara aparitiile fiecarui cuvint de la intrare
6.8 Apelul unei functii
Apelul unei functii, adica notatia expresie(lista_de_expr.), poate fi interpretat ca o operatie binara iar operatorul de apel () poate fi incarcat intr-un anumit fel ca si ceilalti operatori. Lista argument pentru o functie operator() se evalueaza si se verifica potrivit regulilor de pasare al argumentelor obisnuite. Supraincarcarea apelului de functie pare sa fie utila in primul rind pentru a defini tipurile cu numai o singura operatie.
Noi nu am definit un iterator pentru tabloul asociativ de tip assoc. Aceasta s-ar putea face definind o clasa assoc_iterator cu sarcina de a prezenta elementele dintr-un assoc intr-o anumita ordine. Iteratorul necesita acces la datele memorate intr-un assoc si de aceea este facut un friend:
class assoc;
Iteratorul poate fi definit astfel:
class assoc_iterator
pair* operator()()
};
Un assoc_iterator trebuie sa fie initializat pentru un tablou assoc si va returna un pointer spre o pereche noua pair a acelui tablou de fiecare data cind este activat utilizind operatorul (). Cind se ajunge la sfirsitul tabloului se returneaza 0:
main() //numara aparitiile fiecarui cuvint de la intrare
Un tip iterator de acest fel are avantajul fata de un set de functii care fac acelasi lucru: el are datele private propri pentru a tine seama de iteratie. Este de asemenea important ca multe iteratii de un astfel de tip sa poata fi activate simultan.
Evident, aceasta utilizare a obiectelor pentru a reprezenta iteratii nu are nimic de a face cu supraincarcarea operatorului. La multi le plac iteratorii cu operatii de forma first(), next() si last().
6.9 O clasa sir
Iata o versiune mai realista a clasei sir. Ea calculeaza referintele la un sir pentru a minimiza copierea si utilizeaza sirurile de caractere standard din C++ ca si constante.
#include <iostream.h>
#include <string.h>
#include <process.h>
class string;
srep* p;
public:
string(char*); //string x = "abc";
string(); //string x;
string(string&); //string x = string...
string& operator=(char*);
string& operator=(string&);
~string();
char& operator[](int i);
friend ostream& operator<<(ostream&, string&);
friend istream& operator<<(istream&, string&);
friend int operator==(string& x, char* s)
friend int operator==(string& x, string& y)
friend int operator!=(string& x, char* s)
friend int operator!=(string& x, string& y)
};
Constructorii si destructorii sint ca de obicei triviali:
string::string()
string::string(char* s)
string::string(string& x)
string::~string()
}
De obicei, operatorii de asignare sint similari cu constructorii. Ei trebuie sa stearga primul operand sting al lor:
string& string::operator=(char* s)
else
if(p->n == 1)
delete p->s;
p->s = new char[strlen(s)+1];
strcpy(p->s, s);
p->n = 1;
return *this;
}
Este recomandabil sa ne asiguram ca asignarea unui obiect la el insusi lucreaza corect:
string& string::operator=(string& x)
p = x.p;
return *this;
}
Operatorul de iesire este pus cu intentia de a demonstra utilizarea numaratorului de referinte. El face ecou pe fiecare sir de intrare (utilizind operatorul <<, definit mai jos):
ostream& operator<<(ostream& s, string& x)
Operatorul de intrare utilizeaza functia de intrare standard a sirurilor de caractere (&8.4.1):
istream& operator>>(istream& s, string& x)
Operatorul de indexare este furnizat pentru acces la caractere individuale. Indexul este verificat:
void error(char* p)
char& string::operator[](int i)
Programul principal pur si simplu exerseaza operatorii string. El continua sa faca acest lucru pina cind este recunoscut sirul, executa string pentru a salva cuvinte in el si se opreste cind gaseste sfirsitul de fisier. Apoi imprima toate sirurile in ordine inversa.
main()
cout << "here we go back again\n";
for(int i=n-1; 0<=i; i--)
cout << x[i];
}
6.10 Prieteni si Membri
In final, este posibil sa discutam cind sa utilizam membri si cind sa utilizam prieteni pentru a avea acces la partea privata a unui tip definit de utilizator. Anumite operatii trebuie sa fie membri: constructori, destructori si functii virtuale (vezi capitolul urmator).
class X;
La prima vedere nu exista nici un motiv de a alege un friend f(X&) in locul unui membru X::m() (sau invers) pentru a implementa o operatie asupra unui obiect al clasei X. Cu toate acestea, membrul X::m() poate fi invocat numai pentru un "obiect real", in timp ce friend f(X&) ar putea fi apelat pentru un obiect creat printr-o conversie implicita de tip. De exemplu:
void g()
O operatie care modifica starea unui obiect clasa ar trebui de aceea sa fie un membru si nu un prieten. Operatorii care cer operanzi lvalue pentru tipurile fundamentale (=, *=, ++, etc) sint definiti mai natural ca membri pentru tipuri definite de utilizator.
Dimpotriva, daca se cere conversie implicita de tip pentru toti operanzii unei operatii, functia care o implementeaza trebuie sa fie un prieten si nu un membru. Acesta este adesea cazul pentru functii care implementeaza operatori ce nu necesita ope- ranzi lvalue cind se aplica la tipurile fundamentale (+, -, ||, etc.)
Daca nu sint definite tipuri de conversii, pare ca nu sint motive de a alege un membru in schimbul unui prieten care sa aiba un argument referinta sau invers.In anumite cazuri programatorul poate avea o preferinta pentru sintaxa unui apel. De exemplu, multa lume se pare ca prefera notatia inv(m) pentru a inversa o matrice m, in locul alternativei m.inv(). Evident, daca inv() inverseaza matricea m si pur si simplu nu returneaza o matrice noua care sa fie inversa lui m, atunci ea trebuie sa fie un membru.
Toate celelalte lucruri se considera indreptatite sa aleaga un membru: nu este posibil sa se stie daca cineva intr-o zi va defini un operator de conversie. Nu este totdeauna posibil sa se prezica daca o modificare viitoare poate cere modificari in starea obiectului implicat. Sintaxa de apel a functiei membru face mai clar utilizatorului faptul ca obiectul poate fi modificat; un argument referinta este pe departe mai putin evident. Mai mult decit atit, expresiile dintr-un membru pot fi mai scurte decit expresiile lor echivalente dintr-o functie prieten. Functia prieten trebuie sa utilizeze un argument explicit in timp ce membrul il poate utiliza pe acesta implicit. Daca nu se foloseste supraincarcarea, numele membrilor tind sa fie mai scurte decit numele prietenilor.
6.11 Goluri
Ca majoritatea caracteristicilor limbajelor de programare, supraincarcarea operatorului poate fi utilizata atit bine cit si eronat. In particular, abilitatea de a defini sensuri noi pentru operatorii vechi poate fi utilizata pentru a scrie programe care sint incomprehensibile. Sa ne imaginam de exemplu fata unui cititor al unui program in care operatorul + a fost facut sa noteze operatia de scadere.
Mecanismul prezentat aici ar trebui sa protejeze programatorul/cititorul de excesele rele de supraincarcare prevenind pro- gramatorul de schimbarea sensului operatorilor pentru tipurile de date de baza cum este int prin conservarea sintaxei expresiilor si al operatorilor de precedenta. Probabil ca este util sa utilizam intii supraincarcarea operatorilor pentru a mima utilizarea conventionala a operatorilor. Se poate utiliza notatia de apel de functie cind o astfel de utilizare conventionala a operatorilor nu este stabilita sau cind setul de operatori disponibil pentru supraincarcare in C++ nu este adecvat pentru a mima utilizarea conventionala.
6.12 Exercitii
1. (*2). Sa se defineasca un iterator pentru clasa string.
Sa se defineasca un operator de concatenare + si un operator += de "adaugare la sfirsit". Ce alte operatii a-ti dori sa aveti asupra sirurilor?
2. (*1.5). Sa se furnizeze un operator subsir pentru clasa string prin supraincarcarea lui ().
3. (*3). Sa se proiecteze clasa string asa ca operatorul subsir sa fie folosit in partea stinga a unei asignari. Intii sa se scrie o versiune in care un sir poate fi atribuit la un subsir de aceeasi lungime, apoi o versiune in care lungimile pot fi diferite.
4. (*2). Sa se proiecteze o clasa string asa ca ea sa aiba o valoare semantica pentru atribuire, transferul parametrilor, etc.; adica, cind se copiaza reprezentarea sirului si nu structura de control a datei din clasa string.
5. (*3). Sa se modifice clasa string din exemplul precedent pentru a copia siruri numai cind este necesar. Astfel, sa se pastreze o reprezentare comuna a doua siruri pina cind unul din siruri se modifica. Nu incercati sa aveti un operator de subsir care poate fi utilizat in partea stinga in acelasi timp.
6. (*4). Sa se proiecteze o clasa string cu valoarea semantica delayed copy si un operator subsir care poate fi utilizat in partea stinga.
7. (*2). In programul urmator ce conversii se utilizeaza in fiecare expresie?
struct X;
struct Y;
X operator* (X, Y);
int f(X);
X x=1;
Y y=x ;
int i=2;
main()
Sa se defineasca atit X cit si Y de tip intreg. Sa se modifice programul asa ca el sa se execute si sa imprime valorile fiecarei expresii legale.
8. (*2). Sa se defineasca o clasa INT care se comporta ca un int. Indicatie: sa se defineasca INT::operator int().
9. (*1). Sa se defineasca o clasa RINT care se comporta ca un int exceptind faptul ca singurele operatii admise sint + (unar sau binar), - (unar sau binar), *, /, %. Indicatie: sa nu se defineasca RINT::operator int().
10. (*3). Sa se defineasca o clasa LINT care se comporta ca un RINT exceptind faptul ca ea are cel putin o precizie de 64 biti.
11. (*4). Sa se defineasca o clasa care implementeaza o aritmetica de precizie arbitrara. Indicatie: va fi necesar sa se gestioneze memoria intr-un mod similar cu cel facut pentru clasa string.
12. (*2). Sa se scrie un program care sa nu fie citibil prin utilizarea operatorului de supraincarcare si a macrourilor. O idee: sa se defineasca + ca sa insemne - si viceversa pentru INT; apoi sa se utilizeze un macro pentru a defini int care sa insemne INT. Sa se redefineasca functii populare, utilizind argumente de tip referinta si citeva comentarii eronate pentru a crea o confuzie mare.
13. (*3). Sa se permute rezultatul exercitiului precedent cu un friend. Sa se indice fara a rula ce face programul cu friend. Cind veti termina acest exercitiu veti sti ce trebuie sa eliminati.
14. (*2). Sa se rescrie exemplul complex (&6.3.1), exemplul tiny (&6.3.2) si exemplul string (&6.9) fara a utiliza functiile friend. Sa se utilizeze numai functiile membru. Sa se testeze fiecare din versiunile noi. Sa se compare cu versiunile care utilizeaza functiile friend. Sa se rescrie exercitiul 5.3.
15. (*2). Sa se defineasca un tip vec4 ca un vector de 4 flotante. Sa se defineasca operatorul [] pentru vec4. Sa se defineasca operatorii +, -, *, /, =, +=, -=, *=, /= pentru combinatii de vectori de numere flotante.
16. (*3). Sa se defineasca o clasa mat4 ca un vector de 4 vec4. Sa se defineasca operatorul [] care returneaza un vec4 pentru mat4. Sa se defineasca operatiile uzuale cu matrici pentru acest tip. Sa se defineasca o functie care face o eliminare Gauss pentru mat4.
17. (*2). Sa se defineasca o clasa vector similara cu vec4, dar cu dimensiunea data ca un argument pentru constructorul vector::vector(int).
18. (*3). Sa se defineasca o clasa matrix similara cu mat4, dar cu dimensiunile date ca argumente la constructorul matrix::matrix(int, int).
|