This shows you the differences between two versions of the page.
sd-ca:laboratoare:laborator-02 [2015/03/18 12:30] daniel.ciocirlan1607 [Interviu] |
sd-ca:laboratoare:laborator-02 [2016/02/21 19:51] (current) radu.stochitoiu |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Laborator 02 - Noțiuni de C++ ====== | + | ====== Articol 02 - Noțiuni de C++ ====== |
- | Responsabili | + | ===== Obiective ===== |
- | * [[mailto:andrei.vasiliu2211@cti.pub.ro | Andrei Vasiliu]] | + | |
- | * [[mailto:daniel.ciocirlan1607@cti.pub.ro | Daniel Ciocîrlan]] | + | |
+ | În urma parcurgerii acestui articol studentul va: | ||
- | ===== Obiective ===== | + | * învăța ce înseamnă o clasă |
+ | * învăța ce înseamnă constructor / destructor | ||
+ | * afla funcționalitățile claselor / funcțiilor prietene | ||
+ | * realiza supraîncărcarea operatorilor din C++ | ||
+ | * înțelege conceptul de copy constructor | ||
+ | * înțelege conceptul de rule of three | ||
- | În urma parcurgerii acestui laborator studentul va: | ||
- | * înțelege conceptul de template | ||
- | * înțelege conceptul de referințe din C++ | ||
- | * înțelege conceptul de read-only introdus prin identificatorul const | ||
- | ===== Templates ===== | + | ==== Clase ==== |
- | Motivul principal pentru care folosim C++ în cadrul SD este datorită funcționalității oferite de template-uri. | + | Formal am făcut deja primii pași mai sus pentru a implementa o clasă în C++, utilizând keyword-ul //struct//. |
- | Acestea permit generalizarea tipurilor de date folosite în interiorul funcțiilor și claselor. | + | Totuși, ce înseamnă o clasă? Nu trebuie decât să ne gândim la ce am făcut mai sus: |
+ | *am definit un tip de date | ||
+ | *i-am adăugat atribute (am definit ce proprietăți îl caracterizează: partea reală și partea imaginară) | ||
+ | *i-am adăugat metode (am definit cum se comportă: inițializarea și conjugarea) | ||
- | Sintaxa pentru acestea este: | + | Cu această adăugare menționată, putem să ne referim la ceea ce înseamnă o **clasă**, respectiv un **obiect**. |
- | <code c++> | + | |
- | template <class identifier> declaratie; | + | |
- | template <typename identifier> declaratie; | + | |
- | </code> | + | |
- | //declaratie// poate fi fie o funcție, fie o clasă. | + | |
- | Nu există nicio diferență între keyword-ul //class// și //typename// - important este că ceea ce urmează după ele este un placeholder pentru un tip de date. | + | |
- | ==== Function Template ==== | + | Ne referim la o **clasă** ca fiind o amprentă (blueprint) sau descriere generală. |
- | În primul rând template-urile pot fi aplicate funcțiilor. | + | Un **obiect** sau o **instanță a clasei** este o variabilă concretă ce se conformează descrierii clasei. |
- | Un exemplu comun și simplu este următorul: | + | Vom numi **clasă** tipul de date definit de //struct complex// sau //class complex// și **obiect** o instanțiere (o alocare dinamică sau locală) a tipului de date. |
- | <code c++> | + | |
- | template<typename T> | + | |
- | T getMax(T a, T b) { | + | |
- | return a > b ? a : b; | + | |
- | } | + | |
- | </code> | + | |
- | Funcția poate fi apelată astfel: | + | Când discutăm despre tipul de date //complex// ne referim la clasă. |
- | <code c++> | + | Când discutăm despre variabila //number// ne referim la un obiect, o instanță a clasei. |
- | getMax<int>(2, 3); | + | |
- | getMax<double>(3.2, 4.6); | + | |
- | </code> | + | |
- | ==== Class Template ==== | + | ==== Keyword-ul "class" vs. "struct" ==== |
- | Concret, să presupunem că avem o clasă numită KeyStorage care are: | + | |
- | *o cheie (de tip int) | + | |
- | *un membru de date generic (al cărui tip de date nu îl știm la momentul scrierii clasei). | + | |
- | Vrem să putem folosi codul clasei indiferent de tipul de date al membrului. | + | Și totuși, C++ adăugă keyword-ul //class//. Care este diferența între //class// și //struct//? |
+ | Iată cum definim complet clasa de mai sus, separând antetul de implementare și de programul principal. | ||
- | Iată cum putem face acest lucru: | + | <columns 100% 100% -> |
- | <code c++ KeyStorage.h> | + | <code c++ complex.h> |
- | template<typename T> | + | class Complex { |
- | class KeyStorage { | + | double re; |
- | public: | + | double im; |
- | int key; | + | |
- | T member; | + | Complex conjugate(); |
}; | }; | ||
</code> | </code> | ||
- | În funcția main, să presupunem că vrem să folosim clasa cu membrul de tip long. | + | <newcolumn> |
- | <code c++ main.cpp> | + | |
- | #include "KeyStorage.h" | + | <code c++ complex.cc> |
+ | #include "complex.h" | ||
+ | Complex Complex::conjugate() { | ||
+ | Complex conjugate; | ||
+ | conjugate.re = this->re; | ||
+ | conjugate.im = -(this->im); | ||
+ | |||
+ | return conjugate; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | <newcolumn> | ||
+ | |||
+ | <code c++ main.cc> | ||
+ | #include <stdio.h> | ||
+ | #include "complex.h" | ||
int main() { | int main() { | ||
- | KeyStorage<long> keyElement; | + | Complex number; |
+ | number.re = 2; | ||
+ | number.im = 4; | ||
+ | |||
+ | printf("%.2lf %.2lf\n", number.re, number.im); | ||
+ | | ||
return 0; | return 0; | ||
} | } | ||
</code> | </code> | ||
+ | </columns> | ||
- | Practic, oriunde folosim tipul de date T în clasă, este înlocuit cu tipul pe care îl specificăm. | ||
- | ==== Where's the magic happening? ==== | ||
- | Sunt destul de multe lucruri de spus despre template-uri, dar ne vom concentra pe lucrurile care schimbă modul în care ați implementat până acum. | ||
- | Template-urile sunt de fapt indicii pentru compilator pentru a genera cod la rândul lui! | ||
- | Practic, voi îi spuneți compilatorului un șablon generic pe care ați vrea să-l folosiți și el trebuie să fie pregătit să îl pună la dispoziția voastră când aveți nevoie. | ||
- | Ce trebuie să rețineți din asta? Totul se întâmplă la **compile time**, nu la run time. | + | ==== Specificatori de acces ==== |
+ | Am observat mesajul de eroare în urma compilării fișierelor de mai sus. | ||
- | Compilatorul practic analizează modul în care voi folosiți clasa respectivă și generează pentru fiecare mod în care o folosiți șablonul corespunzător. | + | Astfel, **singura diferență** folosirea celor două keyword-uri este nivelul implicit de vizibilitate a metodelor și atributelor. |
- | Folosirea KeyStorage<int> și KeyStorage<float> determină compilatorul să genereze cod pentru ambele clase (înlocuind o dată T cu int și altă cu float). | + | ***private** - pentru clasele declarate cu **class** |
- | + | ***public** - pentru clasele declarate cu **struct** | |
- | ==== Guideline-uri implementare ==== | + | |
- | Pentru că totul se întâmplă la compile time, înseamnă că în momentul în care compilatorul întâlnește secvența de cod ce folosește template-uri trebuie să știe //toate// modurile în care aceasta este folosita. | + | |
- | Asta înseamnă că: | + | Membri precedați de label-ul **private** pot fi folosiți numai în interiorul clasei, în cadrul metodelor acesteia. |
- | *Trebuie să scrieți întreaga implementare în header! //sau// | + | Ei nu pot fi citiți sau modificați din afara clasei. |
- | *Scrieți descrierea clasei generice în header, în fișierul de implementare fiecare metodă declarată este de fapt o funcție cu template și la sfârșitul implementării adăugat //template class numeclasa<numetip>//; | + | |
- | Ultimul rând de fapt forțează folosirea template-ului cu un anumit tip de date și deci compilatorul generează cod corespunzător (trebuie să scrieți asta pentru toate tipurile). | + | Iată cum puteam remedia soluția: |
+ | <code c++ complex.h> | ||
+ | class Complex { | ||
+ | public: | ||
+ | double re; | ||
+ | double im; | ||
- | ==== Clasa KeyStorage ==== | + | Complex conjugate(); |
- | Iată mai jos o structură mai dezvoltată pentru clasa KeyStorage, în care cheia este setată în constructor. | + | }; |
- | . | + | </code> |
- | <code c++ KeyStorage.h> | + | |
- | template<typename T> | + | ==== Constructori și destructori ==== |
- | class KeyStorage { | + | |
+ | Studiați codul de mai jos. | ||
+ | |||
+ | <columns 100% 100% -> | ||
+ | <code c++ complex.h> | ||
+ | class Complex { | ||
public: | public: | ||
- | KeyStorage(int k); | + | // Constructor |
- | ~KeyStorage(); | + | Complex(double re, double im); |
| | ||
- | T getMember(); | + | // Destructor |
- | T setMember(T element); | + | ~Complex(); |
| | ||
+ | double getRe(); | ||
+ | double getIm(); | ||
+ | | ||
+ | Complex conjugate(); | ||
+ | |||
private: | private: | ||
- | T member; | + | double re; |
- | int key; | + | double im; |
}; | }; | ||
+ | </code> | ||
+ | <newcolumn> | ||
+ | |||
+ | <code c++ complex.cc> | ||
+ | #include "complex.h" | ||
+ | Complex::Complex(double re, double im) { | ||
+ | this->re = re; | ||
+ | this->im = im; | ||
+ | } | ||
+ | |||
+ | Complex::~Complex() { | ||
+ | } | ||
+ | |||
+ | Complex Complex::conjugate() { | ||
+ | Complex conjugat(re, -im); | ||
+ | return conjugat; | ||
+ | } | ||
+ | |||
+ | double Complex::getRe() { | ||
+ | return re; | ||
+ | } | ||
+ | |||
+ | double Complex::getIm() { | ||
+ | return im; | ||
+ | } | ||
</code> | </code> | ||
- | Implementarea completa a ei poate fi realizată: | + | </columns> |
- | *în header (în cazul template-urilor, acest mod este cel mai indicat). | + | |
- | *în fișierul de implementare .cc / .cpp (al cărui schelet parțial îl găsiți mai jos). | + | |
- | <code c++ KeyStorage.cpp> | + | <code c++ main.cc> |
- | #include "KeyStorage.h" | + | #include <stdio.h> |
+ | #include "complex.h" | ||
- | template<typename T> | + | int main() { |
- | KeyStorage<T>::KeyStorage(int k) { | + | Complex number(2, 3); |
- | //TODO | + | printf("%lf %lf\n", number.getRe(), number.getIm()); |
+ | |||
+ | return 0; | ||
} | } | ||
+ | </code> | ||
+ | </columns> | ||
- | template<typename T> | + | ==== Constructor ==== |
- | KeyStorage<T>::~KeyStorage() { | + | |
+ | Observăm două bucăți din cod în mod special: | ||
+ | <code c++> | ||
+ | Complex::Complex(double re, double im); | ||
+ | </code> | ||
+ | Linia de mai sus **nu are tip** returnat, spre deosebire de celelalte linii. | ||
+ | Acesta este **constructorul** clasei, care este apelat în momentul alocării unui obiect. | ||
+ | |||
+ | Ce operații sunt uzuale în constructor? | ||
+ | *inițializarea membrilor clasei cu valori predefinite sau date ca parametru | ||
+ | *alocarea memoriei pentru anumiți membri | ||
+ | |||
+ | A doua bucată observată este: | ||
+ | <code c++> | ||
+ | Complex numar(2, 3); | ||
+ | </code> | ||
+ | Până acum nu ați mai alocat astfel structurile. Ce se întâmplă în spate este exact ceea ce intuiți: este apelat constructorul obiectului și se execută instrucțiunile acestuia pentru variabila numar (reprezentată ca pointer prin this, direct în interiorul constructorului). | ||
+ | |||
+ | |||
+ | În constructorul definit mai sus, tot ceea ce se întâmplă este să se inițializeze membri. Pentru asta, C++ vă pune la dispoziție o sintaxă simplă: | ||
+ | <code c++> | ||
+ | Complex::Complex(double real, double imaginar) : | ||
+ | re(real), | ||
+ | im(imaginar) { | ||
} | } | ||
+ | </code> | ||
- | //TODO: restul metodelor. | + | Cei doi constructori sunt identici ca funcționalitate. |
- | // La sfarsit, cu tipurile de date pe care le veti folosi. | + | ==== Copy-constructor ==== |
- | template class KeyStorage<int>; | + | |
- | template class KeyStorage<long>; | + | |
+ | Reprezintă un tip de constructor special care se folosește când se dorește/este necesară o copie a unui obiect existent. Dacă nu este declarat, se va genera unul default de către compilator. | ||
+ | |||
+ | Poate avea unul din următoarele prototipuri | ||
+ | |||
+ | * MyClass(const MyClass& obj); | ||
+ | * MyClass(MyClass& obj); | ||
+ | |||
+ | === Când se apelează? === | ||
+ | |||
+ | 1) Apel explicit | ||
+ | |||
+ | <code c++ explicit_copy_constructor_call.cpp> | ||
+ | MyClass m; | ||
+ | MyClass x = MyClass(m); /* apel explicit al copy-constructor-ului */ | ||
</code> | </code> | ||
- | ===== Referințe ===== | + | 2) Transfer prin valoare ca argument într-o funcție |
- | In C++ există două modalități de a lucra cu adrese de memorie: | + | <code c++ call_by_value.cpp> |
- | * pointeri (la fel ca cei din C) | + | void f(MyClass obj); |
- | * referințe. | + | ... |
+ | MyClass o; | ||
+ | f(o); /* se apelează copy-constructor */ | ||
+ | </code> | ||
- | Referinţa poate fi privită ca un pointer constant inteligent, a cărui iniţializare este forţată de către compilator (la definire) şi care este dereferenţiat automat. | + | 3) Transfer prin valoare ca return al unei funcții |
- | Semantic, referințele reprezintă aliasuri ale unor variabile existente. La crearea unei referinţe, aceasta trebuie iniţializată cu adresa unui obiect (nu cu o valoare constantă). | + | <code c++ return_by_value.cpp> |
+ | MyClass f() | ||
+ | { | ||
+ | MyClass a; | ||
+ | return a; /* se apelează copy-constructor */ | ||
+ | } | ||
+ | </code> | ||
- | Sintaxa pentru declararea unei referințe este: | + | 4) La inițializarea unei variabile declarate pe aceeași linie |
- | tip& referinta = valoare; | + | <code c++ init.cpp> |
+ | MyClass m; | ||
+ | MyClass x = m; /* se apelează copy-constructor */ | ||
+ | </code> | ||
- | Exemplu: | + | |
- | <code > | + | ==== Destructor ==== |
- | int x=1, y=2; | + | Așa cum probabil ați observat, **constructorul** este apelat în mod **explicit** de către voi. **Destructorul** însă, în cazul de mai sus, este apelat **implicit** la terminarea blocului care realizează dealocărea automată a obiectului. |
- | int& rx = x; //referinta | + | |
- | rx = 4; //modificarea variabilei prin referinta | + | Un destructor nu are parametri și se declară în interiorul clasei astfel: |
- | rx = 15; //modificarea variabilei prin referinta | + | <code c++> |
- | rx =y; //atribuirea are ca efect copierea continutului | + | ~Complex(); |
- | //din y in x si nu modificarea adresei referintei | + | |
</code> | </code> | ||
- | Spre deosebire de pointeri: | + | Dacă în constructor sau în interiorul clasei ați fi alocat memorie, cel mai probabil în destructor ați fi făcut curat și ați fi apelat free pe membrul respectiv. |
- | * referinţele sunt iniţializate la creare (pointerii se pot iniţializa oricând) | + | |
- | * referinţa este legată de un singur obiect şi această legătură nu poate fi modificată pentru un alt obiect | + | |
- | * referințele nu au operații speciale, toți operatorii aplicați asupra referințelor sunt de fapt aplicați asupra variabilei referite(de exemplu extragerea adresei unei referințe va returna adresa variabilei referite) | + | |
- | * __nu există referinţe nule__ – ele sunt întotdeauna legate de locaţii de memorie | + | |
- | Referinţele se folosesc: | + | ==== Rule of Three ==== |
- | * în listele de parametri ale funcţiilor | + | |
- | * ca valori de întoarcere ale funcţiilor | + | |
- | __Motivul__ pentru aceste tipuri de utilizări este unul destul de simplu: când se transmit parametrii funcțiilor, se copiază conținutul variabilelor transmise pe stivă, lucru destul de costisitor. Prin transmiterea de referințe, nu se mai copiază nimic, așadar intrarea sau ieșirea dintr-o funcție sunt mult mai putin costisitoare. | + | |
- | ==== Keyword const==== | + | Reprezintă un concept de ** must do** pentru C++. Astfel: |
- | În C++, există mai multe întrebuințări ale cuvântului cheie **const**: | + | <note important>Dacă programatorul și-a declarat/definit unul dintre ** constructor default**, ** operator de assignment** sau ** copy-constructor**, trebuie să îi declare/definească și pe ceilalți 2</note> |
- | * specifică un obiect a cărui valoare nu poate fi modificată | + | |
- | * specifică metodele unui obiect read-only care pot fi apelate | + | |
- | Pentru a specifica, un obiect a cărui valoare nu poate fi modificată, **const** se poate folosi în următoarele feluri: | + | Explicație: dacă funcționalitatea vreunuia dintre cei 3 se vrea mai specială decât cea oferită default, atunci mai mult ca sigur se dorește schimbarea funcționalității default și pentru ceilalți 2 rămași. |
- | * ''const tip variabila'' => specifică o variabilă constantă | + | |
- | * ''tip const& referinta_ct = variabilă;'' => specifică o referință constantă la un obiect, obiectul neputând fi modificat | + | |
- | * ''const int *p_int'' => specifică un pointer la int modificabil, dar conținutul locației de memorie către care ''p_int'' arată __nu__ se poate modifica. | + | |
- | * ''int * const p_int'' => specifică un pointer la int care nu poate fi modificat (Variabilei ''p_int'' nu i se poate asigna nici o valoare, dar conținutul locației de memorie către care ''p_int'' arată se poate modifica) | + | |
+ | <code c++ rule_of_3.cpp> | ||
+ | class Complex | ||
+ | { | ||
+ | private: | ||
+ | int re; | ||
+ | int im; | ||
+ | public: | ||
+ | Complex() | ||
+ | { | ||
+ | re = 0; | ||
+ | im = 0; | ||
+ | printf("constructor default\n"); | ||
+ | } | ||
- | Orice obiect constant poate apela doar funcții declarate constante. O funcție constantă se declară folosind sintaxa: | + | Complex(const Complex& c) |
- | <code c> | + | { |
- | void fct_nu_modifica_obiect() const; //am utilizat cuvântul cheie const | + | re = c.re; |
- | //dupa declarația funcției fct_nu_modifica_obiect | + | im = c.im; |
- | </code> | + | printf("copy contructor\n"); |
+ | } | ||
- | Această declaratie a functiei garantează faptul că obiectul pentru care va fi apelată nu se va modifica. | + | void operator=(const Complex& c) |
+ | { | ||
+ | re = c.re; | ||
+ | im = c.im; | ||
+ | printf("assignment operator\n"); | ||
+ | } | ||
+ | }; | ||
+ | </code> | ||
- | Regula de bază a apelării membrilor de tip funcție ai claselor este: | + | ===== Clase/metode prietene ===== |
- | * funcțiile ''const'' pot fi apelate pe toate obiectele | + | |
- | * funcțiile non-const pot fi apelate doar pe obiectele non-const. | + | |
- | Exemple: | + | Așa cum am văzut în primul articol, fiecare membru al clasei poate avea 3 specificatori de acces: |
+ | * public | ||
+ | * private | ||
+ | * protected | ||
+ | |||
+ | Alegerea specificatorilor se face în special în funcție de ce funcționalitate vrem să exportăm din clasa respectivă. | ||
+ | |||
+ | Dacă vrem să accesăm datele private/protejate din afara clasei, avem următoarele opțiuni: | ||
+ | * Funcții care ne întorc/setează valorile membre | ||
+ | * Funcții/Clase prietene (friend) cu clasa curentă. | ||
+ | |||
+ | O funcție prieten are următoarele proprietăți: | ||
+ | * O funcţie este considerată prietenă al unei clase, dacă în declararea clasei, este declarată funcţia respectivă precedată de specificatorul **friend** | ||
+ | * Declararea unei funcţii prieten poate fi făcută în orice parte a clasei(publică, privată sau protejată). | ||
+ | * Definiţia funcţiei prieten se face global, în afara clasei. | ||
+ | * Funcția declarată ca **friend** are acces liber la orice membru din interiorul clasei. | ||
+ | |||
+ | |||
+ | O clasă prieten are următoarele proprietăți: | ||
+ | * O clasă B este considerată prieten al unei clase A, dacă în declararea clasei A s-a întâlnit expresia: ''friend class B'' | ||
+ | * Clasa B poate accesa orice membru din clasa A, fără nici o restricție. | ||
+ | |||
+ | De asemenea, dacă clasa A este considerată prieten cu clasa B, nu înseamnă că si clasa B este considerată prieten cu clasa A. Nici tranzitivitatea nu este valabilă în relaţia de prietenie dintre clase. | ||
+ | |||
+ | Exemplu: | ||
<code cpp> | <code cpp> | ||
- | //declarație | + | class Complex{ |
- | class Complex { | + | |
private: | private: | ||
int re; | int re; | ||
int im; | int im; | ||
public: | public: | ||
- | Complex(); | + | int GetRe(); |
- | int GetRe() const; | + | int GetIm(); |
- | int GetIm() const; | + | friend double ComplexModul(Complex c); //am declarat fct ComplexModul ca prieten |
- | void SetRe(int re); | + | friend class Polinom; //Acum clasa Polinom care acces deplin la membrii **re** și **im** |
- | void SetIm(int im); | + | |
}; | }; | ||
+ | double ComplexModul(Complex c) | ||
+ | { | ||
+ | return sqrt(c.re*c.re+c.im*c.im); //are voie, intrucat e prietena | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | ===== Supraîncarcarea operatorilor ===== | ||
- | //apelare | + | Un mecanism specific C++ este supraîncarcarea operatorilor, prin care programatorul poate asocia noi semnificaţii operatorilor deja existenţi. De exemplu, dacă dorim ca două numere complexe să fie adunate, în C trebuie să scriem funcții specifice, nenaturale. În C++ putem scrie foarte ușor: |
- | Complex c1; | + | |
- | const Complex c2; | + | |
- | c1.GetRe(); //corect | + | |
- | c1.SetRe(5); //corect | + | |
- | c2.GetRe(); //corect | + | |
- | c2.SetRe(5); //incorect | + | |
+ | <code c> | ||
+ | Complex a(2,3); | ||
+ | Complex b(4,5); | ||
+ | Complex c=a+b; //operatorul + a fost supraîncarcat pentru a aduna două numere complexe | ||
</code> | </code> | ||
+ | Acest lucru este posibil, întrucât un operator este văzut ca o funcție, cu declarația: | ||
- | ==== Funcții care returnează referințe ==== | + | tip_rezultat operator#(listă_argumente); |
- | Pentru clasa Complex, definim funcţiile care asigură accesul la partea reală, respectiv imaginară a unui număr complex: | + | Așadar pentru a supraîncărca un operator pentru o anumită clasă, este necesar să declarăm funcția următoare în corpul acesteia: |
- | double getRe(){ return re; } | + | tip_rezultat operator#(listă_argumente); |
- | double getIm(){ return im; } | + | |
- | Dacă am dori modificarea părţii reale a unui număr complex printr-o atribuire de forma: | ||
- | z.getRe()=2.; | ||
- | constatăm că funcţia astfel definită nu poate apărea în partea stângă a unei atribuiri. | ||
- | Acest neajuns se remediază impunând funcţiei să returneze o referinţă la obiect, adică: | + | Există câteva restricții cu privire la supraîncarcare: |
- | double& getRe(){ return re; } | + | |
+ | * Nu pot fi supraîncărcaţi operatorii: ::, ., .*, ?:, sizeof. | ||
+ | * Setul de operatori ai limbajul C++ nu poate fi extins prin asocierea de semnificaţii noi unor caractere, care nu sunt operatori, de exemplu nu putem defini operatorul === . | ||
+ | * Prin supraîncărcarea unui operator nu i se poate modifica aritatea (astfel operatorul ! este unar şi poate fi redefinit numai ca operator unar). | ||
+ | * Asociativitatea şi precedenţa operatorului se menţin. | ||
+ | * La supraîncărcarea unui operator nu se pot specifica argumente cu valori implicite. | ||
+ | |||
+ | |||
+ | |||
+ | ==== Operatori supraîncărcaţi ca funcţii prieten ==== | ||
+ | |||
+ | |||
+ | |||
+ | Un operator binar va fi reprezentat printr-o funcţie nemembră cu două argumente, iar un operator unar, printr-o funcţie nemembră cu un singur argument. | ||
+ | |||
+ | Utilizarea unui operator binar sub forma **a#b** este interpretată ca **operator#(a,b)**. | ||
+ | |||
+ | Argumentele sunt clase sau referinţe constante la clase. | ||
+ | |||
+ | |||
+ | ==== Supraîncărcarea operatorilor << şi >> ==== | ||
+ | |||
+ | În C++, orice dispozitiv de I/O este văzut drept un stream, așadar operațiile de I/O sunt operații cu stream-uri, care se definesc în felul următor: | ||
+ | * **Citire**: se execută cu operatorul de extracție >>, membru al clasei istream | ||
+ | * **Scriere**: se execută cu operatorul de inserție <<, membru al clasei ostream | ||
+ | |||
+ | Acești operatori pot fi supraîncărcați pentru o clasă pentru a defini operații de I/O direct pe obiectele clasei. | ||
+ | |||
+ | Supraîncărcarea se poate efectua folosind funcții friend utilizând următoarea sintaxă: | ||
+ | |||
+ | <code c> | ||
+ | istream& operator>> (istream& f, clasa & ob); //Acum pot scrie in >> ob | ||
+ | ostream& operator<< (ostream& f, const clasa & ob); //Acum pot scrie out << ob | ||
+ | </code> | ||
+ | |||
+ | <note important>Operatorii >> și << întorc fluxul original, pentru a scrie înlănțuiri de tipul ''f>>ob1>>ob2''. </note> | ||
+ | |||
+ | Funcţiile operator pentru supraîncărcarea operatorilor de I/O le vom declara ca funcţii prieten al clasei care interacţionează cu fluxul. | ||
+ | |||
+ | <code c++ Complex.h> | ||
+ | #include <iostream> | ||
+ | |||
+ | class Complex | ||
+ | { | ||
+ | public: | ||
+ | double re; | ||
+ | double im; | ||
+ | |||
+ | Complex(double real=0, double imag=0): re(real), im(imag) {}; | ||
+ | |||
+ | //supraîncărcarea operatorilor +, - ca functii de tip "friend" | ||
+ | friend Complex operator+(const Complex& s, const Complex& d); | ||
+ | friend Complex operator-(const Complex& s, const Complex& d); | ||
+ | |||
+ | //funcţii operator pentru supraîncărcarea operatorilor de intrare/ieşire | ||
+ | //declarate ca funcţii de tip "friend" | ||
+ | friend std::ostream& operator<< (std::ostream& out, const Complex& z); | ||
+ | friend std::istream& operator>> (std::istream& is, Complex& z); | ||
+ | }; | ||
+ | </code> | ||
+ | |||
+ | <code c++ Complex.cpp> | ||
+ | #include "complex.h" | ||
+ | |||
+ | Complex operator+(const Complex& s, const Complex& d){ | ||
+ | return Complex(s.re+d.re,s.im+d.im); | ||
+ | } | ||
+ | |||
+ | Complex operator-(const Complex& s, const Complex& d){ | ||
+ | return Complex(s.re-d.re,s.im-d.im); | ||
+ | } | ||
+ | |||
+ | std::ostream& operator<<(std::ostream& out, const Complex& z){ | ||
+ | out << "(" << z.re << "," << z.im << ")"<< std::endl; | ||
+ | return out; | ||
+ | } | ||
+ | |||
+ | std::istream& operator>>(std::istream& is, Complex& z){ | ||
+ | is >> z.re >> z.im; | ||
+ | return is; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | |||
+ | <code c++ main.cpp> | ||
+ | #include "complex.h" | ||
+ | |||
+ | int main() { | ||
+ | Complex a(1,1), b(-1,2); | ||
+ | std::cout << "A: " << a << "B: " << b; | ||
+ | std::cout << "A+B: " << (a+b); | ||
+ | std::cin >> b; | ||
+ | std::cout << "B: " << b; | ||
+ | a=b; | ||
+ | std::cout << "A: " << a << "B: " << b; | ||
+ | }</code> | ||
+ | |||
+ | ==== Operatori supraîncărcaţi ca funcţii membre ==== | ||
+ | |||
+ | Funcţiilor membru li se transmite un argument implicit **this** (adresa obiectului curent), motiv pentru care un operator binar poate fi implementat printr-o funcţie membru nestatică cu un singur argument. | ||
+ | |||
+ | Operatorii sunt interpretați în modul următor: | ||
+ | * Operatorul binar **a#b** este interpretat ca **a.operator#(b)** | ||
+ | * Operatorul unar prefixat **#a** este interpretat ca **a.operator#()** | ||
+ | * Operatorul unar postfixat **a#** este interpretat ca **a.operator#(int)** | ||
+ | |||
+ | <code c++ Complex.h> | ||
+ | #include <iostream> | ||
+ | |||
+ | class Complex | ||
+ | { | ||
+ | public: | ||
+ | double re; | ||
+ | double im; | ||
+ | |||
+ | Complex(double real, double imag): re(real), im(imag) {}; | ||
+ | |||
+ | //operatori supraîncărcaţi ca funcţii membre | ||
+ | Complex operator+(const Complex& d); | ||
+ | Complex operator-(const Complex& d); | ||
+ | Complex& operator+=(const Complex& d); | ||
+ | |||
+ | friend std::ostream& operator<< (std::ostream& out, const Complex& z); | ||
+ | friend std::istream& operator>> (std::istream& is, Complex& z); | ||
+ | }; | ||
+ | </code> | ||
+ | |||
+ | <code c++ Complex.cpp> | ||
+ | #include "complex.h" | ||
+ | |||
+ | Complex Complex::operator+(const Complex& d){ | ||
+ | return Complex(re+d.re, im+d.im); | ||
+ | } | ||
+ | |||
+ | Complex Complex::operator-(const Complex& d){ | ||
+ | return Complex(re-d.re, im-d.im); | ||
+ | } | ||
+ | |||
+ | Complex& Complex::operator+=(const Complex& d){ | ||
+ | re+=d.re; | ||
+ | im+=d.im; | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | std::ostream& operator<<(std::ostream& out, const Complex& z){ | ||
+ | out << "(" << z.re << "," << z.im << ")"<< std::endl; | ||
+ | return out; | ||
+ | } | ||
+ | |||
+ | std::istream& operator>>(std::istream& is, Complex& z){ | ||
+ | is >> z.re >> z.im; | ||
+ | return is; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | ==== Supraîncărcarea operatorului de atribuire ==== | ||
+ | |||
+ | Așa cum am amintit mai sus, majoritatea operatorilor pot fi supraîncărcați. O atenție importantă trebuie acordată operatorului de atribuire, dacă nu este supraîncărcat, realizează o copiere membru cu membru. | ||
+ | |||
+ | Pentru obiectele care nu conţin date alocate dinamic la iniţializare, atribuirea prin copiere membru cu membru funcţionează corect, motiv pentru care nu se supraîncarcă operatorul de atribuire. | ||
+ | |||
+ | <note important>Pentru clasele ce conţin date alocate dinamic, copierea membru cu membru, executată în mod implicit la atribuire conduce la copierea pointerilor la datele alocate dinamic, în loc de a copia datele.</note> | ||
+ | |||
+ | Operatorul de atribuire poate fi redefinit numai ca funcţie membră, el fiind legat de obiectul din stânga operatorului =, motiv pentru care va întoarce o referinţă la obiect. | ||
+ | |||
+ | <code c++ String.h> | ||
+ | class String{ | ||
+ | char* s; | ||
+ | int n; // lungimea sirului | ||
+ | |||
+ | public: | ||
+ | String(); | ||
+ | String(const char* p); | ||
+ | String(const String& r); | ||
+ | ~String(); | ||
+ | String& operator=(const String& d); | ||
+ | String& operator=(const char* p); | ||
+ | }; | ||
+ | |||
+ | |||
+ | </code> | ||
+ | |||
+ | <code c++ String.cpp> | ||
+ | #include "String.h" | ||
+ | #include <string.h> | ||
+ | |||
+ | String& String::operator=(const String& d){ | ||
+ | if(this != &d){ //evitare autoatribuire | ||
+ | if(s) //curatire | ||
+ | delete [] s; | ||
+ | n=d.n; //copiere | ||
+ | s=new char[n+1]; | ||
+ | strcpy(s, d.s); | ||
+ | } | ||
+ | return *this; //intoarce referinta la obiectul modificat | ||
+ | } | ||
+ | |||
+ | String& String::operator=(const char* p){ | ||
+ | if(s) | ||
+ | delete [] s; | ||
+ | n=strlen(p); | ||
+ | s=new char[n+1]; | ||
+ | strcpy(s, p); | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | </code> | ||
- | Codul de mai sus returnează o referință către membrul ''re'' al obiectului ''Complex z'', așadar orice atribuire efectuată asupra acestui câmp va fi vizibilă și în obiect. | ||
- | ===== Exerciții ===== | ||
<hidden> | <hidden> | ||
Line 248: | Line 574: | ||
*[**2p**] Setati tipul de return a metodelor de tip getter astfel incat sa puteti modifica valorea intoarsa. | *[**2p**] Setati tipul de return a metodelor de tip getter astfel incat sa puteti modifica valorea intoarsa. | ||
*[**1p**] Arătați funcționalitatea prin adăugarea de cod în fișierul main.cpp. Rezolvați, dacă e cazul, toate erorile/leak-urile depistate de Valgrind. | *[**1p**] Arătați funcționalitatea prin adăugarea de cod în fișierul main.cpp. Rezolvați, dacă e cazul, toate erorile/leak-urile depistate de Valgrind. | ||
- | </hidden> | + | |
+ | *[**5p**] Clasa Complex - clasă ce implementează conceptul de număr complex | ||
+ | *[**2p**] Implementați și folosiți utilizând template-uri clasa Complex, adăugând constructor și destructor. | ||
+ | *[**2p**] Adăugați clasei Complex metode pentru adunare, scădere și înmulțire cu un alt număr complex. | ||
+ | *[**1p**] Arătați funcționalitatea prin adăugarea de cod în fișierul main.cpp. Rezolvați, dacă e cazul, toate erorile/leak-urile depistate de Valgrind. | ||
*[**3p**] Simple use of const | *[**3p**] Simple use of const | ||
Line 255: | Line 585: | ||
*[**0.5p**] Creati un pointer constant la o variabila de tip intreg constanta. | *[**0.5p**] Creati un pointer constant la o variabila de tip intreg constanta. | ||
*[**1.5p**] Initializati pointerul si variabila referita pentru fiecare caz. Explicati si rezolvati erorile de compilare. | *[**1.5p**] Initializati pointerul si variabila referita pentru fiecare caz. Explicati si rezolvati erorile de compilare. | ||
- | <hidden> | ||
*[**5p**] Clasa MappingEntry - conține 2 membri de tipuri potențial diferite și realizează, din punct de vedere conceptual, asocierea între două valori (una se numește //cheie//, iar cealaltă //valoare//). | *[**5p**] Clasa MappingEntry - conține 2 membri de tipuri potențial diferite și realizează, din punct de vedere conceptual, asocierea între două valori (una se numește //cheie//, iar cealaltă //valoare//). | ||
Line 271: | Line 600: | ||
*[**1p**] Arătați funcționalitatea prin adăugarea de cod în fișierul main.cpp. Rezolvați, dacă e cazul, toate erorile/leak-urile depistate de Valgrind. | *[**1p**] Arătați funcționalitatea prin adăugarea de cod în fișierul main.cpp. Rezolvați, dacă e cazul, toate erorile/leak-urile depistate de Valgrind. | ||
- | *[**5p**] Clasa Complex - clasă ce implementează conceptul de număr complex | + | |
- | *[**2p**] Implementați și folosiți utilizând template-uri clasa Complex, adăugând constructor și destructor. | + | |
- | *[**2p**] Adăugați clasei Complex metode pentru adunare, scădere și înmulțire cu un alt număr complex. | + | |
- | *[**1p**] Arătați funcționalitatea prin adăugarea de cod în fișierul main.cpp. Rezolvați, dacă e cazul, toate erorile/leak-urile depistate de Valgrind. | + | |
- | </hidden> | + | |
*[**5p**] Clasa Punct2D - clasă ce implementează conceptul de punct în plan | *[**5p**] Clasa Punct2D - clasă ce implementează conceptul de punct în plan | ||
Line 292: | Line 618: | ||
*[**1p**] Arătați funcționalitatea prin adăugarea de cod în fișierul main.cpp. Rezolvați, dacă e cazul, toate erorile/leak-urile depistate de Valgrind. | *[**1p**] Arătați funcționalitatea prin adăugarea de cod în fișierul main.cpp. Rezolvați, dacă e cazul, toate erorile/leak-urile depistate de Valgrind. | ||
- | <hidden> | + | |
===== Interviu ===== | ===== Interviu ===== | ||
- | Această secțiune nu este punctată și încearcă să vă facă o oarecare idee a tipurilor de întrebări pe care le puteți întâlni la un job interview (internship, part-time, full-time, etc.) din materia prezentată în cadrul laboratorului. | + | Această secțiune nu este punctată și încearcă să vă facă o oarecare idee a tipurilor de întrebări pe care le puteți întâlni la un job interview (internship, part-time, full-time, etc.) din materia prezentată în cadrul articolului. |
* Care este diferența între struct și class în C++? | * Care este diferența între struct și class în C++? |