SDA Lucrarea 2
În acest laborator se introduc noțiuni noi de Programare Orientată pe Obiecte: clasa, obiectul, metoda, constructorul, destructorul, namespace și template.
Structurile in C
Ne amintim că în C, o structură e definită printr-un nume, care reprezintă numele tipului de date, precum și un număr de câmpuri, adică una sau mai multe varibile, definite prin tip și nume, ce reprezintă datele componente ale structurii:
struct Person {
char name[128];
uint8_t age;
char cnp[14];
char address[256];
};
Structura Person
este un tip de dată, prin urmare putem declara și utiliza variabile de tipul acestei structuri:
int main() {
struct Person somePerson;
return 0;
}
În codul de mai sus, s-a declarat o varibilă de tip struct Person
, cu numele somePerson
. Prin definirea unei variabile de tipul structurii, de fapt s-au definit în mod automat toate variabilele membre ale acesteia: name
, age
etc. Având o variabilă de tipul structurii, membrii acesteia se pot accesa cu operatorul .
:
int main() {
struct Person somePerson;
somePerson.age = 20;
return 0;
}
Dacă avem un pointer la o structură, putem accesa membrii acesteia în două moduri:
- Prin dereferențierea pointerului (adică obținerea valorii de la adresa respectivă), folosind operatorul
*
. - Prin utilizarea operatorului
->
care este analog operatorului.
, dar se folosește cu variabile de tip pointer-la-structură, în loc de variabile de tip structură:
int main() {
struct Person somePerson;
somePerson.age = 20;
struct Person * somePersonPointer = &somePerson;
/* Varianta 1 */
strcpy((*somePersonPointer).name, "Andrei");
/* Varianta 2 */
strcpy(somePersonPointer->name, "Andrei");
return 0;
}
Noțiuni introductive de Programare Orientată pe Obiecte (POO)
Ideea de bază de la care a pornit paradigma POO este faptul că o structură poate modela orice obiect din jurul nostru, pentru că orice obiect are proprietăți, care pot fi stocate în variabile membre ale structurii. Aceste obiecte pot fi atât fizice (o minge, un șurub, un tranzistor, o sticlă cu apă), cât și obiecte abstracte (un număr natural, un număr complex, o funcție matematică sau un sortator de valori în virgulă mobilă). Fiecare din aceste obiecte au un set de proprietăți care pot fi identificate. Spre exemplu, o minge are o anumită culoare, o anumită formă, un anumit volum, o anumită masă și un anumit proprietar. Sigur, există și alte proprietați pe care o minge le poate avea (de exemplu materialul de fabricație sau gazul cu care este umplută), dar în general când analizăm un obiect, ne gândim exclusiv la proprietățile relevante pentru aplicația noastră (de exemplu dacă avem o bază de date cu angajați, probabil nu ne înteresează culoarea părului fiecărui angajat, dar ne interesează vârsta și vechimea acestuia). Putem observa că o parte din proprietăți sunt constante pe toată durata de viață a obiectului (cum ar fi culoarea, masa și forma), acestea numindu-se imutabile, iar altele se pot schimba pe perioada de viață a obiectului (de exemplu proprietarul).
Clasa și obiectul
Haideți să definim o structură care să modeleze obiecte de tip minge:
enum Color {
WHITE,
RED,
BLUE,
GREEN
};
enum Shape {
SPHERE,
OVOID
};
struct Ball {
enum Color color;
enum Shape shape;
float volume;
float mass;
Person owner;
};
Pentru a modela complet obiecte reale însă, mai lipsește ceva. Obiectele din lumea reală nu au doar proprietăți, ci și interacționează între ele. Acțiunile se modelează în programare prin funcții, acestea fiind cele care prin execuție modifică proprietățile obiectelor asupra cărora acționează. Să spunem că o minge își poate schimba proprietarul. Definim deci următoarea funcție:
void changeBallOwner(Ball * ball, Person newOwner) {
ball->owner = newOwner;
}
Se vede aici că funcția changeBallOwner
se apelează întotdeauna pentru un obiect de tip Ball. De altfel, această funcție nu are sens decât în contextul unei mingi (altfel spus, trebuie să am o minge ca să-i pot schimba proprietarul). Prin urmare, o metodă mai bună de reprezentare a obiectelor de tip Ball ar fi ca și funcția changeBallOwner
să facă parte din structură, ca și proprietățile acesteia. Astfel, C++ introduce o modificare foarte simplă structurilor din C: acestea pot acum să conțină și funcții:
enum Color {
WHITE,
RED,
BLUE,
GREEN
};
enum Shape {
SPHERE,
OVOID
};
struct Ball {
enum Color color;
enum Shape shape;
float volume;
float mass;
Person owner;
void changeOwner(Person newOwner) {
owner = newOwner;
}
};
Se observă următoarele modificări:
- am redenumit funcția din
changeBallOwner
închangeOwner
, pentru că funcția făcând parte din structura Ball, este evident pentru ce tip de obiect se apelează. - a dispărut primul argument al funcției, cel de tip Ball, deoarece această funcție se va apela acum pentru un obiect de tip Ball folosind operatorul de acces la membri, așa cum se accesează și proprietățile:
int main() {
Ball myBall;
Person me;
myBall.changeOwner(me);
return 0;
}
Supraîncărcarea
În limbajul C, era syntactic incorect să existe mai multe funcții cu același nume. În C++ s-a introdus noțiunea de supraîncărcare, adică posibilitatea de a avea mai multe funcții cu același nume, dar care pot fi diferențiate prin numărul, tipul și/sau ordinea argumentelor:
int max(int a, int b) {
return a > b ? a : b;
}
float max(float a, float b) {
return a > b ? a : b;
}
Constructorul
Constructorii sunt metode speciale ale unei clase care sunt apelați automat atunci când se creează un nou obiect. Constructorii pot fi recunoscuți după două particularități:
- Au același nume cu numele clasei.
- Nu au tip returnat (nici măcar void).
O clasă poate avea unul sau mai mulți constructori, în funcție de necesități, prin mecanismul de supraîncărcare:
enum Color {
UNKNOWN_COLOR,
WHITE,
RED,
BLUE,
GREEN
};
enum Shape {
UNKNOWN_SHAPE,
SPHERE,
OVOID
};
class Ball {
Color color;
Shape shape;
float volume;
float mass;
Person owner;
public:
Ball() {
color = UNKNOWN_COLOR;
shape = UNKNOWN_SHAPE;
volume = -1;
mass = -1;
}
Ball(Color newColor, Shape newShape, float newVolume, float newMass, Person newOwner) {
color = newColor;
shape = newShape;
volume = newVolume;
mass = newMass;
owner = newOwner;
}
void changeOwner(Person newOwner) {
owner = newOwner;
}
};
Rolul unui constructor este de a inițializa proprietățile obiectului cu valori primite ca argumente și de a aloca memoria necesară funcționării corecte a obiectului.
Se observă apariția unei noi linii în codul de mai sus: public:
. Acesta este un modificator de acces care specifică faptul că toți membrii declarați mai jos față de el în clasă pot fi accesați (cu operatorii .
sau ->
) în funcții sau metode din afara clasei. Opusul lui public
este private
. Acest modificator de acces trebuie să existe în codul de mai sus datorită diferenței dintre un struct
și o class
în C++:
- Membrii unei clase au modificatorul implcit de acces
private
- Membrii unei structuri au modificatorul implcit de acces
public
Destructorul
Destructorul este o metodă specială ce aparține unei clase, care se apelează automat când memoria alocată unui obiect este eliberată. Acest lucru se întâmplă în două situații:
- când un obiect este alocat pe stivă (prin declararea lui în interiorul unei funcții), destructorul se apelează automat când funcția blocul de instrucțiuni unde este declarat obiectul se încheie.
- când un obiect este alocat în HEAP, destructorul se apelează automat când memoria este eliberată manual de către programator.
Fiecare clasă are cel mult un destructor care poate fi identificat prin următoarele două caracteristici:
- are numele format din caracterul ~ urmat de numele clasei
- nu are tip returnat (nici măcar void).
enum Color {
UNKNOWN_COLOR,
WHITE,
RED,
BLUE,
GREEN
};
enum Shape {
UNKNOWN_SHAPE,
SPHERE,
OVOID
};
class Ball {
Color color;
Shape shape;
float volume;
float mass;
Person owner;
public:
Ball() {
color = UNKNOWN_COLOR;
shape = UNKNOWN_SHAPE;
volume = -1;
mass = -1;
}
Ball(Color newColor, Shape newShape, float newVolume, float newMass, Person newOwner) {
color = newColor;
shape = newShape;
volume = newVolume;
mass = newMass;
owner = newOwner;
}
void changeOwner(Person newOwner) {
owner = newOwner;
}
~Ball() {
}
};
Rolul unui destructor este, de cele mai multe ori, eliberarea memoriei alocate în HEAP de constructor și eliberarea resurselor angajate de acesta (fișiere/ streamuri deschise, etc). În cazul de față, deoarece constructorii nu au alocat memorie și nu au folosit nici o resursă specială, destructorul nu face nimic.
Crearea de Obiecte
Obiectele se crează pe stivă într-un mod foarte similar declarației de variabile primitive (int, float, etc). Deosebirea este că la crearea unui obiect, trebuie apelat și constructorul:
int main() {
Person somePerson;
Ball stackBallUnknown;
Ball myStackBall(WHITE, SPHERE, 0.0001, 1.4, somePerson);
return 0;
}
Pentru obiectul stackBallUnknown
, se apelează constructorul fără argumente. Pentru myStackBall
, se apelează constructorul cu argumente, valorile acestora fiind date între paranteze, după numele variabilei. Destructorul se apelează pentru ambele obiecte la ieșirea din funcție.
Dacă în C alocarea de memorie în HEAP se făcea cu funcțiile malloc
și calloc
și eliberarea se făcea cu free
, C++ introduce doi operatori: new
și delete
. Diferențele sunt:
malloc
alocă o anumită cantitate de memorie dată în bytes, pe cândnew
alocă memorie pentru un anumit număr de variabile de un anumit tip și apelează automat și constructorul acolo unde este cazul;free
dezalocă o zonă de memorie alocată cumalloc
saucalloc
, pe cânddelete
dezalocă memoria alocată cunew
și apelează destructorul acolo unde este cazul;
int main() {
Person somePerson;
Ball * heapBall1 = new Ball();
Ball * heapBall2 = new Ball(WHITE, SPHERE, 0.0001, 1.5, somePerson);
// play with balls...
delete heapBall1;
delete heapBall2;
return 0;
}
Atenție să nu amestecați funcțiile cu operatorii de alocare/ dezalocare de memorie.
Template-ul
Template-urile permit specializarea unor clase pentru anumite tipuri de date, fără a fi nevoie să rescriem clasa. Acest lucru este foarte util pentru clasele ce sunt folosite pe post de containere de date. Să luăm exemplu unei clase care ține un vector de valori împreună cu dimensiunea sa:
class Vector {
int * array;
uint32_t size;
public:
Vector(uint32_t newSize) {
size = newSize;
array = new int[size]; // echivalent cu "array = (int*) malloc(size * sizeof(int));"
}
int get(uint32_t index) {
return array[index];
}
void set(uint32_t index, int value) {
array[index] = value;
}
~Vector() {
delete [] array; // echivalent cu free(array); pentru memorie alocata cu malloc.
}
};
Se observă că această clasă este folosită foarte elegant pentru implementarea unui vector de numere întregi. Dar ce s-ar întâmpla dacă în loc de numere întregi am vrea să memorăm un vector de numele floating point? Ar trebui să rescriem toată clasa înlocuind fiecare apariție a lui int
cu float
. O soluție mai bună este utilizarea template-urilor, adică șablaonelor de clasă:
template <class T>
class Vector {
T * array;
uint32_t size;
public:
Vector(uint32_t newSize) {
size = newSize;
array = new T[size]; // echivalent cu "array = (T*) malloc(size * sizeof(T));"
}
T get(uint32_t index) {
return array[index];
}
void set(uint32_t index, T value) {
array[index] = value;
}
~Vector() {
delete [] array; // echivalent cu free(array); pentru memorie alocata cu malloc.
}
};
Se observă acum că Vector nu mai este o clasă, ci un template, parametrizat cu un tip generic T. Iată cum se folosește clasa pentru diferite tipuri de date T:
int main() {
Vector<int> intVector(10);
Vector<float> floatVector(20);
Vector<uint16_t> uShortVector(1000);
return 0;
}
Namespace-ul
Cu toate că C++ suportă supraîncarcarea funcțiilor și a metodelor, există totuși situații în care două funcții sau entități (clase, structuri, enumerări etc.) cu același nume trebuie să existe în același timp într-o aplicație. În acest scop C++ introduce conceptul de namespace
. Un namespace
este container pentru variabile, funcții și clase care izolează aceste elemente de alte elemente din alte namespace-uri. Un namespace
este identificat prin nume.
Vom plasa template-ul Vector
într-un namespace cu numele sda
:
namespace sda {
template <class T>
class Vector {
T * array;
uint32_t size;
public:
Vector(uint32_t newSize) {
size = newSize;
array = new T[size]; // echivalent cu "array = (T*) malloc(size * sizeof(T));"
}
T get(uint32_t index) {
return array[index];
}
void set(uint32_t index, T value) {
array[index] = value;
}
~Vector() {
delete [] array; // echivalent cu free(array); pentru memorie alocata cu malloc.
}
};
};
Astfel, o altă clasă sau template cu numele Vector
va putea exista în alt namespace sau în afara oricărui namespace. Pentru a folosi un element dintr-un namespace există 3 variante:
- Se utilizează numele complet al elementului (cu tot cu namespace):
int main() { sda::Vector<int> myVector; return 0; }
- Se declară tot namespace-ul ca fiind folosit în fișier (nerecomandat în practică în proiectele mari):
using namespace sda; int main() { Vector<int> myVector; return 0; }
- Se declară template-ul Vector din namespace-ul sda ca fiind folosit în fișier:
using sda::Vector; int main() { Vector<int> myVector; return 0; }
Standard Template Library (STL)
Limbajul C++ oferă o bibliotecă substanțială de funcții și clase optimizate ce pot fi folosite pentru a rezolva probleme și aplicații de programare. Această bibliotecă poartă numele de Standard Template Library. Majoritatea claselor din acesată bibliotecă fac parte din namespace-ul std
. Pentru laboratorul de azi vom folosi clasa template std::string
.
Clasa template std::string
Pentru a utiliza această clasă în aplicațiile voastre, trebuie să includeți header-ul string
(atenție, fișierele header din STL NU au extensia .h):
#include <string>
Documentația clasei std::string
o puteți găsi pe platforma http://en.cppreference.com: http://en.cppreference.com/w/cpp/string/basic_string